📜 ⬆️ ⬇️

Organizing a large project on the Zend Framework 2/3

The idea of ​​splitting large projects into small parts - the so-called microservice architecture - has recently become more and more popular among developers. This is a good approach for organizing code and developing as a whole, but what should those who have the code base start to take shape long before the peak of the popularity of the microservice architecture? The same question can be attributed to those who are comfortable on the loads on one powerful server, and there is simply no time to rewrite the code. Speaking of our own experience: now we are introducing microservices, but initially our monolith was designed “modular”, so that it was easy to maintain, regardless of volume. Who cares how we organize the code - welcome under cat.

Steve McConnell’s book Complete Code describes a bright philosophical idea that the main challenge facing the developer is managing the complexity of the code. One way to manage complexity is decomposition. Actually, the division of a large piece of code into simpler and smaller ones will be discussed.

Modules


Zend Framework offers us to divide our code into modules . In order not to reinvent the wheel and go along with the framework, let our parts into which we want to logically cut the project be modules in terms of the Zend Framework. Only we will refine the inter-module interaction so that each of them works exclusively in his area of ​​responsibility and does not know anything about the other modules.

Each module consists of a set of classes with a typical purpose: controllers, event listeners, event handlers, command handlers, services, commands, events, filters, entities, and repositories . All the above types of classes are organized into a certain hierarchy, where the upper layer knows about the lower layers, but the lower layers do not know anything about the upper layers (yes, layered architecture, it is the same).
')
At the top of the logical hierarchy are the controllers and event listeners . The first accept commands from users in the form of http-requests, the second react to events in other modules.

With the controller, everything is clear - this is the usual standard MVC controller provided by the framework. Listener of the events, we decided to make one for all modules. It implements the Zend \ EventManager \ ListenerAggregateInterface listener aggregator interface and binds event handlers to events by taking the description from the configuration of each module.

Listener ID
class ListenerAggregator implements ListenerAggregateInterface { /** * @var array */ protected $eventsMap; /** * @var ContainerInterface */ private $container; /** * Attach one or more listeners * * Implementors may add an optional $priority argument; the EventManager * implementation will pass this to the aggregate. * * @param EventManagerInterface $events * * @param int $priority */ public function attach(EventManagerInterface $events, $priority = 1) { $events->addIdentifiers([Event::DOMAIN_LOGIC_EVENTS_IDENTIFIER]); $map = $this->getEventsMap(); $container = $this->container; foreach ($map as $eventClass => $handlers) { foreach ($handlers as $handlerClass) { $events->getSharedManager()->attach(Event::EVENTS_IDENTIFIER, $eventClass, function ($event) use ($container, $handlerClass) { /* @var $handler EventHandlerInterface */ $handler = $container->get($handlerClass); $handler->handle($event); } ); } } } } 


After that, in each of the modules, a map of the events to which the listeners of this module subscribe is set.

 'events' => [ UserRegisteredEvent::class => [ UserRegisteredHandler::class, ], ] 

In fact, the modules communicate with each other exclusively through controllers and events.
If we need to pull up some kind of data visualization from another module (widget), then we use the controller's call to another module via forward () and add the result to the current model of the form:

  $comments = $this->forward()->dispatch( 'Dashboard\Controller\Comment', [ 'action' => 'browse', 'entity' => 'blog_posts', 'entityId' => $post->getId() ] ); $view->addChild($comments, 'comments'); 

If we need to inform other modules that something has happened to us, we throw an event so that other modules respond.

Service classes


We reviewed the controllers and event listeners above; now let's go through the remaining module classes that occupy a lower layer in the logical hierarchy: event handlers, command handlers, services, and repositories.

I will begin, perhaps, with the last. Repositories Conceptually, this is a collection for working with a certain type of entity that can store data somewhere in a remote repository. In our case in the database. They can be implemented, either using standard Zend-s TableGateway and QueryBuilder, or connecting any ORM. Doctrine 2 is perhaps the best tool for working with databases in a large monolith. And the repositories as a concept are already there out of the box.

For example, in the context of Doctrine 2, the repository will look like this:

