Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[FrameworkBundle] Add new TemplateResponse class #21765

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

Closed
wants to merge 9 commits into from

Conversation

pierredup
Copy link
Contributor

@pierredup pierredup commented Feb 26, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets
License MIT
Doc PR TBA

Add a new template reference class to resolve templates to a response (based on a comment from @iltar at symfony/symfony#21732).

This is an alternative approach to the @Template annotation, if you don't want to use annotations and you don't want to inject the templating service into your Controller

Example:

class FooController
{
    public function __invoke()
    {
        return new TemplateResponse('foo:bar:baz.html.twig');
    }
}

The TemplateResponse class implements the TemplateResponseInterface interface, which allows you to customise the response.

class InvoiceResponse implements TemplateResponseInterface
{
    private $invoices;

    public function __construct(array $invoices)
    {
        $this->invoices= $invoices;
    }

    public function build(EngineInterface $engine)
    {
        $total = count($this->invoices);

        if (0 === $total) {
            return new Response($engine->render('/invoice_404.html.twig'));
        }

        if (5 > $total) {
            return new Response($engine->render('/invoice_few.html.twig'));
        }

        return new Response($engine->render('/invoice_many.html.twig'));
    }
}

// ...

class FooController
{
    public function __invoke()
    {
        // ....
        return new InvoiceResponse($invoices);
    }
}

@linaori
Copy link
Contributor

linaori commented Feb 26, 2017

Just something else I had in mind when it comes to this, more of a builder pattern:

<?php

interface ResponseBuilderInterface
{
    public function build(EngineInterface $engine);
}

class TemplatedResponseBuilder implements ResponseBuilderInterface
{
    private $template;
    private $parameters;
    private $statusCode;
    private $headers;

    public function __construct($template, array $parameters = array(), $statusCode = Response::HTTP_OK, array $headers = array())
    {
        $this->template = $template;
        $this->parameters = $parameters;
        $this->statusCode = $statusCode;
        $this->headers = $headers;
    }

    public function build(EngineInterface $engine)
    {
        // or use PSR if you like
        return new Response($engine->render($this->template), ...);
    }
}

Advantage with this setup is that you have 100% control over the view and you can create nice response builders:

class InvoiceResponseBuilder implements ResponseBuilderInterface
{
    private $invoices;

    public function __construct(array $invoices)
    {
        $this->invoices= $invoices;
    }

    public function build(EngineInterface $engine)
    {
        $total = count($this->invoices);

        if (0 === $total) {
            return new Response($engine->render('/invoice_404.html.twig'));
        }

        if (5 > $total) {
            return new Response($engine->render('/invoice_few.html.twig'));
        }

        return new Response($engine->render('/invoice_many.html.twig'));
    }
}

names to be discussed

@pierredup
Copy link
Contributor Author

I think it should be a TemplateResponse class, so that it is a bit more consistent with the JsonResponse and StreamedResponse classes.

It can follow the same concept as you mentioned, but instead of a builder, just apublic function getResponse(EngineInterface $engine) method that can return a response object

@pierredup pierredup changed the title [FrameworkBundle] Add new Template reference class [FrameworkBundle] Add new TemplateResponse class Feb 27, 2017
@pierredup pierredup force-pushed the template-reference branch 3 times, most recently from 315b627 to 10421f5 Compare February 27, 2017 06:48
private $template;
private $parameters;
private $statusCode;
private $headers;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this properties needs getters and/or setters?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think they do, they are not part of the public API

@pierredup pierredup force-pushed the template-reference branch 3 times, most recently from 37bb6fb to 3b788f2 Compare February 27, 2017 07:27
/**
* @author Pierre du Plessis <pdples@gmail.com>
*/
class TemplateResponse implements TemplateResponseInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a
TemplatedResponse instead of TemplateResponse
TemplatedResponseInterface instead of TemplateResponseInterface

Copy link
Contributor Author

@pierredup pierredup Feb 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this again, I don't think the interface should be related to a template response at all. The interface just defines a way to return a Response. The Template(d)Response class is just an implementation rendering a template, but you can just as well return a JsonResponse, which means it's no longer rendering a template. So the name should probably be something more generic, although I'm not sure if just ResponseInterface will do?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could simply return a new JsonResponse in that case like it's done now. It doesn't really make sense to me to first create an object to tell a listener to then create the Response for something you could've done in your controller already, this would trigger the view listeners which is just overhead.

private $template;
private $parameters;
private $statusCode;
private $headers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think they do, they are not part of the public API

private $statusCode;
private $headers;

public function __construct($template, array $parameters = array(), $statusCode = Response::HTTP_OK, array $headers = array())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about cookies and cache information for example?

http://symfony.com/doc/current/components/http_foundation.html#setting-cookies

Internally cookies are added to the ResponseHeaderBag:

    public function setCookie(Cookie $cookie)
    {
        $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
    }

I think for this feature to be really viable, it should be possible to have caching and cookie control as well (hence I was aiming for a builder)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make the parameter list too long. What about dropping the $statusCode and $headers parameters, and include the option to pass a Response object?

public function __construct($template, array $parameters = array(), Response $response = null)

That way we can keep it simple to only pass a template and parameters, but if you want more flexibility like setting cookies then you just create your own Response

@yannickl88
Copy link

I like this idea. However, I think this might be two steps to far for the moment. The @Template annotation is part of the SensioFrameworkExtraBundle and the twig glue that is part of it. What I rather see is a pull request for that bundle that does:

  • Remove the @Template annotation
  • Adds the TemplateResponse which extends the regular Response
  • Make the TemplateListener use the TemplateResponse instead of the array that we have now

The advantage of this is that we can now define the return value of each action to always be Response, which is great for PHP strict types. The other upshot is that you are not mixing concepts which are (for now) spread across different bundles. While I do not mind adding the twig response support to the core, I think it would conflict with the SensioFrameworkExtraBundle as is. They provide the same result in different ways, which I find confusing.

Don't get me wrong though, I like the idea of your PR. This opens the door for other things like PDF responses which set the correct headers or switching templates based on the view data. I just think there needs to be done some prep-work before introducing this to the core.

@pierredup
Copy link
Contributor Author

Remove the @template annotation

I don't think this needs to be removed. The annotation is still a useful feature for anyone that prefers it, but not everyone likes using annotations.

Adds the TemplateResponse which extends the regular Response

That did cross my mind as well, but I wasn't sure how the implementation would look like for it to make sense as a Response class, as I don't think the templating should be tied to the http-kernel or http-foundation components, hence I added it in the framework bundle which ties those components together.

For me the SensioFrameworkExtraBundle adds some nice features to use annotations, but it can be disabled which then means you will lose this feature. Having the ability to create a template response just seems more natural in core

@linaori
Copy link
Contributor

linaori commented Feb 27, 2017

@pierredup I've considered if extending the Response would be a good idea, but this would prevent the view event from being fired:

// call controller
$response = call_user_func_array($controller, $arguments);

// view
if (!$response instanceof Response) {
    $event = new GetResponseForControllerResultEvent($this, $request, $type, $response);
    $this->dispatcher->dispatch(KernelEvents::VIEW, $event);
    // ...
}

@pierredup
Copy link
Contributor Author

pierredup commented Feb 27, 2017

If extending the Response, the event can then be tied into to kernel.response instead (which probably also isn't ideal, because that means the event listener will be triggered on every response instead of only when necessary, adding a bit of unnecessary overhead)

@pierredup
Copy link
Contributor Author

Suggested changes made, ready for review

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Feb 27, 2017
@jvasseur
Copy link
Contributor

I'm not sure about using Response in the name since the class doesn't extends the Response object. Maybe TemplateView would be a more fitting name.

{
private $templating;

public function __construct(EngineInterface $templating)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having a hard dependency to the templating component is not fine IMO, as we worked very hard to make it possible to use Twig natively without wrapping it in the Templating component.

@stof
Copy link
Member

stof commented Feb 27, 2017

I'm not sure this should be in the core itself, especially given that it is strongly tied to the Templating component, that we are slowly deprecating.

@linaori
Copy link
Contributor

linaori commented Feb 27, 2017

@stof perhaps this is something useful for the twig bundle instead?

@fabpot
Copy link
Member

fabpot commented Mar 1, 2017

We are indeed moving away from the Templating component, so adding a feature requiring is a no-go. FrameworkExtraBundle will require Twig in its next major version, and @Template will only work with Twig.

@linaori
Copy link
Contributor

linaori commented Mar 10, 2017

I have one more edge case that I possibly would like to see supported; Redirects.

class InvoiceResponse implements TemplateResponseInterface
{
    // ..

    public function build(EngineInterface $engine)
    {
        $total = count($this->invoices);

        if (0 === $total) {
            // ..
        }

        if (1 === $total) {
            // how do I get my route here?
            return ... new RedirectResponse?('app.invoice.one', [$this->invoices[0]]);
        }

        if (5 > $total) {
            // ..
        }

        // ..
    }
}

My personal goal is to have no dependency on the Router, UrlGenerator or Templating/Twig in my controller.

@pierredup
Copy link
Contributor Author

pierredup commented Mar 10, 2017

I'm not sure if the route and redirect should be solved in this PR, since the goal here is only to create an easy way to render templates without injecting twig into the controller. But for that issue, what about something like a custom RouteRedirectResponse that takes a route name and parameters instead of a URL?

@linaori
Copy link
Contributor

linaori commented Mar 10, 2017

@pierredup a lazy redirect, where you can just fill in the route + parameters is something I've wanted for quite some time, but was rejected in the past. Could be something explicit for this responder though

@fabpot
Copy link
Member

fabpot commented Mar 23, 2017

What about trying to implement a broader feature that makes views a first-class citizen? We've had the view event since the beginning, but we never really provided anything to make it useful out of the box. ResponsedTemplate is just one implementation of a larger scope.

@linaori
Copy link
Contributor

linaori commented Mar 23, 2017

What about the responder pattern? I'm not too familiar/experienced with it myself but it seems to solve certain challenges between the controller and the view.

@fabpot, do you have something in mind or any ideas about the larger scope?

@@ -181,6 +181,10 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerTemplatingConfiguration($config['templating'], $container, $loader);
}

if ($container->hasDefinition('twig')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the container will never have a Twig service here, as extensions can only see services they define themselves.

/**
* @author Pierre du Plessis <pdples@gmail.com>
*/
class TwigTemplateListener implements EventSubscriberInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Twig listener should be in TwigBundle or in the Twig bridge (if it depends on FramworkBundle, it should be in TwigBundle).
FrameworkBundle is not allowed to depend on Twig.

*
* @return Response
*/
public function getResponse(\Twig_Environment $twig);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this interface depends on Twig, so it cannot be in FrameworkBundle

@pierredup
Copy link
Contributor Author

What about trying to implement a broader feature that makes views a first-class citizen?

@fabpot I'd like to finish this PR, do you perhaps have any ideas about what a view as a first-class citizen would look like?

@symfony symfony deleted a comment Sep 27, 2017
@symfony symfony deleted a comment Sep 27, 2017
@weaverryan
Copy link
Member

Let's kick this to 4.1 and see if we can get something really nice and thoughtful done (since we're so close to feature freeze now, I think it would be rushed).

@weaverryan weaverryan modified the milestones: 3.4, 4.1 Sep 27, 2017
@fabpot
Copy link
Member

fabpot commented Oct 1, 2017

Actually, I'm going to close this one. The proposed feature is not generic enough and should probably be experimented in a bundle first, before being integrated in code.

@fabpot fabpot closed this Oct 1, 2017
@mvrhov
Copy link

mvrhov commented Nov 26, 2017

@iltar ViewBundle. No tests for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.