This was deprecated due to low usage of package and too complicated API. Use class that implements Nette\Application\IPresenter
with run(Request $request)
instead.
For more, see issue on Github.
Symfony and Laravel allow decoupled controllers by default thanks to simple principle: controller/presenter = callback. No base class or interface is needed.
People around me are already using single action presenters, but still depend on Nette. Why? Coupling of IPresenter
in Application and Router.
I think framework should help you and not limit you in a way how you write your code. Today we look how to make that happen even for Nette presenters and how to set them free.
When I talked about single action or rather invokable presenters in Nette on 87. Posobota meetup in Prague, people were talking about 3 missconceptions. I'd like clarify them first.
IPresenter
My first attempt decouple presenter from Nette failed on PresenterFactory
:
// ...
if (!$reflection->implementsInterface(IPresenter::class)) {
throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is not Nette\\Application\\IPresenter implementor.");
}
This took me few weeks to figure out because of coupling to latte, providers and layout autodiscovery.
I needed to modify PresenterFactory
, Application->run()
and create own PresneterRoute
.
So yes, when you modify few places, you can use it without IPresenter
interface.
If you use ajax and components, this approach is not probably for you application.
I thought it's impossible to use Nette without components as well, but Ondrej Mirtes from Slevomat take me out of my misery: "We don't use components in Slevomat, just presenters." So feel free to ask him how they do it.
You'll appreciate this approach in applications, where:
Even when some people agreed with invokable/single action approach, they still missed some interface that would enforce a method. I must say __invoke()
method seemed weird to me at first too.
Give invoke()
a try, it's Fine
But I've learned what __invoke()
is and that Symfony and Laravel use it for years.
Also, using an interface would only create a new dependency for something that is already used in specific way. Moreover for controller which every framework bend to its own needs.
__invoke()
is normal method, just like __constructor()
is normal for passing dependencies nowadays.
Why Decouple Controller From Framework?
If you look for reasons to decouple from framework, read this 3 parts series: Framework Independent Controllers by Matthias Noback about independent controllers in Symfony.
Why are Single Action Presenters Great for Growing Projects?
Similar package and post was made by Kevin Dunglas exactly 1,5 year ago. You'll find your answers there.
No more questions, right to the code.
The goal was:
__invoke()
Ideal code for Nette-agnostic presenter would look like this:
namespace App\Presenter;
use Symplify\SymbioticController\Adapter\Nette\Template\TemplateRenderer;
final class StandalonePresenter
{
/**
* @var TemplateRenderer
*/
private $templateRenderer;
public function __construct(TemplateRenderer $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function __invoke(): string
{
return $this->templateRenderer->renderFileWithParameters(
__DIR__ . '/templates/Contact.latte'
);
}
}
Or if you use API and json:
namespace App\Presenter;
use Nette\Application\Responses\JsonResponse;
final class ApiPresenter
{
public function __invoke(): JsonResponse
{
return new JsonResponse('Hi!');
}
}
Module:Presenter:template
=> __DIR__ . '/templates/template.latte
Instead of using magic notation, you can go right with absolute path for templates.
If this would be used by every controller and framework, there would be much lower entry barrier for front-end developers. Another new way to use your IDE the right-way:
But how to register this presenter in router? Since the called action is now not a method in a class but a class, we cannot use common way to add route:
# this won't work
$routes[] = new Route('/contact', 'Contact:default');
$routes[] = new Route('/contact', 'Contact:__invoke');
We need to use presenter as target:
use App\Presenter\ContactPresenter;
$routes[] = new Route('/contact', ContactPresenter::class);
But that won't work either as Route
class requires <presenter>:<method>
format for target.
To solve this, we'll use custom Route that accepts Presenter class as argument.
# app/Router/RouterFactory.php
use App\Presenter\ContactPresenter;
use Symplify\SymbioticController\Adapter\Nette\Routing;
final class RouterFactory
{
public function create(): RouteList
{
$routes = new RouteList;
$routes[] = new PresenterRoute('/contact', ContactPresenter::class);
$routes[] = new Route('<presenter>/<action>', 'Homepage:default');
return $routes;
}
}
It has 2 important tasks:
__invoke()
methodSame way you can use your IDE to open the template, you can use it to open presenter target.
From magic Homepage:default
to clickable class:
composer require symplify/symbiotic-controller
# app/config/config.neon
extensions:
- Symplify\SymbioticController\Adapter\Nette\DI\SymbioticControllerExtension
- Contributte\EventDispatcher\DI\EventDispatcherExtension
That's all :)
__invoke()
. It is your friend, mainly for future.Let me know in the comments. I always like to hear different opinions.