Repository code
 class UserRepository extends BaseRepository { /** * @param UserFilter $filter * @return City|null */ public function findOneUser(UserFilter $filter) { $query = $this->createQuery($filter); Return $query->getQuery()->getOneOrNullResult(); } /** * @param UserFilter $filter * @return \Doctrine\ORM\QueryBuilder */ private function createQuery(UserFilter $filter) { $qb = $this->createQueryBuilder('user'); if ($filter->getEmail()) { $qb->andWhere('user.email = :email') ->setParameter('email', $filter->getEmail()); } if ($filter->getHash()) { $qb->andWhere('user.confirmHash =:hash') ->setParameter('hash', $filter->getHash()); } return $qb; } } 


To retrieve entities from the repository, both simple type parameters and DTO objects can be used, which store a set of parameters that need to be sampled from the database. In our terminology, these are filters (this is how they were named, because with their help we filter entities returned from the repository).

Services are classes that either act as facades to the application logic, or encapsulate the logic of working with external libraries and APIs.

Event handlers and command handlers are in fact a service with one public method, handle (), while they are engaged in changing the state of the system, which none of the other template classes do. By changing the state of the system, we mean any actions to write to the database, to the file system, send commands to third-party APIs, which will lead to changes in the data returned by this API, etc.

In our implementation, the event handler differs from the command handler only in that the DTO, which is passed to it as a parameter, is inherited from the Zend Event. While the command handler can come a command in the form of any entity.

Sample event handler
 class UserRegisteredHandler implements EventHandlerInterface { /** * @var ConfirmEmailSender */ private $emailSender; /** * @var EventManagerInterface */ private $eventManager; public function __construct( ConfirmEmailSender $emailSender, EventManagerInterface $eventManager ) { $this->emailSender = $emailSender; $this->eventManager = $eventManager; } public function handle(Event $event) { if (!($event instanceof UserRegisteredEvent)) { throw new \RuntimeException('   '); } $user = $event->getUser(); if (!$user->isEmailConfirmed()) { $this->send($user); } } protected function send(User $user) { $hash = md5($user->getEmail() . '-' . time() . '-' . $user->getName()); $user->setConfirmHash($hash); $this->emailSender->send($user); $this->eventManager->triggerEvent(new ConfirmationEmailSentEvent($user)); } } 


Sample command handler
 class RegisterHandler { /** * @var UserRepository */ private $userRepository; /** * @var PasswordService */ private $passwordService; /** * @var EventManagerInterface */ private $eventManager; /** * RegisterCommand constructor. * @param UserRepository $userRepository * @param PasswordService $passwordService * @param EventManagerInterface $eventManager */ public function __construct( UserRepository $userRepository, PasswordService $passwordService, EventManagerInterface $eventManager ) { $this->userRepository = $userRepository; $this->passwordService = $passwordService; $this->eventManager = $eventManager; } public function handle(RegisterCommand $command) { $user = clone $command->getUser(); $this->validate($user); $this->modify($user); $repo = $this->userRepository; $repo->saveAndRefresh($user); $this->eventManager->triggerEvent(new UserRegisteredEvent($user)); } protected function modify(User $user) { $this->passwordService->encryptPassword($user); } /** * @throws CommandException */ protected function validate(User $user) { if (!$user) { throw new ParameterIsRequiredException('   user   RegisterCommand'); } $this->validateIdentity($user); } protected function validateIdentity(User $user) { $repo = $this->userRepository; $persistedUser = $repo->findByEmail($user->getEmail()); if ($persistedUser) { throw new EmailAlreadyExists('   email  '); } } } 


DTO objects


Above we described typical classes that implement the logic of the application and the interaction of the application with external APIs and libraries. But for the coordinated work of all of the above, “glue” is needed. This “glue” is Data Transfer Objects , which typify communication between different parts of an application.

In our project, they have a separation:

- Entities - data that represents basic concepts in the system, such as: user, dictionary, word, etc. Basically, they are selected from the database and presented in one form or another in the view scripts.
- Events - DTO, inherited from the Event class, containing information about what has been changed in a module. They can be thrown by command handlers or event handlers. And only event handlers accept and work with them.
- Commands - DTO, containing the data required by the processor. Formed in controllers. Used in command handlers.
- Filters - DTO, containing the parameters of sampling from the database. Anyone can form; used in repositories to build a query to the database.

How does the interaction of parts of the system


