DCI in PHP

102 views
Skip to first unread message

Filis Futsarov

unread,
May 12, 2024, 3:32:36 PMMay 12
to object-composition
Hi guys,

I've tried to come up with solution for PHP that is both practical and usable.

So, basically I created two functions that are setup($context) and inject($rolePlayer, $role), this later one is used by setup() to inject the roles into the objects.

What does setup() do? For that I need to show some code first... IN PHP :D.

This is a simple test.
```php
public function test()
{
$checkingAccount = new Account(1, new Currency(100));
$savingsAccount = new Account(2, new Currency(0));

$context = Dci::setup(new TransferMoneyContext(
$checkingAccount,
$savingsAccount,
new Currency(50)
));

$context->execute();

static::assertEquals(new Currency(50), $checkingAccount->availableBalance());
static::assertEquals(new Currency(50), $savingsAccount->availableBalance());
}
```

```php

class TransferMoneyContext
{
/** @var Account&SourceAccount */
public Account $sourceAccount;

/** @var Account&DestinationAccount */
public Account $destinationAccount;

public Currency $transferAmount;

public function __construct(
Account $source,
Account $destination,
Currency $amount
) {
$this->sourceAccount = $source;
$this->destinationAccount = $destination;
$this->transferAmount = $amount;
}

public function execute()
{
$this->sourceAccount->transferTo();
}
}

trait SourceAccount
{
private TransferMoneyContext $context;

public function transferTo(): void
{
if ($this->availableBalance() < $this->context->transferAmount) {
assert(false, 'Unavailable balance');
} else {
$this->decreaseBalance($this->context->transferAmount);

$this->context->destinationAccount->increaseBalance($this->context->transferAmount);
}
}

abstract public function decreaseBalance(Currency $amount): void;
abstract public function availableBalance(): Currency;
abstract public function updateLog(
string $message,
): void;
}

trait DestinationAccount
{
private TransferMoneyContext $context;

public function transferFrom(): void
{
$this->increaseBalance($this->context->transferAmount);
}

abstract public function increaseBalance(Currency $amount): void;
abstract public function updateLog(string $message): void;
}

```


As you can see in **TransferMoneyContext**, there is this docblock `@var Account&SourceAccount`. This is for two things, proper IDE suggestions and it is also read by setup() to detect which role should be injected into the role player within the context (there can be multiple, simply adding more &). This is a supported PHP syntax from PHP 8 to tell that it must have all this classes (through inheritance). I'm using the same syntax but tell to setup() to inject the role, then the IDE suggestions work perfectly.

As you can also see, I'm using traits. There isn't any supported way by the language to inject a Trait into an object at runtime, so what I'm doing inside inject() is eval(). Let's say that we're running this code (this happens inside setup()):

$superpoweredObject = Dci::inject(new Account, SourceAccount::class);


