Envie de créer un framework ? Quelques conseils

Illustration de l'article

Je traîne souvent sur /r/php et régulièrement je vois des personnes qui partagent leur framework PHP custom, de nature curieuse je lis le thread et analyse le repository mis en lien et bien souvent les frameworks partagés sont fournis en petit (ou gros) défauts.

Si vous avez comme idée de créer un framework (PHP ou autre, ici on parlera de PHP) ou bien que vous en avez déjà créé un et que vous souhaitez l'améliorer ou le maintenir, je vous invite à lire ces quelques conseils.

PHP Standards Recommendations

Les PSR (PHP Standards Recommendations) sont des conventions adoptées et suivies par la "plupart" des développeurs. Elles permettent de standardiser le code et d'améliorer l'interopérabilité de ceux-ci.

Le système de PSR permet de pouvoir proposer sa propre convention puis les membres de la communauté de l'accepter ou non.

Pour exemple, le suivi de la PSR-2 "Coding Style Guide" vous oblige une certaine manière de coder et d'organiser votre code, notamment ;

  • Utiliser 4 espaces pour l'indentation et pas de tabulation
  • Mettre un espace après la déclaration du namespace
  • Les brackets pour les méthodes d'une classe doivent être sur leur propre ligne (pas en fin de ligne donc)
  • ...

Voici un bout de code (repris du site officiel) conforme à la PSR-2 :

<?php
namespace Vendor\Package;

use FooInterface;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class Foo extends Bar implements FooInterface
{
    public function sampleMethod($a, $b = null)
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }

    final public static function bar()
    {
        // method body
    }
}

Si vous faites un framework open-source et donc à l'attention du grand public, il est fortement recommandé de suivre cette convention car cela rendra votre code plus familier aux autres développeurs mais aussi de vous familiariser avec le code des autres.
Le suivi à la lettre de toutes les PSR n'est pas obligatoire mais si vous le faites, les personnes qui adopteront votre outil ne seront pas dépaysé. (et si vous ne suivez pas la PSR-2, alors restez pour le moins consistent dans votre codebase)

Namespaces

Les namespaces (ou "espace de noms" en français) permettent d'organiser votre code proprement. Plutôt que de donner une définition abstraite, voici du code pour expliquer leur utilité.

<?php
namespace App\Controller;

use Framework\Controller\AbstractController;
use Framework\Http\Response;

final class HomeController extends AbstractController
{
    public function index(): Response
    {
        return new Response('Hello World!');
    }
}

Sur ce code on peut voir différentes choses :

  • La class HomeController à été déclarée dans le namespace App\Controller en haut du fichier
  • On utilise AbstractController du namespace Framework\Controller
  • On utilise Response du namespace Framework\Http

Et si on reproduisait ce code sans namespaces ?

<?php
require('./../../Framework/Controller/AbstractController.php');
require('./../../Framework/Http/Response.php');

final class HomeController extends AbstractController
{
    public function index(): Response
    {
        return new Response('Hello World!');
    }
}

Ce code ci fonctionnerait, n'est-ce pas ?

Maintenant que se passe t-il si dans le fichier AbstractController.php une class HomeController avait déjà été déclarée au préalable ? Le code casse tout simplement.
Très bien on peut préfixé toutes les classes de notre projet avec App ou MonNomDeFramework mais on ne sera jamais entièrement sûr qu'elle n'aie pas au préalable été déclarée...

Maintenant que se passe t-il si dans l'application, l'utilisateur du framework change un fichier de place ? Il devra changer tous les chemins de ses require...

Que se passe t-il si dans AbstractController.php et Response.php une même dépendance est déclarée ? Nous utiliserions require_once() partout et donc perdre en performances ? On englobe toutes nos class par un class_exists() ?

Les namespaces permettent beaucoup de choses, notamment la prévention de collisions entre les noms de class mais aussi de pouvoir organiser ses fichiers par thématiques et d'avoir une structure qui ait du sens.

Enfin les namespaces permettent aussi l'autoloading des class avec la norme PSR-4: Autoloader si votre structure de dossiers suit vos namespaces.

Composer

Composer est un gestionnaire de dépendances, il permet de facilement récupérer, gérer et maintenir les dépendances de vos projets en quelques lignes de commandes.

Il a aussi le sérieux avantage de pouvoir gérer vos namespaces. Que ce soit ceux de votre framework grâce à son autoloader mais aussi les namespaces de vos dépendances (e.g. : doctrine, monolog, swiftmailer, ...)

Autant dire que si vous n'utilisez pas Composer vous allez avoir plusieurs maux de têtes pour gérer l'inclusion de vos fichiers les uns dans les autres ainsi qu'à maintenir votre codebase.

