PHP: Am I mixing up event-driven programming with signals-aware interfaces (Signal and Slots / Observer Pattern)?

后端 未结 2 1094
遥遥无期
遥遥无期 2021-01-30 11:50

I\'ve seen a lot of people saying that Symfony2, Zend Framework 2 and others are event-driven.

On the desktop world, by event-driven programming I understand that the a

相关标签:
2条回答
  • 2021-01-30 12:18

    Disclaimer: It's a long answer, but I think it's worth the read with all its references. And IMHO it leads to a conclusive answer.

    I've been struggling upon this subjects the last couple of days and, if I've read all correctly, the answer is:

    Event-driven !== Request-driven

    "[...] I find this the most interesting difference in event collaboration, to paraphrase Jon Udell: request driven software speaks when spoken to, event driven software speaks when it has something to say.

    A consequence of this is that the responsibility of managing state shifts. In request collaboration you strive to ensure that every piece of data has one home, and you look it up from that home if you want it. This home is responsible for the structure of data, how long it's stored, how to access it. In the event collaboration scenario the source of new data is welcome to forget the data the second it's passed to its Message Endpoint."

    Martin Fowler - Event Collaboration (Queries section)

    Based on that assertion, IIRC, modern PHP frameworks implements the Observer Pattern + Intercepting Filters + Singal and Slots, in order to trigger some events during the request cycle.

    But, despite the fact that it adopts some ideas of event-driven architectures, it doesn't seems to support that the whole framework is event-driven (i.e. Symfony2 is an event-driven framewrok).

    We are used to dividing programs into multiple components that collaborate together. (I'm using the vague 'component' word here deliberately since in this context I mean many things: including objects within a program and multiple processes communicating across a network.) The most common way of making them collaborate is a request/response style. If a customer object wants some data from a salesman object, it invokes a method on the salesman object to ask it for that data.

    Another style of collaboration is Event Collaboration. In this style you never have one component asking another to do anything, instead each component signals an event when anything changes. Other components listen to that event and react however they wish to. The well-known observer pattern is an example of Event Collaboration.

    Martin Fowler - Focus on events (section: Using events to collaborate)

    I think that PHP applications are more closely to be event-driven than request-driven only when the focus are on events. If those applications/frameworks are only using events for cross-cutting concerns (AOP), then it's not event-driven. In the same way you would not call it test-driven or domain-driven just because you have some domain objects and unit tests.

    Real world examples

    I've picked some examples to show why those frameworks are not fully event-driven. Despite AOP events, everything is request-driven:

    Note: Although, it can be adapted to be event-driven

    Zend Framework 2

    Let's examine the \Zend\Mvc\Application component:

    It implements the \Zend\EventManager\EventManagerAwareInterface and relies on the \Zend\Mvc\MvcEvent which describes the possible events:

    class MvcEvent extends Event
    {
        /**#@+
         * Mvc events triggered by eventmanager
         */
        const EVENT_BOOTSTRAP      = 'bootstrap';
        const EVENT_DISPATCH       = 'dispatch';
        const EVENT_DISPATCH_ERROR = 'dispatch.error';
        const EVENT_FINISH         = 'finish';
        const EVENT_RENDER         = 'render';
        const EVENT_ROUTE          = 'route';
    
        // [...]
    }
    

    The \Zend\Mvc\Application component itself is event-driven because it doesn't communicates directly with the other components, but instead, it just triggers events:

    /**
     * Run the application
     *
     * @triggers route(MvcEvent)
     *           Routes the request, and sets the RouteMatch object in the event.
     * @triggers dispatch(MvcEvent)
     *           Dispatches a request, using the discovered RouteMatch and
     *           provided request.
     * @triggers dispatch.error(MvcEvent)
     *           On errors (controller not found, action not supported, etc.),
     *           populates the event with information about the error type,
     *           discovered controller, and controller class (if known).
     *           Typically, a handler should return a populated Response object
     *           that can be returned immediately.
     * @return ResponseInterface
     */
    public function run()
    {
        $events = $this->getEventManager();
        $event  = $this->getMvcEvent();
    
        // Define callback used to determine whether or not to short-circuit
        $shortCircuit = function ($r) use ($event) {
            if ($r instanceof ResponseInterface) {
                return true;
            }
            if ($event->getError()) {
                return true;
            }
            return false;
        };
    
        // Trigger route event
        $result = $events->trigger(MvcEvent::EVENT_ROUTE, $event, $shortCircuit);
        if ($result->stopped()) {
            $response = $result->last();
            if ($response instanceof ResponseInterface) {
                $event->setTarget($this);
                $events->trigger(MvcEvent::EVENT_FINISH, $event);
                return $response;
            }
            if ($event->getError()) {
                return $this->completeRequest($event);
            }
            return $event->getResponse();
        }
        if ($event->getError()) {
            return $this->completeRequest($event);
        }
    
        // Trigger dispatch event
        $result = $events->trigger(MvcEvent::EVENT_DISPATCH, $event, $shortCircuit);
    
        // Complete response
        $response = $result->last();
        if ($response instanceof ResponseInterface) {
            $event->setTarget($this);
            $events->trigger(MvcEvent::EVENT_FINISH, $event);
            return $response;
        }
    
        $response = $this->getResponse();
        $event->setResponse($response);
    
        return $this->completeRequest($event);
    }
    

    That's event-driven: You have no clue looking at the code of which router, dispatcher and view renderer will be used, you only know that those events will be triggered. You could hook almost any compatible component to listen and process the events. There's no direct communication between components.

    But, there's one important thing to note: This is the presentation layer (Controller + View). The domain layer indeed can be event-driven, but that's not the case of almost all applications you see out there. **There's a mix between event-driven and request-driven:

    // albums controller
    public function indexAction()
    {
        return new ViewModel(array(
            'albums' => $this->albumsService->getAlbumsFromArtist('Joy Division'),
        ));
    }
    

    The controller component is not event-driven. It communicates directly with the service component. Instead the services should subscribe to events risen by controllers, which is part of the presentation layer. (I'll point out the references about what would be an event-driven domain model at the end of this answer).

    Symfony 2

    Now let's examine the same on the Symfony2 Application/FrontController: \Symfony\Component\HttpKernel\HttpKernel

    It indeed has the main events during the request: Symfony\Component\HttpKernel\KernelEvents

    /**
     * Handles a request to convert it to a response.
     *
     * Exceptions are not caught.
     *
     * @param Request $request A Request instance
     * @param integer $type    The type of the request (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST)
     *
     * @return Response A Response instance
     *
     * @throws \LogicException If one of the listener does not behave as expected
     * @throws NotFoundHttpException When controller cannot be found
     */
    private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
    {
        // request
        $event = new GetResponseEvent($this, $request, $type);
        $this->dispatcher->dispatch(KernelEvents::REQUEST, $event);
    
        if ($event->hasResponse()) {
            return $this->filterResponse($event->getResponse(), $request, $type);
        }
    
        // load controller
        if (false === $controller = $this->resolver->getController($request)) {
            throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". Maybe you forgot to add the matching route in your routing configuration?', $request->getPathInfo()));
        }
    
        $event = new FilterControllerEvent($this, $controller, $request, $type);
        $this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
        $controller = $event->getController();
    
        // controller arguments
        $arguments = $this->resolver->getArguments($request, $controller);
    
        // call controller
        $response = call_user_func_array($controller, $arguments);
    
        // view
        if (!$response instanceof Response) {
            $event = new GetResponseForControllerResultEvent($this, $request, $type, $response);
            $this->dispatcher->dispatch(KernelEvents::VIEW, $event);
    
            if ($event->hasResponse()) {
                $response = $event->getResponse();
            }
    
            if (!$response instanceof Response) {
                $msg = sprintf('The controller must return a response (%s given).', $this->varToString($response));
    
                // the user may have forgotten to return something
                if (null === $response) {
                    $msg .= ' Did you forget to add a return statement somewhere in your controller?';
                }
                throw new \LogicException($msg);
            }
        }
    
        return $this->filterResponse($response, $request, $type);
    }
    

    But besides of being "event-capable" it communicates directly with the ControllerResolver component, hence it's not fully event-driven since the beginning of the request process, though it triggers some events and allows some components to be pluggable (which is not the case of the ControllerResolver that's injected as a constructor parameter).

    Instead, to be a fully event-driven component it should be as in ZF2 Application component:

        // Trigger dispatch event
        $result = $events->trigger(MvcEvent::EVENT_DISPATCH, $event, $shortCircuit);
    

    Prado

    I haven't enough time to investigate the source code, but at first it doesn't seems to be built in a SOLID way. Either way, what's a controller on MVC-alike frameworks, Prado calls it a TPage (Not sure yet):

    http://www.pradosoft.com/demos/blog-tutorial/?page=Day3.CreateNewUser

    And it indeed communicates directly with components:

    class NewUser extends TPage
    {
        /**
         * Checks whether the username exists in the database.
         * This method responds to the OnServerValidate event of username's custom validator.
         * @param mixed event sender
         * @param mixed event parameter
         */
        public function checkUsername($sender,$param)
        {
            // valid if the username is not found in the database
            $param->IsValid=UserRecord::finder()->findByPk($this->Username->Text)===null;
        }
        [...]
    }
    

    I understand that the TPage is an event listener and can be pluggable. But it doesn't make your domain model event-driven. So I think that, in some extent, it's more close to the ZF2 proposal.

    Event-driven examples

    To finish this long answer, here's what a full-blown event-driven application would have:

    Decoupling applications with Domains Events http://www.whitewashing.de/2012/08/25/decoupling_applications_with_domain_events.html

    Event sourcing http://martinfowler.com/eaaDev/EventSourcing.html

    Domain Event Pattern http://martinfowler.com/eaaDev/DomainEvent.html

    Event Collaboration http://martinfowler.com/eaaDev/EventCollaboration.html

    Event Interception http://martinfowler.com/bliki/EventInterception.html

    Message Endpoint http://www.enterpriseintegrationpatterns.com/MessageEndpoint.html

    ... and so on

    0 讨论(0)
  • 2021-01-30 12:35

    PHP is not stateless, HTTP is. To state it simply, we have essentially built a layer on top of a stateless technology upon which we can implement stateful designs. Taken together, PHP and your datastore of choice have all the tools they need to build an application design based on event driven patterns via the tokenization of sessions.

    In a highly generalized way, you can think of HTTP as being for the web what BIOS has been for desktop computing. In fact, take this just a little bit further, and you can easily see the implicit event-driven nature of the web. You said "it's not an event but a whole new request", and I return with, "a whole new request is an event", and I mean that in the design pattern sense of the word. It has concrete semantic meaning relating to your user's interaction with your application.

    Essentially, through patterns like MVC and Front Controller (and by the mechanism of HTTP cookies and PHP sessions), we simply restore session state, and then respond to the event, modifying that state accordingly.

    I like to contemplate the essence of REST: Representational State Transfer... but I would add that we should not forget the implicit implication that state is only transferred when a UI event has occurred. So we maintain the contracts we have with HTTP that we "speak" only in "Representational States" of our model (i.e. a document, JSON, etc), but that is only our dialect. Other systems choose to speak in canvas coordinates, signal db, etc.

    edit/more thoughts

    So I've been pondering it for a while and I think there is a concept which illustrates a little of the ambiguity when discussing these patterns in the realm of PHP via HTTP: determinism. Specifically, once a request is received, the path of PHP execution is deterministic, and this is exactly why it's remarkably difficult to consider an "event-driven" architecture in PHP. My notion is that we should consider one level higher than PHP, to the larger "session" of interaction with the user.

    In desktop computing, we use runloops and a state-ful context to "wait" for events. However, I will argue that the web is actually an improvement over this architecture (in most cases) but ultimately the same pattern. Instead of a runloop and infinite-duration state, we bootstrap our state when an event occurs and then process that event. And instead of just keeping that state in memory and waiting for the next event, we archive that state and close the resources. It could be considered less efficient in one sense (that we need to load state at every "event"), but it could also be called more efficient in that there is never idle state in memory for no reason. We only load state which is actually being consumed/manipulated

    So in this way, think of PHP via HTTP as being event driven at the macro level, while resting assured that any given execution is indeed deterministic and not actually event driven at all. However, we implement a front-controller and MVC pattern so that we can provide application developers with the familiar structure of even driven hooks. When you are working with a decent framework, you simply say "I want to know when a user registers, and the user should be available to me to modify at that time". This is event-driven development. It should not concern you that the framework has bootstrapped the environment for (almost) the sole purpose of calling your hook (vs. the more traditional notion that the environment was already there and you were simply notified of an event). This is what it means to develop PHP in an event-driven way. The controllers determine (based on the request) which event is occurring, and uses whichever mechanism it is designed to use (i.e. observer pattern, hook architecture, etc) to allow your code to handle the event, respond to the event, or whatever nomenclature is most appropriate for your particular framework's semantics.

    0 讨论(0)
提交回复
热议问题