J’ai récemment découvert PHPArkitect, un outil qui permet de définir des règles d’architecture pour son projet PHP. L’idée est simple : écrire des tests qui vérifient que le code respecte les conventions qu’on s’est fixées. Et j’ai été agréablement surpris par la facilité avec laquelle on peut créer ses propres règles.
Le problème de départ
Sur mes projets Symfony, j’aime utiliser des controllers invokables. C’est-à-dire des controllers avec une seule méthode __invoke. Ça force à avoir des controllers plus petits, plus focalisés sur une seule action. Le problème, c’est qu’il n’y a rien qui empêche un développeur (moi y compris) de créer un controller classique avec plusieurs méthodes par habitude.
PHPArkitect à la rescousse
PHPArkitect permet de définir des règles comme celle-ci :
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('we want uniform naming');
La syntaxe est fluide et lisible. On définit un ensemble de classes, on filtre celles qui nous intéressent, puis on vérifie qu’elles respectent certaines conditions.
Créer une règle custom
Le truc intéressant, c’est que PHPArkitect ne propose pas de règle pour vérifier qu’une classe possède une méthode spécifique. Mais créer sa propre règle est vraiment simple. Il suffit d’implémenter l’interface Expression :
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;
}
}
L’implémentation utilise la Reflection API de PHP pour vérifier la présence de la méthode. Rien de compliqué.
Ensuite, on peut utiliser cette règle comme n’importe quelle autre :
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveMethod('__invoke'))
->because('we want all controllers to be invokables');
Autres règles utiles
Une fois lancé, j’ai ajouté d’autres règles à mon projet. Par exemple, pour s’assurer que les repositories implémentent bien une interface :
$rules[] = Rule::allClasses()
->except('App\Repository\*Interface')
->that(new ResideInOneOfTheseNamespaces('App\Repository'))
->should(new Implement('*RepositoryInterface'))
->because('we want the Interface Segregation Principle');
Ou que les DTOs sont bien final et readonly :
$rules[] = Rule::allClasses()
->that(new HaveNameMatching('*Dto'))
->should(new IsFinal())
->andShould(new IsReadonly())
->because('DTO should not be extended or modified');
Conclusion
PHPArkitect s’intègre facilement dans une CI et permet de garder une architecture cohérente sur le long terme. Ce qui m’a plu, c’est la simplicité pour étendre l’outil avec ses propres règles. En quelques lignes, j’ai pu automatiser une vérification que je faisais manuellement en code review.
Si vous avez des conventions d’architecture sur vos projets PHP, je vous conseille d’y jeter un œil.
Laisser un commentaire