-
-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RTM] Fragments as controllers #700
[RTM] Fragments as controllers #700
Conversation
|
Having content elements as controllers means that they get only a request object (with some special attributes) and return a response object, right? Is it then still possible for a content element to add some resources to the page like it is now with
What is the difference between a content element and a frontend module? Should we make them the same thing?
I think I like it in general, but I’m not sure about If they have to be controllers and receive HTTP requests and return HTTP responses. Would’t it be better if a content element receives its data and returns the rendered output and eventually some resources that should be added to the whole page (CSS, JS,...). To render a content element in a fragment it could get wrapped in a ContentElementController that internally renders the element and returns it as a HTTP response object. |
I think we should not discuss this here because it's not related to that PR. And you know the answer ;-)
They are the same thing which is why I built a general
Honestly, a proper HTTP response is the only way to do it right. Otherwise you have to provide too much stuff. Like you wrote: resources, output. But there's more! For example caching (and thus all kinds of caching types, from e-tag to expiry, vary headers etc.). |
|
Generally I like the idea of the controllers with tags, this is what we thought about years ago.
However, I would not use interfaces as it would only allow one "element" per controller. We'd better use a tag attribute for method as other events do.
|
A what? |
|
like this:
{ name: contao.page_type, method: someAction }
We could also add more properties e.g. an id for the internal type field (drop down) and language reference.
|
I disagree because this is bad software design. It violates the SRP. To come back to @ausi's comment about resources. I thought about that and I found the solution for that by asking myself who's responsible for the resources and it's the So for a content element: class MyContentElement implements ContentElementInterface, ProvidesResourcesInterface
{
public function getName();
public function getGroup();
public function renderAction(Request $request);
public function renderBackendAction(Request $request);
public function getResources();
}And all of a sudden, multiple problems we have today are solved:
You could never do that when combining multiple responsibilities into one class.
Yes, I would place it into my interfaces because e.g. content elements need a group, page types don't. See example above. The PR is not finished so I might find more methods that are needed ;-) |
|
Maybe you should also think about flat controllers as well. The listener class must not necessarily render the content element, but it would use injected services to do so. As an example, the |
|
Same applies for controllers. They should always only render one action to follow the SRP. Our content elements share nothing in 95% of all cases. That's why we should never render multiple elements in one class. |
|
I agree on the SRP issue in general and look forward to how this PR evolves. I have some questions on the resources idea outlined above though. |
|
Collecting would work as it does now. Just not by adding some Compiling is a different question and I think it should just be handled like it is now. The page layout settings define whether you want to combine them and where to output them. To cover the use case of "at the moment any developer can just provide any resource by editing the template", I'd provide some "additional resources" settings in both, the content element and front end module settings :-) |
|
There's a fundamental issue in your concept. A service must be stateless, and a content element, module, page etc. must know about it's configuration. I don't think you want to pass the config to each method? |
|
I absolutely don't see any issue in this concept, no.
Of course! Because we don't want state in these classes. So we'll pass our |
|
The config would be needed for the |
Only for legacy modules to determine the assets (modified part of |
|
You don't know that. As an example, you might need to compile a list of selected files (fetching |
|
Maybe something like this would work? public function renderAction(Request $request, ContentModel $entity, ResourceCollection $resources);Anyway, this is implementation detail already and I'm very happy about your feedback on this because we'll certainly have to clarify this. But can I get some general comments on the approach itself first? Or does discussing the details already mean that it is actually a good way and we should intensify looking at it? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some remarks on the current code that jumped into my eyes.
I know it is only a really early preview but I wanted to point them out anyway to not forget them.
| $this->fragments[] = $fragment; | ||
|
|
||
| foreach (self::TYPES as $type) { | ||
| if (is_a($fragment, $type)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should enforce SRP here by only allowing one interface per class.
Either by throwing an exception if multiple present or by breaking here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is a good idea, we can't add another interface in the future if we want to add new features (which we cannot add to the existing interface due to BC :-)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will violate the single responsible approach then and therefore this renders the splitting into multiple "per type" arrays useless?
How shall a class be an insert tag and a content element at the same time? This leads to coupled code with multiple responsibilities.
Can you tell me any example where this is a good idea?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, no obviously this won't work. But something like ContentElementInterface and SerializableInterface. You mean I should check that only one of the types is implemented?
| ]; | ||
|
|
||
| /** | ||
| * @var array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be FragmentInterface[]
| private $fragments = []; | ||
|
|
||
| /** | ||
| * @var array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be FragmentInterface[]
| */ | ||
| public function getFragmentByTypeAndName($type, $name) | ||
| { | ||
| foreach ($this->getFragments($type) as $fragment) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have a lookup map for these too as any fragment can occur more than once (and will as for CE text, headline, ...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
|
|
||
| $fragment = $container->findDefinition($id); | ||
|
|
||
| if (!is_a($fragment->getClass(), $interface, true)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is_a() vs. instanceof?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're working on strings, not objects. That's why :)
That's why we are talking about it.
Yep, from my side it is confirmed as the way to go, let's move on to the details. |
|
New big update 😄 Comments on this:
This means that you can register your own providers now by simply tagging your provider that is implementing the The different fragments can then also very easily be added by using the tag specified in the type provider. app.custom_page_type:
class: AppBundle\Controller\PageType\CustomPage
tags:
- { name: contao.page_type }That's it. You now have full control in your class and you benefit from the following:
Of course, I've implemented an class CustomPage extends AbstractPageType
{
/**
* {@inheritdoc}
*/
public function getName()
{
return 'custom';
}
/**
* {@inheritdoc}
*/
public function renderAction(Request $request)
{
return new Response('So cool, I am rendered inline and I can do whatever I want!');
}
}Now, what about some ESI because my stuff can be cached? Requires a lot of configuration? Nope. Simple as that: class CustomPage extends AbstractPageType
{
/**
* {@inheritdoc}
*/
public function getName()
{
return 'custom';
}
/**
* {@inheritdoc}
*/
public function getRenderStrategy(array $configuration)
{
return 'esi';
}
/**
* {@inheritdoc}
*/
public function renderAction(Request $request)
{
$response = new Response();
$response->setTtl(20); // We can use our proxy cache now!
return $response;
}
}Looking forward to new comments 😎 |
| */ | ||
| public function addFragmentType($interfaceClassName) | ||
| { | ||
| $this->types[] = $interfaceClassName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should throw an exception here when fragments have already been registered as the new type will not have been evaluated for the existing ones otherwise.
The workflow must be:
- register types
- register fragments.
Other solution is to cycle through all existing fragments and evaluate against this type again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can't happen. See addFragment(). If no type is responsible, you cannot add a fragment for it, so you MUST register the type first ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleared up on mumble. Adressed in 478b81a
| foreach ($fragmentTypeProvider->getFragmentTypes() as $fragmentTypeInterface => $tag) { | ||
|
|
||
| // Register the type | ||
| $fragmentRegistry->addMethodCall('addFragmentType', [$fragmentTypeInterface]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need to get splitted in two loops.
- Register types.
- Register handlers.
See remarks above for FragmentRegistry::addFragmentType().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment.
| $ref = new \ReflectionClass($fragment); | ||
|
|
||
| foreach ($this->types as $type) { | ||
| if ($ref->implementsInterface($type)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Symfony that method is generally called supports(…)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah which is nonsense here. You register a fragment for a certain type so you don't need to be asked again if you support it.
|
|
||
| // Resolve the fragment type provider to ask for the types | ||
| /* @var $fragmentTypeProviderInstance $fragmentTypeProvider */ | ||
| $fragmentTypeProviderInstance = $container->resolveServices($fragmentTypeProvider); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have never seen that in Symfony, maybe you can point to an example where they instantiate a service during container compilation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the YamlFileLoader for example.
| * | ||
| * @return bool | ||
| */ | ||
| private function classImplementsInterface($class, $interface) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use is_a? With the third parameter it accepts strings for both arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is_a() did not check for inherited interfaces for me. Reflection is better here.
|
I'm not sure about this thing. It feels rather complicated and a reinvention of something Symfony already provides (the fragment stuff). Especially on the page types I am still very much a fan of the Symfony CMF / dynamic router because it also includes URL to page alias mapping. |
It's not reinventing. It uses the fragment stuff of Symfony. It just maps everything on one controller so you don't have to register it for every type and fragment.
That has nothing to do with this PR. This PR is about how to render the fragments, not how to find the matching one based on the URL. This is still done in the |
|
I don't think it can be added later if we don't carefully plan it. The dynamic router had a very sane approach to me. Given a URL find the matching database record (document) and then look for a controller that can return a response based on that document. We're trying the opposite. Find a "controller" based on an arbitrary name and then setup a configuration that holds something so the "controller" can return a string which is then converted to a response. |
|
I completely disagree. Nothing of what you're writing is in any way true, I'm sorry. You are talking about mapping an URL like I don't want to register a controller for every element that I'm creating. This is pure nonsense. It is even worse if you expect it to map to a database record because I want to render fragments without database entry. |
|
Okay, I reworked this feature a lot again :) There are no more types and I reduced complexity a lot but still increased flexibility at the same time. I cannot stop emphasizing how powerful this is! Register as a service. As easy as tagging it as a fragment: app.page_type.service_layer:
class: AppBundle\Controller\PageType\ServiceLayer
tags:
- { name: contao.fragment }And the class to it which implements all available interfaces, so it's really the most complex version. If you don't want <?php
namespace AppBundle\Controller\PageType;
use Contao\CoreBundle\Controller\FragmentRegistry\QueryParameterProviderInterface;
use Contao\CoreBundle\Controller\FragmentRegistry\RenderStrategyInterface;
use Contao\CoreBundle\Controller\PageType\PageTypeConfigurationInterface;
use Contao\CoreBundle\Controller\PageType\PageTypeInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ServiceLayer implements PageTypeInterface, RenderStrategyInterface, QueryParameterProviderInterface
{
/**
* {@inheritdoc}
*/
public static function getIdentifier()
{
return 'tourismusweb.page_type.service_layer';
}
/**
* {@inheritdoc}
*/
public function supportsConfiguration($configuration)
{
return $configuration instanceof PageTypeConfigurationInterface;
}
/**
* {@inheritdoc}
*/
public function renderAction(Request $request)
{
$pageModel = \PageModel::findByPk($request->query->get('pageId')); // Could be fetched by injecting a service now :-)
if (null === $pageModel) {
throw new NotFoundHttpException();
}
$msg = <<<MSG
Hi there, I am a page type that was fetched via ESI. I am completely independent and I am only the beginning!
It does not make a lot of sense to render me as ESI because as I am a page type, I'm most likely the root of your
page definition. However, it is possible which shows the flexibility of this concept.
I hope that some day in the future, people will start using fragments a lot more so a page is eventually
made up of loads of different fragments that can be cached individually. We can make Contao extremely
fast then!
So long, your page type "%s".
BTW: I'm cached for 60 seconds. Here's proof:
I was parsed at: %s
MSG;
$dt = new \DateTime();
$msg = sprintf($msg, $pageModel->type, $dt->format(\DateTime::ISO8601));
$response = new Response(nl2br($msg));
$response->setTtl(60);
return $response;
}
/**
* {@inheritdoc}
*/
public function getQueryParameters($configuration = null)
{
if (!$this->supportsConfiguration($configuration)) {
throw new \RuntimeException('You have to provide a configuration I can deal with.');
}
/** @var PageTypeConfigurationInterface $configuration */
return ['pageId' => $configuration->getPageModel()->id];
}
/**
* {@inheritdoc}
*/
public function getRenderStrategy($configuration = null)
{
return 'esi';
}
/**
* {@inheritdoc}
*/
public function getRenderOptions($configuration = null)
{
return [];
}
}And here's the output 10 seconds after the first hit: |
9314e7e
to
feebc5a
Compare
bbd62f5
to
c762e19
Compare
e37e989
to
dad087d
Compare


So I would like to reopen this case here :-) Basically, Contao consists of 4 different elements:
All of these clearly are regular controllers. At the beginning I thought it would be a good idea just to register them as a regular route to Symfony and provide them some special attributes. But there are several issues with this approach:
Every bundle would need to provide its own
routing.ymland this is cumbersome. Even if used with the managed edition it would be a nightmare for the developers to add a new content element. I mean like how do you name your route? How can you prevent conflicts? Is there a convention? What kind of attributes can I give my route? etc.In my vision, it should be as simple as a service tag and implementing an interface that guides you to what you need to do. This PR does exactly that (I've implemented it for page types only for now, as this is WIP).
services.yml
MyControllerForPageType.php
That's it. You're done. The interface will tell you what you need to do and the rest is done by Contao.
At the beginning I wanted to make this PR for page types only but it all works the same. It's just different interfaces:
contao_frontend_indexso technically speaking, it is a fragment ;-) But content elements, front end modules and insert tags might be rendered as ESI or hinclude etc. So their interfaces could be extended by agetRenderStrategy()for instance.ToDo's:
Thanks for the feedback!
// @contao/developers