So what inject() is doing is ... (don't get scared). It generates the following class and returns it instantiated but with all the super powers. It extends the role player and copies the required role methods into it, so that it can forward the calls to the role player. And then it also adds the Trait the usual way so that it has the implemented role methods of the role.

I also forward __set() and __get() (for those who know what they're for in PHP).

$rolePlayer holds the original role player, so its properties and methods are accessed through this kind of proxy.

class Account_SourceAccount extends \App\Account
{
use \App\SourceAccount;

private $rolePlayer;


public function decreaseBalance(\App\Currency $amount): void
{
$this->rolePlayer->decreaseBalance(...func_get_args());
}


public function availableBalance(): \App\Currency
{
return $this->rolePlayer->availableBalance(...func_get_args());
}


public function updateLog(string $message): void
{
$this->rolePlayer->updateLog(...func_get_args());
}
}


Then, we also have `$context` inside each Role, this is set by setup() so that the roles can access other roles. That's why the roles in `TransferMoneyContext` are public. This may be improved, but I wanted to have it as untangled as possible from not-very-important extras for now.


I have two questions:
  1. How bad is it that object identity is not preserved? (i think in the past I managed to achieve this but then it was hard to debug because of the way it was done)
  2. The way that it currently works, it is accepted that the required role methods (abstract methods within traits) can be implemented by the other roles that may be injected into the role player. Is this ok or they should only come from the role player?
So this is it!

Please give me your thoughts, I want to publish this on GitHub after I've refined it a bit more. It already works and I've been using it for very simple stuff, but your answers will be the most important ones that I will be able to get probably.

77% of the web uses PHP so I think it will be nice to have the best possible implementation so that at least people can get started with it and experiment. This is the best approach that I have found so far, I've checked all the other solutions but I didn't like them for one reason or another. If you have in mind any other way to improve it, please share.

Thanks,
Filis

Matthew Browne

unread,
May 14, 2024, 10:34:19 AMMay 14
to object-co...@googlegroups.com, Filis Futsarov

Hi Filis,
The short answer is that preserving object identity is essential for DCI, and without that you have something that's "DCI-like" but isn't true to the paradigm. That's not to say that wrapping the role-player with another object to do something similar to DCI isn't useful, and in fact there's no problem with it until you encounter a use case where it matters—at which point it becomes a big problem.

This is a question that has come up repeatedly and there is an FAQ entry about it on the fulloo website. I also wanted to point you to a recent discussion about Java that's quite relevant here as well:

https://groups.google.com/g/object-composition/c/YM0UNIIx_b8/m/L9nYsmIZBAAJ

On 5/12/24 3:32 PM, Filis Futsarov wrote:

This is the best approach that I have found so far, I've checked all the other solutions but I didn't like them for one reason or another.

Was my PHP implementation one of them? I wonder if you could modify that to work better for you, according to your syntax preferences, and modernizing it etc. It contains a similar use of eval to what you mentioned. I'll admit that it can be inconvenient that every data object that might need to play a role needs to implement the RolePlayer trait, but I think this "reverse wrapper" technique (where the role player object wraps the role) may be the only way to implement true DCI in PHP with reasonable syntax, unless you were to develop a solution using source code transformation.

Personally if I were still regularly working in PHP, I think I would probably prefer a source transformation solution, but that would take more time to develop of course, and it might also make debugging more complicated.

The way most source transformation solutions work is to turn the role methods into methods on the context class for the sake of the compiled code, e.g.:

function SourceAccount_transferTo($self) {...}

On this group, we refer to such a solution as "injectionless" since you're not injecting the methods to the object.

As a final alternative, you could consider the following linter (written by Andreas, a member of this group), which uses a manual coding pattern similar to the above but the linter guides you in coding in this style:

https://github.com/ciscoheat/dcisniffer

This syntax is a lot less supportive of a DCI way of thinking, but the linter helps with "mind over matter" :-)

Filis --
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/object-composition/7788e218-b82d-460d-91cc-5ccda63ad104n%40googlegroups.com.

Filis Futsarov

unread,
May 15, 2024, 6:31:37 AMMay 15
to object-composition
Hi Matthew,

Thank you for the reply.

I liked your implementation. I Can't remember very well now, but I think I simply tried to use ActiveRecord for the role player and there were issues with __call(). Anyway, domain objects and ActiveRecord have different rates of change so I could have as well not done it, but I remember the case fitting quite nicely and I ran into that issue there (maybe it could have been fixed). And yes, I was also looking for a "plug & play" sort of implementation .

I know the DCI sniffer, yes.

I will give a thought to the source transformation idea and I may reach out to you for that by email, if you don't mind :)

Thank you!

Filis Futsarov

unread,
May 22, 2024, 10:10:23 AMMay 22
to object-composition
Hi Matthew,

I tweaked your implementation a bit and managed to make it work with Composer (the package manager for those who don't know), Laravel's ActiveRecord and also inject the role in a fancier way using PHP 8.3 syntax :)

If anybody who may come across this post is interested, ping me.

Filis Futsarov

unread,
May 22, 2024, 10:15:11 AMMay 22
to object-composition
I forgot to say thanks!

Matthew Browne

unread,
May 22, 2024, 10:53:51 AMMay 22
to object-co...@googlegroups.com
Glad to hear it! Feel free to raise a PR if you have time.

Filis Futsarov

unread,
May 22, 2024, 12:39:14 PMMay 22
to object-composition
Ok. Btw, I've come across one issue now regarding the required role methods. When I add an abstract method on the Role Trait it throws an exception (not implemented) even if the Domain Object has the method on if. So looking at the code it seems like this was not considered, I'm I right?

Matthew Browne

unread,
May 22, 2024, 1:53:12 PMMay 22
to object-co...@googlegroups.com
Hi Filis,
That's correct, my implementation didn't include a mechanism for specifying or enforcing role-object contracts. If you had success using abstract methods for that before (in your own implementation), then hopefully it could be updated to use that approach...let me know what you find out.

Cheers,
Matt

Filis Futsarov

unread,
May 23, 2024, 9:46:19 AMMay 23
to object-composition
Yes, I'm working on something. I also realised __destruct() doesn't work the way (I think) you expected.

So perhaps a better solution is to make use of the so-called "Template Method" pattern for the Context so that once do() is called it does the cleanup at the end.

Example with the current implementation:

        $account = ...
        $context = new TransferMoney($account,...);
        $context->do();

        // Outside the context, this would be a valid method call, and we don't want that...
        $account->transfer(...);

Matthew Browne

unread,
May 23, 2024, 6:29:23 PMMay 23
to object-co...@googlegroups.com

It's been a long time so I don't remember for sure, but I think I might have just added the __destruct() method as a "better-than-nothing" solution to unbind the role methods. The problem with it, as you discovered, is that it will only be called when it's time for PHP to garbage-collect the context instance, so if you call a role method while that context instance is still in scope, removeAllRoles() won't have been called yet.

Your approach of calling a do() method would avoid that problem, but keep in mind that some DCI contexts have more than one public method. The most common case is for contexts to have a single method to perform some use case, but there can also be more stateful contexts which are more likely to have multiple public methods—think nested contexts - here's an example from Egon's pong game. Another example would be implementing a data model type using a Context, e.g. if the Account object in the money transfer example needed to be a Context for some reason (perhaps if it needed to do something a bit more complex where Ledgers was a role).

I don't know if this would be any better or easier for the programmer to use than just having something like your do() method for every context method, but something like this would be possible:

abstract class Context {
...
function __call($methodName, $args) {
$ret = call_user_func_array([$this, 'contextMethod_' . $methodName], $args);
$this->removeAllRoles();
return $ret;
}
}
...
class TransferMoney extends \DCI\Context
{
...
function contextMethod_transfer() {
$this->sourceAccount->transferOut($this->amount);
}
}
// usage
$moneyTransfer->transfer();


And of course there's the option of just letting the programmer handle cleanup manually; removeAllRoles() is a public method, so you could just call $moneyTransfer->removeAllRoles(); when you're done, but of course that's error-prone because you could forget to do it and nothing would warn you.

Hopefully that gives you some useful food for thought.

Filis Futsarov

unread,
Jun 16, 2024, 4:38:51 AMJun 16
to object-composition
Question: should the Role be able to access RolePlayer's private properties? or that should ONLY happen through the required methods?

I prefer not to, but I wonder if somebody would want to do that...

James O Coplien

unread,
Jun 16, 2024, 6:04:31 AMJun 16
to noreply-spamdigest via object-composition


On 16 Jun 2024, at 10.38, Filis Futsarov <filisf...@gmail.com> wrote:

Question: should the Role be able to access RolePlayer's private properties? or that should ONLY happen through the required methods?

No.

First, this is just wrong on principle.

Second, it requires that all Role-players { P } for a given role R each have those properties. From a design perspective it is no one’s business outside of any object P to know what internal properties it has. That’s just basic information hiding.

Filis Futsarov

unread,
Jun 16, 2024, 6:21:48 AMJun 16
to object-composition
Now that I think about it, this should not only be the case for private properties but also for public properties, right? Especially because of this:

> Second, it requires that all Role-players { P } for a given role R each have those properties.

And there's no way to enforce these...

Thanks

James O Coplien

unread,
Jun 16, 2024, 6:26:20 AMJun 16
to noreply-spamdigest via object-composition

On 16 Jun 2024, at 12.21, Filis Futsarov <filisf...@gmail.com> wrote:

Now that I think about it, this should not only be the case for private properties but also for public properties, right? Especially because of this:

> Second, it requires that all Role-players { P } for a given role R each have those properties.

If by “properties” you mean “data,” then no entity outside of the object should be able to access those. (I know there is a practical exception in C++, but the exception proves the rule.)

Public methods are the only way Roles have of interacting with their Role-players, so of course those methods are accessible to the Role.


And there's no way to enforce these…

I think trygve does a reasonable job of enforcing all of this. Let me know if you find an exception. Concrete code is always a good basis for sound discussion.

Filis Futsarov

unread,
Jun 16, 2024, 6:42:22 AMJun 16
to object-composition
Yes, calling it data makes it much clearer.
And there's no way to enforce these…
By this I meant an "enforcing mechanism" as we do for the required role methods of the Role. So enforcing data!! 🤯🤣

Public methods are the only way Roles have of interacting with their Role-players
Clear now.

> I think trygve does a reasonable job of enforcing all of this. Let me know if you find an exception. Concrete code is always a good basis for sound discussion.
Great, I'll take a look if I need to. In this case though, not allowing Roles to access Role-player's data makes my implementation even easier...

Thanks

Matthew Browne

unread,
Jun 16, 2024, 6:48:44 AMJun 16
to object-co...@googlegroups.com
On 6/16/24 6:42 AM, Filis Futsarov wrote:
By this I meant an "enforcing mechanism" as we do for the required role methods of the Role. So enforcing data!! 🤯🤣

I think that not doing this is actually one of the nice features of DCI. It's great to have the flexibility that any role-player that meets the role-object-contract can play that role (supporting potential future use cases you haven't thought of yet), regardless of how the object chooses to internally store or structure its data.


