Motivation

Complex PHP web applications are based on services. Services are usually objects of one instance accessible through a dependency inversion container. They should have a single purpose, like printing something to PDF, sending an email, sending an SMS, processing CRUD on the file storage, etc. Services often depend on libraries, which also depend on third party libraries. They can also depend through composer on different versions of Php. These dependencies can lead to a dependency hell, where different versions of the same resource are needed. The solution can be spinning the service off to a microservice.

Microservice spin off image

Dependency hell (quicksand)

Libraries and third party dependency

Most of the libraries come in the form of a composer package. Composer is the package manager for PHP. It tries to find the best combination of packages to satisfy application needs. It means the latest possible package of every library is choosen. When the combination is not possible and concurrent versions of the same package are required, the failure is thrown, and the programmer must resolve the conflict manually. Most of the time, the process is completed automatically.

When your app uses only a few libraries, everything runs smoothly. Yet, it can change soon. You’d like to connect to AWS S3 storage, so you add the S3 client package, then you’d like to print the invoice in PDF, so you add a package, etc.

Programmers are lazy, it’s a well-known fact. So they tend not to check third party dependencies of every package included in the project, and they are not usually aware of packages used. Instead of checking they rather rely on Composer. And even if they check the dependencies at the beginning, these dependencies often change over time.

In an ideal world, the authors of the packages would support the older major versions of the package every time they break the backward compatibility and shift to a newer major version. But it seldom happens. Most authors simply don’t have the resources to maintain more than the last major version. This results in security updates only for the latest major version.

Besides third party libraries, there are other dependencies the programmer must pay attention to.

Php version dependency

The first is the version of PHP itself. Many packages have declared which version of PHP to use. For example, ^8.0 means you have to comply at least with version 8.0 and will be “safe” until the 9.0 version is released. I quoted the word safe, because it may not be entirely true. PHP is known for minor versions, to break backward compatibility (as little as it gets), but the threat of the installed version of your package not working properly with your version of PHP is real. Even if you comply with declared versions. This is fortunately often easily solved by simply updating the patch version of the package. The problem arises when you are not on the last version of the package, and is very likely, that the author has not released the patch version.

API version dependency

The second hidden dependency is a dependency on a third party API version. I have mentioned AWS S3 storage and the S3 client you can install. Since the API is also changing from time to time, you have to keep an eye on the version your client is for. You can connect to S3 as a service - part of the AWS (or other) cloud solution, or you can have S3 compatible storage installed locally in your cloud. In the first case, you have to have the latest version of the client package installed all the time. Otherwise, you face the incompatibility problem. In the second case, you have the choice to stay on the older version longer and decide your own upgrade strategy.

Official Docker image dependency

The last dependency is more infrastructure related. You can not linger a long time on the older version of PHP since you probably use Docker for PHP installation. Most of us depend on the official PHP docker image. These images are built on the current version of Linux, which is getting quickly old. It means no security updates available for such image. And who wants to risk running a container full of security holes in the production environment?

Other dependencies can be on the exact PHP extension or system library version, but they are often linked with the PHP version.

Old-style monolithic app

A classical PHP app is a monolithic hegemon usually based on a heavy framework like Symfony or Laravel. The bigger the app is, the more libraries it usually carries. The more libraries, the more packages and here we are dealing with quicksand of Linux, PHP, and packages’ versions.

The right way to deal with the quicksand seems to be to get every package up to date.

Nevertheless, from time to time you get in trouble, even if everything is on the latest version.

The downside of this approach is that it is sometimes extremely time-consuming to keep up with the latest versions of every package. Sometimes, it is even impossible because of the dependencies we mentioned earlier.

New microservice possibilities

Thanks to virtualization and Docker / Kubernetes technology, we can now easily run another PHP app, side by side, with the main PHP app. The second app can even be private on the virtual network, thus serving only as a microservice for the main app. Because it runs on a private network only, we often may omit the security layer.

Loosen the coupling

Calling the microservice

What does it mean to spin off the service to a microservice? First, the service is called in your app using a method call with parameters. If you spin the service off, you should call it using the HTTP API call. You will probably use an HTTP library like GuzzleHTTP for such a purpose.

// calling the service classic way
$service->method($parameter1, $parameter2, ...);

