SOLID est un acronyme représentant cinq principes de bases de la programmation orientée objet permettant le développement de logiciels fiables, évolutifs et robustes. Le framework Symfony est un excellent support pour illustrer chacun de ces principes. Nous verrons ainsi que SOLID est à l’origine de sa flexibilité, sa fiabilité mais aussi de sa maintenabilité et son évolutivité. Nous verrons également comment appliquer ces principes pour améliorer son code métier et perfectionner l’architecture de son application.
5. Symfony
2. Framework PHP pour projets web
1. Ensemble de composants PHP
MVC Pattern
Model View
Requête HTTP Routage Contrôleur Réponse HTTP
5
6. Symfony
HttpKernel
DependencyInjection
HttpFoundation
Routing
EventDispatcher
Cœur du framework qui gère le cycle de requête-réponse HTTP
Couche orientée objet aux spécifications HTTP
Mapper les requêtes aux variables (contrôleur, etc.)
Médiateur permettant aux composants découplés de
communiquer à l’aide d’événements
Conteneur de service qui instancie des services en se basant sur
la configuration et injecte leurs dépendances
et 45 autres, y compris Security, Form, Console, Cache, Asset, Validator, Serializer, etc.
6
7. Symfony
FrameworkBundle
Configure les composants Symfony et les colle ensemble.
Il enregistre les services et les listeners d'événements.
Les bundles sont des plugins réutilisables qui fournissent des services, des routes, des
contrôleurs, des templates, etc.
Your code
Fournit des routes, des contrôleurs, des services, une logique
métier et des templates adaptés à vos besoins.
doctrine/orm DBAL et ORM pour communiquer avec des bases de données.
twig/twig Moteur de template.
7
9. SOLID
– Responsabilité unique
– Ouvert / fermé
– Substitution de Liskov
– Ségrégation des interfaces
– Inversion des dépendances
Réutilisable
Flexible
Évolutif
Compréhensible
Fragile
Dupliqué
Visqueux
Complexe sans raison
SOLID aide à créer le code qui est … et à éviter le code qui est
S
O
L
I
D
9
11. class ReportService
{
public function createReport($data)
{
// working with report manager at domain layer
}
public function getLastThreeReportsByAuthor($author)
{
// working with database at ORM or DBAL layer
}
public function searchBy($criteria)
{
// working with database at ORM or DBAL layer
}
public function export(Report $report)
{
// working with filesystem at by means of PHP functions
}
}
Une classe ne devrait avoir qu'une seule responsabilité.
La responsabilité est une raison de modifier la classe.
Single responsibility principle
11
12. // working with database at ORM and/or DBAL layer
class ReportRepository
{
public function getLastThreeReportsByAuthor($author)
{ ... }
public function searchBy($criteria)
{ ... }
}
// working with report manager at domain layer (business logic)
class ReportManager
{
public function createReport($data)
{ ... }
}
// working with filesystem and pdf, excel, csv, etc.
class ReportExporter
{
public function export(Report $report)
{ ... }
}
Manager
Repository
Exporter
Single responsibility principle
12
13. Feature : Stocker les utilisateurs dans la base de données
Feature : Stocker les commandes dans la base de données
Feature : Authentifier des utilisateurs (formulaire de connexion, HTTP, peu importe …),
stocker les informations d'identification dans la session et authentifier chaque
requête ultérieure
Feature : Lors de la création de la commande,
affecter automatiquement cette commande à un utilisateur connecté
Single responsibility principle
13
14. SecurityContext
Stocke le token d’utilisateur setToken($token), getToken()
Vérifie l’autorisation isGranted('attr', $sub)
AuthenticationProvider Authentifie le token
UserProvider Récupère ou rafraîchit l'utilisateur
EntityManager Toutes les interactions avec la base de données, ORM
OrderListener Notre listener qui modifie la commande avant qu'il ne soit inséré
L’injection du service SecurityContext dans n’importe quel listener
d’EntityManager crée une dépendance circulaire.
Single responsibility principle
14
15. Single responsibility principle
La solution est diviser SecurityContext en AuthorizationChecker et TokenStorage.
AuthorizationChecker
AuthenticationProvider
UserProvider
EntityManager
OrderListener
TokenStorage
getToken()
setToken($token)
isGranted('attr', $sub)
15
17. Les entités doivent être ouvertes à l’extension, mais fermées à la modification.
Ouvert à l’extension:
• Altérer ou ajouter un comportement
• Étendre une entité avec de nouvelles propriétés
Fermé à la modification:
• Adapter une entité sans modifier son code source
• Doter une entité d’une interface claire et stable
Open/closed principle
17
18. Techniques pour rester en conformité avec le principe ouvert / fermé :
• Abstraction / Héritage / Polymorphisme
• Injection de dépendance
• Patrons de conception. Par exemple, ceux du GoF :
• Abstract Factory, Factory Method, Builder, Prototype,
• Bridge, Decorator, Proxy,
• Command, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor.
Open/closed principle
18
• …
19. SymfonyComponentHttpKernel
App
// public/index.php
use AppKernel;
use SymfonyComponentHttpFoundationRequest;
$kernel = new Kernel($env, $debug);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
HttpKernel
- dispatcher
+ handle($request): Response
- container
- httpKernel
Kernel
+ handle($request)
+ registerBundles()
+ configureRoutes()
Kernel
+ registerBundles()
+ configureRoutes()
Toutes les requêtes HTTP arrivent sur le
front controller.
Open/closed principle
19
20. Open/closed principle
Le traitement « requête-réponse » est ouvert à l’extension grâce aux événements levés à
chaque étape.
Request
Response
resolve
controller
REQUEST
resolve
arguments
call
controller
CONTROLLER CONTROLLER_ARGUMENTS
Response?
RESPONSE FINISH_REQUEST
Response
exists
TERMINATE
VIEW
Exception EXCEPTION
20
22. Les objets peuvent être remplacés par des instances de leurs sous-types
sans altérer l'exactitude du programme.
LSP décrit une héritage comportemental –
sémantique plutôt que purement syntaxique.
Liskov substitution principle
22
23. Rectangle
Carré est un rectangle à quatre côtés égaux.
- width
- height
+ setWidth($width)
+ setHeight($height)
+ getArea()
+ setWidth($width)
+ setHeight($height)
Square
function client(Rectangle $rect)
{
$rect->setHeight(8);
$rect->setWidth(3);
assert($rect->area() == 24);
}
Le code client va échouer si on passe un
objet de type Square
class Square extends Rectangle
{
public function setWidth($width)
{
$this->width = $width;
$this->height = $width;
}
public function setHeight($height)
{
$this->height = $height;
$this->width = $height;
}
}
Liskov substitution principle
23
24. • Préconditions ne peuvent pas être renforcées dans une sous-classe
• Postconditions ne peuvent pas être affaiblies dans une sous-classe
• Invariants doivent être préservée dans un sous-type
LSP suggère des conditions comportementales similaires à celles de
programmation par contrat :
Rectangle Précondition: width > 0
Postcondition: $this->width == $width
Invariant: $this->height reste inchangé
Précondition: width > 0
Postcondition: $this->width == $width
Invariant: $this->height reste inchangé
Invariant: $this->width == $this->height
- width
- height
+ setWidth($width)
+ setHeight($height)
+ getArea()
+ setWidth($width)
+ setHeight($height)
Square
Liskov substitution principle
24
25. Autorisation – vérification des droits d'accès/privilèges de l'utilisateur authentifié.
Dans Symfony, l'autorisation est basée sur :
• Token : les informations d'authentification de l'utilisateur (rôles, …),
• Attributs : droits/privilèges à vérifier (ROLE_ADMIN, EDIT, …),
• Objet optionnel : tout objet pour lequel il faut contrôler les droits.
$authorizationChecker->isGranted('ROLE_ADMIN');
$authorizationChecker->isGranted('EDIT’, $comment);
$authorizationChecker->isGranted(['VIEW', 'MANAGE'], $order);
Liskov substitution principle
25
26. Liskov substitution principle
- voters : VoterInterface[]
+ decide($token, $attributes, $object)
AuthorizationChecker
- tokenStorage
- accessDecisionManager
+ isGranted($attributes, $object)
AccessDecisionManager
+ vote($token, $subject, $attributes)
VoterInterface
isGranted renvoie vrai/faux. Il délègue la
décision à AccessDecisionManager::decide.
decide demande à tous les voters de voter
et prend la décision finale.
vote doit renvoyer une de ces valeurs :
ACCESS_GRANTED, ACCESS_ABSTAIN or ACCESS_DENIED.
26
27. DoctrineORM
Doctrine est une bibliothèque PHP axée sur le stockage données et « object mapping ».
Instance d’Article
id: 17
author: Author(id=3, name=Marie)
createdAt: DateTime(2018-10-01)
text: "Salut PHP Forum! Ajour…"
Instance d’Author
id: 3
name: Marie
articles: [Article(id=17, …)]
article
id author_id created_at text
16 2 2018-05-21 In thi…
17 17 2018-10-01 Hi sec…
…
author
id name
2 Albert
3 Marie
4 Isaac
…
27
Liskov substitution principle
28. Doctrine déclenche une série d'événements au cours de la vie des entités stockées :
prePersist et postPersist, preUpdate et postUpdate, preRemove et postRemove, etc.
use DoctrineORMEventLifecycleEventArgs;
use DoctrineORMMapping as ORM;
class Article
{
private $createdAt;
private $updatedAt;
private $createdBy;
private $updatedBy;
/** @ORMPrePersist() */
public function prePersist(LifecycleEventArgs $event)
{
$this->createdAt = new DateTime();
}
/** @ORMPreUpdate() */
public function preUpdate(LifecycleEventArgs $event)
{
$article = $event->getEntity();
$article->setUpdatedAt(new DateTime());
}
}
Précondition: $event->getEntity()
est une instance d’Article
28
Liskov substitution principle
29. use AppEntityArticle;
use DoctrineORMEventLifecycleEventArgs;
class ArticleLifecycleListener
{
/** @var AppEntity|Author */
private $user;
public function prePersist(LifecycleEventArgs $event)
{
$article = $event->getEntity();
if ($article instanceof Article) {
$article->setCreatedBy($this->user);
}
}
public function preUpdate(LifecycleEventArgs $event)
{
$article = $event->getEntity();
if ($article instanceof Article) {
$article->setUpdatedBy($this->user);
}
}
}
Précondition: $event->getEntity()
est n’importe quelle entité
Les listeners-services ont accès à d'autres services via l'injection de dépendances :
29
Liskov substitution principle
31. De nombreuses interfaces spécifiques au client sont préférables à une grande
interface général.
Interface segregation principle
31
Aucun client ne devrait être obligé de dépendre de méthodes qu'il n'utilise pas.
32. Conteneur de
services
Configuration:
yaml, xml, php
Pour chaque service :
- id
- FQCN
- dépendances
- …
- Instancie l'objet de service à la demande
- Instancie ses dépendances
- Injecte des dépendances en service
- Renvoie et stocke le service
- Renvoie la même instance pour des
demandes consécutives
“Compilation”
Container gère les services qui sont des objets utiles.
Interface segregation principle
32
37. Les modules de haut niveau ne doivent pas dépendre de modules de bas niveau.
Les deux devraient dépendre d’abstractions.
Les abstractions ne doivent pas dépendre des détails.
Les détails devraient dépendre des abstractions.
Dependency inversion principle
37
38. Framework
namespace Monolog;
class Logger
{
public function log($message)
{
echo $message;
}
}
namespace Framework;
use MonologLogger;
class Kernel
{
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function handle()
{
$this->logger->log('...');
// ...
}
}
Monolog
Kernel Logger
Dependency inversion principle
38
39. namespace Framework;
class Kernel
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
}
namespace Monolog;
use FrameworkLoggerInterface;
class Logger implements LoggerInterface
{
public function log($message)
{
echo $message;
}
}
namespace Framework;
interface LoggerInterface
{
public function log($message);
}
Framework Monolog
Kernel LoggerLoggerInterface
Dependency inversion principle
39