James O Coplien

unread,
Jun 16, 2024, 6:49:51 AMJun 16
to noreply-spamdigest via object-composition


On 16 Jun 2024, at 12.42, Filis Futsarov <filisf...@gmail.com> wrote:

By this I meant an "enforcing mechanism" as we do for the required role methods of the Role. So enforcing data!!

A class can enforce the presence of a data member in a derived class by itself including the data member…

Roles don’t talk about data.

Filis Futsarov

unread,
Jun 16, 2024, 6:53:20 AMJun 16
to object-composition
I agree.

And well, in that case, you've learned more since your PHP implementation because you were allowing access to data ! 🤣🤣

Look at the "Role" abstract class __get() & __set() methods :)

(I was going to tag you earlier hehe)

Filis Futsarov

unread,
Jun 16, 2024, 6:55:00 AMJun 16
to object-composition
Wops, this last one was for Matthew.

> Roles don’t talk about data.

Right, pure algorithms.

Matthew Browne

unread,
Jun 16, 2024, 8:31:11 AMJun 16
to object-co...@googlegroups.com

Haha, I don't mind you sharing that message with the group.

And well, in that case, you've learned more since your PHP implementation

Hopefully yes :-)

because you were allowing access to data ! 🤣🤣

I agree that those __get() and __set() methods should be removed. I would say though that there could be __get() and __set() on the data class too and in PHP those would be considered part of its public interface...so the next question is if we would ever want that part of the public interface to be included among the public operations in the role-object contract. I would say the answer is maybe yes in theory (for the sake of a few legitimate use cases), but enabling that in my PHP implementation isn't worth it because it allows loopholes like this:

