Enforce architecture rules with Deptrac
Tags: php, architecture, hexagonalarchitecture, cicd
Published: 2023-10-31 08:10:00

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e985axnmsnt7jzg2xjll.png)

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:

- [Deptrac](https://github.com/qossmic/deptrac)
- [PHP Architecture Tester](https://github.com/carlosas/phpat) (as a PHPStan _plugin_)
- [PHPArkitect](https://github.com/phparkitect/arkitect)

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](https://qossmic.github.io/deptrac/concepts/) are:

- Layers: groups of tokens (classes, functions...). An example would be all the classes in our domain layer. Deptrac offers different [collectors](https://qossmic.github.io/deptrac/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:

```yml
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
```

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`](https://www.doctrine-project.org/projects/collections.html) 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:

```bash
deptrac analyse --config-file=hexagonal-layers.depfile.yaml --cache-file=.deptrac.hexagonal-layers.cache --report-uncovered --fail-on-uncovered
```

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    
 -------------------- ----- 
```

Deptrac offers [several formatters](https://qossmic.github.io/deptrac/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:

```yml
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
```

## 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.