Skip to content

Instantly share code, notes, and snippets.

@josecelano
Created March 31, 2015 14:45
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josecelano/ded0a68154376dbec7ac to your computer and use it in GitHub Desktop.
Save josecelano/ded0a68154376dbec7ac to your computer and use it in GitHub Desktop.
Validation in DDD
<?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