Dion Moult Seriously who ever reads this description.

A DCI architecture implementation in PHP

For those unfamiliar with DCI, DCI stands for Data, Context and Interactions. It’s a way to fill the gap in OOP between what an object is and what an object does. It also gives use-case enactment first class status to improve the readability of the code. It was proposed by Trygve Reenskaug (the man behind MVC) and James O. Coplien.

Although DCI implementations have been done in other languages, it’s a bit lacking in PHP. I am only aware of two other implementations – the first is phpcore-dci, by Joe Chrzanowski. Although it does hit first on Google, I believe its implementation is a little backwards and far too restrictive. For example, it injects roles (and their interactions) into data objects rather than the other way around, ie. casting data objects into roles. It also requires a rather silly convention to follow which may not fit your style.

The second is by Jeremy Bush (lead developer of Kohana), as part of his Auto-Modeler-Demo project, which demonstrates quite a few technologies and practices. It’s definitely very good, and in fact has inspired this implementation, but I was not convinced in the casting technique used (via lambda functions).

Without further ado, here’s the implementation:

<?php
class Validation_Exception extends Exception {
    public $errors = array();
    public function __construct($errors) {
        parent::__construct('Multiple exceptions thrown.');
        $this->errors = $errors;
    }
    public function as_array() {
        return $this->errors;
    }
}

/**
 * A dumb data object for a person.
 */
class Person {
    public $name;
    public function __construct(Array $properties) {
        foreach ($properties as $property_name => $property_value) {
            $this->{'set_'. $property_name}($property_value);
        }
    }
    public function get_name() {
        return $this->name;
    }
    public function set_name($n) {
        $this->name = $n;
    }
}

/**
 * Interfaces allows us to specify what data objects can play this role.
 */
interface Actor_Requirements {
    public function get_name();
    public function set_name($n);
}

/**
 * The class that casts the data object as the role
 */
abstract class Cast_Actor extends Person implements Actor_Requirements {
    use Cast_Interactions;

    public function __construct(Person $p) {
        parent::__construct(get_object_vars($p));
    }
}


/**
 * What the role is able to do
 */
trait Cast_Interactions {
    public function link($roles) {
        foreach ($roles as $role_name => $role_instance) {
            $this->$role_name = $role_instance;
        }
    }
}

trait Romeo_Interactions {
    public function call_juliet() {
        echo $this->get_name(), ': Hey Juliet!', "\n";
        $this->juliet->reject_romeo();
    }

    public function leave() {
        echo $this->get_name(), ': Fine then. Goodbye.', "\n";
        //throw new Exception('The play ended unexpectedly.');
    }
}

trait Juliet_Interactions {
    public function reject_romeo() {
        echo $this->get_name(), ': Not now, sorry.', "\n";
        // Not really anything to do for validation, but just for demonstration
        //throw new Validation_Exception(array('Juliet isn\'t following her script.', 'Juliet rejected Romeo.'));
        $this->romeo->leave();
    }
}

/**
 * Inject role interactions into the casting to make our final roleplayer.
 * Separating the Cast_Foo object and the final roleplaying object allow for 
 * reusing generic casts.
 */
class Romeo extends Cast_Actor {
    use Romeo_Interactions;
}

class Juliet extends Cast_Actor {
    use Juliet_Interactions;
}

/*
// An example of how using traits can be useful
class Director extends Cast_Director {
    use Director_Interactions;
    use RomeoInteractions;
    use JulietInteractions;
}
 */

/**
 * Use case: enact Romeo & Juliet
 */
class Context {
    private $romeo;
    private $juliet;

    public function __construct(Person $p1, Person $p2) {
        // Cast objects into roles
        $this->romeo = new Romeo($p1);
        $this->juliet = new Juliet($p2);

        // Defines connections between roles.
        $this->romeo->link(array(
            'juliet' => $this->juliet
        ));
        $this->juliet->link(array(
            'romeo' => $this->romeo
        ));
    }

    public function execute() {
        try {
            $this->romeo->call_juliet();
        } catch (Validation_Exception $e) {
            $errors['validation'] = $e->as_array();
        } catch (Exception $e) {
            $errors['misc'] = $e->getMessage();
        }

        if (isset($errors)) {
            return array(
                'status' => 'failure',
                'errors' => $errors
            );
        } else {
            return array('status' => 'success');
        }
    }
}

$person1 = new Person(array('name' => 'Romeo'));
$person2 = new Person(array('name' => 'Juliet'));

$context = new Context($person1, $person2);
$result = $context->execute();
print_r($result);

Feel free to refactor this for your own architecture – this setup most definitely should not all be in one single file but should be split up as appropriate for autoloading, semantics or organisation.

I hope somebody finds this useful. It’s licensed under the IANAL license.


2 Comments

Dion Moult says: (26 October 2012)

For those interested, an implementation of DCI in a production app can be seen here:

https://github.com/Moult/Eadrax/tree/v3

Andreas Soderlund says: (29 November 2013)

Hello Dion, sorry about my late comment, I hope you’re still interested in DCI! I just wanted to say that your solution for enabling Roles is unfortunately a wrapper around the original data object, which is not allowed in DCI. Object identity (not the same as equality) must be preserved. If it’s not, very subtle bugs can appear, for example when a third party library is comparing objects.

DCI in PHP is a bit tricky because of this. Basically it requires method injection in objects at runtime (when an object is bound to a Role in a Context), and I haven’t seen such a solution so far.

If you have more questions/comments about DCI, we have the official group here, hope to see you! https://groups.google.com/forum/#!forum/object-composition

Best regards,
Andreas

Leave a Comment