DEV Community

Cover image for Enforce architecture rules with Deptrac
Rubén Rubio
Rubén Rubio

Posted on

Enforce architecture rules with Deptrac

Introduction

When we use some architecture in our applications, there are dependencies between layers that we must enforce. This happens, for instance, when we apply hexagonal architecture. In this case, for this architecture, we use these layers, from the most inner to the most outer:

  • Domain: the elements which represent concepts of our domain: value objects, aggregates, entities, domain events, domain services... The code in this layer is pure PHP without external dependencies.
  • Application: the use cases of our applications: commands (that modify our system), and queries (that consume data from our system). In this layer, the code is pure PHP too, without external dependencies.
  • Infrastructure: the code that interacts with external services (database connections or APIs), libraries... We also include in this layer the framework we use and the user interface layer, which includes the controllers.

The dependency between these three layers is from the inside to the outside:

  • Domain must only use elements from within itself. It does not depend on any other layer, nor can it use external libraries.
  • Application may use elements from itself and from the Domain layer. It must not use external libraries, either.
  • Infrastructure may use elements from the Application and Domain layers, besides being allowed to use external libraries.

We can see the dependencies between layers in the following diagram:

Dependencies between layers

However, PHP does not offer any mechanism to enforce these dependencies between layers. This means that, even if we want to apply hexagonal architecture, we could violate any dependency and not notice.

Fortunately, PHP's ecosystem is rich, and there are different tools to validate and enforce our architecture:

In this post, we will see how to set Deptrac up to enforce the rules of hexagonal architecture that we described.

Deptrac

Deptrac is a command-line tool that validates dependencies defined in a configuration file. If there is any violation of those rules, it returns an exit code different of 0. Thus, it is a useful tool to add to the continuous integration of our application.

Concepts

Deptrac's main concepts are:

  • Layers: groups of tokens (classes, functions...). An example would be all the classes in our domain layer. Deptrac offers different collectors to select these tokens: by directory, by namespace, by class name, by function name...
  • Rulesets: rules that define the allowed communications between layers. For instance, the application layer can use elements from the domain layer. By default, there is no allowed dependency between layers. All dependencies must be explicit.
  • Violations: dependency errors between not-allowed layers. For example, if our domain layers access the application layer, which is not allowed.

Definition

We can now write our configuration file in YAML format:

deptrac:
    paths:
        - ./src
    layers:
        # Layer definition
        - name: Domain
          collectors:
              - type: directory
                regex: src/Domain

        - name: Application
          collectors:
              - type: directory
                regex: src/Application

        - name: Infrastructure
          collectors:
              - type: directory
                regex: src/Infrastructure

        # Vendor
        - name: DomainVendor
          collectors:
              - type: className
                regex: ^(Brick\\Math|Brick\\Money|Doctrine\\Common\\Collections|Ramsey\\Uuid)\\.*

        - name: Vendor
          collectors:
              - type: className
                regex: ^(Symfony|CuyZ\\Valinor|League\\ConstructFinder|League\\Tactician)\\.*

    ruleset:
        Domain:
            - DomainVendor
        Application:
            - Domain
            - DomainVendor
        Infrastructure:
            - Domain
            - Application
            - DomainVendor
            - Vendor
Enter fullscreen mode Exit fullscreen mode

First, we need to define the folders with the code to analyze in the paths key. In this case, I used a Symfony project, where the application code usually lives under the src folder.

Next, we define the layers within the layers key. We have one layer for each of our layers, with a directory collector.

Theoretically, our domain should only contain pure PHP code without any external dependencies. Nonetheless, there are some libraries that we want to use in our domain, such as ramsey/uuid to generate UUIDs, or brick/math to work safely with numbers. Another usual example of an allowed external library in our domain is doctrine/collections when we use Doctrine as an ORM, because the relationships between entities must be of type Doctrine\Common\Collection.

The domain should only contain pure PHP code, as we stated, but there are some occasions when we have to make concessions: Is it worth it to reimplement a UUID generator only to keep our domain pure? It is just not worth it. Most of the time, we do not need to reinvent the wheel. We can say no to having external code in our domain and, at the same time, say yes to allowing the libraries we need in our domain. It must always be a whitelist, i.e., we need to choose which libraries we allow in our domain and not allow them all by default. We need to make these decisions consciously and critically.