Performances

Bien que PHP7 ait gagné fort en performance, si vous décidez de créer votre propre framework il faudra que celui-ci soit performant aussi pour qu'il ait de l'attrait aux potentiels utilisateurs.

Dans votre code ceci ne devrait pas exister :

for ( $i = 0; $i <= count($myArray); $i++ ) {
    // ...
}

Cet exemple est sûrement le plus connu

Cette boucle est plus lente que si on assignait count($myArray) à une variable puis exécuter la boucle for.
Pourquoi ? Sur chaque itération de la boucle, PHP redemande quelle longueur fait $myArray.
Sur des petits tableaux cela peut sembler ridicule comme différence en performances mais cela est toujours bon à prendre, surtout dans le cas d'un framework, où les utilisateurs pourront potentiellement loop plusieurs millions de fois sur l'une des fonctions de votre framework et donc baisser drastiquement les performances.

La "bonne" façon de faire le code précédent serait donc :

$myArrayLength = count($myArray);
for ( $i = 0; $i <= $myArrayLength; $i++ ) {
    // ...
}

ou encore :

for ( $i = 0, $j = count($myArray); $i <= $j; $i++ ) {
    // ...
}

Les principes SOLID

Les principes SOLID définissent quelques bases pour la programmation orientée objet. Elles permettent de spécifier une ligne directrice dans le développement d'un projet et ainsi le rendre plus fiable et robuste.

Suivre ces principes vous permettra d'éviter notamment ce qu'on appelle le "spaghetti code" en déclarant vos class ayant un but précis. (Single Responsability Principle)

Vos utilisateurs de framework (ou vous même) puisse manipuler vos class facilement sans tout casser (Open/Closed Principle)

De façon plus générale rendre votre code plus maintenable, compréhensible et évolutif.

Vous pourrez aussi voir du coté d'autres principes, tel que KISS, YAGNI et DRY qui eux aussi vous apporteront conseils dans le développement de votre projet.

La documentation

La documentation est un point primordial de tous les projets, sans elle cela devient vite compliqué pour vous de maintenir votre framework, mais surtout pour vos utilisateurs de comprendre votre outil et donc de l'adopter.

<?php
namespace Framework\Controller;

use Framework\Controller\AbstractController;
use Framework\Http\Response;

final class HomeController extends AbstractController;
{
    public function index(): Response
    {
        return new Response('Hello World!', 200, []);
    }
}

Sans documentation comment savoir comment fonctionne la class AbstractController ? Comment savoir quels arguments prend la class Response ?

Pensez à écrire une documentation claire et précise de votre framework pour que les utilisateurs n'aient pas à lire votre code source de fond en comble.
Il est à noter que l'écriture d'une documentation se fait en parallèle du développement et non à la fin.

Tests unitaires

Les tests unitaires sont sûrement la chose qui manque le plus sur tous les framework homemade partagés sur reddit.
Et pourtant c'est bien la chose la plus importante qui devrait être présente dans votre framework...

Les tests unitaire vous accordent beaucoup d'avantages; comme le suivi de la qualité de votre code, sans tests, comment savoir si la nouvelle fonctionnalité que vous avez implémenté n'a pas cassé une partie de votre code que vous ne soupçonniez même pas ?

