Exemple

L'objectif fixé est de développer une calculatrice quatre opérations, avec pour contraintes :

  • De créer un composant réutilisable ;
  • De déléguer l'ensemble de la logique au serveur (pour illustrer les services asynchrones) ;
  • De pouvoir instancier plusieurs calculatrices sur la même page, toutes indépendantes.

La page de test

Nous nommerons le nouveau composant « calculette ». Il sera défini dans le fichier composants/calculette/calculette.php, et nous en plaçons trois instances de tailles différentes sur une page de test :

<?php
declare(strict_types=1);
require_once( "lib/Elements/start.php" );
require_once( "composants/calculette/calculette.php" );

new CalculetteController("c1");
new CalculetteController("c2");
new CalculetteController("c3");

Service::HandleRequest();

new MainDocument( <<<EOT
    <h1:Calculette controller=c1 />
    <h2:Calculette controller=c2 />
    <h3:Calculette controller=c3 />
EOT);
?>

Pour que chaque composant soit indépendant, nous avons besoin de trois contrôleurs reliées aux trois composants. Ils sont nommé c1, c2 et c3.

Dans la page de test, les trois contrôleurs sont instanciés. Puis le document est créé. Il comporte trois balise H1, H2 et H3, chacune étant géré par le composant Calculette relié à un contrôleur.

La Vue

La vue est une classe héritant de l'élément HTML Element. Elle est stockée dans le fichier composants/calculette/calculette.php

class Calculette extends Element {
    public function alive( CalculetteController $controller ) : void {

        $this->ownerDocument->includeStyle( urlFromFile(__DIR__."/calculette.css") );

        $this->ownerDocument->addScriptOnce( <<<EOT
            On( "click" , "table.calculette td:not([colspan])" , (e)=>{
                let table = e.target.closest("table");
                remoteCall( table.id, e.target.textContent ).then( (r)=> {
                    table.querySelector("td[colspan]").textContent = r+'.';
                });
            });
        EOT);

        $controller->init();

        $name = $controller->name;
        $this->innerHTML = "<table id=$name class=calculette>".
                    "<tr><td colspan=4>0.".
                    "<tr><td>7<td>8<td>9<td>/".
                    "<tr><td>4<td>5<td>6<td>*".
                    "<tr><td>1<td>2<td>3<td>-".
                    "<tr><td>AC<td>0<td>=<td>+";
    }
}

La vue inclue une feuille de style. Pour que le composant puisse être placé n'importe où dans l'arborescence du site, la fonction urlFromFile() est utilisée pour convertir le chemin du fichier css sur le serveur, en url.

Puis elle crée un script qui, sur un clic sur un bouton, enverra au serveur son libellé. En retour, le serveur lui retournera le texte à afficher sur l'écran. Ce script ne sera ajouté qu'une seule fois, et servira à toutes les calculettes instanciées sur la page.

La vue étant exécutée au chargement de la page uniquement, elle initialise la calculatrice gérée par le contrôleur en appelant $controller->init();

Enfin, elle produit son contenu HTML. Il s'agit d'une simple table. On peut noter que la syntaxe HTML y est un peu malmenée, mais HTML5 possède des règles sémantiques complexes de gestion des tags, que le parseur inclus respecte.

La feuille de styles composants/calculette/calculette.css est très simple :
table.calculette, table.calculette td {
    border:1px solid black;
    border-radius: 5px;
    background-color:black;
    margin:50px auto;
}

table.calculette td[colspan] {
    text-align:right;
    padding-right:0.3em;
    background-color:#9f9;
}

table.calculette td:not([colspan]) {
    text-align: center;
    width:3em;
    background-color:#eee;
    cursor:pointer;
}

Le contrôleur

Enfin, nous avons besoin d'un contrôleur. Puisqu'un contrôleur est très souvent spécifique à sa vue, son code est généralement situé dans le même fichier.

Chaque contrôleur aura son propre stockage persistant pour sauvegarder son état, lu et écrit par get/setSession()

Le contrôleur gère la logique de la calculatrice, modélisée à la manière des modèles des années 70 avec une mémoire de quatre cases :

  • L'état en cours ;
  • Le registre A ;
  • Le registre B ;
  • Le dernier opérateur sélectionné.

La méthode init() replace ces valeurs dans une situation de départ, la méthode calc() exécute un calcul, et la méthode finiteStateMachine() assure la logique, et la méthode service() expose le service web. Sur appui de la touche «AC», elle remet à zéro la calculatrice. Dans tous les autres cas, elle appelle la machine à état et renvoie au client sa réponse.

class CalculetteController extends Controller {
    public function init() {
        $this->setSession( [ 0,0,0,'+' ] );
    }

    public static function calc( int $a, int $b, string $op ) {
        return match ($op) {
            "+" => $a+$b,
            "-" => $a-$b,
            "/" => $a/$b,
            "*" => $a*$b,
        };
    }

    public function finiteStateMachine( $touche ) {
        list( $s, $a, $b, $o ) = $this->getSession();
        $v = strpos("+-*/",$touche)!==false ? 1 : ( $touche=='=' ? 2 : 0 );
        switch( $s*4+$v ) {
            case 0: $a=$a*10+$touche; break;
            case 1: case 9: $o=$touche; $b=0; $s=1; break;
            case 4: $b=$b*10+$touche; $r=$b; break;
            case 5: $a=self::calc($a,$b,$o); $o=$touche; $b=0; break;
            case 6: case 10: $a=self::calc($a,$b,$o); $s=2; break;
            case 8: $a=+$touche; $s=1; break;
        }
        $this->setSession( [$s,$a,$b,$o] );
        return (string)($r??$a);
    }

    public function service( mixed $touche, string $name ) {
        if( $touche=="AC" ) {
            $this->init();
            return "0";
        }
        return $this->finiteStateMachine( $touche );
    }
}

Résultat

Au final, la page est rendue ainsi, et on obtient bien trois calculatrices indépendantes et fonctionnelles (testez-les !).

L'indépendance du composant est maximale :

  • Son contrôleur reçoit les requêtes du client, les traite, et renvoie une réponse ;
  • Sa vue dispose les éléments HTML nécessaires, inclue une feuille de style et un script, sans interférer sur le code de la page.

0.
789/
456*
123-
AC0=+

0.
789/
456*
123-
AC0=+

0.
789/
456*
123-
AC0=+