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.
Leave a Reply