Comment j’ai migré mon front historique CakePHP vers un front Angular

Benjamin MARCHAND
7 min readFeb 19, 2020

Il y’a quelques années, mon associé et moi avons développé une application de gestion des stocks en ligne. Cet outil destiné aux PME / ETI permet de gérer les stocks, les commandes, les alertes de niveaux, les dates de péremption, les lots, la traçabilité, etc.

J’avais pris le parti d’utiliser un framework PHP de très bonne facture : CakePHP, en version 3 au moment de l’écriture du code. Ce framework possède de très nombreuses qualités : une configuration simplifiée (mais pas simpliste), un ORM performant, un pattern MVC maitrisé, une gestion des routes très souple et un très bon suivi technique.

J’ai développé l’application comme on développait en 2014 : monolithe, framework PHP, pages statiques générées côté serveur, statefull. La partie dynamique assurée par la “fameuse” librairie jQuery.

Mais avec le succès grandissant de notre application, de nouveaux besoins fonctionnels sont apparus : champs dynamiques, faire du offline, notifications push. Nous avons également développé l’application mobile de réservations et d’inventaires avec le framework Ionic. L’utilisation de ce framework a fait apparaître des choses géniales qui nous manquait côté interface utilisateur PHP : scan de codes-barres, notifications push, mode offline.

La décision a été prise de moderniser notre application pour qu’elle soit plus réactive et plus adaptée aux besoins de nos clients. Mais comment faire pour passer d’une application “old-school” PHP vers une application “moderne” de 2020 ? J’espère que vous tirerez profit de notre retour d’expérience.

Etape 1 — passer l’application en Stateless

L’idée principale des applications “scalable” est la capacité de pouvoir dupliquer les instances pour répondre aux charges CPU/Réseau dues aux sollicitations utilisateurs.

Cette duplication implique que les serveurs ne doivent pas avoir d’”état” ni d’informations stockées sur le serveur, c’est ce qui est appelé “stateless”.

Côté base de donnée, ce n’était pas un problème, sachant que nous étions parti sur une BDD hébergée en externe dés le début du projet.

Par contre, il y’a au moins 2 états présents sur notre serveur unique : les sessions utilisateurs et les images des produits.

Sessions utilisateurs

Pour la partie sessions utilisateurs, CakePHP permet de stocker les sessions utilisateurs dans la base de données ou dans un système de cache. 1er bon point, nous passons au stockage des sessions dans la BDD via la configuration suivante :

Configure::write('Session', [
'defaults' => 'database'
]);

CakePHP fourni la structure de schéma SQL permettant de créer la table “sessions”, ce qui nous facilite la vie.

Images stockées

La partie légèrement plus complexe fut le stockage des images uploadées. Nous avons dû pour cela opérer 2 modifications dans la structure de la table de données : 1 champ indiquant le chemin relatif de l’image, et 1 champ indiquant le nom du fichier. Cette séparation facilite le traitement ultérieur des documents images (génération de thumbnail par exemple).

Côté CakePHP, j’ai du utiliser un “behavior” (classe de comportement CakePHP) qui permet de générer des traitements automatisés lors d’une action sur la table de données. Un “behavior” existe sur ce sujet, il s’appelle “Josegonzalez/Upload.Upload” dans composer.

Ce behavior récupère automatiquement la variable $_FILES du serveur HTTP et converti les métadonnées dans des champs de la table de donnée. Ce behavior va ensuite prendre le fichier et le déposer dans un filesystem via un objet “adapter”.

Pour le configurer plus finement et atteindre notre objectif de “stateless” j’ai couplé ce behavior à un adaptateur pour le stockage AWS S3 : “League\Flysystem\AwsS3v3”.

La configuration du “behavior” est le suivant :

Configuration de Josegonzalez/Upload.Upload

La configuration de l’adaptateur S3 AWS est :

Configuration de l’adaptateur S3 AWS

Avec cette configuration, les images sont automatiquement uploadées vers S3 lors de la création ou modification d’un produit. Le chemin relatif au bucket S3 est stocké dans la table associée. Et le nom du bucket est géré en fonction de l’environnement d’exécution.

Avec ces 2 mécanismes (sessions et images), notre application est désormais entièrement “stateless” !

Etape 2 — Migrer partiellement le front vers Angular

La migration vers Angular fut la seconde étape de notre processus. Cette migration devait être progressive, sans rupture avec les habitudes des utilisateurs et sans interruptions de service majeures.

Une solution transitoire fut trouvée en injectant directement les scripts angular dans les vues templates “.ctp” avec le tag principal d’injection Angular.

En paramétrant correctement les routes Angular, lors du chargement du script, le bon composant Angular était ainsi instancié puis injecté dans le DOM.

J’ai donc débuté un projet Angular vide, puis paramétré les scripts de sorties du transpileur Angular vers le chemin webroot “javascript” de CakePHP :

Chemin de sortie de la compilation Angular

Charge ensuite au .ctp de récupérer les scripts Angular et d’y associer le point d’entrée DOM “app-root” (je l’ai factorisé dans un élément CakePHP) :

<app-root>
<div class="spinner">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
</app-root>
<script src="/javascript/runtime.js" type="module"></script>
<script src="/javascript/polyfills.js" type="module"></script>
<script src="/javascript/styles.js" type="module"></script>
<script src="/javascript/main.js" type="module"></script>

De cette façon, Angular charge tout son système, surveille la route actuelle, et injecte le bon composant. Il est donc nécessaire de bien paramétrer les routes pour que chacune des URLs de CakePHP soient bien prises en compte dans Angular, par exemple :

const routes: Routes = [
{
path: 'produits/index',
component: ProduitsIndexComponent
},
{
path: 'produits/add',
component: EditComponent,
pathMatch: 'full'
},
{
path: 'produits/add/:id',
component: EditComponent
},
{
path: 'produits/view/:id',
component: ViewComponent
},
{
path: 'produits/view',
component: ViewComponent,
pathMatch: 'full'
},
];

L’inconvénient de cette technique est que si on navigue entre les pages, à chaque fois tout les scripts Angular sont chargés. Mais en optimisant au maximum la configuration, en utilisant les caches des assets statiques de CakePHP, le chargement ne prends que quelques millisecondes.

Mais le but est atteint : les pages sont désormais dynamiques, et nous pouvons commencer à jouer avec la réactivité d’Angular. C’est de toute manière une solution transitoire. L’objectif étant de migrer complètement vers Angular.

Ce passage en composants Angular a nécessité de modifier le comportement interne des contrôleurs CakePHP. Il n’est plus question maintenant de rediriger en fonction de la méthode de la requête (GET / POST), de gérer les messages utilisateurs avec le Flash helper, de produire les variables pour la vue, etc. Il faut dorénavant “parler” avec le contrôleur en json uniquement !

Un contrôleur avant la migration “full json”

j’en profite également pour rationaliser les routes afin d’avoir une même route pour l’ajout ou la modification :

Un contrôleur après la migration “full json” — beaucoup plus simple

De surcroît, la gestion des FormData d’Angular vers CakePHP a nécessité de modifier les requêtes reçues via un middleware. Oui, car j’ai appris que l’objet javascript FormData ne gère pas les types boolean et null, mais les converti en chaîne de caractères.

La conversion du FormData codé dans un middleware CakePHP

Etape 3 — Migrer complètement le front vers Angular

Petit à petit, route par route, j’ai développé des composants Angular pour chacun des écrans CakePHP.

Vient ensuite l’objectif ultime : la bascule complète du site en full Angular pour la partie front.

Pour ce faire, j’ai créé un layout “Angular” dans lequel j’ai chargé mes scripts Angular. J’ai ensuite modifié l’index de l’AppController.php pour charger par défaut le layout “angular”

public function index()
{
$this->viewBuilder()
->setLayout('angular');
}

Puis j’indique à mon routeur CakePHP de charger ce layout lorsque la racine est sollicitée :

$routes->connect('/', ['controller' => 'App',
'action' => 'index']);

Pour que la page Angular soit bien rechargée, il est nécessaire de changer la stratégie de chargement des routes côté Angular en HashLocationStrategy dans l’app.module.ts de Angular :

{provide: LocationStrategy, useClass: HashLocationStrategy},

De cette façon, si l’utilisateur recharge la page, il récupérera toujours la bonne page Angular et non la route /:controller/:action de CakePHP.

Reste la question du cas où l’utilisateur viendrait à saisir manuellement une ancienne URL de notre application, sans le #. Ce cas de figure n’est pas (encore) résolu, mais le sera en redirigeant vers une route Angular par CakePHP dans un middleware.

Conclusion

C’est un beau challenge technique : migrer d’une application “mono” vers une application Web 2.0 à partir de PHP.

Nous avons petit à petit transformé notre serveur CakePHP en serveur full REST-JSON sans que l’utilisateur n’ait été trop impacté.

La partie login / authentification n’a pas nécessité de migration : nous utilisons toujours la partie PHP pour afficher le formulaire de login, une fois le cookie reçu en provenance du serveur, le client Angular utilise nativement le cookie de domaine.

Au final, la souplesse de CakePHP nous as permis de réaliser cette migration, et elle ouvre l’opportunité de rajouter dorénavant de nouvelles fonctionnalités “modernes” : nous pourrons ainsi coupler CakePHP à un RabbitMQ, et un serveur NodeJs en Websocket pour les notifications Angular. Nous pourrons même rajouter des fonctionnalités PWA, et passer progressivement de Ionic pour notre application mobile, vers Nativescript.

Si vous désirez plus de renseignements ou d’informations, n’hésitez pas à me laisser un commentaire !

Au plaisir :)

--

--