Home / 
Blog / 
Composer Patches in Magento 2
thumbnail

Composer Patches in Magento 2

In everyday practice, we use some external modules, files, and solutions. In PHP they are declared in the composer.json file and installed in the vendor directory. But what to do, when some package is broken or contains some security bugs? Of course, you can update it, but sometimes – due to the huge amount of dependencies – it is impossible. 

In those cases, you can always use patches. In this quick tutorial, I’m going to show you how to apply composer patches in any composer-based project.

  1. Install package for managing patches in composer:

    composer require cweagans/composer-patches

    This package will allow you to manage all patches in your project. You will be able to add new, edit existing patches and delete the old ones. It is very simple to use and it’s written 100% in PHP language 😉

  2. The next step is to choose a file that you want to change. For test purposes let’s say that you want to change the class CallbackInvoker in magento/framework-message-queue package. This class was affected by a bug until the release of version 2.3.5, so in fact, this is a real life example.

    public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback)
    {
      $this->poisonPillVersion = $this->poisonPillRead->getLatestVersion();
      for ($i = $maxNumberOfMessages; $i > 0; $i--) {
        do {
          $message = $queue->dequeue();
          // phpcs:ignore Magento2.Functions.DiscouragedFunction
        } while ($message === null && (sleep(1) === 0));
        if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) {
          $queue->reject($message);
          // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage
          exit(0);
        }
        $callback($message);
      }
    }

    This function can get stuck in an infinite loop if there are no messages. It must be changed. You can add an if statement to check if there are no messages. If the statement will return true, the loop should be closed with the exit command. Our method after changes looks like this:

    public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback)
      {
        $this->poisonPillVersion = $this->poisonPillRead->getLatestVersion();
        $noMessages = 0;
        for ($i = $maxNumberOfMessages; $i > 0; $i--) {
        do {
        $message = $queue->dequeue();
        if ($message === null) {
        $noMessages++;
        if ($noMessages > 10) {
        exit;
      }
      }
        // phpcs:ignore Magento2.Functions.DiscouragedFunction
      } while ($message === null && (sleep(1) === 0));
        if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) {
        $queue->reject($message);
        // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage
        exit(0);
      }
        $callback($message);
      }
      }
  3.  Ok, we have a ready solution. But we don’t want to apply this manually every time when the vendor directory is rebuilt by the composer. We have to create a patch. This patch will be a file containing a diff between old class and class with our changes. To generate correct diff file I suggest to follow the steps below:
    1. Create a new test directory and add an empty git repository:
      mkdir test && cd test && git init
    2. Our class is in the root directory of magento/framework-message-queue so we don’t have to create additional catalogs. But if we would like to change something different – for example: some test class – we have to create a directory reflecting the original catalog: Test/Unit/Bulk for PublisherTest.php.
    3. Copy and paste original class CallbackInvoker.php. Commit original file:
      git add CallbackInvoker.php && git commit -m "a"
    4. Now make changes in the class (in our example: add $noMessage variable and exit statement). Commit your changes:
      git add CallbackInvoker.php && git commit -m "b"
    5. Last but not least – generate diff file which will be your patch:
      git diff --patch --output="patch_name.diff" commit_id_1 commit_id_2

      (to get commit ids you can use command: git log -2)

  4. When the patch file is ready you have to copy it to your project. The best practice is to keep it in the separate directory. Let’s create a new catalog in the root directory in the project:
    mkdir -p patches/composer

    And copy generated diff:

    cp {your_path}/patch_name.diff patches/composer/
    
  5. Ready patch is already in your project. Now it is time to let the composer know that it needs to be installed. Open composer.json file and find object “extra”. Here you have to add those lines:
    "extra": {
          "patches": {
            "magento/framework-message-queue": {
            "patch description": "patches/composer/patch_name.diff"
            }
          }
      }

    “magento/framework-message-queue” is of course package name which we want to change. “Patch description” – here you can write whatever you want. If a patch is connected with some open issue on github it is good practice to paste here ID of this issue.

  6. If the patch and edited composer.json file are ready, we can finally install it!
    composer install -v

    If output is green and you see no errors, you can open affected file in vendor – changes from patch should be applied.

  7. Don’t forget to update also composer.lock file to prevent different files on local, dev and production environment:
    composer update --lock
...