Thus, we define a layer named VendorDomain, where we specify the libraries we allow in our domain. In this case, we use a collector by namespace.

At the same time, in the infrastructure layer, we allow access to third-party libraries. We could be tempted to include the whole vendor in the paths to analyze. However, this would slow the execution down, and it would also analyze the whole vendor and the dependencies within it.

Instead, if we specify the libraries that we allow in our infrastructure layer, we not only simplify and accelerate the analysis, but we also prevent transitive dependencies, i.e., depending on our code on third-party libraries we did not explicitly allow.

Therefore, we define another, more general layer, named Vendor, where we define third-party libraries we allow in our infrastructure layer. In this case, we use a collector by namespace.

We can now define the rulesets between layers:

  • Domain: it can use DomainVendor, as we explained.
  • Application: it can access Domain and its allowed layers, i.e., DomainVendor.
  • Infrastructure it can access Domain, Application and Vendor.

Execution

We can now execute Deptrac:

deptrac analyse --config-file=hexagonal-layers.depfile.yaml --cache-file=.deptrac.hexagonal-layers.cache --report-uncovered --fail-on-uncovered
Enter fullscreen mode Exit fullscreen mode

By default, Deptrac uses the configuration file deptrac.yaml, and the cache file .deptrac.cache. However, we pass both arguments when executing it to avoid conflicts in case we have more Deptrac configurations.

We also specify the --report-uncovered option, so it fails if there is any uncovered dependency, i.e., dependencies that exist and we did not define. Thus, the validation is stricter.

The output is:

 -------------------- ----- 
  Report                    
 -------------------- ----- 
  Violations           0    
  Skipped violations   0    
  Uncovered            0    
  Allowed              222  
  Warnings             0    
  Errors               0    
 -------------------- ----- 
Enter fullscreen mode Exit fullscreen mode

Deptrac offers several formatters that we will not cover in this post.

Simplified configuration

We saw that we had to specify all the dependencies for all layers. For example, for the Application layer, we needed to specify both dependencies Domain and DomainVendor as allowed. It is a bit redundant, as we would like to allow access from the Application to the Domain layer, and every other layer the latter depends on.

Fortunately, Deptrac offers this functionality: it consists of prepending + to the dependencies. Thus, we could rewrite our configuration file as follows:

deptrac:
    paths:
        - ./src
    layers:
        # Layer definition
        - name: Domain
          collectors:
              - type: directory
                regex: src/Domain

        - name: Application
          collectors:
              - type: directory
                regex: src/Application

        - name: Infrastructure
          collectors:
              - type: directory
                regex: src/Infrastructure

        # Vendor
        - name: DomainVendor
          collectors:
              - type: className
                regex: ^(Brick\\Math|Brick\\Money|Doctrine\\Common\\Collections|Ramsey\\Uuid)\\.*

        - name: Vendor
          collectors:
              - type: className
                regex: ^(Symfony|CuyZ\\Valinor|League\\ConstructFinder|League\\Tactician)\\.*

    ruleset:
        Domain:
            - DomainVendor
        Application:
            - +Domain
        Infrastructure:
            - +Application
            - Vendor
Enter fullscreen mode Exit fullscreen mode

Conclusions

With Deptrac set up, everyone who collaborates on our project will follow the architecture we defined. To enforce it, the best way is to integrate Deptrac into our continuous integration pipeline.

The configuration of layers we saw is applicable to the use of Bounded Contexts within the same project to prevent contexts from accessing other contexts, which is not allowed when using this pattern.

Summary

  • We saw a possible definition of layers for applying hexagonal architecture.
  • We explained that PHP does not offer any mechanism to enforce any architecture.
  • We listed utilities to validate the architecture of our application, with Deptrac among them.
  • We briefly explained the concepts that Deptrac uses internally.
  • We saw a possible Deptrac configuration to enforce hexagonal architecture.
  • We executed Deptrac in strict mode to ensure that all our code was analyzed.
  • Finally, we saw a way to simplify the configuration file.

Top comments (0)