Best way to wire up an event dispatcher with the component model

8 months ago

jahudka
Member | 66
+
0
-

Hi everyone,

I'm looking for a universally acceptable way to hook up components as listeners of an event dispatcher service. Imagine this:

<?php
abstract class BasePresenter extends Nette\Application\UI\Presenter {

    public function createComponentCartWidget() : CartWidgetControl {
        // ...
    }

}

class ProductListingPresenter extends BasePresenter {

    public function createComponentProduct() : Nette\Application\UI\Multiplier {
        return new Nette\Application\UI\Multiplier(function(int $id) : ProductControl {
            // ...
        }
    }

}
?>

Let's first set a couple of very reasonable expectations:

  • The whole app is AJAX-enabled – almost all requests are typically sent using AJAX.
  • If the user clicks the “Add to Cart” button in a product listing, both the product

    and the cart will be updated.

  • If the user clicks the “Empty cart” button in the ever-present cart widget while

    they are at a product listing which contains one or more of the products they

    previously added to their cart, both the cart and the relevant products will be

    updated.

The obvious solution is that both components will be listeners of an event dispatcher
service which will notify them of any relevant updates and they'll redraw themselves
appropriately. The obvious issue with this solution is that we need to register the
listeners before the event is dispatched, which will typically be within a signal handler.
The easy solution for simple scenarios such as this one might be to just pop a couple of
$this->getComponent(...); lines in the presenter's startup(). But even though it's
been suggested by some, that's actually a Really Bad Solution for a number
of reasons: first, it means the presenter must know which components to initialise early
and where in the component model are they located (neither of which the presenter should care,
or even know, about); second, it means components are no longer lazy (which may suck a lot
if there's hundreds of them – and it's also unnecessary unless an event is actually dispatched).

So the proper solution most probably involves some kind of a “bridge” between
the event dispatcher and any interested components – a service which somehow knows
which components want to subscribe to which events, as well as where to find these
components within the component model, if they're even present for the current presenter.
This bridge would then subscribe to the relevant events and upon first receiving
each unique event it would create any components that are interested in it.

I have a pretty decent mental model of how this could work, but before I set out to
write code, I thought I'd ask if there's by any chance a community-approved solution
already out there.

8 months ago

Felix
Nette Core | 879
+
0
-

Hi @jahudka.

I think you have a good inview into this issue. IMHO, there is no best practice in that way. It heavily depends on what you're trying to make.

I really don't like approach in this blogpost (https://pehapkari.cz/…ce-udalosti/), perhaps for same reason as you. It makes presenter so complicated and responsible for knowning about all components.

Maybe I'm wrong, but I'm using EventDispatcher (Symfony EventDispatcher) to handle some domain logic, not for just redraw snippets.

I've resolved it couple years later on one e-shop in this way:

There's a complicated component model for basket, productList, sales, vouchers and other components. But, it's together connected to the shopping layer (We called it that way).

That shopping layer has a ShoppingComponentFactory, it creates propel components and what more, it connects components between themselfs. The solution is so simple from that point, we're using simple Nette on<> event model. Each component has its events, produces events and other components listen for that events.

I can't tell it's the preferable way, but it works like charm couple years. Sometimes simplest solutions are the best. :-)

Just don't mess the presenter with some advanced connections to components/component-model, it breaks your code soon or later.

(Sorry for typos)

Last edited by Felix (2018-04-06 22:40)

8 months ago

dkorpar
Member | 55
+
0
-

I had similar issue recently with single page checkout . However you turn around you have to break something and some component will do more then it should…
I don't like approach where one component even know for other unless it's his child, but when you have one checkout page that combines everything (basket, delivery, vouchers, payment…) and while doing any action on any component should redraw most of other components you have to do some tradeoffs and see where to put…
I'm not a fan of any global event listener IMHO that's worst solution because sooner or later you just won't know what's happening when…
I went in a way that presenter is router to all actions that are then being called in components so he's responsible for which components it needs to redraw, but any logic and anything that does something is written in component itself.
So for example changing shipping method:

/**
 * @param int $shipping_method_id
 */
public function handleChangeShippingMethod(int $shipping_method_id)
{
    $this->getDeliveryControl()->changeShippingMethod($shipping_method_id);
}

protected function createComponentDeliveryControl() : DeliveryControl
{
    $control = $this->deliveryControlFactory->create();
    $control->onChange[] = function () {
        $this->redrawAllPriceRelatedControls();
    };
    return $control;
}

protected function getDeliveryControl() : DeliveryControl
{
    return $this->getComponent('deliveryControl');
}

I'm not really happy because presenter is doing a bit more then it should but I did not really found better solution.
In the end this works nice now and it's not problematic to maintain (for now)…

I'm allways going in a way that its better that presenter knows for component then oposite way around if possible…

Also component have their own component and it all goes up and doing as much as possible at bottom level and as little as possible at parents levels…

Last edited by dkorpar (2018-04-06 23:47)

8 months ago

jahudka
Member | 66
+
0
-

Hi @Felix and @dkorpar,

thanks for your replies! I was actually thinking of doing something similar to what @Felix mentions and ended up doing almost exactly the same thing as @dkorpar lol.. My use-case is really simple for now, but it won't be in the long term, that's why I've been looking for The Right Solution that would work even for more complex stuff..

Right now I have a component that only ever gets attached as a direct child of the presenter, so I created an interface which the presenter can implement with a simple method to get the component just like @dkorpar's getDeliveryControl(). The component knows nothing about an event dispatcher and instead just exposes a public refreshEntry() method. This is wired to the event dispatcher using a sort of “middleware” service like this:

<?php
class DeliveryRefreshListener {
    private $currentPresenter;

    public function handlePresenter(Application $application, IPresenter $presenter) : void {
        if ($presenter instanceof IDeliveryControlOwner) {
            $this->currentPresenter = $presenter;
        } else {
            $this->currentPresenter = null;
        }
    }

    public function handleInterestingEvent(SomeEntity $entry) : void {
        if ($this->currentPresenter) {
            $this->currentPresenter->getDeliveryControl()->refreshEntry($entry);
        }
    }
}
?>

This service is listening for the onPresenter event of Nette\Application\Application, as well as for the interestingEvent of the event dispatcher.

The nice thing about it is that it can “create” the relevant component lazily at the last minute if the request is being handled by a presenter that has the component, but it still fails to address the fact that even during normal rendering the component might not be used for the current action of the presenter – most of my presenters are single-action these days, but it's still something to be mindful of.

Furthermore it means that the presenter must know about the component – which is okay if the component is a direct child of the presenter, but it would kinda suck to have it know about components at a deeper level. Last but not least it means that the fact that the component is interested in updates to SomeEntity must be reflected (and repeated) in many places – compare that with tagging the component in the configuration (or better yet, using a decorator to tag all the components interested in any events).

The Awesome Solution I'm seeking is basically something that would work via static analysis, so that it can be wired up when the DI container is being compiled. I'm starting to think that such a solution would basically need to (partially) bypass and / or hijack the Component Model..

Btw I'm a great fan of the Component Model (I hate Symfony for not having it, or something similar), but there's a couple of things about it that just rankle.. For example – there's no way to know if a container has a specific child component for sure without “accidentally” creating it – calling isset($container['child']) only works for child components that have already been created; there's no built-in hasComponent() that would check for the presence of a factory method in case the component doesn't exist yet. Removing a component has little effect when the container can just recreate it silently when it's next requested. Also accessing critical functionality through string keys feels shaky in an era of strict typing.. I'm thinking something should be done about all this.. :innocent: