Last active
February 8, 2019 14:39
-
-
Save mbrowne/5562643 to your computer and use it in GitHub Desktop.
Wrapper-based DCI in PHP accounting for the object identity problem
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 | |
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; | |
} | |
} |
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 | |
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); | |
} | |
} | |
} |
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 | |
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)); | |
} | |
} |
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 | |
$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(); |
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 | |
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