Interaction with the system is divided into data reading and data modification. If the requested URL should only give the data, then the interaction is structured as follows:

1) The data from the user in the raw form come to the controller action.
2) Using Zend InputFilter, filter them and validate.
3) If they are valid, then in the controller we form a DTO filter.
4) Then everything depends on whether the resulting data from one repository is obtained or compiled from several. If from one, then we call the repository from the controller, passing in an object formed at the 3rd step to the search method. If the data needs to be compiled, then we create a service that will act as a facade for several repositories, and we are already giving it a DTO. Service also pulls the necessary repositories and assembles data from them.
5) We return the received data in the ViewModel, after which the view script is rendered.
You can visualize the components involved in the acquisition of data, as well as the movement of this data using the scheme:



If the requested URL should change the state of the system:

1) The data from the user in the raw form come to the controller action.
2) Using Zend InputFilter, filter them and validate.
3) If they are valid, then in the controller we form DTO commands.
4) Run the command handler in the controller, passing the command to it. It is considered correct to send a command to the command bus. We used Tactician as a tire. But in the end, we decided to directly launch the handler, since although the teams received an additional level of abstraction, which theoretically gave us the opportunity to start some commands asynchronously, but in the end we were inconvenienced that we had to subscribe to the response from the team in order to find out the result of its development. And since we do not have a distributed system and do something asynchronously - this is the exception rather than the rule, we decided to ignore the abstraction for the sake of usability.
5) The command handler changes data using services and repositories, and generates an event, passing the modified data there. Then throws an event into the event bus.
6) The event handler (s) catches the event and performs its transformations. If necessary, also throws an event with information about which transformations have been made.
7) After all event handlers are processed, the control flow returns from the command to the controller, where, if necessary, the result returned by the command handler is taken and sent to the ViewModel.

Schematically, the relationship between the elements, as well as how the calls between components occur, is shown in the figure:



An example of intermodular interaction


The simplest and most illustrative example is the registration of a user with the sending of a letter to confirm an email address. In this chain, two modules are worked out: User, who knows everything about users and has code that allows users to operate on user entities (including registering); as well as the Email module, which knows how, what and to whom to send.

The user module catches data from the registration form into its controller and saves the user to the database, after which it generates a UserRegisteredEvent ($ user) event, passing it the saved user.

 public function handle(RegisterCommand $command) { $user = clone $command->getUser(); $this->validate($user); $this->modify($user); $repo = $this->userRepository; $repo->saveAndRefresh($user); $this->eventManager->triggerEvent(new UserRegisteredEvent($user)); } 

From this event to this event can be subscribed from zero to several listeners in other modules. In our example, the Email module, which generates a verification hash, sends the hash to the letter template and sends the generated email to the user. After that, again, it generates a onfirmationEmailSentEvent ($ user) event, where the user entity to which the confirmation hash is added substitutes.

 protected function send(User $user) { $hash = md5($user->getEmail() . '-' . time() . '-' . $user->getName()); $user->setConfirmHash($hash); $this->emailSender->send($user); $this->eventManager->triggerEvent(new ConfirmationEmailSentEvent($user)); } 

After that, the User module will have to catch the event of sending the letter and save the confirmation hash to the database.

On this intermodular interaction should be completed. Yes, you often want to directly pull the code from the next module, but this will lead to the connectedness of the modules and kill at the root the possibility in the long run to bring each of the modules into a separate project.

Instead of conclusion


This, in fact, simple code structuring can be achieved by dividing the project into independent small parts. A project cut into such parts is much easier to maintain than a project written in solid form.

In addition, when developing code using this approach, it will be much easier to switch to the microservice approach. Since microservices will actually exist, albeit in the context of a single project. It will only be necessary to bring each module into its own separate project and replace the Zend event bus with its own implementation, which will send events via HTTP or RabbitMQ. But this is purely theoretical speculation and food for thought.

Bonuses for Habr's readers


Online Courses

We give you access for a year to the English course for self-study "Online course".
To access, simply follow the link . The activation period of the promotional code is until September 1, 2017.

Individually by Skype

Summer intensive English courses - we reserve the application according to the link .
Classes are held at any time convenient for you.
Promo code for 35% discount: 6habra25
Valid until May 22. Enter it when paying or use the link .

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


All Articles