toubiz/phparch

Architecture Testing for PHP projects

4.0.1 2021-02-01 11:04 UTC

This package is auto-updated.

Last update: 2024-12-29 06:52:06 UTC


README

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 toubiz/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 given composer.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')

Shorthand for monorepos: addComposerBasedComponent

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');

Examples