Created
March 31, 2015 14:45
-
-
Save josecelano/ded0a68154376dbec7ac to your computer and use it in GitHub Desktop.
Validation in DDD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class PaymentController | |
{ | |
public function makePayment() | |
{ | |
// POST data: | |
// fromCustomerId | |
// toCustomerId | |
// amount | |
$makePaymentCommand = $this->createMakePaymentCommand(); | |
// Form data_class is MakePaymentCommand | |
$createPaymentForm = $this->formFactory->createPaymentForm($makePaymentCommand); | |
$createTransactionForm->handleRequest($request); | |
// Form validation using Symfony validation.yml | |
if (!$createTransactionForm->isValid()) { | |
// ... Invalid form | |
// delegates in MakePaymentCommand validation using validation.yml | |
return $response; | |
} | |
$makePaymentCommand = $createTransactionForm->getData(); | |
try { | |
$this->commandBus->handle($makePaymentCommand); | |
// Payment OK | |
// ... | |
return new RedirectResponse($successUrl); | |
} catch (AuthorizationFailedException $e) { | |
return $errorResponse; | |
} | |
} | |
} | |
class MakePaymentCommand | |
{ | |
/** | |
* @var string | |
*/ | |
public $fromCustomerId; | |
/** | |
* @var string | |
*/ | |
public $toCustomerId; | |
/** | |
* @var float | |
*/ | |
public $amount; | |
} | |
class MakePaymentCommandHandler | |
{ | |
public function handle(Message $command) | |
{ | |
// Option 1 | |
$command->validate(); | |
// Option 2: this case is implemented below | |
$this->makePaymentCommandValidator->validate($command); | |
$fromCustomerId = $command->fromCustomerId; | |
$toCustomerId = $command->toCustomerId; | |
$amount = $command->amount; | |
// Where to put this is a question for another post | |
$this->authenticationService->checkAutentication(); | |
$this->authorizationService->checkAuthorization(PaymentAction::CREATE); | |
// Should this be validated in the command? in the service? | |
$fromCustomer = $this->customerRepository->userById(new CustomerId($fromCustomerId)); | |
if ($fromCustomer === null) { | |
thrown new CustomerNotFoundException(); | |
} | |
$toCustomer = $this->customerRepository->userById(new CustomerId($toCustomerId)); | |
if ($toCustomer === null) { | |
thrown new CustomerNotFoundException(); | |
} | |
$this->paymentservice->makePayment($fromCustomer, $toCustomer, new Money($amount, 'EUR')); | |
} | |
} | |
interface ValidationSpecification | |
{ | |
/** | |
* @return boolean | |
*/ | |
public function IsSatisfiedBy($object); | |
} | |
class CustomerIdValidSpecification implements ValidationSpecification | |
{ | |
/** | |
* @var string $object | |
* @return boolean | |
*/ | |
public function isSatisfiedBy($object) | |
{ | |
// Internally repository uses version 4 (random) UUID for ids. | |
// It only validates that string is a valid id (not if exists in database) | |
if ($this->customerRepository->isValidIdentity($customerIdString)) { | |
return true; | |
} else { | |
return false; | |
} | |
} | |
} | |
// Name should be ToCustomerIdValidSpecification inside MakePaymentCommand namespace | |
class ToCustomerIdValidMakePaymentCommandSpecification | |
{ | |
/** | |
* @var string $object | |
* @return boolean | |
*/ | |
public function IsSatisfiedBy($object) | |
{ | |
// This command specification uses another base specification | |
return $customerIdValidSpecification->isSatisfiedBy($object->toCostumerId()); | |
} | |
} | |
// Name should be ToCustomerIdValidSpecification inside MakePaymentCommand namespace | |
class FromCustomerIdValidMakePaymentCommandSpecification | |
{ | |
/** | |
* @var string $object | |
* @return boolean | |
*/ | |
public function IsSatisfiedBy($object) | |
{ | |
// This command specification uses another base specification | |
$customerIdValidSpecification = new CustomerIdValidSpecification(); | |
return $customerIdValidSpecification->isSatisfiedBy($object->fromCostumerId()); | |
} | |
} | |
class CustomerExistSpecification implements ValidationSpecification | |
{ | |
/** | |
* @var CustomerId $object | |
* @return boolean | |
*/ | |
public function isSatisfiedBy(CustomerId $object) | |
{ | |
// It chceks if user exists | |
$fromCustomer = $this->customerRepository->userById(new CustomerId($fromCustomerId)); | |
if ($fromCustomer !== null) { | |
return true; | |
} else { | |
return false; | |
} | |
} | |
} | |
// http://stackoverflow.com/questions/5818898/where-to-put-global-rules-validation-in-ddd | |
interface Validator | |
{ | |
public function isValid($object); | |
public function brokenRules($object); | |
} | |
class MakePaymentCommandValidator implements Validator | |
{ | |
private $rules; | |
function __construct() | |
{ | |
$rules[] = new FromCustomerIdValidMakePaymentCommandSpecification(); | |
$rules[] = new ToCustomerIdValidMakePaymentCommandSpecification(); | |
} | |
public function isValid($makePaymentCommand) | |
{ | |
if (count($this->brokenRules($makePaymentCommand)) > 0) | |
return false; | |
else | |
return true; | |
} | |
public function brokenRules($makePaymentCommand) | |
{ | |
$brokenRules = array(); | |
foreach($rules as $rule) { | |
if (!$rule->isSatisfiedBy($makePaymentCommand) | |
$brokenRules[] = get_class($rule); // Specification could have a getName or Id method. | |
} | |
} | |
} | |
class PaymentService | |
{ | |
/** | |
* @var PaymentValidator | |
*/ | |
private $paymentValidator; | |
public function makePayment(Customer $fromCustomer, Customer $toCustomer, Money $amount) | |
{ | |
// Could we use specifications with a service? | |
if ($fromCustomer->balance->greaterThanOrEqual($amount)) { | |
throw new NotEnoughBalance(); | |
} | |
// Can throw an exception is any of the customer does not exist. | |
// It is a duplicate code as it is done before calling the service. | |
// And I do not like to injnect the validator in the Payment constructor. | |
$payment = new Payment($fromCustomer, $toCustomer, $amount, $this->paymentValidator); | |
// BEGIN execute atomically | |
$fromCustomer->decreaseBalance($amount); | |
$toCustomer->increaseBalance($amount); | |
$this->customerRepository->update($fromCustomer); | |
$this->customerRepository->update($toCustomer); | |
// END execute atomically | |
// We could use Accounts but the main purpose of the sample is validation | |
$this->paymentRepository->update($payment); | |
} | |
} | |
class Customer | |
{ | |
public function decreaseBalance(Money $amount) | |
{ | |
// ... | |
} | |
public function increaseBalance(Money $amount) | |
{ | |
// ... | |
} | |
// ... | |
} | |
class PaymentValidator implements Validator | |
{ | |
private $rules; | |
function __construct() | |
{ | |
$rules[] = new FromCustomerExistsPaymentSpecification(); | |
$rules[] = new ToCustomerExistsPaymentSpecification(); | |
} | |
public function isValid($payment) | |
{ | |
if (count($this->brokenRules($payment)) > 0) | |
return false; | |
else | |
return true; | |
} | |
public function brokenRules($payment) | |
{ | |
$brokenRules = array(); | |
foreach($rules as $rule) { | |
if (!$rule->isSatisfiedBy($payment) | |
$brokenRules[] = get_class($rule); // Specification could have a getName or Id method. | |
} | |
} | |
} | |
// Name should be FromCustomerExistsSpecification inside Payment namespace | |
class FromCustomerExistsPaymentSpecification | |
{ | |
/** | |
* @var string $object | |
* @return boolean | |
*/ | |
public function IsSatisfiedBy($object) | |
{ | |
// This command specification uses another base specification | |
$customerExistSpecification = new CustomerExistSpecification(); | |
return $customerExistSpecification->isSatisfiedBy($object); | |
} | |
} | |
class Payment | |
{ | |
/** | |
* @var Customer | |
*/ | |
private $fromCustomer; | |
/** | |
* @var ToCustomer | |
*/ | |
private $toCustomer; | |
/** | |
* @var Money | |
*/ | |
private $amount; | |
/** | |
* @var PaymentValidator | |
*/ | |
private $paymentValidator; | |
function __construct(Customer $fromCustomer, Customer $toCustomer, Money $amount, PaymentValidator $paymentValidator) | |
{ | |
// Validation implemented with type hinting. | |
// For others parameters we could use Assertion::**** | |
$this->fromCustomer = $fromCustomer; | |
$this->toCustomer = $toCustomer; | |
$this->amount = $amount; | |
$this->paymentValidator = $paymentValidator; | |
} | |
function isValid() | |
{ | |
return $this->paymentValidator->isValid($this); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment