Clean architecture with ddd layering in php
description
Transcript of Clean architecture with ddd layering in php
Clean Architecture using DDD layering in PHP
Leonardo Proietti@_leopro_
1. Clean Architecture
Definition of Clean Architecture
Definition of Clean Architecture
Independent of FrameworksTestable
Independent of UIIndependent of Database
Independent of any external agency
Definition of Clean Architecture
Independent of FrameworksTestable
Independent of UIIndependent of Database
Independent of any external agency
Definition of Clean Architecture
Independent of FrameworksTestable
Independent of UIIndependent of Database
Independent of any external agency
Definition of Clean Architecture
Independent of FrameworksTestable
Independent of UIIndependent of Database
Independent of any external agency
Definition of Clean Architecture
Independent of FrameworksTestable
Independent of UIIndependent of Database
Independent of any external agency
Definition of Clean Architecture
Independent of FrameworksTestable
Independent of UIIndependent of Database
Independent of any external agency
Hey bro, I respect your opinion but ...
It isn't just my opinion
Do you know "Uncle Bob", isn't it?
I’m just another dwarf.
The Clean Architecture
The Dependency Rule
“This rule says that code dependencies can only point inwards. Nothing in an inner circle
can know anything at all about something in an outer circle.”
(http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
The Dependency Rule
“This rule says that code dependencies can only point inwards. Nothing in an inner circle
can know anything at all about something in an outer circle.”
(http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
2. Domain Driven Design
What is Domain Driven Design?
What is Domain Driven Design?
“it is a way of thinking and a set of priorities, aimed at accelerating software projects that
have to deal with complicated domains”
(Eric Evans, "Domain Driven Design")
What is Domain Driven Design?
“it is a way of thinking and a set of priorities, aimed at accelerating software projects that
have to deal with complicated domains”
(Eric Evans, "Domain Driven Design")
What is Domain Driven Design?
“it is a way of thinking and a set of priorities, aimed at accelerating software projects that
have to deal with complicated domains”
(Eric Evans, "Domain Driven Design")
What is Domain Driven Design?
“is a collection of principles and patterns that help developers craft elegant object systems”
(http://msdn.microsoft.com/en-us/magazine/dd419654.aspx)
What is Domain Driven Design?
“is a collection of principles and patterns that help developers craft elegant object systems”
(http://msdn.microsoft.com/en-us/magazine/dd419654.aspx)
What is Domain Driven Design?
“is an approach to software development for complex needs by connecting the
implementation to an evolving model”
(http://en.wikipedia.org/wiki/Domain-driven_design)
What is Domain Driven Design?
“is an approach to software development for complex needs by connecting the
implementation to an evolving model”
(http://en.wikipedia.org/wiki/Domain-driven_design)
Mmhh interesting … but what does it mean?
Make yourself comfortable
3. DDD Core
Domain
“Every software program relates to some activity or interest of its user. That subject area
to which the user applies the program is the domain of the software”
(Eric Evans, "Domain Driven Design")
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")
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")
Sounds familiar?
Model
“A domain model [...] is not just the knowledge in a domain expert’s head; it is a rigorously organized and selective abstraction of that
knowledge.”
(Eric Evans, "Domain Driven Design")
Model
“A domain model [...] is not just the knowledge in a domain expert’s head; it is a rigorously organized and selective abstraction of that
knowledge.”
(Eric Evans, "Domain Driven Design")
Next it’s maybe the most important thing in DDD
Ubiquitous Language
“the domain model can provide the backbone for that common language [...]. The vocabulary of that UBIQUITOUS LANGUAGE includes the names of classes and prominent operations”
(Eric Evans, "Domain Driven Design")
Ubiquitous Language
It’s a shared jargon between domain experts and developers, based on Domain Model
Take care of Ubiquitous Language
What does “coffee” mean?
Alberto Brandolini AKA ziobrando
Ubiquitous Language
“changes to the language will be recognized as changes in the domain model”
(Eric Evans, "Domain Driven Design")
Context
“The setting in which a word or statement appears that determines its meaning.”
4. DDD Building Blocks
Entity
An object with “clear identity and a life-cycle with state transitions that we care about.”
(http://dddsample.sourceforge.net/characterization.html)
Are these entities?
It depends.
It depends.
“We don't assign seats on our flights, so feel free to sit in any available seat”
Value Object
“An object that contains attributes but has no conceptual identity. They should be treated as
immutable.”
(http://en.wikipedia.org/wiki/Domain-driven_design)
Value Object
“A small simple object, like money or a date range, whose equality isn't based on identity.”
(http://martinfowler.com/eaaCatalog/valueObject.html)
Are these value objects?
In most of the contexts, but ...
Beware about Anemic Domain Model
Beware about Anemic Domain Model
Both Entity and Value Object should have data and behaviours.
(http://www.martinfowler.com/bliki/AnemicDomainModel.html)
Few other concepts
RepositoryAggregate
Domain Event
Repository
“A REPOSITORY represents all objects of a certain type as a conceptual set. It acts like a
collection, except with more elaborate querying capability”
(Eric Evans, "Domain Driven Design")
Repository
“All repositories provide methods that allow client to request objects matching some
criteria”
(Eric Evans, "Domain Driven Design")
Repository
“Although most queries return an object or a collection of objects, it also fits within the concept to return some types of summary
calculation”
(Eric Evans, "Domain Driven Design")
Aggregate
“A DDD aggregate is a cluster of domain objects that can be treated as a single unit.”
(http://martinfowler.com/bliki/DDD_Aggregate.html)
Aggregate
“DDD aggregates are domain concepts (order, clinic visit, playlist), while collections are
generic.”
(http://martinfowler.com/bliki/DDD_Aggregate.html)
Domain Event
“Captures the memory of something interesting which affects the domain”
(http://martinfowler.com/eaaDev/DomainEvent.html)
How long does it take?
5. DDD Layering
Layering
“We need to decouple the domain objects from other functions of the system, so we can avoid
confusing the domain concepts wiht other concepts”
(Eric Evans, "Domain Driven Design")
Layering
“We need to decouple the domain objects from other functions of the system, so we can avoid
confusing the domain concepts wiht other concepts”
(Eric Evans, "Domain Driven Design")
Layering
(http://guptavikas.wordpress.com/2009/12/01/domain-driven-design-an-introduction/)
Layering
(http://dddsample.sourceforge.net/architecture.html)
Domain
“The domain layer is the heart of the software, and this is where the interesting stuff happens.”
(http://dddsample.sourceforge.net/architecture.html)
Application
“The application layer is responsible for driving the workflow of the application, matching the
use cases at hand”
(http://dddsample.sourceforge.net/architecture.html)
Interface
“This layer holds everything that interacts with other systems”
(http://dddsample.sourceforge.net/architecture.html)
Interface
“This layer holds everything that interacts with other systems”
(http://dddsample.sourceforge.net/architecture.html)
Controller Form
View API
Infrastructure
“In simple terms, the infrastructure consists of everything that exists independently of our application: external
libraries, database engine, application server, messaging backend and so on.”
(http://dddsample.sourceforge.net/architecture.html)
Separation of concerns
Services
Services
“Sometimes, it just isn’t a thing.”
(Eric Evans, "Domain Driven Design")
Services
Domain ServicesApplication Services
Infrastructural Services
Domain Services
“If a SERVICE were devised to make appropriate debits and credits for a found
transfer, that capability would belong in the domain layer”
(Eric Evans, "Domain Driven Design")
Application Services
“if the banking application can convert and export our transactions into a spreadsheet file
[...] that export is an application SERVICE”
(Eric Evans, "Domain Driven Design")
Infrastructural Services
“a bank might have an application that sends an e-mail [...]. The interface that encapsulates
the email system, [...] is a SERVICE in the infrastructure layer”
(Eric Evans, "Domain Driven Design")
6. Code First
Persistence Ignorance
“In DDD, we don't consider any databases. DDD is all about the domain, not about the
database, and Persistence Ignorance (PI) is a very important aspect of DDD”
(http://williamdurand.fr/2013/08/20/ddd-with-symfony2-making-things-clear/)
YAGNI
You aren't gonna need it.You don’t need a Database or a Framework to
modelling the Domain
YAGNI
You aren't gonna need it.You don’t need a Database or a Framework to
modelling the Domain
Where should I start then?!?
Understanding the Domain
Talking with domain experts
DDD is Agile, we should be iterative
(http://dddsample.sourceforge.net/architecture.html)
7. Let’s code
Our starting domain
I need a tool to plan my trips.Every trip must have at least one route and every route has one or more leg.A leg has one date and one location.
Code
Here, a complete sample code:https://github.com/leopro/trip-planner
You can follow the building steps, starting from the first commit.
Code
Let’s focus on some steps
composer.json
{ "name": "my trip planner", "autoload": { "psr-0": { "": "src/" } }, "require": { "php": ">=5.3.3", "doctrine/collections": "v1.2", }, "require-dev": { "phpunit/phpunit": "4.0.*" }, "config": { "bin-dir": "bin" }}
composer.json
{ "name": "my trip planner", "autoload": { "psr-0": { "": "src/" } }, "require": { "php": ">=5.3.3", "doctrine/collections": "v1.2", }, "require-dev": { "phpunit/phpunit": "3.7.*" }, "config": { "bin-dir": "bin" }}
I don't need anything more to start
composer.json
{ "name": "my trip planner", "autoload": { "psr-0": { "": "src/" } }, "require": { "php": ">=5.3.3", "doctrine/collections": "v1.2", }, "require-dev": { "phpunit/phpunit": "3.7.*" }, "config": { "bin-dir": "bin" }}
Ok, I have a dependency on doctrine/collections ...
composer.json
{ "name": "my trip planner", "autoload": { "psr-0": { "": "src/" } }, "require": { "php": ">=5.3.3", "doctrine/collections": "v1.2", }, "require-dev": { "phpunit/phpunit": "3.7.*" }, "config": { "bin-dir": "bin" }}
… the missing (SPL) Collection/Array/OrderedMap interface
composer.json
{ "name": "my trip planner", "autoload": { "psr-0": { "": "src/" } }, "require": { "php": ">=5.3.3", "doctrine/collections": "v1.2", }, "require-dev": { "phpunit/phpunit": "3.7.*" }, "config": { "bin-dir": "bin" }}
Anyway, you can put a boundary
<?php
namespace Leopro\TripPlanner\Domain\Contract;
use Doctrine\Common\Collections\Collection as DoctrineCollection;
interface Collection extends DoctrineCollection {}
<?php
namespace Leopro\TripPlanner\Domain\Adapter;
use Doctrine\Common\Collections\ArrayCollection as DoctrineArrayCollection;use Leopro\TripPlanner\Domain\Contract\Collection;
class ArrayCollection extends DoctrineArrayCollection implements Collection {}
Domain
Our starting domain
I need a tool to plan my trips.Every trip must have at least one route and every route has one or more leg.A leg has one date and one location.
<?php
namespace Leopro\TripPlanner\Domain\Tests;
use Leopro\TripPlanner\Domain\Entity\Trip;
class TripTest extends \PHPUnit_Framework_TestCase{ public function testCreateTripReturnATripWithFirstRoute() { $trip = Trip::create('my first planning'); $this->assertInstanceOf('Leopro\TripPlanner\Domain\Entity\Trip', $trip); $this->assertEquals(1, $trip->getRoutes()->count()); }}
<?php
namespace Leopro\TripPlanner\Domain\Entity;
use Leopro\TripPlanner\Domain\Adapter\ArrayCollection;
class Trip{ private $name, private $routes;
private function __construct($name, Route $route) { $this->name = $name; $this->routes = new ArrayCollection(array($route)); }
public function create($name) { return new self($name, new Route); }
public function getRoutes() { return $this->routes; }}
<?php
namespace Leopro\TripPlanner\Domain\Entity;
class Route{
}
Our starting domain
I need a tool to plan my trips.Every trip must have at least one route and every route has one or more leg.A leg has one date and one location.
<?php
namespace Leopro\TripPlanner\Domain\Tests;
use Leopro\TripPlanner\Domain\Entity\Route;
class RouteTest extends \PHPUnit_Framework_TestCase{ public function testCreateRouteAddingALeg() { $route = Route::create('my first trip'); $route->addLeg('06-06-2014');
$this->assertEquals(1, $route->getLegs()->count()); }
<?php
namespace Leopro\TripPlanner\Domain\Entity;
class Route{ private $name; private $legs;
private function __construct($name) { $this->name = $name; $this->legs = new ArrayCollection(); }
public static function create($tripName) { return new self('first route for trip: ' . $tripName); }
public function addLeg($date) { $leg = Leg::create($date); $this->legs->add($leg); }
//...
<?php
namespace Leopro\TripPlanner\Domain\Entity;
class Route{ private $name; private $legs;
private function __construct($name) { $this->name = $name; $this->legs = new ArrayCollection(); }
public static function create($tripName) { return new self('first route for trip: ' . $tripName); }
public function addLeg($date) { $leg = Leg::create($date); $this->legs->add($leg); }
//...
Wait, we really want two legs with the same date?
The model is changing
I need a tool to plan my trips.Every trip must have at least one route and every route has one or more leg
and two leg with the same date for the same route are not allowed .
A leg has one date and one location.
/** * @expectedException \...\DateAlreadyUsedException */public function testNoDuplicationDateForTheSameRoute(){ $route = Route::create('my first trip'); $route->addLeg('06-06-2014'); $route->addLeg('06-06-2014');}
<?php
namespace Leopro\TripPlanner\Domain\Entity;
class Route{
//...
public function addLeg($date){ $leg = Leg::create($date);
$dateAlreadyUsed = function($key, $element) use($leg) { return $element->getDate() == $leg->getDate(); };
if ($this->legs->exists($dateAlreadyUsed)) { throw new DateAlreadyUsedException($date . ' already used'); }
$this->legs->add($leg);}
//...
Our starting domain
I need a tool to plan my trips.Every trip must have at least one route and every route has one or more leg.A leg has one date and one location.
<?php
namespace Leopro\TripPlanner\Domain\Tests;
use Leopro\TripPlanner\Domain\Entity\Leg;
class LegTest extends \PHPUnit_Framework_TestCase{ public function testCreateLegReturnsALegWithDateAndLocation() { $leg = Leg::create('01/01/2014', 'd/m/Y', -3.386665, 36.736908);
$this->assertInstanceOf('Leopro\TripPlanner\Domain\Entity\Leg', $leg);
$location = $leg->getLocation(); $this->assertInstanceOf('Leopro\TripPlanner\Domain\Entity\Location', $location);
$point = $location->getPoint(); $this->assertInstanceOf('Leopro\TripPlanner\Domain\ValueObject\Point', $point); $this->assertEquals(-3.386665, $point->getLatitude()); $this->assertEquals(36.736908, $point->getLongitude()); }}
<?php
namespace Leopro\TripPlanner\Domain\Entity;
use Leopro\TripPlanner\Domain\ValueObject\Date;
class Leg{ private $date; private $location;
private function __construct(Date $date, Location $location) { $this->date = $date; $this->location = $location; }
public static function create($date, $dateFormat, $latitude, $longitude) { $date = new Date($date, $dateFormat); return new self( $date, Location::create($date->getFormattedDate(), $latitude, $longitude) ); }
//..
<?php
namespace Leopro\TripPlanner\Domain\Entity;
use Leopro\TripPlanner\Domain\ValueObject\Point;
class Location{ private $name; private $point;
private function __construct($name, Point $point) { $this->name = $name; $this->point = $point; }
public static function create($name, $latitude, $longitude) { return new self($name, new Point($latitude, $longitude) ); }
public function getPoint() { return $this->point; }}
<?php
namespace Leopro\TripPlanner\Domain\Tests;
use Leopro\TripPlanner\Domain\ValueObject\Point;
class PointTest extends \PHPUnit_Framework_TestCase{ public function testDistance() { $firstPoint = new Point(-3.386665, 36.736908); $secondPoint = new Point(-3.428112, 35.932846);
$this->assertEquals(89, $firstPoint->getCartographicDistance($secondPoint)); $this->assertEquals(98, $firstPoint->getApproximateRoadDistance($secondPoint)); }}
<?php
namespace Leopro\TripPlanner\Domain\Tests;
use Leopro\TripPlanner\Domain\ValueObject\Point;
class PointTest extends \PHPUnit_Framework_TestCase{ public function testDistance() { $firstPoint = new Point(-3.386665, 36.736908); $secondPoint = new Point(-3.428112, 35.932846);
$this->assertEquals(89, $firstPoint->getCartographicDistance($secondPoint)); $this->assertEquals(98, $firstPoint->getApproximateRoadDistance($secondPoint)); }}
Value Object
getCartographicDistance()getApproximateRoadDistance()
<?php
namespace Leopro\TripPlanner\Domain\ValueObject;
class Point{ private $latitude; private $longitude;
public function __construct($latitude, $longitude) { $this->latitude = $latitude; $this->longitude = $longitude; }
//..
public function getApproximateRoadDistance(Point $point, $degreeApproximation = 10) { $distance = $this->getCartographicDistance($point);
return round($distance + $distance * ($degreeApproximation / 100)); }
public function getCartographicDistance(Point $point){ $earthRadius = 3958.75;
$dLat = deg2rad($point->getLatitude() - $this->latitude); $dLng = deg2rad($point->getLongitude() - $this->longitude);
$a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($this->latitude)) * cos(deg2rad($point->getLatitude())) * sin($dLng / 2) * sin($dLng / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a)); $dist = $earthRadius * $c;
$meterConversion = 1.609344; $geopointDistance = $dist * $meterConversion;
return round($geopointDistance, 0);}
public function getCartographicDistance(Point $point){ $earthRadius = 3958.75;
$dLat = deg2rad($point->getLatitude() - $this->latitude); $dLng = deg2rad($point->getLongitude() - $this->longitude);
$a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($this->latitude)) * cos(deg2rad($point->getLatitude())) * sin($dLng / 2) * sin($dLng / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a)); $dist = $earthRadius * $c;
$meterConversion = 1.609344; $geopointDistance = $dist * $meterConversion;
return round($geopointDistance, 0);}
Got the point?
Application
<?php
namespace Leopro\TripPlanner\Application\Command;
use Leopro\TripPlanner\Application\UseCase\UseCaseInterface;
class CommandHandler{ private $useCases;
public function registerCommands(array $useCases) { foreach ($useCases as $useCase) { if ($useCase instanceof UseCaseInterface) { $this->useCases[$useCase->getManagedCommand()] = $useCase; } else { throw new \LogicException(‘...'); } } }
//...
<?php
namespace Leopro\TripPlanner\Application\Command;
use Leopro\TripPlanner\Application\UseCase\UseCaseInterface;
class CommandHandler{ //...
public function execute($command) { try {
$commandClass = get_class($command); if (!array_key_exists($commandClass, $this->useCases)) { throw new \LogicException($commandClass . ' is not a managed command'); }
$this->useCases[get_class($command)]->run($command); } catch (\Exception $e) { throw $e; } }}
<?php
namespace Leopro\TripPlanner\Application\Command;
use Leopro\TripPlanner\Application\UseCase\UseCaseInterface;
class CommandHandler{ //...
public function execute($command) { try {
$commandClass = get_class($command); if (!array_key_exists($commandClass, $this->useCases)) { throw new \LogicException($commandClass . ' is not a managed command'); }
$this->useCases[get_class($command)]->run($command); } catch (\Exception $e) { throw $e; } }}
You can move the state of the domain, through commands
<?php
namespace Leopro\TripPlanner\Application\Command;
use Leopro\TripPlanner\Application\Contract\CommandInterface;use Leopro\TripPlanner\Domain\Adapter\ArrayCollection;
class CreateTripCommand implements CommandInterface{ private $name;
public function __construct($name) { $this->name = $name; }
public function getRequest() { return new ArrayCollection( array( 'name' => $this->name ) ); }}
<?php
namespace Leopro\TripPlanner\Application\UseCase;
class CreateTripUseCase extends AbstractUseCase implements UseCaseInterface{ private $tripRepository;
public function __construct(TripRepository $tripRepository) { $this->tripRepository = $tripRepository; }
public function run(CommandInterface $command) { $this->exceptionIfCommandNotManaged($command);
$request = $command->getRequest();
$trip = Trip::createWithFirstRoute(new TripIdentity(uniqid()), $request->get('name'));
$this->tripRepository->add($trip);
return $trip; }}
<?php
namespace Leopro\TripPlanner\Application\UseCase;
class CreateTripUseCase extends AbstractUseCase implements UseCaseInterface{ private $tripRepository;
public function __construct(TripRepository $tripRepository) { $this->tripRepository = $tripRepository; }
public function run(CommandInterface $command) { $this->exceptionIfCommandNotManaged($command);
$request = $command->getRequest();
$trip = Trip::createWithFirstRoute(new TripIdentity(uniqid()), $request->get('name'));
$this->tripRepository->add($trip);
return $trip; }}
Defining a TripRepository interface ...
<?php
namespace Leopro\TripPlanner\Domain\Contract;
use Leopro\TripPlanner\Domain\Entity\Trip;use Leopro\TripPlanner\Domain\ValueObject\TripIdentity;
interface TripRepository{ /** * @param TripIdentity $identity * @return \Leopro\TripPlanner\Domain\Entity\Trip */ public function get(TripIdentity $identity);
/** * @param Trip $trip * @return void */ public function add(Trip $trip);}
<?php
namespace Leopro\TripPlanner\Domain\Contract;
use Leopro\TripPlanner\Domain\Entity\Trip;use Leopro\TripPlanner\Domain\ValueObject\TripIdentity;
interface TripRepository{ /** * @param TripIdentity $identity * @return \Leopro\TripPlanner\Domain\Entity\Trip */ public function get(TripIdentity $identity);
/** * @param Trip $trip * @return void */ public function add(Trip $trip);}
… and interfaces for Validator and Event Dispatcher
<?php
namespace Leopro\TripPlanner\Application\Contract;
interface Validator{ /** * @param $value * @return \Leopro\TripPlanner\Domain\Contract\Collection */ public function validate($value);}
interface EventDispatcher{ /** * @param array $listeners * @return EventListener[] */ public function registerListeners(array $listeners);
/** * @param $event */ public function notify($name, $event);}
About validation
In DDD, entities should be always valid.
About validation
But if you ask“where do I put validation?”you'll get different answers.
About validation
If you are using commands, validate the command itself, is a
good trade-off.
Infrastructure
Framework's revenge
composer.json
"require": { "php": ">=5.3.3", "doctrine/collections": "v1.2", "symfony/symfony": "~2.4", "doctrine/dbal": "dev-master", "doctrine/orm": "dev-master", "doctrine/doctrine-bundle": "dev-master", "twig/extensions": "~1.0", "symfony/assetic-bundle": "~2.3", "symfony/swiftmailer-bundle": "~2.3", "symfony/monolog-bundle": "~2.4", "sensio/distribution-bundle": "~2.3", "sensio/framework-extra-bundle": "~3.0", "sensio/generator-bundle": "~2.3", "incenteev/composer-parameter-handler": "~2.0", "doctrine/data-fixtures": "dev-master", "doctrine/migrations": "dev-master", "doctrine/doctrine-migrations-bundle": "dev-master", "doctrine/doctrine-fixtures-bundle": "dev-master"},
Mapping entities
app/config/config.yml
orm: auto_generate_proxy_classes: "%kernel.debug%" auto_mapping: false mappings: TripPlannerDomain: type: yml prefix: Leopro\TripPlanner\Domain\Entity dir: %kernel.root_dir%/../src/Leopro/TripPlanner/InfrastructureBundle/Resources/config/doctrine/entity is_bundle: false TripPlannerDomainValueObjects: type: yml prefix: Leopro\TripPlanner\Domain\ValueObject dir: %kernel.root_dir%/../src/Leopro/TripPlanner/InfrastructureBundle/Resources/config/doctrine/value_object is_bundle: false
InfrastructureBundle/Resources/config/entity/Route.orm.yml
Leopro\TripPlanner\Domain\Entity\Trip: type: entity table: trip embedded: identity: class: Leopro\TripPlanner\Domain\ValueObject\TripIdentity fields: name: type: string length: 250 manyToMany: routes: targetEntity: Leopro\TripPlanner\Domain\Entity\Route joinTable: name: trip_routes joinColumns: link_id: referencedColumnName: identity_id inverseJoinColumns: report_id: referencedColumnName: internalIdentity cascade: ["persist"]
Validate commands
InfrastructureBundle/Resources/config/validation.yml
Leopro\TripPlanner\Application\Command\CreateTripCommand: properties: name: - NotBlank: ~
Leopro\TripPlanner\Application\Command\AddLegToRouteCommand: properties: tripIdentity: - NotBlank: ~ routeIdentity: - NotBlank: ~ date: - NotBlank: ~ dateFormat: - NotBlank: ~ latitude: - NotBlank: ~ longitude: - NotBlank: ~
Configuring services
src/LeoPro/TripPlanner/InfrastructureBundle/Resources/config/services.xml
<services>
<!-- Exposed Services --> <service id="trip_repository" alias="trip_repository.doctrine"></service>
<service id="command_handler" class="%application.command_handler.class%"> <argument type="service" id="infrastructure.validator"/> <argument type="service" id="application.event_dispatcher"/> </service>
</services>
src/LeoPro/TripPlanner/InfrastructureBundle/Resources/config/services.xml
<services>
<!-- Not Exposed Services --> <service id="application.event_dispatcher" public="false" class="%application.event_dispatcher.class%"> </service>
<service id="use_case.create_trip" public="false" class="...\CreateTripUseCase"> <argument type="service" id="trip_repository"/> <tag name="use_case"/> </service>
<service id="use_case.add_leg_to_route" public="false" class="Leopro\TripPlanner\Application\UseCase\AddLegToRouteUseCase"> <argument type="service" id="trip_repository"/> <tag name="use_case"/> </service>
<service id="use_case.update_location" public="false" class="Leopro\TripPlanner\Application\UseCase\UpdateLocationUseCase"> <argument type="service" id="trip_repository"/> <tag name="use_case"/> </service>
</services>
src/LeoPro/TripPlanner/InfrastructureBundle/Resources/config/services.xml
<services>
<!-- Adapter --> <service id="infrastructure.validator" public="false" class="%infrastructure.validator.class%"> <argument type="service" id="validator"/> </service>
<service id="infrastructure.event_dispatcher_adapter" public="false" class="%infrastructure.event_dispatcher_adapter.class%"> <argument type="service" id="event_dispatcher"/> <tag name="event_dispatcher_listener"/> </service>
<!-- Concrete Implementations --> <service id="trip_repository.doctrine" public="false" class="%infrastructure.trip_repository.doctrine.class%"> <argument type="service" id="doctrine.orm.entity_manager"/> </service>
</services>
Adapter
Ops … some parts of the frameworks do not fit our
interfaces.
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Adapter;
use Leopro\TripPlanner\Application\Contract\Validator as ApplicationValidatorInterface;use Leopro\TripPlanner\Domain\Adapter\ArrayCollection;use Symfony\Component\Validator\Validator\ValidatorInterface;
class Validator implements ApplicationValidatorInterface{ private $validator;
public function __construct(ValidatorInterface $validator) { $this->validator = $validator; }
public function validate($value) { $applicationErrors = new ArrayCollection(); $errors = $this->validator->validate($value); foreach ($errors as $error) { $applicationErrors->set($error->getPropertyPath(), $error->getMessage()); }
return $applicationErrors; }}
Repository
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Repository;
use Doctrine\ORM\EntityManager;use Leopro\TripPlanner\Domain\Contract\TripRepository as TripRepositoryInterface;
class TripRepository implements TripRepositoryInterface{ private $em;
public function __construct(EntityManager $em) { $this->em = $em; }
public function get(TripIdentity $identity) { $qb = $this->em->createQueryBuilder() ->select('t') ->from("TripPlannerDomain:Trip", 't') ->where('t.identity.id = :identity');
$qb->setParameter('identity', $identity);
return $qb->getQuery()->getOneOrNullResult(); }
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Repository;
use Doctrine\ORM\EntityManager;use Leopro\TripPlanner\Domain\Contract\TripRepository as TripRepositoryInterface;
class TripRepository implements TripRepositoryInterface{ public function add(Trip $trip) { $this->em->persist($trip); $this->em->flush(); }}
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Repository;
use Doctrine\ORM\EntityManager;use Leopro\TripPlanner\Domain\Contract\TripRepository as TripRepositoryInterface;
class TripRepository implements TripRepositoryInterface{ public function add(Trip $trip) { $this->em->persist($trip); $this->em->flush(); }}
Then it’s like a Doctrine repository?!?
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Repository;
use Doctrine\ORM\EntityManager;use Leopro\TripPlanner\Domain\Contract\TripRepository as TripRepositoryInterface;
class TripRepository implements TripRepositoryInterface{ public function add(Trip $trip) { $this->em->persist($trip); $this->em->flush(); }}
No, it’s quite different
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Repository;
use Leopro\TripPlanner\Domain\Contract\TripRepository as TripRepositoryInterface;
class TripRepository implements TripRepositoryInterface{ private $myThirdPartApiClient;
public function __construct(ApiClient $myThirdPartApiClient) { $this->myThirdPartApiClient = $myThirdPartApiClient; }
public function get(TripIdentity $identity) { $this->myThirdPartApiClient->get($identity); }
public function add(Trip $trip) { $this->myThirdPartApiClient->store($trip); }
<?php
namespace Leopro\TripPlanner\InfrastructureBundle\Repository;
use Leopro\TripPlanner\Domain\Contract\TripRepository as TripRepositoryInterface;
class TripRepository implements TripRepositoryInterface{ private $myThirdPartApiClient;
public function __construct(ApiClient $myThirdPartApiClient) { $this->myThirdPartApiClient = $myThirdPartApiClient; }
public function get(TripIdentity $identity) { $this->myThirdPartApiClient->get($identity); }
public function add(Trip $trip) { $this->myThirdPartApiClient->store($trip); }
It’s another possible Repository implementation
Presentation
<?php
namespace Leopro\TripPlanner\PresentationBundle\Controller;
use Leopro\TripPlanner\PresentationBundle\Form\Type\CreateTripType;
class ApiController extends Controller{ /** * @Route("/", name="create_trip") * @Template */ public function createTripAction(Request $request) { $form = $this->createForm(new CreateTripType()); $form->handleRequest($request);
if ($form->isValid()) { $trip = $this->get('command_handler')->execute($form->getData());
return new Response('ok'); }
return array( 'form' => $form->createView(), ); }}
<?php
namespace Leopro\TripPlanner\PresentationBundle\Form\Type;
use Leopro\TripPlanner\Application\Command\CreateTripCommand;
class CreateTripType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name') ->add('save', 'submit'); }
public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Leopro\TripPlanner\Application\Command\CreateTripCommand', 'empty_data' => function (FormInterface $form) { $command = new CreateTripCommand( $form->get('name')->getData() );
return $command; }, )); }}
One step to the finish line
What have I learned?
A clean architecture helps in avoiding the Big Ball of Mud.
Also starting with a very simple domain
Iteration by iteration
Complexity could grow
If our system is tightly coupled
and the domain is scattered
we are losing the chance of responding to changes.
Talking about testing ...
… independence from frameworks, database, UI ...
… means talking about business.
Let the code speak the language of the business
First, taking care of the model
Then choosing the right tool
...
We reached the finish line, well done.
Thank you :-)
Credits
● Eric Evans, - "Domain Driven Design"● “Uncle Bob” - http://blog.8thlight.com/uncle-bob/archive.html and books● http://williamdurand.fr/2013/08/20/ddd-with-symfony2-making-things-clear/● http://williamdurand.fr/2013/08/07/ddd-with-symfony2-folder-structure-and-
code-first/● http://verraes.net/2013/04/decoupling-symfony2-forms-from-entities/● http://www.whitewashing.
de/2012/08/22/building_an_object_model__no_setters_allowed.html● http://nicolopignatelli.me/valueobjects-a-php-immutable-class-library/● http://welcometothebundle.com/domain-driven-design-and-symfony-for-
simple-app/● http://www.slideshare.net/SteveRhoades2/implementing-ddd-concepts-in-
php
Credits
● http://devlicio.us/blogs/casey/archive/2009/02/16/ddd-aggregates-and-aggregate-roots.aspx
● http://www.slideshare.net/perprogramming/application-layer-33335917● http://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-
design/● http://gorodinski.com/blog/2012/04/14/services-in-domain-driven-design-
ddd/● http://www.slideshare.net/jeppec/agile-ddd-cqrs● http://www.slideshare.net/thinkddd/practical-domain-driven-design-cqrs-
and-messaging-architectures● http://www.codeproject.com/Articles/339725/Domain-Driven-Design-Clear-
Your-Concepts-Before-Yo● http://lostechies.com/jimmybogard/2008/05/21/entities-value-objects-
aggregates-and-roots/
Credits
● http://www.slideshare.net/ziobrando/gestire-la-complessit-con-domain-driven-design
● http://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/● http://lostechies.com/jimmybogard/2009/09/03/ddd-repository-
implementation-patterns/● http://www.sapiensworks.com/blog/post/2012/04/18/DDD-Aggregates-
And-Aggregates-Root-Explained.aspx● http://www.udidahan.com/2009/06/29/dont-create-aggregate-roots/● http://jblewitt.com/blog/?p=241● http://www.sapiensworks.com/blog/post/2013/10/18/Modelling-Aggregate-
Roots-Relationships.aspx● http://www.sapiensworks.com/blog/post/2013/01/15/Domain-Driven-
Design-Aggregate-Root-Modelling-Fallacy.aspx
Credits
● http://lostechies.com/jamesgregory/2009/05/09/entity-interface-anti-pattern● http://www.slideshare.net/piotrpelczar/cqrs-28299581● http://richarddingwall.name/2009/10/13/life-inside-an-aggregate-root-part-
1/● http://lostechies.com/jimmybogard/2010/02/24/strengthening-your-domain-
aggregate-construction/● http://guptavikas.wordpress.com/2009/12/21/domain-driven-design-
creating-domain-objects/● http://gorodinski.com/blog/2012/05/19/validation-in-domain-driven-design-
ddd/● http://verraes.net/2013/12/related-entities-vs-child-entities/● http://devlicio.us/blogs/casey/archive/2009/02/20/ddd-the-repository-
pattern.aspx● http://gojko.net/2009/09/30/ddd-and-relational-databases-the-value-object-
dilemma/