Clean architecture with ddd layering in php

Post on 23-Aug-2014

692 Views

Category:

Internet

7 Downloads

Preview:

Click to see full reader

DESCRIPTION

The slides of my talk at PUGRoma. Here, a complete sample code https://github.com/leopro/trip-planner Presentation is also here: http://t.co/5EK56yYBmQ

Transcript

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/

top related