SlideShare a Scribd company logo
1 of 46
Download to read offline
Madisoft
Protezione
deldominioe
Validazione:
Come,doveeperché
Madisoft
LuiGI
CARDAMONE
Senior Backend Developer @ Madisoft
@CardamoneLuigi
LuigiCardamone
Madisoft
ILCONTESTO
Madisoft
LAYERED Architectures
Separare il codice applicativo e di
dominio dal codice infrastrutturale
Vantaggi:
● Indipendenza dal framework
● Indipendenza dall’infrastruttura
tecnologica
● Indipendenza dai servizi esterni
● Manutenibilità
● TDD
DB Domain
Application
Infrastructure
File
system
Web
Server
Madisoft
ILPROBLEMA
Madisoft
Madisoft
Browser → Richiesta POST piena di stringhe → Variabili tipizzate e/o Dominio con classi
Request Method: POST
Content-Type:
application/x-www-form-urlencoded
Content:
name=Luigi&year=2021&date=2021-05-13&enabled=1&optional=
[
'name' => "Luigi",
'year' => "2021",
'date' => "2021-05-13",
'enabled' => "1",
'optional' =>
]
int boolean
float string
DateTime
User Book
Order Store
Category
POST ?
Madisoft
Come affrontiamoilproblema?
Infrastructure Application Domain
Console
Command
Web
Controller
Api
Controller
Application
Service
Entity
Tipizzazione e completezza
dei dati (+ UI)
Regole di dominio a livello
generale
Regole di dominio a livello di entità
(invarianti)
Symfony Form, Validator e
Serializer
Eccezioni Eccezioni e
Assertion
DTO
string $name;
string $email;
bool $active;
Madisoft
GLiInvariantiDiCLASSE
Gli invarianti sono una serie di condizioni che sono sempre vere in una determinata classe
● Definire degli invarianti ci aiuta a formalizzare le specifiche di dominio
● Mantenere gli invarianti significa proteggere il dominio e avere entità sempre in uno
stato valido
● Come si mantengono?
○ Costruttore
○ Metodi che modificano lo stato
○ Ogni informazione che arriva da fuori va quindi validata
Madisoft
UNCASOPRATICO
Madisoft
Bookstore
● Una semplice applicazione per gestire internamente gli ordini di acquisto di un
negozio che spedisce libri in formato digitale
● Per ogni ordine bisogna memorizzare i dati del cliente:
○ Nome e Cognome
○ Email
● Si può acquistare un libro alla volta
● Su ogni ordine può essere applicato uno sconto tra 0 e 50%
● Per tutti gli ordini pagati serve memorizzare l’id del pagamento
● È possibile modificare l’email finché l’ordine non è pagato
Madisoft
DOminioeUseCases
● Add Book
● Edit Book
● Create Order
● Edit Order Email
● Add Order Payment
Order
- id: int
- fullName: string
- email: string
- discountPercentage: int
- book: Book
- amountInCents: int
- paid: bool
- paymentId: string
● Show Books
● Show Orders
Book
- title: string
- author: string
- priceInCents: int
0..N 1
Madisoft
class Order
{
private int $id;
private string $fullName;
private string $email;
private int $discountPercentage;
private Book $book;
private int $amountInCents;
private bool $paid;
private ?string $paymentId;
}
DomainEntityOrder.php
Madisoft
UNPassofalso
Madisoft
Iniziamo l’implementazione di “Create Order”
partendo dalla UI …
Usiamo il componente Form di Symfony...
Madisoft
class CreateOrderType extends AbstractType
{
private BookRepository $bookRepository;
public function __construct(BookRepository $bookRepository) {
$this->bookRepository = $bookRepository;
}
public function buildForm(FormBuilderInterface $builder, array $options): void {
$builder
->add('fullName', TextType::class)
->add('email', EmailType::class)
->add('discountPercentage', IntegerType::class)
->add('book', EntityType::class, [
'class' => Book::class,
'choices' => $this->bookRepository->findAll(),
])
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Order::class,
]);
}
}
FormCreateOrderType.php
Madisoft
class WebOrderController extends AbstractController
{
/**
* @Route("/create-order", name="create_order")
*/
public function index(Request $request): Response
{
$form = $this->createForm(CreateOrderType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Il form è valido, processo la richiesta
$this->addFlash('success', 'Form Valido!');
}
return $this->render('create-order.html.twig', [
'form' => $form->createView(),
]);
}
}
ControllerWebOrderController.php
Madisoft
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('fullName', TextType::class, [
'constraints' => [
new NotBlank()
],
])
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(),
new Email()
],
])
->add('discountPercentage', IntegerType::class, [
'constraints' => [
new NotBlank(),
new Range(['min' => 0, 'max' => 50])
],
])
->add('book', EntityType::class, [
'class' => Book::class,
'choices' => $this->bookRepository->findAll(),
])
->add('save', SubmitType::class)
;
}
FormCreateOrderType.php
Madisoft
// …
public function index(Request $request, EntityManagerInterface $em): Response
{
//Costruttore senza parametri e con inizializzazioni di default
$order = new Order();
$form = $this->createForm(CreateOrderType::class, $order);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($order);
$em->flush();
$this->addFlash(
'success',
sprintf('Order #%s created successfully', $order->getId())
);
}
return $this->render('create-order.html.twig', [
'form' => $form->createView(),
]);
}
ControllerWebOrderController.php
Madisoft
Aggiungo i setter
Madisoft
class Order
{
// ...
public function setFullName(?string $fullName): void
{
$this->fullName = $fullName;
}
public function setEmail(?string $email): void
{
$this->email = $email;
}
public function setBook(?Book $book): void
{
$this->book = $book;
}
public function setAmountInCents(int $amountInCents): void
{
$this->amountInCents = $amountInCents;
}
public function setDiscountPercentage(?int $discountPercentage): void
{
$this->discountPercentage = $discountPercentage;
}
}
DomainEntityOrder.php
Manca solo l’importo totale!
Madisoft
// …
public function index(Request $request, EntityManagerInterface $em): Response
{
//Costruttore senza parametri e con inizializzazioni di default
$order = new Order();
$form = $this->createForm(CreateOrderType::class, $order);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$bookPrice = $order->getBook()->getPriceInCents();
$amount = intval($bookPrice * (100 - $order->getDiscountPercentage()) / 100);
$order->setAmountInCents($amount);
$em->persist($order);
$em->flush();
$this->addFlash(
'success',
sprintf('Order #%s created successfully', $order->getId())
);
}
return $this->render('create-order.html.twig', [
'form' => $form->createView(),
]);
}
ControllerWebOrderController.php
!??
Madisoft
SETTERS+CostruttoreVuoto=Problemi
/**
* @Route("/pay_order", name="pay_order")
*/
public function payOrder(Request $request) {
// get $order from repository ...
$order->setPaid(true);
$this->em->flush();
}
/**
* @Route("/import", name="import")
*/
public function import(Request $request)
{
$order = new Order();
$order->setEmail('invalid-email');
$order->setDiscountPercentage(80);
$order->setAmountInCents(-100);
// save order ...
}
● Rompe la pagina che mostra gli Id di
pagamento degli ordini pagati
● Rompe il flusso che invia i PDF per email
● Altera la pagina dei conteggi totali
Setters + Costruttore Vuoto → Dominio non protetto → Dati Corrotti → Problemi
Madisoft
Entità eform
Approccio sbagliato per vari motivi:
● La presenza dei setter e di un costruttore vuoto ci porta ad avere Entità Anemiche
○ Si perde il concetto di invariante
● Anemic Domain Model è un grosso problema!
○ Ci induce a spostare la logica fuori entità (e a replicarla)
○ Rompe l’incapsulamento
● Ci porta ad avere Entità che possono essere create in uno stato invalido
○ Il Form prima riempie l’oggetto e poi valida
○ Gli attributi di classe sono diventati nullabili
○ Qualsiasi flusso che non passa dal form può portare a dati corrotti
○ Nessuna protezione del dominio
Madisoft
Sulla retta via
Madisoft
class Order
{
private int $id;
// Stringa non vuota
private string $fullName;
// Email valida
private string $email;
// Lo sconto deve essere tra 0 e 50
private int $discountPercentage;
// Sempre diverso da null
private Book $book;
// Sempre maggiore di 0
private int $amountInCents;
// true se e solo se PaymentId valorizzato
private bool $paid;
// inizialmente null
private ?string $paymentId;
}
● Una semplice applicazione per gestire
internamente gli ordini di acquisto di un
negozio che spedisce libri in formato
digitale
● Per ogni ordine bisogna memorizzare i dati
del cliente:
○ Nome e Cognome
○ Email
● Si può acquistare un libro alla volta
● Su ogni ordine può essere applicato uno
sconto tra 0 e 50%
● Per tutti gli ordini pagati serve memorizzare
l’id del pagamento
● Deve essere possibile visualizzare
l’importo di tutti gli ordini pagati
● E’ possibile modificare l’email finché
l’ordine non è pagato
Madisoft
class Order
{
// properties ...
public function __construct(string $fullName, string $email, Book $book, int $discountPercentage)
{
$this->fullName = $fullName;
$this->email = $email;
$this->book = $book;
$this->discountPercentage = $discountPercentage;
$this->paid = false;
$this->paymentId = null;
$this->amountInCents = intval($book->getPriceInCents() * (100 - $discountPercentage) / 100);
}
}
DomainEntityOrder.php
Madisoft
public function __construct(string $fullName, string $email, Book $book, int $discountPercentage)
{
if (empty($fullName)) {
throw new InvalidFullNameException();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
if ($discountPercentage > 50 || $discountPercentage < 0) {
throw new InvalidDiscountPercentageException();
}
$this->fullName = $fullName;
$this->email = $email;
$this->book = $book;
$this->discountPercentage = $discountPercentage;
$this->paid = false;
$this->paymentId = null;
$this->amountInCents = intval($book->getPriceInCents() * (100 - $discountPercentage) / 100);
}
DomainEntityOrder.php
Madisoft
public function __construct(string $fullName, string $email, Book $book, int $discountPercentage)
{
Assertion::notEmpty($fullName);
Assertion::email($email);
Assertion::range($discountPercentage, 0, 50);
$this->fullName = $fullName;
$this->email = $email;
$this->book = $book;
$this->discountPercentage = $discountPercentage;
$this->paid = false;
$this->paymentId = null;
$this->amountInCents = intval($book->getPriceInCents() * (100 - $discountPercentage) / 100);
}
DomainEntityOrder.php
Madisoft
public function changeEmail(string $email)
{
Assertion::email($email);
if ($this->paid) {
throw new CannotEditPaidOrderException();
}
$this->email = $email;
}
public function addPayment(string $paymentId)
{
Assertion::notEmpty($paymentId);
if ($this->paid) {
throw new OrderAlreadyPaidException();
}
$this->paymentId = $paymentId;
$this->paid = true;
}
// Nessun altro metodo oltre ai getter
DomainEntityOrder.php
Madisoft
Entità semprevalidE
● Per come è stata progettata, l’entità Order è sempre valida!
○ Protegge i propri invarianti
● Non c’è modo di crearla in un stato invalido
● Anche usando i metodi pubblici, non c’è modo di portarla in uno stato invalido
● Abbiamo aggiunto un importante strato di protezione
Madisoft
Adesso che Order non ha più i setter come
cambia l’uso del Form Symfony?
Madisoft
class CreateOrderDto
{
private ?string $fullName = null;
private ?string $email = null;
private ?int $bookId = null;
private ?int $discountPercentage = null;
// + getters + setters (or public attributes)
}
ApplicationDtoCreateOrderDto.php
Madisoft
/**
* @Route("/create-order", name="create_order")
*/
public function createOrder(OrderService $orderService, Request $request): Response
{
$createOrderDto = new CreateOrderDto();
$form = $this->createForm(CreateOrderType::class, $createOrderDto);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$id = $orderService->create($createOrderDto);
$this->addFlash('success', sprintf('Order #%s created successfully', $id));
}
return $this->render('create-order.html.twig', [
'form' => $form->createView(),
]);
}
ControllerWebOrderController.php
Nel FormType dobbiamo aggiornare data_type inserendo CreateOrderDto
Madisoft
class OrderService
{
private OrderRepositoryInterface $orderRepository;
private BookRepositoryInterface $bookRepository;
// Costruttore con dependency injection
public function create(CreateOrderDto $createOrderDto)
{
// throws BookNotFoundException
$book = $this->bookRepository->findOneById($createOrderDto->getBookId());
$order = new Order(
$createOrderDto->getFullName(),
$createOrderDto->getEmail(),
$book,
$createOrderDto->getDiscountPercentage(),
);
$this->orderRepository->save($order);
return $order->getId();
}
}
ApplicationOrderService.php
Use Case generico invocabile da
qualsiasi punto: altri controller,
console command, altri service, ecc
?string $fullName;
?string $email;
?int $bookId;
?int $discountPercentage;
Madisoft
DTOconcampinullable
● Come possiamo evitarlo?
● Il Form Symfony riempie il DTO e dopo lo
valida
● Cambiamo il flusso!
○ Usiamo il Form solo per validare la
Request
○ Se la Request è valida la
convertiamo nel DTO
● Per la conversione usiamo il Symfony
Serializer
Madisoft
/**
* @Route("/create-order", name="create_order")
*/
public function createOrder(OrderService $orderService, Request $request): Response
{
$form = $this->createForm(CreateOrderType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$createOrderDto = $normalizer->denormalize(
$form->getData(),
CreateOrderDto::class
);
$id = $orderService->create($createOrderDto);
$this->addFlash('success', sprintf('Order #%s created successfully', $id));
}
return $this->render('create-order.html.twig', [
'form' => $form->createView(),
]);
}
ControllerWebOrderController.php
string $fullName;
string $email;
int $bookId;
int $discountPercentage;
Madisoft
Come cambia la validazione in un controller
API che riceve un JSON?
Riusare lo stesso Symfony Form ?
Usare il Param Converter abbinato a FOSRestBundle ?
Oppure:
Usare il Validator e il Serializer di Symfony
/**
* @Route("/api/create-order", name="api_create_order")
* @ParamConverter("createOrderDto", converter="fos_rest.request_body")
*/
public function createOrder(
CreateOrderDto $createOrderDto, ConstraintViolationListInterface $validationErrors
) {
// check $validationErrors and process $createOrderDto
}
Madisoft
class ApiOrderController extends AbstractController
{
/**
* @Route("/api/create-order", name="api_create_order")
*/
public function createOrder(Request $request, ValidatorInterface $validator) {
$constraints = new Collection([
'fullName' => new NotBlank(),
'email' => [new NotBlank(), new Email()],
'discountPercentage' => [new NotBlank(), new Range(['min' => 0, 'max' => 50])],
'bookId' => [new NotBlank(), new Type('int')]
]);
$violations = $validator->validate($request->toArray(), $constraints);
if (count($violations) > 0) {
return new JsonResponse(['validationErrors' => $this->formatErrors($violations)], 400);
}
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$createOrderDto = $normalizer->denormalize(
$request->toArray(),
CreateOrderDto::class
);
$orderId = $this->orderService->create($createOrderDto);
return new JsonResponse(['orderId' => $orderId]);
}
}
ControllerApiOrderController.php
Validazione
Conversione
Madisoft
REQUEST:
POST http://127.0.0.1:8080/api/create-order
Content-Type: application/json
{
"name": "Luigi Cardamone",
"email": "luigi@local.local",
"discountPercentage": 100,
"bookId": "1"
}
RESPONSE:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"validationErrors": [
"[fullName] => This field is missing.",
"[discountPercentage] => This value should be between 0 and 50.",
"[bookId] => This value should be of type int.",
"[name] => This field was not expected."
]
}
Madisoft
ValidazioneREPLICATA eUi
Madisoft
Validazionereplicata
● Nell’esempio precedente, i seguenti due controlli sono replicati:
○ DiscountPercentage tra 0 e 50
○ Formato dell’email valida
● Sono controllati sia nel Dominio che nel Controller: come posso evitarlo?
○ Rimuovo il controllo dal Dominio?
○ Lo rimuovo dal controller?
● Se rimuovo dal controller, tutto il flusso funziona ma al primo errore di validazione ottengo
una pagina di errore: come posso risolvere?
○ Tipizzare le eccezioni (UserReadableException)
○ Intercettare le eccezioni nei controller e mostrarle nel Form o nel JSON
Madisoft
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$id = $orderService->create($createOrderDto);
$this->addFlash('success', sprintf(Order #%s created', $id));
} catch (UserReadableException | AssertionFailedException $e) {
$form->addError(new FormError($e->getMessage()));
}
}
return $this->render('create-order.html.twig', [
'form' => $form->createView(),
]);
ControllerWebOrderController.php
Idem nel caso del Controller API: mappiamo le eccezioni nell’array validationErrors
Madisoft
Quandoreplicare?
● Gli errori UI non sono più paralleli ma diventano sequenziali
● Bisogna replicare i controlli di validazione solo quando c’è un valore aggiunto
per l’esperienza utente. Scegliere in base al contesto:
○ Pagina di signup
○ Api per un Web Service
○ Comando CLI
● La protezione sul dominio ci da flessibilità
● La nostra applicazione non dipende più dalla Validazione UI
○ Può anche essere spostata totalmente nel FE
Madisoft
CONCLUSIONI
● Validazione multi livello
● Astrarre il concetto di richiesta
● Invertire deserializzazione e
validazione
● Protezione del dominio! Sempre!
Madisoft
CREDITS: This presentation template was created by Slidesgo,
including icons by Flaticon, and infographics & images by
Freepik and illustrations by Stories
Grazie!
Domande?
@CardamoneLuigi
labs.madisoft.it

