You can also use a Role Object pattern and good old aggregation.
Instead of having smart Entities that contain all your business logic, you make them dumb and move all the business logic into Roles that aggregate the dumb Entities then. Your behaviors are then first-class citizens living inside the Roles.
Example:
interface BannableUser
{
public function ban();
}
Having an interface with one particular behavior follows the Interface Segregation Principle. It also dramatically increases possible reuse since you are more likely to reuse individual behaviors than an Entity with an application-specific collection of behaviors.
Now to implement that, you create an appropriate Role Class:
class BannableUserRole implements BannableUser
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function ban()
{
$this->user->isBanned = true;
}
}
You still have a User entity but it's completely stripped of all behaviors. It's essentially just a bag of Getters and Setters or public properties. It represents what your System is. It's the data part, not the interaction part. The interaction is inside the Roles now.
class User
{
public $isBanned;
// … more properties
}
Now assuming you have some sort of Web UI, you can do the following in your controller:
class BanUserController implements RequestHandler
{
// …
public function handleRequest(Request $request)
{
$userId = $request->getVar('user_id');
$user = $this->userRepository->findById($userId);
$bannableUser = new BannableUserRole($user);
$bannableUser->ban();
}
}
You can decouple this further by moving the actual lookup of the User and assignment of the Role into a UseCase class. Let's call it Context:
class BanUserContext implements Context
{
public function run($userId)
{
$user = $this->userRepository->findById($userId);
$bannableUser = new BannableUserRole($user);
$bannableUser->ban();
}
}
Now you have all the business logic inside your Model layer and fully isolated from your User Interface. The Contexts are what your system does. Your Controller will only delegate to the appropriate Context:
class BanUserController implements RequestHandler
{
// …
public function handleRequest(Request $request)
{
$this->banUserContext->run($request->getVar('user_id'));
}
}
And that's it. No need for Runkit or similar hackery. The above is a simplified version of the Data Context Interaction architectural pattern, in case you want to further research this.