Skip to content

Instantly share code, notes, and snippets.

@mbrowne
Created December 20, 2013 04:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbrowne/8050276 to your computer and use it in GitHub Desktop.
Save mbrowne/8050276 to your computer and use it in GitHub Desktop.
DCI reverse wrapper technique for PHP (for full implementation, see https://github.com/mbrowne/dci-php)
<?php
namespace DCI;
/**
* DCI Role base class
*/
abstract class Role
{
/**
* The data object playing this role (the RolePlayer).
* @var RolePlayerInterface
*/
protected $data;
/**
* The context to which this role belongs
* @var Context
*/
protected $context;
function __construct(RolePlayerInterface $data, Context $context) {
$this->data = $data;
$this->context = $context;
$context->_addToInternalRolePlayerArray($data);
}
/**
* Get a data property
*/
function __get($propName) {
return $this->data->$propName;
}
/**
* Set a data property
*/
function __set($propName, $val) {
$this->data->$propName = $val;
}
/**
* Returns whether or not the property is set on the data object.
* Note that for private/protected properties, this will always return false.
* A future version of this library may change that.
*/
function __isset($propName) {
return isset($this->data->$propName);
}
/**
* Call a method on the data object. Data object methods should only be getters, setters,
* or simple data manipulation methods (or very simple calculation methods like a getName()
* method that returns a first and last name concatenated together).
* All other behavior should go in role methods, which are called normally and don't involve
* this __call function.
*/
function __call($methodName, $args) {
//If the method is public
if (in_array($methodName, get_class_methods($this->data))) {
return call_user_func_array(array($this->data, $methodName), $args);
}
else {
throw new \BadMethodCallException(
"The method '$methodName' does not exist on the class '".get_class($this->data)."'
nor on any of the roles it's currently playing.");
}
return call_user_func_array(array($this->data, $methodName), $args);
}
}
<?php
namespace DCI;
/**
* DCI base RolePlayer trait
* All data/domain objects that could potentially be used as role players in
* a DCI context should use this trait, or inherit from a base class that uses this trait
*
* NOTE: In an ideal DCI implementation, it would be possible to override a data object method
* "foo" with a role method also named "foo". Unfortunately, the __call() magic method in
* PHP only gets called if a method isn't found, so the call $this->foo() will always go to the data
* object if there is a "foo" method defined there. So the names of role methods always need to be
* different from any existing methods on the data class.
*/
trait RolePlayer
{
/**
* An array of the methods currently being played by this object,
* indexed by the classname of the context. The method names are keys
* pointing to the role objects, e.g.:
*
* $roleMethods = array(
* 'MoneyTransferContext' => array(
* 'withdraw' => [SourceAccount role object]
* 'transferFrom' => [SourceAccount role object]
* 'deposit' => [Destinationaccount role object]
* )
* )
*
* The current struture obviously does not allow for more than one role in the same
* context with the same method name, but that feature is certainly possible
* and may be added in the future.
*
* @var array
*/
private $roleMethods = array();
/**
* The currently active DCI context
* @var DCI\Context
*/
private $currentContext;
private $currentContextClassName;
/**
* Add a role to this object
* @param string $roleName
* @param Context $context
* @return \DCI\RolePlayer
*/
function addRole($roleName, Context $context) {
$this->_setCurrentContext($context);
$roleClassName = $this->currentContextClassName.'\Roles\\'.$roleName;
if (!class_exists($roleClassName, false)) {
throw new \InvalidArgumentException("The role '$roleClassName' is not defined
(it should be defined in the same *file* as the context to which it belongs)."
);
}
$role = new $roleClassName($this, $context);
$this->bindRoleMethods($role);
return $this;
}
function removeRole($roleName, Context $context) {
$roleMethods = &$this->roleMethods[get_class($context)];
if ($roleMethods) {
foreach ($roleMethods as $methodName => $role) {
if (preg_match('/\\'.$roleName.'$/i', get_class($role))) {
unset($roleMethods[$methodName]);
}
}
}
return $this;
}
protected function bindRoleMethods($role) {
//Add the role methods to the $this->roleMethods array
...
}
/**
* IMPORTANT!
* If subclasses implement __call(), they MUST call parent::__call()
* (either before or after their own __call() logic) or else role methods will not work!
*/
function __call($methodName, $args) {
if (isset($this->roleMethods[$this->currentContextClassName][$methodName])) {
$role = $this->roleMethods[$this->currentContextClassName][$methodName];
}
if (!isset($role) || !method_exists($role, $methodName)) {
//This throws \DCI\BadMethodCallException instead of just \BadMethodCallException for an important reason.
//See \BadMethodCallException for what that reason is.
throw new \DCI\BadMethodCallException(
"There is no public method '$methodName' on class '".get_class($this)."' nor on any of the roles it is currently playing ".
"(note that it might not be playing any roles, in which case this is just a regular bad method call). ".
"If the role belongs to a context that acts as a sub-context in another context, make sure that the parent context initialized the ".
'sub-context correctly, e.g. $this->fooContext = $this->initSubContext(new \UseCases\Foo($arg1, $arg2)).');
}
//Call $role->$methodName() with the given arguments
return call_user_func_array(array($role, $methodName), $args);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment