How to keep a healthy architecture of projects? It’s not easy, especially with a big team with different levels of experiences. Even if we have our system designed and the architecture is really good, others can degrade it over time.
One possibility is code reviews to watch over a project’s code. CR is important but we can’t check everything every time so we need something that helps automating this process.
With help comes Architecture Tests. For PHP we have a couple of solutions like Deptrac, PHP Architecture Tester, Arkitect.
In this article I’m going to focus on Deptrac – how to test architecture of projects and also how to test integrations between each other.
Deptrac does not allow nesting rules to test everything in one set of rules – a model’s architecture, integrations and dependencies. So we have to define separate files with rules for layers, integrations and modules. Let’s go through all of these.
Example
Check already configured Deptrac’s configuration here for more https://github.com/trocho/deptrac-example
Deptrac
By default, Deptrac allow us to do everything so it is important to have defined layers for all files/directories in our project and using following options. But it informs us only about using something from a not defined layer in a defined one but not when we user a defined layer in some not covered by a layer part of project.
--report-uncovered --fail-on-uncovered
Layers
Rules for dependencies between layers in all modules. We define layers and dependencies between each other. Also it does not allow someone to create a new layer. When someone does, we will see it in the Deptrac’s report as an uncovered by tests part of code.
Example: https://github.com/trocho/deptrac-example/blob/master/deptrac.layers.yaml
But it treats directories like, for example, Domain in all modules as a one layer so it doesn’t protect us from dependencies between modules – it means we can use the domain layer from module A in the application layer in module B. It works like that because we have one set of rules for all modules and they aren’t aware of each other.
The part of regex for src: includes layers (Application or Domain, etc.) from each module and treats all of those as one layer. To solve this problem we can create rules for dependencies between modules.
I prefer to have rules for dependencies between modules. It also allows us to be aware of those dependencies between.
Layer’s rules protect us vertically – it doesn’t allow making a connection between the yellow boxes without permission.
Exceptions
What when we would like to add some exceptions to our rules? For example, Doctrine forces us to use their Collections. If it is ok for us we would like to add it as an exception. We can do that in the following example.
Vendor
Vendors have to be defined as a layer too. It allows specifying rules about which layer can use vendors.
Standard PHP Library (SPL)
We don’t want to treat the SPL classes like vendors so there is a separate layer. Each project has coupling to PHP’s functions and it’s ok. For me SPL clases are the same.
PHP Standards Recommendations (PSR)
PSR’s classes aren’t the same thing as other Vendors so I prefer to make a separate layer and rules for them.
Modules and Integrations
Example: https://github.com/trocho/deptrac-example/blob/master/deptrac.modules.yaml
Modules
The module rules allow us to specify which module can integrate to another. For example you only want to integrate Module A with Module B so when someone adds a new dependency between another couple of modules then Deptrac reports an error.
How does it cooperate with the previous rules of Layers? The Layer’s rules don’t know that there are dependencies between Domain Layer from Module A to the Module B’s Application Layer but Module’s rules see it.
Module’s rules protect us horizontally – it doesn’t allow making a connection between yellow boxes without permission.
Integrations
Example: https://github.com/trocho/deptrac-example/blob/master/deptrac.modules.yaml
Layers and rules are defined with _Downstream and _Upstream suffix. More about types of integrations as well as Downstream and Upstream patterns you can read in the article about Context Mapping and about integration styles here in Enterprise Integration Patterns.
Already we have defined that module A can integrate with B but how the integration should look like?
For example, Module A has a facade in the User Interface layer for integrations with for other modules and we want to protect using the facade across all layers in module B. Integration’s rules allow us to specify that Module B can use the facade only in the Infrastructure layer. You can specify another way of integration if you would like it to.
Integration’s rules protect us crossly – it defines which layer from a module can integrate with which layer from another.
Summary
Layers
Dependencies between layers – which module can integrate with which. It protects us vertically.
Modules
Dependencies between modules – which module can integrate with which. It protects us horizontally.
Integrations
How modules integrate together – which a module’s layer can integrate with another module’s layer . It protects us crossly.
Architecture testing helps us to keep architecture clean. Helping developers understand how modules integrate together. Protecting integrations helps us when we want to migrate a module to a microapp when needed – testing shows us all integration between modules and then we can easily change the way the modules integrate each other. Those are just a couple of benefits it brings to us.