More Related Content

What's hot

A testing strategy for hexagonal applications
A testing strategy for hexagonal applicationsA testing strategy for hexagonal applications
A testing strategy for hexagonal applications
Matthias Noback
 

What's hot (20)

Coding with golang
Coding with golangCoding with golang
Coding with golang
 
Uruchomienie i praca z laravel w wirtualnym kontenerze docker'a
Uruchomienie i praca z laravel w wirtualnym kontenerze docker'aUruchomienie i praca z laravel w wirtualnym kontenerze docker'a
Uruchomienie i praca z laravel w wirtualnym kontenerze docker'a
 
Terraform introduction
Terraform introductionTerraform introduction
Terraform introduction
 
REST Easy with Django-Rest-Framework
REST Easy with Django-Rest-FrameworkREST Easy with Django-Rest-Framework
REST Easy with Django-Rest-Framework
 
Clean architecture - Protecting the Domain
Clean architecture - Protecting the DomainClean architecture - Protecting the Domain
Clean architecture - Protecting the Domain
 
Dot net platform and dotnet core fundamentals
Dot net platform and dotnet core fundamentalsDot net platform and dotnet core fundamentals
Dot net platform and dotnet core fundamentals
 
Getting Started with Infrastructure as Code
Getting Started with Infrastructure as CodeGetting Started with Infrastructure as Code
Getting Started with Infrastructure as Code
 
A testing strategy for hexagonal applications
A testing strategy for hexagonal applicationsA testing strategy for hexagonal applications
A testing strategy for hexagonal applications
 
SOLID Principles and The Clean Architecture
SOLID Principles and The Clean ArchitectureSOLID Principles and The Clean Architecture
SOLID Principles and The Clean Architecture
 
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
How to test infrastructure code: automated testing for Terraform, Kubernetes,...How to test infrastructure code: automated testing for Terraform, Kubernetes,...
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
 
Domain Driven Design Quickly
Domain Driven Design QuicklyDomain Driven Design Quickly
Domain Driven Design Quickly
 
SOLID & Design Patterns
SOLID & Design PatternsSOLID & Design Patterns
SOLID & Design Patterns
 
用 Go 語言實戰 Push Notification 服務
用 Go 語言實戰 Push Notification 服務用 Go 語言實戰 Push Notification 服務
用 Go 語言實戰 Push Notification 服務
 
TypeScript Introduction
TypeScript IntroductionTypeScript Introduction
TypeScript Introduction
 
CQRS recipes or how to cook your architecture
CQRS recipes or how to cook your architectureCQRS recipes or how to cook your architecture
CQRS recipes or how to cook your architecture
 
Desenvolvendo Apps Nativos com Flutter
Desenvolvendo Apps Nativos com FlutterDesenvolvendo Apps Nativos com Flutter
Desenvolvendo Apps Nativos com Flutter
 
Terraform
TerraformTerraform
Terraform
 
Go Programming language, golang
Go Programming language, golangGo Programming language, golang
Go Programming language, golang
 
[cb22] Your Printer is not your Printer ! - Hacking Printers at Pwn2Own by A...
[cb22]  Your Printer is not your Printer ! - Hacking Printers at Pwn2Own by A...[cb22]  Your Printer is not your Printer ! - Hacking Printers at Pwn2Own by A...
[cb22] Your Printer is not your Printer ! - Hacking Printers at Pwn2Own by A...
 
Run Jenkins as Managed Product on ECS - AWS Meetup
Run Jenkins as Managed Product on ECS - AWS MeetupRun Jenkins as Managed Product on ECS - AWS Meetup
Run Jenkins as Managed Product on ECS - AWS Meetup
 

Similar to Protezione del dominio e Validazione: come, dove e perché (sfday 2021)

Similar to Protezione del dominio e Validazione: come, dove e perché (sfday 2021) (9)

Hexagonal architecture ita
Hexagonal architecture itaHexagonal architecture ita
Hexagonal architecture ita
 
Layered Expression Trees feat. CQRS
Layered Expression Trees feat. CQRSLayered Expression Trees feat. CQRS
Layered Expression Trees feat. CQRS
 
Flavio atzeni smau mi 2013
Flavio atzeni smau  mi 2013Flavio atzeni smau  mi 2013
Flavio atzeni smau mi 2013
 
Dojo nuovo look alle vostre applicazioni web Domino
Dojo nuovo look alle vostre applicazioni web DominoDojo nuovo look alle vostre applicazioni web Domino
Dojo nuovo look alle vostre applicazioni web Domino
 
Drupal diventa un CMF e WordPress che fa? Slide WordCamp Milano 2019
Drupal diventa un CMF e WordPress che fa? Slide WordCamp Milano 2019Drupal diventa un CMF e WordPress che fa? Slide WordCamp Milano 2019
Drupal diventa un CMF e WordPress che fa? Slide WordCamp Milano 2019
 
L'Arte del Templating: Typoscript, Fluid e Grid Elements
L'Arte del Templating: Typoscript, Fluid e Grid ElementsL'Arte del Templating: Typoscript, Fluid e Grid Elements
L'Arte del Templating: Typoscript, Fluid e Grid Elements
 
Case study: un approccio modulare in un progetto legacy
Case study: un approccio modulare in un progetto legacyCase study: un approccio modulare in un progetto legacy
Case study: un approccio modulare in un progetto legacy
 
Entity Framework 6 for developers, Code-First!
Entity Framework 6 for developers, Code-First!Entity Framework 6 for developers, Code-First!
Entity Framework 6 for developers, Code-First!
 
MongoDB User Group Padova - Overviews iniziale su MongoDB
MongoDB User Group Padova - Overviews iniziale su MongoDBMongoDB User Group Padova - Overviews iniziale su MongoDB
MongoDB User Group Padova - Overviews iniziale su MongoDB
 

