Implementing DCI in PHP

Disclaimer: This post is intended only as a means of clarifying my own ideas on implementing DCI in PHP. I haven't yet done any real research on if/how others in the PHP community are accomplishing this, but as a matter of practice, I prefer to get  my own ideas down before influencing them with the ideas of others. Once I have a better understanding of how best to implement DCI in PHP, I'll publish a new post and update this one with a link.

Introduction

DCI is one today's coolest cutting-edge programming models. It's so new that a lot of development communities are still trying to figure out how best to implement DCI in their language (the biggest challenge being run-time method injection, which happens to be a staple of DCI). PHP is no exception, and I've decided to tackle implementing DCI in PHP as both a thought experiment and, perhaps, my new way of doing things.

The DCI paradigm, as I understand it today, has three basic rules:

  1. Data (what the system is) should be encapsulated in a manner that only includes the  most basic functionality necessary to access the data (e.g. getters and setters).
  2. Context (or use case) expand data objects to include the methods/actions necessary to the specific use case through method injection.
  3. Interaction (what the system does) introduces that data objects to the context(s) necessary to accomplish a particular task or set of tasks and then preforms the task(s).

To implement DCI in PHP we can:

  1. Develop a base Data class that provides data storage and primitive getters and setters.
  2. Develop a Context interface that defines methods for assigning/unassigning roles as needed.
  3. Implement the Context interface in a ContextualData class, using __call() to execute the injected methods.

Implementing DCI in PHP

The Data Class

class Data
{
    protected $_data;

    public function __construct($data)
    {
        $this->_data = $data;
    }

    public function __get($name)
    {
        if (array_key_exists($name, $this->_data)) {
            return $this->_data;
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        $this->_data[$name] = $value;
    }
}

The basic Data class meets the requirements set forth in the first rule: data can be accessed to either read or write, but that's it.

The Context Interface

interface Context
{
    public function hasRole($role);

    public function assign($role, $methods);

    public function unassign($role);

    public function unassignAll();
}

The Context interface defines basic functionality to:

  • determine whether a given role as been assigned
  • assign a role
  • unassign a role
  • unassign all roles (remove all context

Implementing the Context Interface

Now comes the actual logic necessary to implement DCI in PHP:

class ContextualData extends Data implements Context
{
    protected $_roles;

    public function __construct($data)
    {
        parent::__construct($data);
        $this->_roles = array();
    }

    public function __call($function, $args)
    {
         foreach ($this->_roles as $role)
         {
             if (array_key_exists($function, $role)) {
                 $func_args = array_merge(array('data' => $this), $args);
                 return call_user_func_array($role[$function], $func_args);
             }
         }
    }

    public function hasRole($role)
    {
         return array_key_exists($role, $this->_roles);
    }

    public function assign($role, $methods)
    {
        $this->_roles[$role] = $methods;
    }

    public function unassign($role)
    {
        unset($this->_roles[$role]);
    }

    public function unassignAll()
    {
        $this->_roles = array();
    }
}

In this implementation, a role is a named array using the method names as the keys and callable object (e.g. anonymous functions) as the keys. To assign a role, you pass the role name and the role array to the assign() method. To assign a role, you simply pass the role name to unassign().

Wrapping Up

Now that we have the data and context taken care of, let's wrap up with an example of interaction using the cliché (if not classic) use case of transferring money between two bank accounts:

// Instantiate two accounts
$account1 = new ContextualData(array('balance' => 100));
$account2 = new ContextualData(array('balance' => 25));

// Define the source and destination account roles
$source_account = array(
        'withdraw' => function($data, $amount) {
                $data->balance = $data->balance - $amount;
            }
);
$dest_account = array(
        'deposit' => function($data, $amount) {
                $data->balance = $data->balance + $amount;
            }
);

// Assign the roles and execute the transfer
$account1->assign('SourceAccount', $source_account);
$account2->assign('DestAccount', $dest_account);
$account1->withdraw(50);
$account2->deposit(50);
echo "Account 1 balance is: " . echo $account1->balance . PHP_EOL;
echo "Account 2 balance is: " . echo $account2->balance . PHP_EOL;

The output of our script would be:

Account 1 balance is: 50
Account 2 balance is: 75

Thoughts?

Like I said in my disclaimer, I'm not really claiming that this is the best way to implement DCI in PHP. I just wanted to get my own thoughts down somewhere and, perhaps, get some feedback from others.