SourceRole {
  function withdraw($amount) {
    $this->balance =
$this->balance - $amount;
  }
}

...
class Account {
  public $balance;
}


It's worth noting that the whole getter/setter discussion is a bit different in PHP than in other languages like C# or Java. PHP has many faults, including making properties and methods public by default when you don't have a modifier. But something I think it got right (or at least is a helpful feature) is that it actually has more of a distinction between messages and methods than most other languages, which allows you to start with a simple data property but not be locked into that. For example, let's say this was the first version of the Employee class and its usage:

class Employee {
  public $salary;
}

$bob = new Employee();
$bob->salary = 100000;


This isn't a realistic example because $salary should never have just been a public data property in the first place (it shouldn't allow zero or negative numbers for one thing), but set that aside for the moment and imagine that the public $salary property met all the business requirements of the first version of the app without any bugs. But after some time passes, there's a new requirement to add some additional validation. The Employee class could be changed as follows:

class Employee {
  private $salary;
 
  public function __get($key) {
    if ($key == 'salary') {
      return $this->salary;
    }
  }

  public function __set($key, $val) {
    if ($key == 'salary') {
      if ($this->validateSalary($val)) {
        $this->salary = $val;
      }
    }
  }

  ...

}

$bob = new Employee();
$bob->salary = 100000;


I'm not saying this to teach you about PHP (you clearly already know this stuff), but so you and the group see what I'm talking about... notice how the usage ($bob->salary = 100000) didn't change, so that whether it's a public data property or a method with some logic in it is just an implementation detail that the author of the data class can change if/when needed.

So it's not quite as simple as saying that you're "exposing internal data" as soon as you use a public data property in PHP—from the perspective of the consumer, public __get() and __set() methods or even plain old public properties can be considered part of the object's public interface, and if done carefully, they might not violate any encapsulation rules but simply be a means of achieving a more convenient syntax for the consumer, e.g.:

$bob->salary = 100000;
echo $bob->salary;


instead of:

$bob->setSalary(100000);
echo $bob->getSalary();


But at the end of the day, sticking to regular methods only (no public properties and no magic methods like __get(), __set(), or __call() ) makes for a role-object contract that's obvious to everyone and should avoid lots of potential issues.

--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To unsubscribe from this group and stop receiving emails from it, send an email to object-composit...@googlegroups.com.

James O Coplien

unread,
Jun 16, 2024, 3:09:12 PMJun 16
to noreply-spamdigest via object-composition
Roles enact parts of use cases.

I don’t think I would ever write a business use case where one of the steps was a “get” or a “set.”

