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
}
Have you found a tpyo in the post? Please submit a pull request with a fix :)

Hello, I am Jiří Pudil

My photo

I am a full-stack web developer from Brno, Czech Republic. I contribute to open-source projects, write a technical blog, and speak at meetups and conferences.

Learn more about me
Content licensed under