Composer Patches

I was never the type of developer who uses patches. Usually, when working with Magento 2, I create a new package, or also known as a Magento 2 module to fix a bug. For the last 2 years, I use patching to apply a fix in a vendor directory in case there is a problem with a 3rd party library. Also, I see more occurrences of library patching from other developers.

The purpose

A patch, when created for a 3rd party library, is used to fix a broken functionality or a bug. It usually happens when there is no time to wait for a new version of the library. Or, an effort that is required to “do it right” is higher than a patch-fix. In this case, patching libraries makes sense as a time-to-market is fast compared to an official release of a vendor package.

A problem with patching

Some developers misinterpret the idea of patching and create patches in order to add new functionality on top of an existing one. It makes it harder to read the original source code of a vendor. You might need to open a patch file, read the source code of this file in a format, that is unusual to read when writing the source code. Here I am not talking about nerds that write code in a FarCommander and perform every operation via a terminal and read all code without highlighting with Vim.

For me personally, every time I need to create a patch, I open google and search for a list of commands that will give me the patch that is valid for a composer.

The process

Let’s say I need to change a file that is located under a vendor directory. The vendor directory is usually included in a .gitignore file and all the changes will not be added to a git repository and lost. In this case, I create a patch file, that provides a fix for a vendor library.

Let me share with you my process of creating a patch.

Step 1: identify the problem

The first step is to find the root cause of the problem. I use xDebug as my go-to debugging tool for PHP debugging. It may happen then xDebug won’t help if a problem is located somewhere on a frontend side e.g. JavaScript. In this case, Chrome browser Inspect is a thing to go with.

Let’s say, I have a problem with one of the PHP classes, that is responsible for providing a country code in an ISO3166 format. For example, Belgium’s country code is BE, and the ISO3166 format is 056. The resulting value is returned as an integer value.

Below is a getNumber() method implementation.

    public function getNumber($countryCode): int
    {
        $data = $this->iso3166->alpha2($countryCode);
        if (isset($data['numeric'])) {
            return (int) $data['numeric'];
        }
        throw new Exception("Country code can't be retrieved.");
    }

The problem with this implementation is that the 056 value is converted to integer 56. It breaks integration with a 3rd-party payment system, that expects to get 056, but it gets 56 as a country code for Belgium.

Step 2: decide on a fix

I might think of a few ways on how to fix this problem. First of all, I can create a new library, that provides a class with the fixed version of the getNumber() method.

The return type is incorrect as it should be returned as a string rather than an integer. In this case, we preserve the original ISO31566 value that contains a leading 0.

As a result, the improved getNumber() method is as below:

    public function getNumber($countryCode): string
    {
        $data = $this->iso3166->alpha2($countryCode);
        if (isset($data['numeric'])) {
            return (string) $data['numeric'];
        }
        throw new Exception("Country code can't be retrieved.");
    }

When going with the new library that provides a fix, in this case, a small one-line fix, there could be a problem when we write more library infrastructure code and supporting files than the actual fix.

In the case of Magento 2, we need to create a new Magento 2 extension, add required registration.php, module.xml, composer.json files. We would need to create a new PHP class with the method and a fix. And, finally, write a preference for a class in the di.xml file. The PHP class might need to be a child of the original class. So the problem with the __construct() method injection is sorted.

Here, we talk about 2 lines of code that is a fix for the ISO3166 country code problem. And, probably, a patch would be a better approach here, especially, when we really fix a problem, without introducing new functionality. I appreciate that we change the API by changing the signature of the getNumber() method. But this is a fix that has to happen in order to improve the situation with the broken functionality.

Step 3: create a patch

I usually create a copy of the file a needs to modify it in the same directory as the original file. Then I run a diff -u filea.php fileb.php > diff.patch, but I found out that this requires an additional effort.

There is a symplify/vendor-patches that allows creating patches. But it runs with PHP 8+. In most cases, I work with legacy libraries that run on PHP 7.2+.

But if you work with legacy PHP versions like me, please go ahead and add the required file that needs patching to git. After the patch, we will revert this change.

git add -f vendor/pronko/global-payments-meta/GlobalPaymentsHppSecure/Gateway/Country.php

Run the git status command to check that the file is added to the git.

It is time to perform the required changes to the getNumber() method directly in the vendor’s file that we’ve just added to git.

We can run the git diff command to verify our changes.

git diff vendor/pronko/global-payments-meta/GlobalPaymentsHppSecure/Gateway/Country.php

As a result, we should see our changes.

Once we’ve verified that all changes are correct, let’s create a patch file.

git diff vendor/pronko/global-payments-meta/GlobalPaymentsHppSecure/Gateway/Country.php  > patches/diff.patch

As a result, the new diff.patch file is created under the patches directory of the project. A good practice is to name the file in a way that gives you a better idea of the changes.

Let’s update the composer.json file of the project and add the patch to the extra section. We can also provide some description of the patch file reference.

"extra": {
    "patches": {
        "pronko/global-payments-meta": {
            "Fix for Invalid data in the HPP_SHIPPING_COUNTRY field.": "patches/CSS-143-HPP_COUNTRY_FIELD.patch"
        }
    }
}

Let’s remove the file from the git version control to revert changes.

git reset vendor/pronko/global-payments-meta/GlobalPaymentsHppSecure/Gateway/Country.php

Finally, run the composer update command to apply the patch for the vendor library.

Gathering patches for root package.
Removing package pronko/global-payments-meta so that it can be re-installed and re-patched.
  - Removing pronko/global-payments-meta (1.1.0)
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
Gathering patches for root package.
Gathering patches for dependencies. This might take a minute.
  - Installing pronko/global-payments-meta (1.1.0): Loading from cache
  - Applying patches for pronko/global-payments-meta
    patches/CSS-143-HPP_COUNTRY_FIELD.patch (Fix for Invalid data in the HPP_SHIPPING_COUNTRY field.)

As a result, the method is patched with the patch we’ve created.

Conclusion

Patching is evil when used in wrong occurrences like adding new features. But it can be right and quick when fixing 3rd party vendor libraries. In my case, it is a matter of 2 lines of code change in order to fix the issue. I happen to fix 1 character in one of the libraries to fix a bug.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *