My structure on Symfony | Grafikart


Quite a few of you asked me about the structure adopted for the development of the site under Symfony. Also, I suggest you share with you the objective of this structure and the reasons behind these choices.

Why not use the basic MVC structure?

By default, frameworks group classes according to their role (controller, entity, repository, events ...). This division is logical for a small project, but very quickly becomes limiting when working on a larger project (we end up with folders containing many files (which do not necessarily concern the same sections of the site).

/ Controller
/ Model
/ Subscriber
/ Repository
  CategoryRepository.php
  PostRepository.php
  SpamRepository.php
  UserRepository.php
  ...

A first solution is to introduce a new folder level.

/ Controller
  / Account
  / Blog
  /Race
  /Forum
/ Model
  / Account
  / Blog
  /Race
  /Forum
/ Subscriber
  / Account
  / Blog
  /Race
  /Forum
/ Repository
  / Account
    UserRepository.php
  / Blog
    PostRepository.php
    CategoryRepository.php
  /Race
    ...
  /Forum
    SpamRepository.php
  ...

This allows the logic to be grouped a little better but makes navigation in the code more difficult because you have to navigate in parallel in several levels of files each time you want to work on a piece of the application.

Another solution is to separate directly by the context rather than by the role of the class:

/ Account
/ Blog
  / Model
  / Subscriber
  / Repository
/Race
  / Model
  / Subscriber
  / Repository
/Forum
  / Model
  / Subscriber
  / Repository

This approach is more in line with my way of working, because in general I work on a specific functionality rather than on a type of class.

Despite everything, this approach cannot be applied to the entire application because some parts can be transverse and this structure must be modulated according to the situation.

The new structure

This structure is inspired by different principles such as the context system, Domain Driven Development or even hexagonal architecture without necessarily respecting them to the letter.

The idea is to separate our application with 4 main folders:

  • Domain, contains the classes that are used to manage the business logic of the application. The domain must operate in isolation and can expose these methods through Services or via the Repository.
  • Infrastructure, defines the infrastructure elements that allow the domain to communicate with the system (file system, sending emails, database ...).
  • Http, contains the classes which allow to interact with the system from HTTP layers.
  • Command, contains the commands that allow you to interact with the system from the CLI.

In this approach, the layer HTTP or Command is only an interface which allows to communicate with our system.

How do things communicate together?

This approach works well in theory, but in reality things are more complex and different systems need to communicate with each other. For this communication we rely on the system ofevent and of subscriber of the framework.

When the domain performs an operation, an event is issued to allow other systems to graft their logic.

Some examples

To better understand this separation, we will take some concrete examples

The research

To begin with, we will see how the search works.

  • Layer HTTP allows you to create a tutorial via a form. The data in this form can be sent to the domain via an object (either directly with the entity, or with a specific DTO style object).
  • The service, in the domain, will take care of recording the data and will emit a specific event (type CreatedCourseEvent).
  • In infrastructure, more specifically Search, a Subscriber will listen to the event and trigger the indexing of the course in its system.

The search system exposes a service that allows you to retrieve the results via the database. The interface must be as simple as possible to be easily implemented (as much as possible, interfaces with too many methods should be avoided because they are more difficult to replace).

SearchInterface
{
    / **
     * @param string () $ types
     * /
    public function search (string $ q, array $ types = (), int $ limit = 50, int $ page = 1): SearchResult;
}

This service is then used by the HTTP layer

        / **
     * @Route ("/ recherche", name = "search")
     * /
    public function search (
        Request $ request,
        SearchInterface $ search,
        PaginatorInterface $ paginator
    ): Response {
        $ q = $ request-> query-> get ('q')?: '';
        $ page = (int) $ request-> get ('page', 1);
        $ results = $ search-> search ($ q, (), 10, $ page);
        $ paginatedResults = new Pagination ($ results-> getTotal (), $ results-> getItems ());

        return $ this-> render ('pages / search.html.twig', (
            'q' => $ q,
            'total' => $ results-> getTotal (),
            'results' => $ paginator-> paginate ($ paginableResults, $ page),
        ));
    }

Creating a message on the forum

This system is more complex than the previous one because it involves more elements but the division remains the same. We start again from the HTTP layer which will communicate with a service in the domain Forum.

public function createMessage (Topic $ topic, User $ user, string $ content) {
    $ message = (new Message ())
        -> setCreatedAt (new  DateTime ())
        -> setUpdatedAt (new  DateTime ())
        -> setTopic ($ topic)
        -> setContent ($ content)
        -> setAuthor ($ user);
    $ topic-> setUpdatedAt (new  DateTime ());
    $ dispatcher-> dispatch (new PreMessageCreatedEvent ($ message));
    $ em-> persist ($ message);
    $ em-> flush ();
    $ dispatcher-> dispatch (new MessageCreatedEvent ($ message));
}

Here we emit 2 events to have more control over the triggering of behaviors. Several systems also subscribe to these 2 events to add the different behaviors.

  • Infrastructure Spam will subscribe to the event PreMessageCreatedEvent and check if the message contains unsuitable words to mark it as spam before its persistence.
  • Infrastructure Mailing will subscribe to the event MessageCreatedEvent and will schedule the sending of the email via an asynchronous system.
  • Infrastructure Notification will subscribe to the event MessageCreatedEvent and create a notification for all participants of the topic and also send an event NotificationCreatedEvent.
  • Infrastructure Mercury respond to the event NotificationCreatedEvent by creating an instant notification that will be pushed to the client via a Server-sent events.

If the message is spam the notification system will remain silent and the event MessageCreatedEvent will be re-sent when the message is validated by the administrator.

The problems of this organization

This approach worked for me and solved the concerns I had with the basic structure, but it is not without its flaws.

The first problem is the discovery of services by the framework. This is because frameworks often tend to use the folder structure to load certain types of classes or to identify their role (for example, in some frameworks, all Subscriber must be in a specific folder to be detected as listeners to. events).

The second problem is posed by the event system which can make the logic more complex to unfold. If we take the example of message creation, it is difficult to identify what really happens when a message is created and we have to go through all the listeners to unfold the logic of our system and in what order.

Finally, if the project increases in complexity, a possible improvement would be to divide the domains according to the scenarios of use.

/ Domain
  / Account
    / DeleteAccount
    / UpdateAccountInformation
    / UpdateNotificationSetting