The first step is to understand the classes and their responsibilities. The project is publicly available on GitHub at the following URL: https://github.com/distvan/php-microservice. In the repository’s doc folder, you'll find a UML class diagram that illustrates the classes, interfaces, and their relationships. This diagram helps visualize the overall structure of the application. Each class is a self-contained unit, adhering to the Separation of Concerns principle. Furthermore, since each class has a single reason to change, the design also complies with the Single Responsibility Principle. The application accepts an HTTP POST form request at the /watermark endpoint, with an image parameter containing the URL of an image. It downloads the specified image, applies a watermark, and returns the watermarked image as a binary response.
Let’s take a closer look.
The entry point of the application is the index.php file located in the public folder, which serves as a bootstrap. It uses a class loader (autoload.php) based on the PSR-4 standard and relies on Composer for package management. You can review the package dependencies in the composer.json file.
Environment-specific settings, such as logging configuration and the base watermark image name, are stored in a .env file. To access these variables, the application uses an existing package that handles environment variable loading.
The Dependency Injection (DI) container is responsible for resolving class dependencies and managing inversion of control. Its primary goal is to make the application more maintainable and testable. For a deeper dive into the topic, refer to my latest blog post about containers.
The DI container in this project is implemented as a separate, modular component that can be easily attached to the application. I intentionally kept it minimal and straightforward, so it can be replaced with any other container that implements the same standard interface.
The Router class is responsible for registering, storing, and managing route definitions. It matches incoming HTTP requests to the appropriate Route based on the HTTP method and URI path, effectively mapping each request to the corresponding controller and action.
The Dispatcher class handles the incoming request by using the Router to find a matching route. It then invokes the route's handler and returns the resulting response.
The RoutesConfigurator class is separated according to the Single Responsibility Principle. Its sole purpose is to define and register routes in a centralized and maintainable way.
The WatermarkController class is responsible for handling incoming requests. It performs tasks such as extracting data from the request, delegating processing to services, and formatting the response. The controller serves as the gateway between the external world and the internal logic of the microservice.
Its typical responsibilities include:
- Receiving the request
- Extracting relevant data
- Validating the data (or delegating validation)
- Passing clean data to the service layer
- Returning the formatted response
In the controller class, I used constructor injection to provide its dependencies. To handle service instantiation, I implemented the factory pattern. This ensures that the creation logic is centralized in a single place, promoting consistency, testability, and separation of concerns.
This article demonstrated a simple yet effective implementation of a microservice in PHP, focusing on clean architecture principles such as Separation of Concerns, Single Responsibility, and Dependency Injection. By organizing the application into modular components—like the router, dispatcher, controller, and service layers—we achieved a design that is both maintainable and extensible.
Top comments (2)
I think it a good exercise if you want to learn more about building a framework.
But for a microservice it feels over-engineered.
The
Application
could be one function.The
handle
method has no benefit.So instead of using a class to do constructor dependency injection, just use function arguments.
The function is as testable as the class.
The
RoutesConfigurator
looks like the only reason for theContainer
to exist.In the index.php file you could have done this instead.
Microservices have a limited amount of routes. If it was a whole website I would agree with the abstraction.
I'm quite sure the
configure
method$router
argument should be passed on by reference to be picked up by theDispatcher
instance. If you do it by value, the method should return the router instance.Why have a
WatermarkServiceFactory
? I understand the need for theLoggerfactory
because it is a third party library. But for the custom service the extra code has no benefit.Thank you for your valuable feedback and for taking the time to review my code — I really appreciate it! You're absolutely right, the implementation is over-engineered — I did it mainly for educational purposes to explore the design patterns. Thanks again for the insightful comment!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.