Discovering PHPArkitect: testing your PHP code architecture

I recently discovered PHPArkitect, a tool that allows you to define architecture rules for your PHP project. The idea is simple: write tests that verify your code respects the conventions you’ve set. And I was pleasantly surprised by how easy it is to create your own rules.

The problem

On my Symfony projects, I like using invokable controllers. That means controllers with a single __invoke method. It forces you to have smaller controllers, more focused on a single action. The problem is that nothing prevents a developer (myself included) from creating a classic controller with multiple methods out of habit.

PHPArkitect to the rescue

PHPArkitect allows you to define rules like this one:

$rules[] = Rule::allClasses()
    ->that(new ResideInOneOfTheseNamespaces('App\Controller'))
    ->should(new HaveNameMatching('*Controller'))
    ->because('we want uniform naming');

The syntax is fluent and readable. You define a set of classes, filter the ones you’re interested in, then verify they meet certain conditions.

Creating a custom rule

The interesting thing is that PHPArkitect doesn’t provide a rule to check if a class has a specific method. But creating your own rule is really simple. You just need to implement the Expression interface:

class HaveMethod implements Expression
{
    public function __construct(private string $name)
    {
    }

    public function describe(ClassDescription $theClass, string $because): Description
    {
        return new Description("should have a method that matches {$this->name}", $because);
    }

    public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
    {
        if (! $this->hasExpectedMethod($theClass)) {
            $violation = Violation::create(
                $theClass->getFQCN(),
                ViolationMessage::selfExplanatory($this->describe($theClass, $because)),
                $theClass->getFilePath()
            );
            $violations->add($violation);
        }
    }

    private function hasExpectedMethod(ClassDescription $theClass): bool
    {
        try {
            $reflectionClass = new ReflectionClass($theClass->getFQCN());
        } catch (\ReflectionException) {
            return false;
        }

        $methods = array_filter(
            $reflectionClass->getMethods(),
            fn(ReflectionMethod $method): bool => $method->getName() === $this->name
        );

        return [] !== $methods;
    }
}

The implementation uses PHP’s Reflection API to check for the method’s presence. Nothing complicated.

Then you can use this rule like any other:

$rules[] = Rule::allClasses()
    ->that(new ResideInOneOfTheseNamespaces('App\Controller'))
    ->should(new HaveMethod('__invoke'))
    ->because('we want all controllers to be invokables');

Other useful rules

Once I got started, I added other rules to my project. For example, to ensure repositories implement an interface:

$rules[] = Rule::allClasses()
    ->except('App\Repository\*Interface')
    ->that(new ResideInOneOfTheseNamespaces('App\Repository'))
    ->should(new Implement('*RepositoryInterface'))
    ->because('we want the Interface Segregation Principle');

Or that DTOs are final and readonly:

$rules[] = Rule::allClasses()
    ->that(new HaveNameMatching('*Dto'))
    ->should(new IsFinal())
    ->andShould(new IsReadonly())
    ->because('DTO should not be extended or modified');

Conclusion

PHPArkitect integrates easily into a CI pipeline and helps maintain consistent architecture over the long term. What I liked is how simple it is to extend the tool with your own rules. In just a few lines, I was able to automate a check I used to do manually during code review.

If you have architecture conventions on your PHP projects, I recommend checking it out.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *