SlideShare a Scribd company logo
Rich Domain Model with 
Symfony2 and Doctrine2 
Leonardo Proietti 
@_leopro_ 
Symfony Day Italy 
Milan, 10 October 2014
twitter.com/_leopro_ 
github.com/leopro 
linkedin.com/in/leonardoproietti
1. Domain Modeling
domain = problem space 
domain model = solution space
Domain 
“A Domain [...] is what an organization 
does and the world it does in.” 
(Vaughn Vernon, “Implementing Domain-Driven Design”)
Domain Model 
“A model is a simplification. It is an 
interpretation of reality that abstracts the 
aspects relevant to solving problem at hand 
and ignores extraneous detail.” 
(Eric Evans, "Domain Driven Design")
A model
Domain Model 
“In DDD, domain model refers to a 
class.” 
(Julie Lerman, http://msdn.microsoft.com/en-us/magazine/dn385704.aspx)
Domain Model 
“An object model of the domain that 
incorporates both behavior and data.” 
(Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
Domain Model 
“An object model of the domain that 
incorporates both behavior and data.” 
(Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
Why Rich?
First, because it’s not anemic 
Martin Fowler 
http://martinfowler.com/bliki/AnemicDomainModel.html
<?php 
class Anemic 
{ 
private $shouldNotChangeAfterCreation; 
private $shouldBeChangedOnlyOnEdit; 
private $couldBeChangedAnytime; 
public function setCouldBeChangedAnytime($couldBeChangedAnytime) {} 
public function setShouldBeChangedOnlyOnEdit($shouldBeChangedOnlyOnEdit) {} 
public function setShouldNotChangeAfterCreation($shouldNotChangeAfterCreation {} 
}
<?php 
class SomeService 
{ 
public function doStuffOnCreation() 
{ 
$anemic = new Anemic(); 
$anemic->setCouldBeChangedAnytime('abc'); 
$anemic->setShouldNotChangeAfterCreation('def'); 
$unitOfWork->persist($anemic); 
$unitOfWork->flush(); 
} 
}
<?php 
class SomeService 
{ 
public function doStuffOnEdit() 
{ 
$anemic = new Anemic(); 
$anemic->setCouldBeChangedAnytime('abc'); 
$anemic->setShouldBeChangedOnlyOnEdit(‘123’); 
$unitOfWork->persist($anemic); 
$unitOfWork->flush(); 
} 
}
<?php 
class SomeService 
{ 
public function doStuffOnEdit() 
{ 
Loss of memory 
$anemic = new Anemic(); 
$anemic->setCouldBeChangedAnytime('abc'); 
$anemic->setShouldBeChangedOnlyOnEdit(‘123’); 
$unitOfWork->persist($anemic); 
$unitOfWork->flush(); 
} 
}
<?php 
class SomeService 
{ 
public function doStuffOnEdit() 
{ 
$anemic = new Anemic(); 
$anemic->setCouldBeChangedAnytime('abc'); 
$anemic->setShouldBeChangedOnlyOnEdit(‘123’); 
$anemic->setShouldNotChangeAfterCreation('def'); 
$unitOfWork->persist($anemic); 
$unitOfWork->flush(); 
} 
}
<?php 
class BehaviouralClass 
{ 
private $shouldNotChangeAfterCreation; 
private $shouldBeChangedOnlyOnEdit; 
private $couldBeChangedAnytime; 
public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) 
{ 
$this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; 
$this->couldBeChangedAnytime = $couldBeChangedAnytime; 
} 
public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) 
{ 
$this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; 
$this->couldBeChangedAnytime = $couldBeChangedAnytime; 
} 
}
<?php 
class BehaviouralClass 
{ 
private $shouldNotChangeAfterCreation; 
private $shouldBeChangedOnlyOnEdit; 
private $couldBeChangedAnytime; 
It's not still rich, lacks of ... 
public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) 
{ 
$this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; 
$this->couldBeChangedAnytime = $couldBeChangedAnytime; 
} 
public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) 
{ 
$this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; 
$this->couldBeChangedAnytime = $couldBeChangedAnytime; 
} 
}
Ubiquitous Language 
“the domain model can provide the backbone 
for that common language [...]. The vocabulary 
of that ubiquitius language includes the names 
of classes and prominent operations” 
(Eric Evans, "Domain Driven Design")
What does “coffee” mean? 
Alberto Brandolini AKA @ziobrando
A bit of strategy
Our domain is an online game that simulate the soccer’s world.
Bounded Contexts
What does “player” mean 
in our domain?
The meaning of “player” 
Within game engine context 
a model of a real soccer player, 
modelled with behaviours to fit the 
requirements of the game engine.
The meaning of “player” 
Within data import context 
a model of a real soccer player, but 
modelled for a simple CRUD.
The meaning of “player” 
Within user profile context 
a model of the user of the website, 
who plays the game.
League Team 
Calendar Player 
Lineup 
Coach 
Core context
League Team 
Calendar Player 
Lineup 
Coach 
(the player in “user 
context”) 
Core context
Player 
Game context User context 
Uuid 
Name 
Roles 
Uuid 
Email 
Password
Data and Behaviours
<?php 
class League 
{ 
private $id; 
private $name; 
private $teams; 
public function __construct(Uuid $uuid, $name) 
{ 
$this->id = $uuid; 
$this->name = $name; 
$this->teams = new ArrayCollection(); 
} 
public function registerTeam(Team $team) 
{ 
$this->teams->add($team); 
} 
}
<?php 
class League 
{ 
private $id; 
private $name; 
private $teams; 
A team must do a registration 
public function __construct({ 
to the league 
Uuid $uuid, $name) 
$this->id = $uuid; 
$this->name = $name; 
$this->teams = new ArrayCollection(); 
} 
public function registerTeam(Team $team) 
{ 
$this->teams->add($team); 
} 
}
<?php 
class League 
{ 
private $id; 
private $genericInfo; 
private $teams; 
public function __construct(Uuid $uuid, 
LeagueGenericInfo $leagueGenericInfo) 
{ 
$this->id = $uuid; 
$this->genericInfo = $leagueGenericInfo; 
$this->teams = new ArrayCollection(); 
} 
// ...}
<?php 
class LeagueGenericInfo 
{ 
private $name; 
private $description; 
private $country; 
public function __construct($country, $description, $name) 
{ 
$this->country = $country; 
$this->description = $description; 
$this->name = $name; 
} 
// … getters and behaviours}
<?php 
class LeagueGenericInfo 
{ 
private $name; 
private $description; 
private $country; 
Value object 
public function __construct($country, $description, $name) 
{ 
$this->country = $country; 
$this->description = $description; 
$this->name = $name; 
} 
// … getters and behaviours}
Validation: invariants and input
<?php 
class League 
{ 
// ... 
public function registerTeam(Team $team) 
{ 
if (!$this->canLeagueAcceptAnotherRegistration()) { 
throw new DomainException('Not more places available'); 
} 
$this->teams->add($team); 
} 
private function canLeagueAcceptAnotherRegistration() 
{ 
if ($this->teams->count() == 8) { 
return false; 
} 
return true; 
} 
}
<?php 
class League 
{ 
// ... 
public function registerTeam(Team $team) 
{ 
if (!$this->canLeagueAcceptAnotherRegistration()) { 
League protects its invariants 
throw new DomainException('Not more places available'); 
} 
$this->teams->add($team); 
} 
private function canLeagueAcceptAnotherRegistration() 
{ 
if ($this->teams->count() == 8) { 
return false; 
} 
return true; 
} 
}
<?php 
class League 
{ 
// ... 
public function getTeams() 
{ 
return $this->teams; 
} 
}
<?php 
class League 
{ 
// ... 
private function getTeams() 
{ 
return $this->teams; 
} 
}
<?php 
class LeagueGenericInfo 
{ 
private $name; 
private $description; 
private $country; 
private static $countries; 
public function __construct($country, $description, $name) 
{ 
if(!isset(static::$countries)) { 
static::$countries = require __DIR__.'/countries.php'; 
} 
if (!array_key_exists($name, static::$countries)) { 
throw new UnknownCountryException($country); 
} 
$this->country = $country; 
// .. thanks to Mathias Verraes for “Money” ;-) 
}}
<?php 
class LeagueGenericInfo 
{ 
private $name; 
private $description; 
private $country; 
Input validation 
private static $countries; 
public function __construct($country, $description, $name) 
{ 
if(!isset(static::$countries)) { 
static::$countries = require __DIR__.'/countries.php'; 
} 
if (!array_key_exists($name, static::$countries)) { 
throw new UnknownCountryException($country); 
} 
$this->country = $country; 
// .. thanks to Mathias Verraes for “Money” ;-) 
}}
<?php 
class LeagueGenericInfo 
{ 
private $name; 
private $description; 
private $country; 
Could be private static also $countries; 
placed in commands 
public function __construct($country, $description, $name) 
{ 
if(!isset(static::$countries)) { 
static::$countries = require __DIR__.'/countries.php'; 
} 
if (!array_key_exists($name, static::$countries)) { 
throw new UnknownCountryException($country); 
} 
$this->country = $country; 
// .. thanks to Mathias Verraes for “Money” ;-) 
}}
<?php 
class League 
{ 
public function render() 
{ 
$properties = [ 
'id' => $this->id, 
'name' => $this->genericInfo->getName(), 
'description' => $this->genericInfo->getDescription(), 
'country' => $this->genericInfo->getCountry(), 
]; 
$teams = new ArrayCollection(); 
foreach ($this->teams as $team) { 
$teams->add($team->render()); 
} 
$properties['teams'] = $teams; 
return new ArrayCollection($properties); 
} 
}
<?php 
class League 
{ 
public function render() 
{ 
$properties = [ 
'id' => $this->id, 
'name' => $this->genericInfo->getName(), 
'description' => $this->genericInfo->getDescription(), 
'country' => $this->genericInfo->getCountry(), 
]; 
Return a read-only object 
$teams = new ArrayCollection(); 
foreach ($this->teams as $team) { 
$teams->add($team->render()); 
} 
$properties['teams'] = $teams; 
return new ArrayCollection($properties); 
} 
}
<?php 
class Team 
{ 
private $id; 
private $players; 
public function __construct(Uuid $uuid) 
{ 
$this->id = $uuid; 
$this->players = new ArrayCollection(); 
} 
public function firePlayer($id) 
{ 
foreach ($this->players as $key => $player) { 
if ($player->getId() == $id) { 
$this->players->remove($key); 
} 
} 
} 
}
<?php 
class Team 
{ 
private $id; 
private $players; 
Traverse the collections 
public function __construct(Uuid $uuid) 
{ 
$this->id = $uuid; 
$this->players = new ArrayCollection(); 
} 
public function firePlayer(Player $playerToFire) 
{ 
foreach ($this->players as $key => $player) { 
if ($player->getId() == $playerToFire->getId()) { 
$this->players->remove($key); 
} 
} 
} 
}
The Player should have a 
relation towards the Team?
Be iterative using TDD/BDD
<?php 
class LeagueTest extends PHPUnit_Framework_TestCase 
{ 
/** 
* @test 
* @expectedException 
*/ 
public function leagueMustHaveMaximumEightTeams() 
{ 
// … 
$genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); 
$league = new League($uuid, $genericInfo); 
$team = $this->getMockBuilder('Team') // … 
for ($x=0; $x<=8; $x++) { 
$league->registerTeam($team); 
} 
} 
}
<?php 
class LeagueTest extends PHPUnit_Framework_TestCase 
{ 
/** 
* @test 
* @expectedException 
*/ 
public function leagueMustHaveMaximumEightTeams() 
{ 
// … 
$genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); 
$league = new League($uuid, $genericInfo); 
$team = $this->getMockBuilder('Team') // … 
for ($x=0; $x<=8; $x++) { 
$league->registerTeam($team); 
} 
} 
}
<?php 
class LeagueTest extends PHPUnit_Framework_TestCase 
{ 
/** 
* @test 
* @expectedException 
*/ 
public function leagueMustHaveMaximumEightTeams() 
{ 
// … 
The same Team 
can do more than one registration 
to the League?!? 
$genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); 
$league = new League($uuid, $genericInfo); 
$team = $this->getMockBuilder('Team') // … 
for ($x=0; $x<=8; $x++) { 
$league->registerTeam($team); 
} 
} 
}
<?php 
class League 
{ 
//.. 
public function registerTeam(Team $team) 
{ 
$this->canLeagueAcceptRegistrationOf($team); 
$this->teams->add($team); 
} 
private function canLeagueAcceptRegistrationOf(Team $applicantTeam) 
{ 
if (!$this->canLeagueAcceptAnotherRegistration()) { 
throw new DomainException('Not more places available'); 
} 
foreach ($this->teams as $key => $team) { 
if ($team->getId() == $applicantTeam->getId()) { 
throw new DomainException('Team already registered'); 
} 
} 
} 
}
<?php 
class League 
{ 
//.. 
public function registerTeam(Team $team) 
{ 
$this->canLeagueAcceptRegistrationOf($team); 
$this->teams->add($team); 
} 
And so on ... 
private function canLeagueAcceptRegistrationOf(Team $applicantTeam) 
{ 
if (!$this->canLeagueAcceptAnotherRegistration()) { 
throw new DomainException('Not more places available'); 
} 
foreach ($this->teams as $key => $team) { 
if ($team->getId() == $applicantTeam->getId()) { 
throw new DomainException('Team already registered'); 
} 
} 
} 
}
2. Doctrine (v2.5)
Awareness
We are using the entities of 
the Persistence Model as 
entities of our Domain 
Awareness 
Model
League.orm.yml 
League: 
type: entity 
table: league 
embedded: 
id: 
class: ValueObjectUuid 
genericInfo: 
class: ValueObjectLeagueGenericInfo 
oneToMany: 
contratti: 
targetEntity: Team 
mappedBy: league 
fetch: EXTRA_LAZY
League.orm.yml 
League: 
type: entity 
table: league 
embedded: 
id: 
class: ValueObjectUuid 
genericInfo: 
class: ValueObjectLeagueGenericInfo 
oneToMany: 
contratti: 
targetEntity: Team 
mappedBy: league 
fetch: EXTRA_LAZY
League.orm.yml 
League: 
type: entity 
table: league 
embedded: 
id: 
class: ValueObjectUuid 
genericInfo: 
class: ValueObjectLeagueGenericInfo 
oneToMany: 
contratti: 
targetEntity: Team 
mappedBy: league 
fetch: EXTRA_LAZY 
Uuid.orm.yml 
Uuid: 
type: embeddable 
id: 
uuid: 
type: string 
length: 36
League.orm.yml 
League: 
type: entity 
table: league 
embedded: 
id: 
class: ValueObjectUuid 
genericInfo: 
class: ValueObjectLeagueGenericInfo 
oneToMany: 
contratti: 
targetEntity: Team 
mappedBy: league 
fetch: EXTRA_LAZY 
Uuid.orm.yml 
Uuid: 
type: embeddable 
id: 
uuid: 
type: string 
length: 36
<?php 
class TeamRepository implements TeamRepositoryInterface 
{ 
private $em; 
public function __construct(EntityManager $em) 
{ 
$this->em = $em; 
} 
}
Persisting entities 
<?php 
class TeamRepository implements TeamRepositoryInterface 
{ 
public function add(Team $team) 
{ 
$this->em->persist($team); 
$this->em->flush(); 
} 
}
Avoid collection hydration 
(foreach, toArray) 
<?php 
class TeamRepository implements TeamRepositoryInterface 
{ 
public function getWithoutPlayers($id) 
{ 
$qb = $this->em->createQueryBuilder(); 
$qb 
->select('t', 'p') 
->from("Team", 't') 
->leftJoin('t.players', 'p', Join::WITH, $qb->expr()->andX( 
$qb->expr()->eq('p.id.uuid', ':pid') 
)) 
->where('c.id.uuid = :id') 
->setMaxResults(1); 
$qb->setParameter('id', $id); 
$qb->setParameter('pid', null); 
return $qb->getQuery()->getOneOrNullResult(); 
} 
}
Retrieve an object joined with empty collection 
<?php 
class TeamRepository implements TeamRepositoryInterface 
{ 
public function getWithPlayers($id) 
{ 
$qb = $this->em->createQueryBuilder(); 
$qb 
->select('t', 'p') 
->from("Team", 't') 
->leftJoin(t.players', 'p', Join::WITH, $qb->expr()->andX( 
$qb->expr()->eq('p.status', ':status') 
)) 
->where('t.id.uuid = :id'); 
$qb->setParameter('status', 'on_the_market'); 
$qb->setParameter('id', $id); 
return $qb->getQuery()->getOneOrNullResult(); 
} 
}
Get paginated list of Teams with Player joined 
<?php 
use DoctrineORMToolsPaginationPaginator; 
class TeamRepository implements TeamRepositoryInterface 
{ 
public function paginate($first, $max) 
{ 
$qb = $this->em->createQueryBuilder(); 
$qb 
->select('t', 'p') 
->from("Team", 't') 
->leftJoin('t.players', 'p') 
->setFirstResult($first) 
->setMaxResults($max); 
$paginator = new Paginator($qb->getQuery()); 
return $paginator->getIterator(); 
} 
}
3. Symfony (v2.5)
<?php 
class FirePlayerCommand implements Command 
{ 
public $teamId; 
public $playerId; 
public function getRequest() 
{ 
return new Request( 
[ 
'teamId' => $this->teamId, 
'playerId' => $this->playerId 
] 
); 
} 
}
<?php 
class Request extends ArrayCollection implements RequestInterface 
{ 
public function __construct(array $values) 
{ 
parent::__construct($values); 
} 
public function get($key, $default = null) 
{ 
if (!parent::containsKey($key)) { 
throw new DomainException(); 
} 
$value = parent::get($key); 
if (!$value && $default) { 
return $default; 
} 
return $value; 
} 
}
<?php 
class CommandHandler 
{ 
private $dispatcher; 
private $useCases; 
public function __construct(EventDispatcherInterface $dispatcher) 
{ 
$this->dispatcher = $dispatcher; 
} 
public function registerUseCases($useCases) 
{ 
foreach ($useCases as $useCase) { 
if ($useCase instanceof UseCase) { 
$this->useCases[$useCase->getManagedCommand()] = $useCase; 
} else { 
throw new LogicException(''); 
} 
} 
} 
// ...}
<?php 
class CommandHandler 
{ 
// ... 
public function execute(Command $command) 
{ 
try { 
$this->dispatcher 
->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); 
$this->useCases[get_class($command)]->run($command); 
$response = new Response(); 
$this->dispatcher 
->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); 
return $response; 
} catch (DomainException $e) { 
$this->dispatcher 
->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); 
return new Response($e->getMessage(), Response::STATUS_KO); 
} 
} 
}
<?php 
class CommandHandler 
{ 
// ... 
public function execute(Command $command) 
{ 
try { 
$this->dispatcher 
->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); 
$this->useCases[get_class($command)]->run($command); 
$response = new Response(); 
$this->dispatcher 
->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); 
return $response; 
} catch (DomainException $e) { 
$this->dispatcher 
->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); 
return new Response($e->getMessage(), Response::STATUS_KO); 
} 
} 
}
<?php 
class FirePlayerUseCase implements UseCase 
{ 
private $repository; 
public function __construct(TeamRepositoryInterface $repository) 
{ 
$this->repository = $repository; 
} 
public function run(Command $command) 
{ 
$request = $command->getRequest(); 
$team = $this->repository->get( 
$request->get('teamId') 
); 
$team->firePlayer( 
$request->get('playerId') 
); 
$this->repository->add($team); 
} 
}
Commands and Use Cases could be used standalone
<?php 
class CommandHandlerCompilerPass implements CompilerPassInterface 
{ 
public function process(ContainerBuilder $container) 
{ 
if (!$container->hasDefinition('command_handler')) { 
return; 
} 
$definition = $container->getDefinition('command_handler'); 
$taggedServices = $container->findTaggedServiceIds('use_case'); 
$useCases = array(); 
foreach ($taggedServices as $id => $attributes) { 
$useCases[] = new Reference($id); 
} 
$definition->addMethodCall( 
'registerUseCases', 
array($useCases) 
); 
} 
}
<service id="use_case.fire_player" public="false" class="UseCaseFirePlayerUseCase"> 
<argument type="service" id="repository_team"/> 
<tag name="use_case" /> 
</service>
<?php 
class MyController extends Controller 
{ 
public function modifyLeagueAction(Request $request, $id) 
{ 
$reader = $this->get('reader'); 
$league = $reader->getLeague($id); 
$command = ModifyLeagueCommand::fromArray($league); 
$form = $this->createForm(new ModifyLeagueType(), $command); 
$form->handleRequest($request); 
if ($form->isValid()) { 
$commandHandler = $this->get('command_handler'); 
$response = $commandHandler->execute($command); 
if ($response->isOk()) { 
//... 
} 
} 
return array( 
'form' => $form->createView() 
); 
} 
}
<?php 
class MyController extends Controller 
{ 
public function modifyLeagueAction(Request $request, $id) 
{ 
$reader = $this->get('reader'); 
$league = $reader->getLeague($id); 
$command = ModifyLeagueCommand::fromArray($league); 
$form = $this->createForm(new ModifyLeagueType(), $command); 
$form->handleRequest($request); 
if ($form->isValid()) { 
$commandHandler = $this->get('command_handler'); 
$response = $commandHandler->execute($command); 
if ($response->isOk()) { 
//... 
} 
} 
return array( 
'form' => $form->createView() 
); 
} 
}
<?php 
class MyController extends Controller 
{ 
public function modifyLeagueAction(Request $request, $id) 
{ 
$reader = $this->get('reader'); 
Consider using a service for 
$league = $reader->getLeague($id); 
reading $command = ModifyLeagueCommand::operations, fromArray($league); 
instead 
$form = $this->createForm(new ModifyLeagueType(), $command); 
use $form->the handleRequest($repository request); 
directly 
if ($form->isValid()) { 
$commandHandler = $this->get('command_handler'); 
$response = $commandHandler->execute($command); 
if ($response->isOk()) { 
//... 
} 
} 
return array( 
'form' => $form->createView() 
); 
} 
}
<?php 
class ModifyLeagueType extends CreateNewsType 
{ 
public function buildForm(FormBuilderInterface $builder, array $options) 
{ 
$builder 
->add('leagueId’, ‘’hidden') 
->add('name') 
->add('save', 'submit') 
; 
} 
public function setDefaultOptions(OptionsResolverInterface $resolver) 
{ 
$resolver->setDefaults(array( 
'data_class' => ModifyLeagueCommand::CLASS, 
)); 
} 
}
<?php 
class ModifyLeagueCommand implements Command 
{ 
public $leagueId; 
public $name; 
public function getRequest() 
{ 
return new Request( 
[ 
leagueId => $this->leagueId, 
name => $this->name 
] 
); 
} 
}
Command validation 
ModifyLeagueCommand: 
properties: 
leagueId: 
- NotBlank: ~ 
name: 
- NotBlank: ~
Using CQRS and Event Sourcing things change deeply
Rich domain model with symfony 2.5 and doctrine 2.5
?
https://joind.in/talk/view/12214
Credits 
A special thanks to @_orso_ the first who told me about rich models 
- Eric Evans - "Domain Driven Design" 
- Vaughn Vernon - “Implementing Domain-Driven Design” 
- http://www.slideshare.net/ziobrando/gestire-la-complessit-con-domain-driven-design 
- http://verraes.net/2013/12/related-entities-vs-child-entities/ 
- http://www.whitewashing.de/2012/08/22/building_an_object_model__no_setters_allowed.html 
- http://www.infoq.com/articles/ddd-contextmapping 
- http://nicolopignatelli.me/valueobjects-a-php-immutable-class-library/ 
- http://welcometothebundle.com/domain-driven-design-and-symfony-for-simple-app/ 
- http://www.slideshare.net/perprogramming/application-layer-33335917 
- http://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-design/ 
- http://www.slideshare.net/thinkddd/practical-domain-driven-design-cqrs-and-messaging-architectures 
- http://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/ 
- http://gojko.net/2009/09/30/ddd-and-relational-databases-the-value-object-dilemma/
Credits 
- http://verraes.net/2013/06/unbreakable-domain-models/ 
- http://www.mehdi-khalili.com/orm-anti-patterns-part-4-persistence-domain-model 
- http://martinfowler.com/bliki/BoundedContext.html 
- http://www.substanceofcode.com/2007/01/17/from-anemic-to-rich-domain-model/ 
- http://gorodinski.com/blog/2012/04/25/read-models-as-a-tactical-pattern-in-domain-driven-design-ddd/ 
- http://www.sapiensworks.com/blog/post/2013/05/01/DDD-Persisting-Aggregate-Roots-In-A-Unit-Of-Work.aspx 
- http://simon-says-architecture.com/2011/09/06/ddd-by-the-book/ 
- http://scaledagileframework.com/domain-modeling/ 
- http://www.codeproject.com/Articles/555855/Introduction-to-CQRS

