PHPArch is no longer maintained
This project is no longer maintained. When I started development, similar tools did not yet exist or, in the case of deptrac, did not cover my use-case. By now, better tools exist and I am happy to point users towards those.
- PHPAt is the spiritual sucessor to PHPArch and even contains some of it's code
- Deptrac takes more of a graph-based approach to the problem
Both serve as good replacements if you have previously used PHPArch.
PHPArch
What is this?
PHPArch is a work in progress architectural testing library for PHP projects. It is inspired by archlint (C#) and archunit (java).
It can be used to help enforce architectural boundaries in an application in order to prevent the architecture from rotting over time by introducing dependencies across previously well defined architectural boundaries.
Installation
You can install PHPArch using composer. If you don't know what composer is then you probably don't need a library for architectural testing.
$ composer require j6s/phparch
Simple Namespace validation
The most simple type of check PHPArch can help you with are simple namespace based checks: Setup rules for which namespace is allowed or forbidden to depend on which other namespace.
public function testSimpleNamespaces()
{
(new PhpArch())
->fromDirectory(__DIR__ . '/../../app')
->validate(new ForbiddenDependency('Lib\\', 'App\\'))
->validate(new MustBeSelfContained('App\\Utility'))
->validate(new MustOnlyDependOn('App\\Mailing', 'PHPMailer\\PHPMailer'))
->assertHasNoErrors();
}
Available Validators
Currently the following validators are available:
ForbiddenDependency
Lets you declare that one namespace is not allowed to depend on another namespace.MustBeSelfContained
Lets you declare that a namespace must be self-contained meaning that it may not have any external dependencies.MustOnlyDependOn
Lets you declare that one namespace must only depend on another namespace.MustOnlyHaveAutoloadableDependencies
checks if all dependencies are autoloadable in the current environment. This can be helpful if two packages should not have any dependencies on each other but they still sneak in because the packages are often used together.AllowInterfaces
is a wrapper for validators that allows dependencies if they are to interfaces.MustOnlyDependOnComposerDependencies
checks if all dependencies in a given namespace are declared in the givencomposer.json
file. This is useful to prevent accidental dependencies if one repository contains multiple packages.ExplicitlyAllowDependency
is a wrapper for validators that allows a specific dependency.
Most architectural boundaries can be described with these rules.
Defining an architecture
PHPArch also contains a fluent API that allows you to define a component based architecture which is then validated. The API is based on components which are identified by one or more namespaces instead of Layers or 'Onion Peels' because it is the simplest way to communicate any architecture - no matter what the implementation details of it are.
public function testArchitecture()
{
$architecture = (new Architecture())
->component('Components')->identifiedByNamespace('J6s\\PhpArch\\Component')
->mustNotDependOn('Validation')->identifiedByNamespace('J6s\\PhpArch\\Validation');
(new PhpArch())
->fromDirectory(__DIR__ . '/../../app')
->validate($architecture)
->assertHasNoErrors();
}
Most of defining an architecture is only syntactic sugar over the namespace validators above. The following methods allow you to add assertions to your component structure:
mustNotDependOn
mustNotDependDirectlyOn
mustNotBeDependedOnBy
mustOnlyDependOn
mustNotDependOnAnyOtherComponent
mustOnlyDependOnComposerDependencies
dissallowInterdependence
isAllowedToDependOn
Syntactic sugar: Bulk definition of components
While the speaking Api for defining an architecture is great it can get convoluted and
hard to read if you have a lot of components. The components
method can be used to define
components using a simple associative array where the key is the component name and the
value is the namespaces that define the component. This way definitions of components and
setting up dependency rules can be split into 2 steps for better readability.
// This
$architecture->components([
'Foo' => 'Vendor\\Foo',
'Bar' => [ 'Vendor\\Bar', 'Vendor\\Deep\\Bar' ]
]);
// Is the same as this
$architecture->component('Foo')
->identifiedByNamespace('Vendor\\Foo')
->component('Bar')
->identifierByNamespace('Vendor\\Bar')
->identifiedByNamespace('Vendor\\Deep\\Bar')
Syntactic sugar: Chaining multiple dependency rules
If a non-existing component is referenced in one of these methods then it will be created.
These methods will also set the referenced component as the currently active one - so when using
->mustNotDependOn('FooBar')
all future operations reference the FooBar
component.
In order to chain multiple dependency rules for a single component there are some convenience methods available:
andMustNotDependOn
andMustNotBeDependedOnBy
// This
(new Architecture)
->component('Foo')
->mustNotDependOn('Bar')
->andMustNotDependOn('Baz')
// Is this same as this:
(new Architecture())
->component('Foo')->mustNotDependOn('Bar')
->component('Foo')->mustNotDependOn('Baz')
addComposerBasedComponent
Shorthand for monorepos: In case one repository contains multiple packages that all have their own composer.json
file it is easy to accidentally use a method or class of something that is not in the composer.json
file of the current package.
To prevent this the Architecture->mustOnlyDependOnComposerDependencies
method and the
MustOnlyDependOnComposerDependencies
validator can be used to check if all used namespaces are
declared in a given composer.json
file:
$architecture = (new Architecture)
->component('vendor/subpackage')
->identifierByNamespace('Vendor\\Subpackage\\')
->mustOnlyDependOnComposerDependencies('packages/subpackage/composer.json');
However, composer.json
already contains information about the package name and namespaces.
Therefore the addComposerBasedComponent
method can be used in order to make
things easier:
$architecture = (new Architecture)
->addComposerBasedComponent('packages/subpackage/composer.json');