PHP y DDD: ¿Cómo garantizar que solo un servicio pueda llamar a un método en una entidad?

I'm working with a domain model, in which I have a Reservation class:

class Reservation
{
    public function changeStatus($status) { ... }
}

Debido a que el changeStatus() method should only be called in a context where all appropriate notifications are sent (emails, ...) I would like to restrict the call to this method to a ReservationService:

class ReservationService
{
    public function confirmReservation(Reservation $reservation)
    {
        $reservation->changeStatus(Reservation::STATUS_CONFIRMED);
        // commit changes to the db, send notifications, etc.
    }
}

Because I'm working with PHP, there is no such concept as visibilidad del paquete or clases de amigos, así que mi changeStatus() method is just public and therefore callable from anywhere in the application.

The only solution I found to this problem, is to use some kind of doble despacho:

class Reservation
{
    public function changeStatus(ReservationService $service)
    {
        $status = $service->getReservationStatus($this);
        $this->setStatus($status);
    }

    protected function setStatus($status) { ... }
}

The potential drawbacks are:

  • That complicates the design a bit
  • That makes the entity aware of the Service, no sure whether that's actually a drawback or not

Do you guys have any comment on the above solution, or a better design to suggest to restrict access to this changeStatus() ¿método?

preguntado el 08 de noviembre de 11 a las 12:11

4 Respuestas

Use an interface which enforces the context you need:

interface INotifiable {
  public function updated( $reservation );
}

class Reservation {
  public function changeStatus( $status, INotifiable $notifiable ){
    $this->setStatus( $status );
    $notifiable->updated( $this );
  }
}

class EmailNotifier implements INotifiable {
  public function updated( $reservation ){
    $this->sendUpdateEmail( $reservation ); //or whatever
  }
}

The reservation then doesn't need to know anything about the service. An alternative would be to define events on Reservation, but that's added complexity you probably don't need.

respondido 08 nov., 11:16

That's an interesting approchach! Could you just develop a bit on events, what does it mean exactly? - BenMorel

Aquí hay una descripción general rápida: alexatnet.com/content/programming-events-php. You could also look into Domain Events (martinfowler.com/eaaDev/DomainEvent.html) but that's probably overkill. - freír

You can send messages from one domain entity to another. Only objects that are capable of producing certain messages will call the method in question. A code snippet is below. This solution is for projects where dependency injection is a sort of religion like here PHP+DDD.

The Reservation Service gets a message factory. The factory is injected through the constructor method. Objects that don't have this factory cannot issue this sort of messages. (Of course you must restrict object instantiation to factories.)

class Domain_ReservationService
{

    private $changeStatusRequestFactory;

    public function __construct(
        Message_Factory_ChangeStatusRequest $changeStatusRequestFactory
    ) {
        $this->changeStatusRequestFactory = $changeStatusRequestFactory;
    }

    public function confirmReservation(Domain_Reservation $reservation) {

        $changeStatusRequest = $changeStatusRequestFactory->make(
            Reservation::STATUS_CONFIRMED
        );

        $reservation->changeStatus($changeStatusRequest);
        // commit changes to the db, send notifications, etc.

    }

}

The Reservation object checks the contents of the message an decides what to do.

class Domain_Reservation
{

    public function changeStatus(
        Message_Item_ChangeStatusRequest $changeStatusRequest
    ) {
        $satus = $changeStatusRequest->getStatus();
        ...
    }

}

Message object is a DDD value object. (Sometimes it acts like a strategy.)

class Message_Item_ChangeStatusRequest
{
    private $status;

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

    public function getStatus() {
        return $this->$status;
    }

}

This factory produces messages.

class Message_Factory_ChangeStatusRequest
{

    public function make($status) {
        return new Message_Item_ChangeStatusRequest ($status);
    }

}

All domain objects are produced by this layer factory.

class Domain_Factory
{

    public function makeReservationService() {
        return new Domain_ReservationService(
            new Message_Factory_ChangeStatusRequest()
        );
    }

    public function makeReservation() {
        return new Domain_Reservation();
    }

}

The classes above can be used in your application as follows.

$factory = new Domain_Factory();
$reservationService = $factory->makeReservationService();
$reservation = $factory->makeReservation();
$reservationService->confirmReservation($reservation);

But I don't see why you don't want to use $reservation->beConfirmed() instead of passing status constants.

Respondido 30 ago 12, 19:08

Yo no estoy en contra $reservation->beConfirmed(), that's probably better actually. I just want to restrict calls to this method to the Service only. Your messaging solution seems to just add complexity to me, and no added value, as any object could create a ChangeStatusRequest; not just the ReservationService! - BenMorel

I see. I know little ofthe system you make.Another option could be to introduce a status object that is shared between the Reservation and ReervationService.A Reservation has one status object as a private property. The ReservationService has an array of status objects of all reservations created. This array is also private to ReservationService. You may want to use spl_object_hash() to organize this array. So inside ReservationService->confirmReservation(Reservation $reservation) you get the object ID of the reservation. By the ID you find the satus in array and change it. Only RS can do it. - Alexander Novikov

Your initial solution is also quite ok I think. We used it once in a project to pass the state of a DDD-entity to its DDD-repository. - Alexander Novikov

It actually sounds like this is missing a very important concept, namely a Gestor de procesos. A ProcessManager represents a distributed business transaction spanning multiple contexts. In reality it is a simple Máquina de estados finitos.

An example workflow:

  1. A PlaceReservationCommand es enviado a la ReservationContext, which handles it and publishes a ReservationWasPlacedEvent que el ReservationProcessManager is subscribed to.
  2. La ReservationProcessManager recibe el ReservationWasPlacedEvent and validates that it can transition to the next step. I then sends a NotfiyCustomerAboutReservationCommand de las personas acusadas injustamente llamadas NotificationContext. It now listens to ReservationNotificationFailedEvent y ReservationNotificationSucceededEvent.
  3. Ahora el ReservationProcessManager envía el ConfirmReservationCommand de las personas acusadas injustamente llamadas ReservationContext only when it received the ReservationNotificationSucceededEvent.

The trick here is that there is no status field in Reservation. El Gestor de procesos is responsible of tracking the state of this business transaction. Most likely there is a ReservationProcess Aggregate that contains the statuses like ReservationProcess::INITIATED, ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTED, ReservationProcess::CUSTOMER_NOTIFIED, ReservationProcess::CONFIRMATION_REQUESTED y ReservationProcess::CONFIRMED. The last state indicates a finite state that marks the process as hecho.

Respondido 05 ago 16, 12:08

Interesting, but in terms of entities / database, where is the reservation status actually stored then? - BenMorel

La ProcessManager must persist its own state anyway, most probably in a ReservationProcess Aggregate that holds a reference to the Reservation's identity to be able to recover from outages i.e. - Thomas Ploch

One of the things that the Symfony2 and FLOW3 frameworks have adopted is tagging their stable public API with an @api annotation comment.

While this is not exactly what you're looking for, it comes close. It documents the parts of your API that users can rely on. Plus your entity does not have to know about the service, avoiding the evil circular dependency.

Ejemplo:

class Request
{
    /**
     * Gets the Session.
     *
     * @return Session|null The session
     *
     * @api
     */
    public function getSession()
    {
        return $this->session;
    }
}

respondido 08 nov., 11:16

I'm accepting this answer, which is a pragmatic approach, even though I've kept my original approach (doble despacho) for now! - BenMorel

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.