Symfony and Twig templates. How to dynamically ext

2019-07-21 16:10发布

问题:

Let me describe a situation. You have a profile form and a form where users can upload personal profile related documents. Since the profile form is already very long the documents moved to a new form.

Everything works great. Now we want to use the bootstrap tabs to do Profile | Documents for user friendliness.

Now I know because we are using two separate forms if you submit the documents the changes on the profile won't save and vice versa.

I have added the document form in the tab using

<div role="tabpanel" class="tab-pane" id="documents">
    {{ render(controller('ManyAppBundle:Document:createDocument', {'viewOnly': true})) }}
</div>

The 'viewOnly': true is a query parameter and is not required by the action.

My question now becomes if the profile tab renders the document template it must only show the upload widget and the submit where as when you go directly to the document page it must show the title and side bar and everything.

So I tried

{% if not viewOnly %}
    {% extends ... %}
{% endif %}

That gave problems because you can't use extends within a if(Causes a ("Node "1" does not exist for Node "Twig_Node".") error).

Other similar question provided this solution

{% extends viewOnly == false ? ... %}

When viewOnly is false it must extend the base template used by all other templates but if it is true I only want to show this:

{{ form_start(form, { 'style': 'horizontal', 'col_size': 'sm' }) }}
    {% if form.documents is defined %}
        {{ form_row(form.documents) }}
    {% endif %}

    {{ form_row(form.submit, { 'attr': { 'class': 'btn btn-success' } }) }}
{{ form_end(form) }}

But now with the top

{% extends viewOnly == false ? ... %}

if viewOnly becomes true it fails with Template "" can't be find.

Is there a way to say extends this specific template that will be the same result of not extending any template?

Or alternatively is there a way of saying extend this when viewOnly is false or just do nothing?

回答1:

The logic should sit in the controller action.

What you can do is create 2 twigs, a parent twig that extends and includes another child twig that have data.

In your controller, based on condition you can render the extended twig or the child one.

In your case, controller code might be something like this :

if ($request->query->get('view-only') === true) {
    return $this->render('MyAppBundle:Default:data.html.twig'); // this twig hasn't extended; only data.
}
else {
    return $this->render('MyAppBundle:Default:index.html.twig'); // this twig has data.html.twig included and extended.
}

I hope that fixes your issue. :)



回答2:

There are basically 3 solutions to determine how to switch between extending different types of templates within a twig template.

The first will be a very simple true or false logic based. You can do this within the twig file easily as in the following example

{% extends entity.isExpired == false ? active.html.twig : expired.html.twig %}

The second will be as @Jeet suggested doing this in a controller. This can also be as simple as a boolean check but as advanced as additional entity checks as in the following example

/* User Controller */
public function showAction($id)
{
    $user = $this->get('myapp.repository.user')->findBy($id);
    $isUser = $user instanceof User;

    if (!$isUser) {
        return $this->redirectToBloggersPage();
    }

    /* $params is what you will normally return to the template */
    $params = array (
        'user' => $user,
    );

    if ($user->hasPostedBlog()) {
        $params['blog'] = $this->get('myapp.repository.blog')->findByUserLatest($user);

        return $this->render('MyAppBundle:User:user_and_blog_view.html.twig', $params);
    }
    else {
        return $this->render('MyAppBundle:User:user_view.html.twig', $params);
    }
}

The third will be as @Thomas suggested doing this in a separate twig function. This will be done in a Twig extension where you can inject services as in the following example

/* Twig extension */
public function extendBookForm()
{
    if ($this->getSecurityContext()->isGranted('IS_AUTHENTICATED_FULLY')) {
        return "user_view.html.twig";
    }

    return "public_view.html.twig";
}

/* Twig file */
{% extends extend_book_from() %}

My original question does not have a solution for the outcome I wanted. I am taking it that the way I hoped for is not possible and if you dynamically want to check what to extend you must always extend more than one template since you will have to extend something for each outcome.

These are the different ways you will be able to do dynamic template extensions.



回答3:

Although I like Jeet's solution better, here is more complicated alternative. Keep in mind, this solution is only educational.

  1. Create a new Twig extension (if you don't already have one).
  2. Inject the request into your extension.
  3. Create a Twig function extend_from (see below for sample code) that returns the template based on the request.
  4. Call the Twig function into the extend block.

The Twig function could like the following:

/**
 * Returns the template to be extended
 * Notes: Code assumes that the request data is a protected property
 *        on the Twig extension.
 *
 * @param  string  $template = "full.html.twig"  the full layout template
 * @return string  the template to be extended
 */
public function extendFrom($template = "full.html.twig")
{
    if ($this->request->query->get('view-only') === true) {
        return "empty_layout.html.twig";
    }

    return $template;
}

In your template:

{# Templates assumes that your extended templates have a "content block" #}
{% extends extend_from() %}

{% block content %}
    {# Your template content here #}
{% endblock %}

It's a bit of "magic" solution to me. The best approach is to consider your content as a module. Either you render it alone or you include into your layout.

In my project for example:

  1. I create a page template + a partial template.
  2. The page template includes the partial template which includes a bunch of modules.
  3. When the user navigates around the site I ajax and load the partial template in the already-rendered page.

In the future, I can render the entire page, part of the page or just one of the modules in that page.

It's a bit complicated but it becomes very powerful with these BaseThemeBundle which allows me to override any templates.



回答4:

My solution to this problem was to create an empty.html view with just this in it

{% block content %}
{% endblock %}

Not a great solution, but much simpler than some of the other methods, so I don't have to add more clutter.