Protezione del dominio e Validazione: come, dove e perché (sfday 2021)

  • 2. Madisoft LuiGI CARDAMONE Senior Backend Developer @ Madisoft @CardamoneLuigi LuigiCardamone
  • 4. Madisoft LAYERED Architectures Separare il codice applicativo e di dominio dal codice infrastrutturale Vantaggi: ● Indipendenza dal framework ● Indipendenza dall’infrastruttura tecnologica ● Indipendenza dai servizi esterni ● Manutenibilità ● TDD DB Domain Application Infrastructure File system Web Server
  • 7. Madisoft Browser → Richiesta POST piena di stringhe → Variabili tipizzate e/o Dominio con classi Request Method: POST Content-Type: application/x-www-form-urlencoded Content: name=Luigi&year=2021&date=2021-05-13&enabled=1&optional= [ 'name' => "Luigi", 'year' => "2021", 'date' => "2021-05-13", 'enabled' => "1", 'optional' => ] int boolean float string DateTime User Book Order Store Category POST ?
  • 8. Madisoft Come affrontiamoilproblema? Infrastructure Application Domain Console Command Web Controller Api Controller Application Service Entity Tipizzazione e completezza dei dati (+ UI) Regole di dominio a livello generale Regole di dominio a livello di entità (invarianti) Symfony Form, Validator e Serializer Eccezioni Eccezioni e Assertion DTO string $name; string $email; bool $active;
  • 9. Madisoft GLiInvariantiDiCLASSE Gli invarianti sono una serie di condizioni che sono sempre vere in una determinata classe ● Definire degli invarianti ci aiuta a formalizzare le specifiche di dominio ● Mantenere gli invarianti significa proteggere il dominio e avere entità sempre in uno stato valido ● Come si mantengono? ○ Costruttore ○ Metodi che modificano lo stato ○ Ogni informazione che arriva da fuori va quindi validata
  • 11. Madisoft Bookstore ● Una semplice applicazione per gestire internamente gli ordini di acquisto di un negozio che spedisce libri in formato digitale ● Per ogni ordine bisogna memorizzare i dati del cliente: ○ Nome e Cognome ○ Email ● Si può acquistare un libro alla volta ● Su ogni ordine può essere applicato uno sconto tra 0 e 50% ● Per tutti gli ordini pagati serve memorizzare l’id del pagamento ● È possibile modificare l’email finché l’ordine non è pagato
  • 12. Madisoft DOminioeUseCases ● Add Book ● Edit Book ● Create Order ● Edit Order Email ● Add Order Payment Order - id: int - fullName: string - email: string - discountPercentage: int - book: Book - amountInCents: int - paid: bool - paymentId: string ● Show Books ● Show Orders Book - title: string - author: string - priceInCents: int 0..N 1
  • 13. Madisoft class Order { private int $id; private string $fullName; private string $email; private int $discountPercentage; private Book $book; private int $amountInCents; private bool $paid; private ?string $paymentId; } DomainEntityOrder.php
  • 15. Madisoft Iniziamo l’implementazione di “Create Order” partendo dalla UI … Usiamo il componente Form di Symfony...
  • 16. Madisoft class CreateOrderType extends AbstractType { private BookRepository $bookRepository; public function __construct(BookRepository $bookRepository) { $this->bookRepository = $bookRepository; } public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('fullName', TextType::class) ->add('email', EmailType::class) ->add('discountPercentage', IntegerType::class) ->add('book', EntityType::class, [ 'class' => Book::class, 'choices' => $this->bookRepository->findAll(), ]) ->add('save', SubmitType::class) ; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Order::class, ]); } } FormCreateOrderType.php
  • 17. Madisoft class WebOrderController extends AbstractController { /** * @Route("/create-order", name="create_order") */ public function index(Request $request): Response { $form = $this->createForm(CreateOrderType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // Il form è valido, processo la richiesta $this->addFlash('success', 'Form Valido!'); } return $this->render('create-order.html.twig', [ 'form' => $form->createView(), ]); } } ControllerWebOrderController.php
  • 18. Madisoft public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('fullName', TextType::class, [ 'constraints' => [ new NotBlank() ], ]) ->add('email', EmailType::class, [ 'constraints' => [ new NotBlank(), new Email() ], ]) ->add('discountPercentage', IntegerType::class, [ 'constraints' => [ new NotBlank(), new Range(['min' => 0, 'max' => 50]) ], ]) ->add('book', EntityType::class, [ 'class' => Book::class, 'choices' => $this->bookRepository->findAll(), ]) ->add('save', SubmitType::class) ; } FormCreateOrderType.php
  • 19. Madisoft // … public function index(Request $request, EntityManagerInterface $em): Response { //Costruttore senza parametri e con inizializzazioni di default $order = new Order(); $form = $this->createForm(CreateOrderType::class, $order); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $em->persist($order); $em->flush(); $this->addFlash( 'success', sprintf('Order #%s created successfully', $order->getId()) ); } return $this->render('create-order.html.twig', [ 'form' => $form->createView(), ]); } ControllerWebOrderController.php
  • 21. Madisoft class Order { // ... public function setFullName(?string $fullName): void { $this->fullName = $fullName; } public function setEmail(?string $email): void { $this->email = $email; } public function setBook(?Book $book): void { $this->book = $book; } public function setAmountInCents(int $amountInCents): void { $this->amountInCents = $amountInCents; } public function setDiscountPercentage(?int $discountPercentage): void { $this->discountPercentage = $discountPercentage; } } DomainEntityOrder.php Manca solo l’importo totale!
  • 22. Madisoft // … public function index(Request $request, EntityManagerInterface $em): Response { //Costruttore senza parametri e con inizializzazioni di default $order = new Order(); $form = $this->createForm(CreateOrderType::class, $order); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $bookPrice = $order->getBook()->getPriceInCents(); $amount = intval($bookPrice * (100 - $order->getDiscountPercentage()) / 100); $order->setAmountInCents($amount); $em->persist($order); $em->flush(); $this->addFlash( 'success', sprintf('Order #%s created successfully', $order->getId()) ); } return $this->render('create-order.html.twig', [ 'form' => $form->createView(), ]); } ControllerWebOrderController.php !??
  • 23. Madisoft SETTERS+CostruttoreVuoto=Problemi /** * @Route("/pay_order", name="pay_order") */ public function payOrder(Request $request) { // get $order from repository ... $order->setPaid(true); $this->em->flush(); } /** * @Route("/import", name="import") */ public function import(Request $request) { $order = new Order(); $order->setEmail('invalid-email'); $order->setDiscountPercentage(80); $order->setAmountInCents(-100); // save order ... } ● Rompe la pagina che mostra gli Id di pagamento degli ordini pagati ● Rompe il flusso che invia i PDF per email ● Altera la pagina dei conteggi totali Setters + Costruttore Vuoto → Dominio non protetto → Dati Corrotti → Problemi
  • 24. Madisoft Entità eform Approccio sbagliato per vari motivi: ● La presenza dei setter e di un costruttore vuoto ci porta ad avere Entità Anemiche ○ Si perde il concetto di invariante ● Anemic Domain Model è un grosso problema! ○ Ci induce a spostare la logica fuori entità (e a replicarla) ○ Rompe l’incapsulamento ● Ci porta ad avere Entità che possono essere create in uno stato invalido ○ Il Form prima riempie l’oggetto e poi valida ○ Gli attributi di classe sono diventati nullabili ○ Qualsiasi flusso che non passa dal form può portare a dati corrotti ○ Nessuna protezione del dominio
  • 26. Madisoft class Order { private int $id; // Stringa non vuota private string $fullName; // Email valida private string $email; // Lo sconto deve essere tra 0 e 50 private int $discountPercentage; // Sempre diverso da null private Book $book; // Sempre maggiore di 0 private int $amountInCents; // true se e solo se PaymentId valorizzato private bool $paid; // inizialmente null private ?string $paymentId; } ● Una semplice applicazione per gestire internamente gli ordini di acquisto di un negozio che spedisce libri in formato digitale ● Per ogni ordine bisogna memorizzare i dati del cliente: ○ Nome e Cognome ○ Email ● Si può acquistare un libro alla volta ● Su ogni ordine può essere applicato uno sconto tra 0 e 50% ● Per tutti gli ordini pagati serve memorizzare l’id del pagamento ● Deve essere possibile visualizzare l’importo di tutti gli ordini pagati ● E’ possibile modificare l’email finché l’ordine non è pagato
  • 27. Madisoft class Order { // properties ... public function __construct(string $fullName, string $email, Book $book, int $discountPercentage) { $this->fullName = $fullName; $this->email = $email; $this->book = $book; $this->discountPercentage = $discountPercentage; $this->paid = false; $this->paymentId = null; $this->amountInCents = intval($book->getPriceInCents() * (100 - $discountPercentage) / 100); } } DomainEntityOrder.php
  • 28. Madisoft public function __construct(string $fullName, string $email, Book $book, int $discountPercentage) { if (empty($fullName)) { throw new InvalidFullNameException(); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidEmailException(); } if ($discountPercentage > 50 || $discountPercentage < 0) { throw new InvalidDiscountPercentageException(); } $this->fullName = $fullName; $this->email = $email; $this->book = $book; $this->discountPercentage = $discountPercentage; $this->paid = false; $this->paymentId = null; $this->amountInCents = intval($book->getPriceInCents() * (100 - $discountPercentage) / 100); } DomainEntityOrder.php
  • 29. Madisoft public function __construct(string $fullName, string $email, Book $book, int $discountPercentage) { Assertion::notEmpty($fullName); Assertion::email($email); Assertion::range($discountPercentage, 0, 50); $this->fullName = $fullName; $this->email = $email; $this->book = $book; $this->discountPercentage = $discountPercentage; $this->paid = false; $this->paymentId = null; $this->amountInCents = intval($book->getPriceInCents() * (100 - $discountPercentage) / 100); } DomainEntityOrder.php
  • 30. Madisoft public function changeEmail(string $email) { Assertion::email($email); if ($this->paid) { throw new CannotEditPaidOrderException(); } $this->email = $email; } public function addPayment(string $paymentId) { Assertion::notEmpty($paymentId); if ($this->paid) { throw new OrderAlreadyPaidException(); } $this->paymentId = $paymentId; $this->paid = true; } // Nessun altro metodo oltre ai getter DomainEntityOrder.php
  • 31. Madisoft Entità semprevalidE ● Per come è stata progettata, l’entità Order è sempre valida! ○ Protegge i propri invarianti ● Non c’è modo di crearla in un stato invalido ● Anche usando i metodi pubblici, non c’è modo di portarla in uno stato invalido ● Abbiamo aggiunto un importante strato di protezione
  • 32. Madisoft Adesso che Order non ha più i setter come cambia l’uso del Form Symfony?
  • 33. Madisoft class CreateOrderDto { private ?string $fullName = null; private ?string $email = null; private ?int $bookId = null; private ?int $discountPercentage = null; // + getters + setters (or public attributes) } ApplicationDtoCreateOrderDto.php
  • 34. Madisoft /** * @Route("/create-order", name="create_order") */ public function createOrder(OrderService $orderService, Request $request): Response { $createOrderDto = new CreateOrderDto(); $form = $this->createForm(CreateOrderType::class, $createOrderDto); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $id = $orderService->create($createOrderDto); $this->addFlash('success', sprintf('Order #%s created successfully', $id)); } return $this->render('create-order.html.twig', [ 'form' => $form->createView(), ]); } ControllerWebOrderController.php Nel FormType dobbiamo aggiornare data_type inserendo CreateOrderDto
  • 35. Madisoft class OrderService { private OrderRepositoryInterface $orderRepository; private BookRepositoryInterface $bookRepository; // Costruttore con dependency injection public function create(CreateOrderDto $createOrderDto) { // throws BookNotFoundException $book = $this->bookRepository->findOneById($createOrderDto->getBookId()); $order = new Order( $createOrderDto->getFullName(), $createOrderDto->getEmail(), $book, $createOrderDto->getDiscountPercentage(), ); $this->orderRepository->save($order); return $order->getId(); } } ApplicationOrderService.php Use Case generico invocabile da qualsiasi punto: altri controller, console command, altri service, ecc ?string $fullName; ?string $email; ?int $bookId; ?int $discountPercentage;
  • 36. Madisoft DTOconcampinullable ● Come possiamo evitarlo? ● Il Form Symfony riempie il DTO e dopo lo valida ● Cambiamo il flusso! ○ Usiamo il Form solo per validare la Request ○ Se la Request è valida la convertiamo nel DTO ● Per la conversione usiamo il Symfony Serializer
  • 37. Madisoft /** * @Route("/create-order", name="create_order") */ public function createOrder(OrderService $orderService, Request $request): Response { $form = $this->createForm(CreateOrderType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); $createOrderDto = $normalizer->denormalize( $form->getData(), CreateOrderDto::class ); $id = $orderService->create($createOrderDto); $this->addFlash('success', sprintf('Order #%s created successfully', $id)); } return $this->render('create-order.html.twig', [ 'form' => $form->createView(), ]); } ControllerWebOrderController.php string $fullName; string $email; int $bookId; int $discountPercentage;
  • 38. Madisoft Come cambia la validazione in un controller API che riceve un JSON? Riusare lo stesso Symfony Form ? Usare il Param Converter abbinato a FOSRestBundle ? Oppure: Usare il Validator e il Serializer di Symfony /** * @Route("/api/create-order", name="api_create_order") * @ParamConverter("createOrderDto", converter="fos_rest.request_body") */ public function createOrder( CreateOrderDto $createOrderDto, ConstraintViolationListInterface $validationErrors ) { // check $validationErrors and process $createOrderDto }
  • 39. Madisoft class ApiOrderController extends AbstractController { /** * @Route("/api/create-order", name="api_create_order") */ public function createOrder(Request $request, ValidatorInterface $validator) { $constraints = new Collection([ 'fullName' => new NotBlank(), 'email' => [new NotBlank(), new Email()], 'discountPercentage' => [new NotBlank(), new Range(['min' => 0, 'max' => 50])], 'bookId' => [new NotBlank(), new Type('int')] ]); $violations = $validator->validate($request->toArray(), $constraints); if (count($violations) > 0) { return new JsonResponse(['validationErrors' => $this->formatErrors($violations)], 400); } $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); $createOrderDto = $normalizer->denormalize( $request->toArray(), CreateOrderDto::class ); $orderId = $this->orderService->create($createOrderDto); return new JsonResponse(['orderId' => $orderId]); } } ControllerApiOrderController.php Validazione Conversione
  • 40. Madisoft REQUEST: POST http://127.0.0.1:8080/api/create-order Content-Type: application/json { "name": "Luigi Cardamone", "email": "luigi@local.local", "discountPercentage": 100, "bookId": "1" } RESPONSE: HTTP/1.1 400 Bad Request Content-Type: application/json { "validationErrors": [ "[fullName] => This field is missing.", "[discountPercentage] => This value should be between 0 and 50.", "[bookId] => This value should be of type int.", "[name] => This field was not expected." ] }
  • 42. Madisoft Validazionereplicata ● Nell’esempio precedente, i seguenti due controlli sono replicati: ○ DiscountPercentage tra 0 e 50 ○ Formato dell’email valida ● Sono controllati sia nel Dominio che nel Controller: come posso evitarlo? ○ Rimuovo il controllo dal Dominio? ○ Lo rimuovo dal controller? ● Se rimuovo dal controller, tutto il flusso funziona ma al primo errore di validazione ottengo una pagina di errore: come posso risolvere? ○ Tipizzare le eccezioni (UserReadableException) ○ Intercettare le eccezioni nei controller e mostrarle nel Form o nel JSON
  • 43. Madisoft $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $id = $orderService->create($createOrderDto); $this->addFlash('success', sprintf(Order #%s created', $id)); } catch (UserReadableException | AssertionFailedException $e) { $form->addError(new FormError($e->getMessage())); } } return $this->render('create-order.html.twig', [ 'form' => $form->createView(), ]); ControllerWebOrderController.php Idem nel caso del Controller API: mappiamo le eccezioni nell’array validationErrors
  • 44. Madisoft Quandoreplicare? ● Gli errori UI non sono più paralleli ma diventano sequenziali ● Bisogna replicare i controlli di validazione solo quando c’è un valore aggiunto per l’esperienza utente. Scegliere in base al contesto: ○ Pagina di signup ○ Api per un Web Service ○ Comando CLI ● La protezione sul dominio ci da flessibilità ● La nostra applicazione non dipende più dalla Validazione UI ○ Può anche essere spostata totalmente nel FE
  • 45. Madisoft CONCLUSIONI ● Validazione multi livello ● Astrarre il concetto di richiesta ● Invertire deserializzazione e validazione ● Protezione del dominio! Sempre!
  • 46. Madisoft CREDITS: This presentation template was created by Slidesgo, including icons by Flaticon, and infographics & images by Freepik and illustrations by Stories Grazie! Domande? @CardamoneLuigi labs.madisoft.it