Technical

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.

Spread the love
Tagged: , , , , , ,

3 Comments

  1. 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

  2. Andreas makes an interesting point. There is a way to actually inject something resembling a “method” into an object in PHP at runtime. I have not tried this in PHP 7, but I know it worked in PHP 5. Unfortunately, in PHP 5 it was very expensive, so I’m hoping it works better in 7. I will try it soon.

    Anyway, the way it works is by taking advantage of an unadvertised way in which PHP is actually a lot like JavaScript. You see, with PHP objects, you can actually add properties to them dynamically after they have been constructed, just by pretending that the property already existed. So you can set it to an anonymous function and presto, injected method! Well, almost. The problem is if you then try to call it like a normal method, it doesn’t work. But you can make it work using one of the standard magic methods that PHP supports. I can’t remember for sure, but it is probably the __call() method, which gets invoked when you try to call a method that does not exist.

    Alternatively, you could have the property already exist, but just be set to null or something like that. Then you can inject an implementation at runtime by setting it to an anonymous function. But that would still rely on the same magic to make it callable with a method syntax.

Leave a Reply

Your email address will not be published. Required fields are marked *