I turn into <code>

Blog

Decoupling components from presenters

In order for components in Nette to be as reusable as possible, it is necessary to decouple them from presenters. The cleanest way out of this is to invert the dependency. After all, it's the presenter that requires the component, not vice versa.

Let's start with a simple component for adding posts to a discussion thread. It encapsulates a form, displays a flash message after successful submission, and does a PRG redirection.

class NewPostControl extends Nette\Application\UI\Control
{
    protected function createComponentForm()
    {
        $form = new Nette\Application\UI\Form();
        $form->addText('name', 'Your name')
            ->setRequired();
        $form->addTextArea('text', 'Message')
            ->setRequired();

        $form->addProtection();
        $form->addSubmit('save', 'Save');
        $form->onSuccess[] = [$this, 'processForm'];
        return $form;
    }

    public function processForm($_, $values)
    {
        // somehow save the values
        // and get the new $post entity
        $this->presenter->flashMessage('Post ' . $post->id . ' saved.');
        $this->presenter->redirect('this');
    }
}

The dependency is clear there: the component depends on the presenter to display a flash message and redirect. We'll now make use of Nette's events system to invert the dependency: the presenter will then be able to register listeners that e.g. display flash messages or redirect the user. The component will simply trigger the event, unaware of and, more importantly, unconcerned with what the presenter does.

Let's start with adding the event itself. It has to be a public property whose name starts with on:

class NewPostControl extends Nette\Application\UI\Control
{
    /** @var callable[] */
    public $onSave = [];

    // ... the rest of the component's code
}

Now, modify the processForm method. Nette uses __call() to automagically trigger an event when you call an eponymous method, passing provided arguments to the registered listeners:

public function processForm($_, $values)
{
    // somehow save the values
    // and get the new $post entity
    $this->onSave($post);
}

And register a listener in the presenter:

protected function createComponentNewPost()
{
    $control = new NewPostControl();
    $control->onSave[] = function (Post $post) {
        $this->flashMessage('Post ' . $post->id . ' saved.');
        $this->redirect('this');
    };
    return $control;
}

The next time a new post is submitted, the component will fire its onSave event and the listener registered in the presenter will take care of flash messages and redirects. The component is decoupled from the presenter and can be easily reused at some other place that may require a slightly different behavior.

Last words of wisdom: to make the code more understandable, it's a good practice to annotate the event with the signature of its listeners (and also provide doc for the magic @method if you want your IDE to autocomplete it):

/**
 * @method void onSave(Post $post)
 */
class NewPostControl extends Nette\Application\UI\Control
{
    /** @var callable[] array of function(Post $post) */
    public $onSave = [];

    // ... the rest of the component's code
}

This post took 1 of coffee to write.

If you liked it, don't forget to

comments powered by Disqus
More on my blog

Filtering data by user input with Kdyby/Doctrine

Listing data is essentially the most crucial part of websites. Be it products, articles, photos or whatnots, we usually need to provide the user the way to filter and/or sort the data by some preset parameters. I'll show you how to encapsulate such filtering within an object, build a user interface (in other words, a form) upon it, and use it with Kdyby/Doctrine's query objects to actually filter the data on the database level.

and 17 more posts
Content licensed under Creative CommonsAttribution