I think having a “get” or a “set” anywhere (not just in DCI) is a design smell.

Matthew Browne

unread,
Jun 16, 2024, 3:31:09 PMJun 16
to object-co...@googlegroups.com
On Sun, Jun 16, 2024, 3:09 PM James O Coplien <jcop...@gmail.com> wrote:
Roles enact parts of use cases.

I don’t think I would ever write a business use case where one of the steps was a “get” or a “set.”

I suppose that role methods would usually correspond to use case steps, but we're talking about role *player* methods. And there's a "get" in many versions of the money transfer example - getBalance (in the Source role).


I think having a “get” or a “set” anywhere (not just in DCI) is a design smell.

I agree, if by that you mean that it's usually a sign of a flawed design - but not always. My setSalary example was just the first thing that came to mind, and it's a good example of the design smell you're talking about. That should probably be 'promote' or 'changeSalary'. But sometimes you need a property to be changeable and it's really just a set...perhaps something like setColor() on a Circle object or setInstructor() on a Course object (the latter is more questionable and would depend on the requirements). Or would you say that those should always be avoided too?


James O Coplien

unread,
Jun 16, 2024, 3:51:55 PMJun 16
to noreply-spamdigest via object-composition


On 16 Jun 2024, at 21.30, Matthew Browne <mbro...@gmail.com> wrote:

I suppose that role methods would usually correspond to use case steps, but we're talking about role *player* methods.

And those are methods of objects. Objects also should bear public interfaces that relate to their behavior rather than to their implementation.


And there's a "get" in many versions of the money transfer example - getBalance (in the Source role).

I think that is a mistake. It should be “balance”.


I think having a “get” or a “set” anywhere (not just in DCI) is a design smell.

I agree, if by that you mean that it's usually a sign of a flawed design - but not always. My setSalary example was just the first thing that came to mind, and it's a good example of the design smell you're talking about. That should probably be 'promote' or 'changeSalary'.

Salary should be a role which is queried when necessary. It’s not a concept that is owned by an employee but is a contract between the employee and employer, so I feel it just belongs in the Context as a Role in its own right.

If an Employee needs the value of its Salary in a computation it merely gets it from the Context (by just saying “Salary”).

Salary is more than a scalar number but usually also entails policies, time bases (hourly or annually) and other information.

If there are many Employees in the Context and the issue of Salary comes up, you probably need a Payroll Role that can convert an Emloyee ID to the right Salary object.

But sometimes you need a property to be changeable and it's really just a set...perhaps something like setColor() on a Circle object or setInstructor() on a Course object (the latter is more questionable and would depend on the requirements). Or would you say that those should always be avoided too?

I’d be inclined to say “yes” in most cases (except for Color, which is probably a value rather than an object), but would have to judge each example case by case.

The instructor/course mapping is an association which is an object in itself. Why did you arbitrarily decide to make the instructor a field of the course, rather than the course a field of the instructor? Or should you have both? I think that either way is implementation-think and that the association object is the right answer.

Matthew Browne

unread,
Jun 16, 2024, 7:30:51 PMJun 16
to object-co...@googlegroups.com
On Sun, Jun 16, 2024, 3:51 PM James O Coplien <jcop...@gmail.com> wrote:

The instructor/course mapping is an association which is an object in itself. Why did you arbitrarily decide to make the instructor a field of the course, rather than the course a field of the instructor? Or should you have both? I think that either way is implementation-think and that the association object is the right answer.

This is interesting; I never thought much about association objects like this from a domain modeling perspective, unless it's an association that has its own properties (e.g. characterName in a Actor-Movie association). My main experience is with having properties on both sides (a bidirectional relationship), thanks to some help from ORM libraries, which I find convenient and intuitive. But setting that aside and thinking about the pure mental model, I'm not so sure - if it works it works I suppose (and the assocation object would certainly work).

Side-note: it's not so easy to find good resources with realistic examples of these kind of old-school OOP design techniques and guidelines such as avoiding getters and setters. It seems like most programmers these days just think of domain objects as bags of data, which is unfortunate (outside of functional programming, and even there as we have seen in past threads on this group, rich object models and DCI can work). Martin Fowler's "anemic domain model" anti-pattern is still pretty well-known, but not well-understood. OTOH, one of the main problems DCI addresses is too much coupling of data and behavior in the same class, so those programming without DCI can't really be blamed for rejecting the idea that data-bag objects are an anti-pattern. (In case anyone is confused by that sentence, I'm saying they're rejecting the idea that domain objects should contain behavior in addition to data.)


Reply all
Reply to author
Forward
0 new messages