📜 ⬆️ ⬇️

Experience in implementing PSR standards in a single project

Hello!
In this article I want to talk about my experience of moving to a “modern trend-compliant” platform in one legacy project.


It all started about a year ago, when they threw me into the “old” (new for me) department.
Before that, I worked with symfony / Laravel. Turning to a project with a self-written framework, the number of WTFs just went off scale, but over time everything turned out to be not so bad.
First, the project worked. Secondly, the use of design patterns could be traced: there was a dependency container, ActiveRecord and QueryBuilder.
Plus, there was an additional level of abstraction over the container, logger, work with queues and the beginnings of the service layer (business logic was not dependent on the HTTP layer, in some places the logic was removed from the controllers).


Next, I will describe those things that were difficult to put up with:


1. Logger log4php


Logger itself worked well. But there were fat cons:



2-6. The controllers were of the form:


<?php class AwesomeController { public function actionUpdateCar($carId) { $this->checkUserIsAuthenticated(); if ($carId <= 0) { die('  '); } $car = Car::findById($carId); $name = @$_POST['name']; container::getCarService()->updateNameCar($car, $name); echo json_decode([ 'message' =>' ' ]); } } 


7. Lack of global logging of application errors


The maximum that could be found in the logs is the message text. More information about the error could be obtained after repeating at the development booth. For additional logging on the combat environment, it was necessary to install try / catch blocks in the required controller method.


8. Configuring a dependency container


The dependency container resembled a symfony 2.4 times container. Each service required registration and descriptions how to build it. Having experience with the container laravel, where autowiring is used to the maximum, I wanted to get rid of routine actions. Also, the lack of autowiring reduced the programmers' desire to write separate services (create new classes) as a separate business task, as this implied the need to edit the container configuration. In this case, there is always the likelihood of making a mistake and losing even more time.


9. Routing


Routing was logical and simple, based on Yii1.
The address of the form www.carexchange.ru/awesome_controller/update_car meant the execution of the AwesomeController controller and the actionUpdateCar method.
But, unfortunately, there were nested subdirectories of the site and we had to create urls like
www.carexchange.ru/awesome_controller_for_car_insite_settings_approve/update_car
It is not annoying, but the restriction is strange


10. Hardcode url'ov


Routing was simple, so there was no possibility of generating the url automatically (why complicate). This led to thousands of links that were hardened in php and js. Of course, we rarely change urls, but sometimes this happens. And look for them on the project is difficult.


It's time to change something!


With the arrival of another programmer, questions began to be raised about the possibility of refactoring, there was a desire to make it “more humane”. “Humane” - read as usual for a modern developer. It was difficult to read and maintain the existing code-> long-> expensive.


After several discussions with the management, a green flag was obtained and work began on proof of concept.


From the outset, it was decided to adhere to modern standards. These are PSR recommendations, and following the standard behavior of other frameworks. New developers working on any modern framework had to understand where to find controllers, how services are assembled, and where to write business logic.


If you take a closer look at the voiced claims, then we note: the application-level code (controllers) and the infrastructure layer (container) suffer.
Business logic was written separately and did not depend on the HTTP level - we leave it as it is. We don’t touch Active Record and QueryBuilder either, since they worked and were not much different from the same doctrine / dbal.


Choosing a framework


In fact, the choice here was not great. Dragging all laravel or symfony for the sake of a layer over HTTP doesn't make sense And the necessary components can always be connected via composer.
A serious choice was between two micro-frameworks: Slim and Zend .
Both of these frameworks fully support PSR-7 and PSR-11.


Why not Lumen? The main reason, of course, is that Lumen can hardly be called “micro” along with all the good stuff . Build Lumen into an existing project is difficult. The dependency container is not easy to replace (you need to respect the illuminate contract). The PSR-7 contract is supported by the framework, but it still depends on symfony / http-foundation .


At first I took Zend seriously. But having spent 2 days, looking at the implementation of the application in the ideology of "all middleware", seeing how the container config is formed, I presented with horror how I will explain to less experienced developers how invokables differ from factories, and when to write aliases. Perfectionists and academics of Zend should be to their liking. The application works through the pipeline and middleware. But I was afraid of a higher entrance threshold, while the move should have been easy, ideally inconspicuous.


Then I switched to Slim. Its implementation in the project took less than a day. The choice of controllers (old and new sample) was implemented through middleware. On Slim and stopped. In distant plans to go to the pipeline with PSR-15 middleware.


Container selection


Here I’ll just say that I stopped at league / container and try to explain my choice.


  1. This is support for PSR-11.
    Now most containers already support PSR-11, but a year ago only a small part supported the container / interop interface.
  2. Autowiring.
  3. Sintasis is quite simple, as opposed to the same zend-servicemanager .
  4. Service providers, allowing to write modules even more in isolation.
    In illuminate / container, providers are registered at the application level, and in league / container, providers are registered at the container level. Thus, the application depends only on the container, and the container depends on the service providers.
  5. Container delegation. This feature turned out to be decisive for the container replacement stage, so I’ll reveal it in more detail.
    If desired, there may be several PSR-11 compatible containers inside the league / container.
    Possible scenario: you decided to change your old container to symfony / dependency-injection . To go gradually, you can connect league / container and put your old container and symfony container in the delegates. When searching for a service, your old container will be polled first, then there will be a search in the symfony container. In the next step, you can transfer the descriptions of all the services to the symfony container and leave it only. Since the code depends on the PSR-11 interface - the changes are minimal.

Abstraction over HTTP selection


There are only 3 options:



By the way, Slim is moving to allocating the HTTP implementation to a separate package (expected in branch 4.0).
Symfony bridge was not wanted because of the extra code and unnecessary dependency. Since Slim doesn’t limit us in anything, the preference was given to the implementation of Zend. This only increased the independence of the application code from the HTTP layer.


Logging


Nothing but a monolog comes to mind. He was screwed. During development PHPConsoleHandler and ChromePHPHandler are useful .


Routing


Slim out of the box has a FastRoute. On its basis appeared named routes. URL generation implemented via global helper ( Like here )


So what has changed?


Now our controller looks like this:


 <?php namespace Controllers; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\JsonResponse; use Domain\Car\Services\CarService; class AwesomeController { /** * @var CarService */ private $carService; public function __construct(CarService $carService) { $this->carService = $carService; } public function actionUpdateNameCar(ServerRequestInterface $request, $carId): ResponseInterface { if ($carId <= 0) { throw new BadRequestException('  '); } $car = $this->carService->getCar($carId); $name = $request->getParsedBody()['name']; $this->carService->updateNameCar($car, $name); return new JsonResponse([ 'message' => 'updateNameCar ' ]); } } 

Of course, in real code, things like $request->getParsedBody()['name'] and new JsonResponse moved to another level of abstraction with additional checks.


As a bonus: depending on the environment, it is possible to substitute services in the container and run functional tests.


Finally


As you can see, a lot of practices with the ideology “so easy” are borrowed from Laravel. He really sets the trends.


The application received a new framework after it has worked for 7 years. As far as I know, the old samopisny framework also did not appear immediately. And no one can guarantee that we will not want to change the framework in 5 years. Therefore, the code was written as independent as possible from the chosen framework. Business logic did not depend on the application before, and now the controllers do not depend on the framework. The controllers depend on PSR-7 compliant requests and return PSR-7 responses. And controllers are assembled by an application dependent on a PSR-11 compatible container.


Slim works through middleware and it is easier to add common logic (logging application errors, handling user input errors). Autowiring controllers works great, in fact the controllers have become services.


By the way, here you can see an example of the inclusion of autowiring in slim.


Of course, the application continues to evolve and has already switched to standard caching and event interfaces, but their implementation was a little less bloody :).


')

Source: https://habr.com/ru/post/337692/


All Articles