Organization of a large project on Zend Framework 2/3

    The idea of ​​dividing large projects into small parts - the so-called microservice architecture - has been gaining popularity among developers lately. This is a good approach for organizing code, and development in general, but what about those whose code base began to take shape long before the peak in popularity of microservice architecture? The same question can be attributed to those who are comfortable with the load on one powerful server, and there is simply no time to rewrite the code. Speaking about 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 to Cat.

    The book "Perfect Code" by Steve McConnell describes a bright philosophical idea that the main challenge that a developer faces is managing code complexity. 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 in line 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 finalize the intermodular interaction so that each of them works exclusively in their 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 these types of classes are organized into a certain hierarchy, where the upper layer knows about the lower layers, but the lower ones do not know anything about the upper layers (yes, layered architecture, it is the same).

    At the top of the logical hierarchy are controllers and event listeners . The former receive commands from users in the form of http-requests, the latter respond to events in other modules.

    Everything is clear with the controller - it is a standard standard MVC-controller provided by the framework. The listener of events, we decided to make one on all modules. It implements the interface of the listener aggregator Zend \ EventManager \ ListenerAggregateInterface and binds event handlers to events, taking the description from the configuration of each module.

    Listener code
    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);
                        }
                    );
                }
            }
        }
     }
    


    Then, in each of the modules, a map of events is set for which listeners of this module subscribe.

    	 	 	
    '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 call of another module via forward () and add the result to the current view model:

     $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 happened to us, we throw an event so that other modules respond.

    Service classes


    The controllers and event listeners we reviewed above, now we will go through the remaining classes of the module, which in the logical hierarchy occupy the lower layer: event handlers, command handlers, services and repositories.

    I'll start, perhaps, with the latter. 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 by connecting any ORM. Doctrine 2 is perhaps the best tool for working with databases in a large monolith. And repositories as a concept are already out of the box.

    For example, in the context of Doctrine 2, the repository would 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 obtain entities from the repository, you can use both simple type parameters and DTO objects that store a set of parameters by which you need to organize a selection from the database. In our terminology, these are filters (as they were called, because with their help we filter entities returned from the repository).

    Services - 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- this is actually a service with one public handle () method, while they are engaged in changing the state of the system, which no other template classes do. By changing the state of the system we mean any actions to write to the database, to the file system, sending commands to third-party APIs, which will lead to a change 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 a command in the form of any entity can come to the command handler.

    Event Handler Example
    	 	 	
    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));
       }
    }
    


    Command Handler Example
    	 	 	
    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


    Typical classes that implement the logic of the application and the interaction of the application with external APIs and libraries have been described above. But for the coordinated work of all of the above, “glue” is needed. Such “glue” are Data Transfer Objects , which typify communication between different parts of the application.

    In our project, they have a separation:

    - Entities - data that represents the 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 view scripts.
    - Events - DTO inherited from the Event class, containing data about what has been changed in some 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 necessary for the processor. Formed in controllers. Used in command handlers.
    - Filters - DTO containing the parameters of the selection from the database. Anyone can shape; used in repositories to build a database query.

    How the interaction of parts of the system


    Interaction with the system is divided into reading data and changing data. If the requested URL should only give the data, then the interaction is constructed in this way:

    1) The data from the user in raw form comes into the action of the controller.
    2) Using Zend's 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 is obtained from one repository or compiled from several. If it’s from one, then we call the repository from the controller, passing the object generated in 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 to several repositories, and we already transfer the DTO to it. The service pulls the necessary repositories and composes the data from them.
    5) We give the received data to the ViewModel, after which the view script is rendered.
    You can visualize the components involved in obtaining 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 raw form is sent to the controller action.
    2) Using Zend's 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 transfer the command to the command bus. We used as a Tactician bus. But in the end, they decided to start the handler directly, because with the team bus, although they received an additional level of abstraction, which theoretically gave us the opportunity to run part of the commands asynchronously, they ended up inconvenient in having to subscribe to the response from the team to find out the result of its development. And since we do not have a distributed system and perform something asynchronously - this is the exception rather than the rule, we decided to neglect abstraction for the sake of usability.
    5) The command handler changes the data using services and repositories, and generates an event, passing the changed data there. Then throws the event to 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 conversions were made.
    7) After processing all the event handlers, 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 calls occur between the components, is shown in the figure:



    Intermodular Interaction Example


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

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

    	 	 	
    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 zero to several listeners in other modules can be subscribed to this event. In our example, the Email module, which generates a confirmation hash, transfers the hash to the message template and sends the generated message to the user. After which, again, it generates a ConfirmationEmailSentEvent ($ user) event, which substitutes the user entity to which the confirmation hash is added.

    	 	 	
       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 a letter and save the confirmation hash to the database.

    At this intermodule interaction should be completed. Yes, you often want to directly pull the code from a neighboring module, but this will lead to the connectedness of the modules and will kill the root in the long run the ability to take each of the modules into its own separate project.

    Instead of a conclusion


    Thus, in fact, a simple structuring of the code can achieve the separation of the project into independent small parts. A project cut into similar parts is much easier to maintain than a one-piece project.

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

    Bonuses for readers of Habr


    Online courses

    We give you one-year access to an English course for independent study of the “Online Course”.
    To access, just follow the link . The activation code is valid until September 1, 2017.

    Individually on Skype.

    Summer Intensive English Courses - leave the application here .
    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 .

    Also popular now: