Céline Morin
Développeuse Back-end Symfony chez BiiG.
Nouveau projet, nouvelle équipe et nouveau client pour moi. Comme d’habitude j’ai hâte de commencer le projet et de connaître ce client pour qui je n’ai jamais travaillé. Je commence à faire mon enquête pour me faire une vague idée sur ce client qui m’est inconnu.
Et j’en passe…
Vous vous imaginez bien que même avant de commencer je me suis dis, ça va être drôle, je sens qu’on va bien s’amuser !
Problème principal : trouver un outil qui nous permette de transformer les Webservices SOAP et leurs méthodes en langage objet. Ça tombe bien, une petite librairie PHP nous permet d’effectuer une génération de classes à partir des WSDL (langage qui permet de décrire l'emplacement d'un service web ainsi que ses opérations), Génial !! Problème résolu, YES !
Second problème (celui qui nous intéresse aujourd'hui) : comment pouvons-nous travailler sans dépendre de leur « super » Webservices SOAP ?
C'est là que nous avons l'idée de mettre en place une architecture hexagonale, car ça va nous permettre de détacher tout ce qui est métier de l’infrastructure et ainsi on pourra utiliser soit les Webservices SOAP, soit une autre source de données (SQLite pour nous).
Voici l'article que nous avons utilisé comme point d'entrée, dans notre démarche : Pérennisez votre métier avec l'architecture hexagonale
Concrètement nous avons toute notre partie métier, qui possède la logique de l'application, ce qu'elle doit faire exactement. Autour de laquelle nous pouvons connecter une API, une BDD, un service de cache, etc.
Tous les services ne doivent en aucun cas impacter la logique métier, il faut donc faire en sorte que tous soient un maximum indépendants.
Pour ce faire nous allons créer des interfaces permettant aux adaptateurs des différents services de se brancher aux métiers sans l'impacter.
Avec cette architecture nous pourrons ajouter n'importe quel service pour traiter nos données (repository, persister) sans qu'il y n'ait d'impact sur le métier !
Voici le découpage de notre application dans le répertoire /src
:
src
├── AppBundle
│ ├── AppBundle.php
│ ├── Command
│ ├── Component
│ ├── Controller
│ ├── DependencyInjection
│ ├── Dictionary
│ ├── Doctrine
│ ├── Event
│ ├── Exception
│ ├── Form
│ ├── Http
│ ├── Monolog
│ ├── Provider
│ ├── Repository
│ ├── Resources
│ ├── Response
│ ├── Security
│ ├── Sort
│ ├── Transformer
│ ├── Twig
│ ├── Validator
│ └── Webview
├── Domain
│ ├── Model
│ ├── Persister
│ └── Repository
└── Infrastructure
├── Cache
├── Doctrine
├── Sqlite
└── Webservice
/AppBundle :
code symfony
/Provider :
services faisant le lien entre les Repository et les services symfony (ex. token_storage)/Domain :
contient une abstraction du métier
/Model:
représente les modèles de données utilisés sur l’application/Persister:
interface définissant comment les modèles sont sauvegardés/Repository:
interface définissant comment les modèles sont récupérés/Infrastructure :
contient les implémentations des interfaces du répertoire /DomainVoici notre démarche de développement pour chaque nouvelle fonctionnalité :
Persister
et Repository
. Notre politique est de créer uniquement ce dont on a besoin. Si nous avons simplement besoin de récupérer les données, on ne crée qu’un repository et vice-versa.Voici à quoi peut ressembler notre application aujourd'hui :
Repository
(lecture des données)
Persister
(écriture des données)
Nous allons ajouter la possibilité de lire un article de blog.
Article
, avec un titre et un contenu.namespace AppBundle\Domain\Model;
class Article
{
/** @var string */
private $title;
/** @var string */
private $content;
/**
* @param string $title
* @param string $content
*/
public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content = $content;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @param string $title
*/
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* @return string
*/
public function getContent(): string
{
return $this->content;
}
/**
* @param string $content
*/
public function setContent(string $content)
{
$this->content = $content;
}
}
Repository
, car nous souhaitons lire le contenu d'un article.namespace AppBundle\Domain\Repository;
use AppBundle\Domain\Model;
interface Article
{
/**
* @param string $title
*
* @return Model\Article
*/
public function getArticleByTitle(string $title): Model\Article;
}
namespace AppBundle\Infrastructure\Sqlite\Repository;
use Doctrine\DBAL\Connection;
use AppBundle\Domain\Model;
use AppBundle\Domain\Repository;
class Article implements Repository\Article
{
private $connection;
/**
* @param Connection $connection
* @param Hydrator\Article $hydrator
*/
public function __construct(Connection $connection, Hydrator\Article $hydrator)
{
$this->connection = $connection;
}
public function getArticleByTitle(string $title): Model\Article
{
$qb = $this->connection->createQueryBuilder();
$result = $qb
->select('a.*')
->from('article', 'a')
->where('a.title = :title')
->setParameter('title', $title)
->execute()
->fetch()
;
if ($result === false) {
throw new NotFoundException('Cette article n\'existe pas.');
}
return $this->hydrator->hydrate($result);
}
}
Repository
avec les Webservices SOAPnamespace AppBundle\Infrastructure\Webservice\Repository;
use AppBundle\Webservice\Article\ArticleService;
use AppBundle\Domain\Model;
use AppBundle\Domain\Repository;
use AppBundle\Infrastructure\Webservice\RequestBuilder\LireArticle;
class Article implements Repository\Article
{
private $articleService;
private $requestBuilder;
/**
* @param ArticleService $articleService
* @param LireArticle $requestBuilder
*/
public function __construct(
ArticleService $articleService,
LireArticle $requestBuilder
) {
$this->articleService = $articleService;
$this->requestBuilder = $requestBuilder;
}
/**
* {@inheritdoc}
*/
public function getArticleByTitle(string $title): Model\Article
{
$request = $this->requestBuilder->build($title);
$response = $this->articleService->lireArticle($request);
return $response;
}
}
Chaque service utilisé a sa propre manière d'aller chercher l'information, mais le métier, lui doit seulement utiliser la méthode
getArticleByTitle
. Donc, pour n'importe quel service utilisé la façon de récupérer les données pour la partie métier reste la même.
Alors, comment je peux affirmer que cette architecture m'a facilité la vie. Et bien nous nous sommes retrouvés avec des Webservices en panne toute une journée, alors que nous devions travailler sur le projet. Du coup la partie SQLite nous a permis d'avancer sans coupure de développement, pas de pause involontaire dans le projet.
Une partie métier bien maitrisée, on a pu vraiment se concentrer sur les fonctionnalités désirées par le client. Et cette architecture nous a permis de certifier au client que toute la partie métier n'aura pas à être modifiée s'il décide de passer des Webservices SOAP à une API REST par exemple. Et oui, nous n'aurons que les adaptateurs à ajouter, elle est pas belle la vie ?!
Pour les nouvelles personnes arrivant sur le projet il est vrai que ce n'est pas tout simple, mais dès qu'ils ont le projet en main ça roule plutôt pas mal ! Voici pour preuve le petit témoignage d'un de mes collègues qui a dû travailler sur le projet alors que tout était déjà en place :
"C'est très dur de comprendre l'architecture quand on débarque sur le projet. Mais après tu vois et comprends pourquoi c'est fait comme ça, et tu te dis : Ah oui c'est pas mal au final"
Malgré les bons côtés de ce style d'architecture je ne pense pas qu'il soit la solution à tous les problèmes. Comme pour tous projets il faut bien sûr réfléchir à ce qui convient le mieux aux besoins et aux contraintes clients.