// calling the service using GuzzleHTTP client
$client->post("path/to/method", [
    'headers' => [
        'Content-Type' => 'application/json',
    ],
    'json' => [
        'data' => [
            "parameter1" => $parameter1,
            "parameter2" => $parameter2,
            ...
        ]
    ]
]);

Front controller of microservice

You can create your microservice based on a framework, but it seems like an overkill to me. Since it is not a public service, we may not implement a security layer. Our microservice will have only a few endpoints, so no complex routing is necessary. We can end up with something like this:

// basic front controller

if (
    $_SERVER['REQUEST_METHOD'] === 'GET' && 
    preg_match('/\/getData\/(\d+)/', $_SERVER['REQUEST_URI'], $matches)
) {
    // get data by id endpoint
    ...
} else if (
    $_SERVER['REQUEST_METHOD'] === 'POST' && 
    $_SERVER['REQUEST_URI'] === '/addData'
) {
    // add data endpoint
    ...
} else {
    // 404 not match endpoint
    ...
}

Deploying microservice

When deploying a PHP application in Kubernetes or Docker, we usually create two containers. One with PHP FPM and the second with NginX. We can follow this strategy for every microservice, keeping them independent of each other, but I would prefer to share one NginX for all microservices unless you have some with frequent access. The advantage is that you have to set up only one HTTP client in your app. You also save a bit of memory.

Docker chart image

# default.config nginx file

server {
    # ... 

    location /print {
        
        # Replace with your PHP service address and port
        fastcgi_pass php_microservice_print:9000;

        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location /encrypt {
        
        # Replace with your PHP service address and port
        fastcgi_pass php_microservice_encrypt:9000;

        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # ...
}

When to spin off the service

  • service maintenance cycle differs from the rest of the app
    You build a printing service on the TCPDF library, create the interface for your app, and the functionality is sufficient for you. Likely, you won’t touch it for a couple of years.

When not to spin off

  • you build a short life app
    The app is built only for one-time event. It is not supposed to be improved in the future.
  • your app is simple without complex dependencies
    The app has a very simple dependency tree and no competing packages.
  • there is too complex API of your service
    The API of the service called in your app is too complex to be transformed into an HTTP call.
  • the service is handling the main purpose of your app
    Usually the main purpose of the web app is to store data and restore data in a certain form. Spinning for example the ORM service is not wise.
  • local resources needed
    Your service processes some of the local resources (files, local hardware, etc.) or uses a lot of memory.

Special case - HTTP call wrappers

Many web services use HTTP API, but they are usually called through an official library wrapper, which is often available for most programming languages. In the case of PHP, they are available as a composer package.

Since these services are already HTTP wrappers, it makes no other sense to shift them into another HTTP wrapper than isolating the version of the library. A better solution is often to use HTTP API directly with the HTTP client of your choice. The downside is you have to learn the API more thoroughly, especially the error handling.

Examples of spinned off services

  • print server
  • encryption service
  • mail service
  • sms service
  • security service

Summary

We have discussed the way how to turn classical service into microservice. This technique solves the problems with dependency hell, is easier to maintain in the long term horizon, and creates isolation for service. Nevertheless, there are also some challenges.

Pros

  • dependency hell avoidance
    This is the main purpose of the technique.
  • easier long-term maintenance
    If you have a service that works just fine and doesn’t require new features, you aren’t probably motivated to any change. Yet, the app dependencies can force you to move to a newer version very often. In the microservice approach you can go your own pace of partial upgrades.
  • isolation
    Many services use environment variables, often carrying secrets nobody should know. In a microservice architecture, your app is completely isolated, reducing the potential for harm to a minimum.

Cons

  • building the infrastructure
    Shifting the method call to http API takes some resources. You should carefully decide whether building the infrastructure for microservices exceeds the cons.
  • validations
    When calling the service in your app, the service is within your code. In the microservice approach, you should validate at least types of input parameters, but often stronger validation is welcome.
  • longer response time
    Using microservices over the HTTP protocol can result in longer response times. You shouldn’t rely on them in heavy-duty apps.
  • increased HTTP traffic
    It is obvious the HTTP traffic in your infrastructure can get high.
  • not a 100% availability of microservice
    The microservice could be unavailable at the moment and you should treat this situation in your app. This is not necessary for the classic monolithic approach.
  • error handling
    There is a new layer involved, so a more complex error-handling mechanism is needed.