More Related Content

Rich domain model with symfony 2.5 and doctrine 2.5

  • 1. Rich Domain Model with Symfony2 and Doctrine2 Leonardo Proietti @_leopro_ Symfony Day Italy Milan, 10 October 2014
  • 4. domain = problem space domain model = solution space
  • 5. Domain “A Domain [...] is what an organization does and the world it does in.” (Vaughn Vernon, “Implementing Domain-Driven Design”)
  • 6. Domain Model “A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving problem at hand and ignores extraneous detail.” (Eric Evans, "Domain Driven Design")
  • 8. Domain Model “In DDD, domain model refers to a class.” (Julie Lerman, http://msdn.microsoft.com/en-us/magazine/dn385704.aspx)
  • 9. Domain Model “An object model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
  • 10. Domain Model “An object model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
  • 12. First, because it’s not anemic Martin Fowler http://martinfowler.com/bliki/AnemicDomainModel.html
  • 13. <?php class Anemic { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function setCouldBeChangedAnytime($couldBeChangedAnytime) {} public function setShouldBeChangedOnlyOnEdit($shouldBeChangedOnlyOnEdit) {} public function setShouldNotChangeAfterCreation($shouldNotChangeAfterCreation {} }
  • 14. <?php class SomeService { public function doStuffOnCreation() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 15. <?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 16. <?php class SomeService { public function doStuffOnEdit() { Loss of memory $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 17. <?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 18. <?php class BehaviouralClass { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) { $this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) { $this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } }
  • 19. <?php class BehaviouralClass { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; It's not still rich, lacks of ... public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) { $this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) { $this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } }
  • 20. Ubiquitous Language “the domain model can provide the backbone for that common language [...]. The vocabulary of that ubiquitius language includes the names of classes and prominent operations” (Eric Evans, "Domain Driven Design")
  • 21. What does “coffee” mean? Alberto Brandolini AKA @ziobrando
  • 22. A bit of strategy
  • 23. Our domain is an online game that simulate the soccer’s world.
  • 25. What does “player” mean in our domain?
  • 26. The meaning of “player” Within game engine context a model of a real soccer player, modelled with behaviours to fit the requirements of the game engine.
  • 27. The meaning of “player” Within data import context a model of a real soccer player, but modelled for a simple CRUD.
  • 28. The meaning of “player” Within user profile context a model of the user of the website, who plays the game.
  • 29. League Team Calendar Player Lineup Coach Core context
  • 30. League Team Calendar Player Lineup Coach (the player in “user context”) Core context
  • 31. Player Game context User context Uuid Name Roles Uuid Email Password
  • 33. <?php class League { private $id; private $name; private $teams; public function __construct(Uuid $uuid, $name) { $this->id = $uuid; $this->name = $name; $this->teams = new ArrayCollection(); } public function registerTeam(Team $team) { $this->teams->add($team); } }
  • 34. <?php class League { private $id; private $name; private $teams; A team must do a registration public function __construct({ to the league Uuid $uuid, $name) $this->id = $uuid; $this->name = $name; $this->teams = new ArrayCollection(); } public function registerTeam(Team $team) { $this->teams->add($team); } }
  • 35. <?php class League { private $id; private $genericInfo; private $teams; public function __construct(Uuid $uuid, LeagueGenericInfo $leagueGenericInfo) { $this->id = $uuid; $this->genericInfo = $leagueGenericInfo; $this->teams = new ArrayCollection(); } // ...}
  • 36. <?php class LeagueGenericInfo { private $name; private $description; private $country; public function __construct($country, $description, $name) { $this->country = $country; $this->description = $description; $this->name = $name; } // … getters and behaviours}
  • 37. <?php class LeagueGenericInfo { private $name; private $description; private $country; Value object public function __construct($country, $description, $name) { $this->country = $country; $this->description = $description; $this->name = $name; } // … getters and behaviours}
  • 39. <?php class League { // ... public function registerTeam(Team $team) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } $this->teams->add($team); } private function canLeagueAcceptAnotherRegistration() { if ($this->teams->count() == 8) { return false; } return true; } }
  • 40. <?php class League { // ... public function registerTeam(Team $team) { if (!$this->canLeagueAcceptAnotherRegistration()) { League protects its invariants throw new DomainException('Not more places available'); } $this->teams->add($team); } private function canLeagueAcceptAnotherRegistration() { if ($this->teams->count() == 8) { return false; } return true; } }
  • 41. <?php class League { // ... public function getTeams() { return $this->teams; } }
  • 42. <?php class League { // ... private function getTeams() { return $this->teams; } }
  • 43. <?php class LeagueGenericInfo { private $name; private $description; private $country; private static $countries; public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
  • 44. <?php class LeagueGenericInfo { private $name; private $description; private $country; Input validation private static $countries; public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
  • 45. <?php class LeagueGenericInfo { private $name; private $description; private $country; Could be private static also $countries; placed in commands public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
  • 46. <?php class League { public function render() { $properties = [ 'id' => $this->id, 'name' => $this->genericInfo->getName(), 'description' => $this->genericInfo->getDescription(), 'country' => $this->genericInfo->getCountry(), ]; $teams = new ArrayCollection(); foreach ($this->teams as $team) { $teams->add($team->render()); } $properties['teams'] = $teams; return new ArrayCollection($properties); } }
  • 47. <?php class League { public function render() { $properties = [ 'id' => $this->id, 'name' => $this->genericInfo->getName(), 'description' => $this->genericInfo->getDescription(), 'country' => $this->genericInfo->getCountry(), ]; Return a read-only object $teams = new ArrayCollection(); foreach ($this->teams as $team) { $teams->add($team->render()); } $properties['teams'] = $teams; return new ArrayCollection($properties); } }
  • 48. <?php class Team { private $id; private $players; public function __construct(Uuid $uuid) { $this->id = $uuid; $this->players = new ArrayCollection(); } public function firePlayer($id) { foreach ($this->players as $key => $player) { if ($player->getId() == $id) { $this->players->remove($key); } } } }
  • 49. <?php class Team { private $id; private $players; Traverse the collections public function __construct(Uuid $uuid) { $this->id = $uuid; $this->players = new ArrayCollection(); } public function firePlayer(Player $playerToFire) { foreach ($this->players as $key => $player) { if ($player->getId() == $playerToFire->getId()) { $this->players->remove($key); } } } }
  • 50. The Player should have a relation towards the Team?
  • 52. <?php class LeagueTest extends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
  • 53. <?php class LeagueTest extends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
  • 54. <?php class LeagueTest extends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … The same Team can do more than one registration to the League?!? $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
  • 55. <?php class League { //.. public function registerTeam(Team $team) { $this->canLeagueAcceptRegistrationOf($team); $this->teams->add($team); } private function canLeagueAcceptRegistrationOf(Team $applicantTeam) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } foreach ($this->teams as $key => $team) { if ($team->getId() == $applicantTeam->getId()) { throw new DomainException('Team already registered'); } } } }
  • 56. <?php class League { //.. public function registerTeam(Team $team) { $this->canLeagueAcceptRegistrationOf($team); $this->teams->add($team); } And so on ... private function canLeagueAcceptRegistrationOf(Team $applicantTeam) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } foreach ($this->teams as $key => $team) { if ($team->getId() == $applicantTeam->getId()) { throw new DomainException('Team already registered'); } } } }
  • 59. We are using the entities of the Persistence Model as entities of our Domain Awareness Model
  • 60. League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY
  • 61. League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY
  • 62. League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY Uuid.orm.yml Uuid: type: embeddable id: uuid: type: string length: 36
  • 63. League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY Uuid.orm.yml Uuid: type: embeddable id: uuid: type: string length: 36
  • 64. <?php class TeamRepository implements TeamRepositoryInterface { private $em; public function __construct(EntityManager $em) { $this->em = $em; } }
  • 65. Persisting entities <?php class TeamRepository implements TeamRepositoryInterface { public function add(Team $team) { $this->em->persist($team); $this->em->flush(); } }
  • 66. Avoid collection hydration (foreach, toArray) <?php class TeamRepository implements TeamRepositoryInterface { public function getWithoutPlayers($id) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin('t.players', 'p', Join::WITH, $qb->expr()->andX( $qb->expr()->eq('p.id.uuid', ':pid') )) ->where('c.id.uuid = :id') ->setMaxResults(1); $qb->setParameter('id', $id); $qb->setParameter('pid', null); return $qb->getQuery()->getOneOrNullResult(); } }
  • 67. Retrieve an object joined with empty collection <?php class TeamRepository implements TeamRepositoryInterface { public function getWithPlayers($id) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin(t.players', 'p', Join::WITH, $qb->expr()->andX( $qb->expr()->eq('p.status', ':status') )) ->where('t.id.uuid = :id'); $qb->setParameter('status', 'on_the_market'); $qb->setParameter('id', $id); return $qb->getQuery()->getOneOrNullResult(); } }
  • 68. Get paginated list of Teams with Player joined <?php use DoctrineORMToolsPaginationPaginator; class TeamRepository implements TeamRepositoryInterface { public function paginate($first, $max) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin('t.players', 'p') ->setFirstResult($first) ->setMaxResults($max); $paginator = new Paginator($qb->getQuery()); return $paginator->getIterator(); } }
  • 70. <?php class FirePlayerCommand implements Command { public $teamId; public $playerId; public function getRequest() { return new Request( [ 'teamId' => $this->teamId, 'playerId' => $this->playerId ] ); } }
  • 71. <?php class Request extends ArrayCollection implements RequestInterface { public function __construct(array $values) { parent::__construct($values); } public function get($key, $default = null) { if (!parent::containsKey($key)) { throw new DomainException(); } $value = parent::get($key); if (!$value && $default) { return $default; } return $value; } }
  • 72. <?php class CommandHandler { private $dispatcher; private $useCases; public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } public function registerUseCases($useCases) { foreach ($useCases as $useCase) { if ($useCase instanceof UseCase) { $this->useCases[$useCase->getManagedCommand()] = $useCase; } else { throw new LogicException(''); } } } // ...}
  • 73. <?php class CommandHandler { // ... public function execute(Command $command) { try { $this->dispatcher ->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); $this->useCases[get_class($command)]->run($command); $response = new Response(); $this->dispatcher ->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); return $response; } catch (DomainException $e) { $this->dispatcher ->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); return new Response($e->getMessage(), Response::STATUS_KO); } } }
  • 74. <?php class CommandHandler { // ... public function execute(Command $command) { try { $this->dispatcher ->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); $this->useCases[get_class($command)]->run($command); $response = new Response(); $this->dispatcher ->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); return $response; } catch (DomainException $e) { $this->dispatcher ->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); return new Response($e->getMessage(), Response::STATUS_KO); } } }
  • 75. <?php class FirePlayerUseCase implements UseCase { private $repository; public function __construct(TeamRepositoryInterface $repository) { $this->repository = $repository; } public function run(Command $command) { $request = $command->getRequest(); $team = $this->repository->get( $request->get('teamId') ); $team->firePlayer( $request->get('playerId') ); $this->repository->add($team); } }
  • 76. Commands and Use Cases could be used standalone
  • 77. <?php class CommandHandlerCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition('command_handler')) { return; } $definition = $container->getDefinition('command_handler'); $taggedServices = $container->findTaggedServiceIds('use_case'); $useCases = array(); foreach ($taggedServices as $id => $attributes) { $useCases[] = new Reference($id); } $definition->addMethodCall( 'registerUseCases', array($useCases) ); } }
  • 78. <service id="use_case.fire_player" public="false" class="UseCaseFirePlayerUseCase"> <argument type="service" id="repository_team"/> <tag name="use_case" /> </service>
  • 79. <?php class MyController extends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); $league = $reader->getLeague($id); $command = ModifyLeagueCommand::fromArray($league); $form = $this->createForm(new ModifyLeagueType(), $command); $form->handleRequest($request); if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
  • 80. <?php class MyController extends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); $league = $reader->getLeague($id); $command = ModifyLeagueCommand::fromArray($league); $form = $this->createForm(new ModifyLeagueType(), $command); $form->handleRequest($request); if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
  • 81. <?php class MyController extends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); Consider using a service for $league = $reader->getLeague($id); reading $command = ModifyLeagueCommand::operations, fromArray($league); instead $form = $this->createForm(new ModifyLeagueType(), $command); use $form->the handleRequest($repository request); directly if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
  • 82. <?php class ModifyLeagueType extends CreateNewsType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('leagueId’, ‘’hidden') ->add('name') ->add('save', 'submit') ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => ModifyLeagueCommand::CLASS, )); } }
  • 83. <?php class ModifyLeagueCommand implements Command { public $leagueId; public $name; public function getRequest() { return new Request( [ leagueId => $this->leagueId, name => $this->name ] ); } }
  • 84. Command validation ModifyLeagueCommand: properties: leagueId: - NotBlank: ~ name: - NotBlank: ~
  • 85. Using CQRS and Event Sourcing things change deeply
  • 87. ?
  • 89. Credits A special thanks to @_orso_ the first who told me about rich models - Eric Evans - "Domain Driven Design" - Vaughn Vernon - “Implementing Domain-Driven Design” - http://www.slideshare.net/ziobrando/gestire-la-complessit-con-domain-driven-design - http://verraes.net/2013/12/related-entities-vs-child-entities/ - http://www.whitewashing.de/2012/08/22/building_an_object_model__no_setters_allowed.html - http://www.infoq.com/articles/ddd-contextmapping - http://nicolopignatelli.me/valueobjects-a-php-immutable-class-library/ - http://welcometothebundle.com/domain-driven-design-and-symfony-for-simple-app/ - http://www.slideshare.net/perprogramming/application-layer-33335917 - http://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-design/ - http://www.slideshare.net/thinkddd/practical-domain-driven-design-cqrs-and-messaging-architectures - http://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/ - http://gojko.net/2009/09/30/ddd-and-relational-databases-the-value-object-dilemma/
  • 90. Credits - http://verraes.net/2013/06/unbreakable-domain-models/ - http://www.mehdi-khalili.com/orm-anti-patterns-part-4-persistence-domain-model - http://martinfowler.com/bliki/BoundedContext.html - http://www.substanceofcode.com/2007/01/17/from-anemic-to-rich-domain-model/ - http://gorodinski.com/blog/2012/04/25/read-models-as-a-tactical-pattern-in-domain-driven-design-ddd/ - http://www.sapiensworks.com/blog/post/2013/05/01/DDD-Persisting-Aggregate-Roots-In-A-Unit-Of-Work.aspx - http://simon-says-architecture.com/2011/09/06/ddd-by-the-book/ - http://scaledagileframework.com/domain-modeling/ - http://www.codeproject.com/Articles/555855/Introduction-to-CQRS

Editor's Notes

  1. A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving problem at hand and ignores extraneous detail.
  2. il matching uno a uno tra sottodominio e bounded context è una condizione desiderabile, non un vincolo
  3. il modello cambia spesso, tanto quanto la conoscenza che acquisiamo e che ci porta a comprendere come risolvere i problemi; TDD si sposa bene
  4. non accoppiare con le annotations
  5. non accoppiare con le annotations
  6. non accoppiare con le annotations
  7. non accoppiare con le annotations
  8. non accoppiare con le annotations
  9. non accoppiare con le annotations
  10. non accoppiare con le annotations
  11. non accoppiare con le annotations
  12. non accoppiare con le annotations
  13. non accoppiare con le annotations
  14. non accoppiare con le annotations
  15. non accoppiare con le annotations
  16. non accoppiare con le annotations
  17. non accoppiare con le annotations
  18. non accoppiare con le annotations
  19. non accoppiare con le annotations
  20. non accoppiare con le annotations
  21. non accoppiare con le annotations
  22. non accoppiare con le annotations
  23. non accoppiare con le annotations