Les tests unitaire rendent votre développement plus agile, si demain votre communauté souhaite un refactor sur la gestion de la database (ce qui est un point essentiel d'une application), les tests unitaires vont permettront de pouvoir refactoriser votre code plus sereinement et de détecter directement les points de casse de votre code.

Les tests unitaires permettent aussi de mieux comprendre vos problèmes de design; si c'est facile de tester un bout de code ou une partie de votre application cela veut dire que son élaboration est bonne et que sa responsabilité est contrôlée harmonieusement. Si dans le cas contraire, tester votre code devient vite un problème c'est que votre solution n'est peut-être pas adaptée.

Les tests unitaire jouent aussi en accord avec votre documentation, pouvoir lire des tests unitaire clair permet aux autres développeurs de mieux comprendre la logique et le déroulement de votre projet.

Bref, si vous faites un framework, faites des tests !
Vous pouvez commencer par lire la documentation de PHPUnit si vous n'en faisiez pas jusqu'à maintenant.

Utilisez des dépendances

Créer un framework est un challenge, un challenge tellement chronophage que vous pouvez vous permettre d'utiliser des dépendances externes.
Vous voulez créer un framework pour vos applications car Symfony/Laravel/Zend ne vous conviennent pas ? Ok, vous allez donc réécrire tout à la main, mais vous aimiez bien Doctrine, pourquoi réécrire un ORM ? Utilisez les dépendances que vous souhaitez pour vous faciliter la tâche.

Pour garder du challenge rendez votre framework agnostique de sa gestion de la database, du système de routing, etc...
C'est un avantage pour un framework si vous laissez la possibilité à vos utilisateurs le choix de leur outil au sein de votre framework. Certains préféreront Doctrine à Eloquent, d'autres voudront utiliser FastRoute plutôt que AltoRouter, ...

Bref, utilisez des dépendances, laissez vos utilisateurs en utiliser aussi.

Ne taisez pas les erreurs

J'ai vu dans certaines codebase ce genre de code :

$myVariable = @function($param1, $param2, null);

Taire une potentielle erreur peut rendre l'utilisation de votre framework un vrai cauchemard à débuguer, ne le faites juste pas.
Si votre code peut résulter en une erreur selon son utilisation alors gérer ça avec des exceptions.

Gérer les exceptions

Lors du développement sur un framework, il est important de savoir lorsque quelque chose ne va pas, pour cela il est un bon choix de gérer les exceptions que peut renvoyer le code des utilisateurs de votre framework mais aussi les exceptions que votre framework peut lancer.

Cela permet un meilleur workflow de développement pour vous et votre communauté.

<?php
namespace Framework\Http;

use Framework\Http\ResponseInterface;
use Framework\Http\Exception\BuildResponseException;

class Response implements ResponseInterface
{
    private $content;
    private $code;
    private $params;

    public function __construct(string $content, int $code = 200, array $params = null)
    {
        if ( strlen($content) <= 0 ) {
            throw new BuildResponseException('A Response cannot be empty.');
        }

        $this->content = $content;
        $this->code = $code;
        $this->params = $params;
    }

    public function buildResponse()
    {
        // ...
    }
}

L'exemple est très simple, si l'utilisateur essaie de construire une réponse vide cela lancera l'erreur BuildResponseException que vous pouvez rattraper dans l'exécution du framework. Cela renvoi un message clair et conçis pour l'utilisateur qui utilise mal votre class Response.

Gérer la database

Si dans votre framework vous souhaitez créer une couche pour gérer la database, vous pouvez le faire, mais surtout n'utilisez pas les fonctions mysql, celles-ci sont dépréciées, ne sont plus disponible sous PHP7 et ne comportent pas d'interface orientée objet.

Tournez vous plutôt vers PDO ou mysqli (PDO de préférence personnelle).

Documentez vous

Mettez toutes les chances de votre coté et mettez vous à jour sur les languages que vous allez utiliser, connaître les bonnes pratique, les outils déjà existants, ...
Vous voulez créer un framework ? Documentez vous avant, ayez déjà un peu d'expérience sur un ou des autres pour voir comment il(s) fonctionne(nt), vous en inspirer.
Vous voulez créer un outil/CLI ? Regardez ce qui existe déjà et qui ressemble à ce que vous voulez faire, cela permettra de voir ce qui manque aux outils existants et ce que devrait implémenter le vôtre. Lisez les commentaires des communautés sur ce que les outils existants devraient améliorer, etc...

Créer un projet utile est beaucoup plus motivant que si vous faites votre projet dans un coin sans jamais le réutiliser...

Ne recopier pas non plus des frameworks existants, il y a déjà beaucoup trop de CodeIgniter-like sur /r/php. (CodeIgniter étant loin d'être une référence en terme de bonnes pratique)
Inspirez vous de ce qui se fait mais ne copier pas forcément ligne par ligne.

Conclusion

Créer un framework from scratch est un challenge, je vous invite fortement à utiliser un ou plusieurs framework au préalable avant de vous lancer dans la conception du vôtre.

Cela reste néanmoins un excellent exercice personnel pour mieux comprendre le fonctionnement des autres frameworks, le fonctionnement du language dans lequel on l'écrit et de façon générale d'évoluer en tant que développeur.

Si l'aventure vous tente et bien déjà, bonne chance ! C'est un exercice qui demande du temps et de la patience, mais vous en ressortirez accompli :p

Voici quelques liens en plus si vous souhaitez vous lancer dans l'aventure :

Et encore quelques thèmes et ressources dans lesquels vous devriez vous intéresser :

  • PHP Stan est un outil d'analyse de votre code avant même l'exécution de celui-ci, il vous aide à trouver des bugs avant même d'écrire des tests de votre code.
  • Dependency Injection Container Les notions de conteneurs et d'injection de dépendances sont très importantes lors de l'élaboration de framework sophistiqué et maintenable.

Resources:


Laisser un commentaire