Skip to content

Instantly share code, notes, and snippets.

@mbrowne
Last active February 8, 2019 14:39
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbrowne/5562643 to your computer and use it in GitHub Desktop.
Save mbrowne/5562643 to your computer and use it in GitHub Desktop.
Wrapper-based DCI in PHP accounting for the object identity problem
<?php
namespace DomainObjects;
class Account
{
protected $balance = 0;
function __construct($initialBalance) {
$this->balance = $initialBalance;
}
function getBalance() {
return $this->balance;
}
function increaseBalance($amount) {
$this->balance += $amount;
}
function decreaseBalance($amount) {
$this->balance -= $amount;
}
}
<?php
namespace Contexts
{
use DCI\Role,
DomainObjects\Account,
Contexts\MoneyTransfer\Roles;
class MoneyTransfer extends \DCI\Context
{
//These would ideally be private but they need to be public so that the roles can access them,
//since PHP doesn't support inner classes
public $sourceAccount;
public $destinationAccount;
public $amount;
function __construct($sourceAccount, $destinationAccount, $amount) {
$this->sourceAccount = Roles\SourceAccount::init($sourceAccount, $this);
$this->destinationAccount = Roles\DestinationAccount::init($destinationAccount, $this);
$this->amount = $amount;
}
function transfer() {
$this->sourceAccount->transfer($this->amount);
}
function test() {
$nestedContext = new MoneyTransfer($this->sourceAccount, $this->destinationAccount, $this->amount);
var_dump($nestedContext->sourceAccount === $this->sourceAccount); //true
}
}
}
//Roles are defined in a sub-namespace of the context as a workaround for the fact that
//PHP doesn't support inner classes
namespace Contexts\MoneyTransfer\Roles
{
use DCI\Role;
class SourceAccount extends Role
{
function withdraw($amount) {
$this->decreaseBalance($amount);
}
function transfer($amount) {
$this->context->destinationAccount->deposit($amount);
$this->withdraw($amount);
}
}
class DestinationAccount extends Role
{
function deposit($amount) {
$this->increaseBalance($amount);
}
}
}
<?php
namespace DCI;
abstract class Role
{
/**
* The underlying data object
* @var object
*/
public $data;
/**
* The context to which this role belongs
* @var Context
*/
protected $context;
//These makes it possible private/protected properties on the data object to be accessed
protected $dataReflClass;
protected $dataPublicReflProperties = array();
/**
* Contexts should not call this contructor directly
* (this constructor should only be called by Context::bindRole())
*
* @param object $dataObject
* @param Context $context
*/
function __construct($dataObject, Context $context) {
if (!is_object($dataObject)) {
throw new \InvalidArgumentException("\$dataObject must be an object (it could be a collection object but not a regular array).");
}
$this->data = $dataObject;
//Store reflection data for potential use by the __get(), __set(), __isset(), and __call() methods
$this->dataReflClass = new \ReflectionClass($this->data);
$refl_properties = $this->dataReflClass->getProperties(\ReflectionProperty::IS_PUBLIC);
foreach ($refl_properties as $prop) {
$this->dataPublicReflProperties[$prop->name] = $prop;
}
$this->context = $context;
}
/**
* Bind the methods of this role to a data object
* @param object $dataObject
* @param Context $context
* @return Role
*/
static function init($dataObject, $context) {
return $context->bindRole($dataObject, get_called_class());
}
/**
* __get
*
* Allows properties of data objects to be accessed directly (e.g. $this->some_property) instead of
* having to go through $this->data (e.g. $this->data->some_property).
*
* @param string
*/
function __get($propName)
{
if (in_array($propName, $this->dataPublicReflProperties)) {
return $this->data->$propName;
}
elseif (method_exists($this->data, 'get_'.$propName)) {
return $this->data->{'get_'.$propName}();
}
else {
//If we've reached here, then it's a private or protected property on the data object
$refl_prop = $this->dataReflClass->getProperty($propName);
$refl_prop->setAccessible(true);
return $refl_prop->getValue($this->data);
}
}
/**
* __set
*
* Allows properties of data objects to be set directly (e.g. $this->some_property = 'new value') instead of
* having to go through $this->data (e.g. $this->data->some_property = 'new value').
*
* @param string
* @pram mixed
*/
function __set($propName, $val) {
if (in_array($propName, $this->dataPublicReflProperties)) {
$this->data->$propName = $val;
}
elseif (method_exists($this->data, 'set_'.$propName)) {
$this->data->{'set_'.$propName}($val);
}
else {
//Allow new data properties to be created on the fly
//We assume that the programmer wants to create a previously undefined data property rather than a
//new role property. Role properties should always be declared in the role class, but creating
//data properties on the fly can sometimes be useful.
$this->data->$propName = $val;
}
}
function __isset($propName) {
return isset($this->data->$propName);
}
/**
* __call
*
* Delegates to the data object
*
* @param string $method
* @param array $args
*/
function __call($method, $args) {
if (method_exists($this->data, $method)) {
return call_user_func_array(array($this->data, $method), $args);
}
else throw new \BadMethodCallException("The method '$method' does not exist on the class ".get_class($this)." nor on the class ".get_class($this->data));
}
}
<?php
$acct1 = new \DomainObjects\Account(20);
$acct2 = new \DomainObjects\Account(0);
$moneyTransfer = new \Contexts\MoneyTransfer($acct1, $acct2, 10);
$moneyTransfer->transfer();
var_dump($acct1->getBalance(), $acct2->getBalance());
$moneyTransfer->test();
<?php
namespace Contexts
{
use DCI\Role,
DomainObjects\Account,
Contexts\MoneyTransferContext\Roles;
class MoneyTransferContext extends \DCI\Context
{
//These would ideally be private but they need to be public so that the roles can access them,
//since PHP doesn't support inner classes
public $sourceAccount;
public $destinationAccount;
public $amount;
function __construct($sourceAccount, $destinationAccount, $amount) {
$this->sourceAccount = Roles\SourceAccount::init($sourceAccount, $this);
$this->destinationAccount = Roles\DestinationAccount::init($destinationAccount, $this);
$this->amount = $amount;
}
function transfer() {
$this->sourceAccount->transfer($this->amount);
}
function test() {
$nestedContext = new MoneyTransferContext($this->sourceAccount, $this->destinationAccount, $this->amount);
var_dump($nestedContext->sourceAccount === $this->sourceAccount); //true
}
}
}
//Roles are defined in a sub-namespace of the context as a workaround for the fact that
//PHP doesn't support inner classes
namespace Contexts\MoneyTransferContext\Roles
{
use DCI\Role;
class SourceAccount extends Role
{
function withdraw($amount) {
$this->decreaseBalance($amount);
}
function transfer($amount) {
$this->context->destinationAccount->deposit($amount);
$this->withdraw($amount);
}
}
class DestinationAccount extends Role
{
function deposit($amount) {
$this->increaseBalance($amount);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment