SonataAdminBundle configureShowFields with nested entities - php

For a project which uses symfony2 and the SonataAdminBundle, I am trying to figure out how to embed complete related entities in the show action.
To get more into detail, let's say I have a Article and a Comment. On the show view for the Article, I'd like to show each Comment with multiple properties as well as an EDIT on each and a CREATE to add another comment to that Article.
I was able to get it to get it to display a list of Comment entities which link to the Entity by using ->add('comments'), but that's not enough. I need to have the entity really embedded!
Is there a way to do this without coding it on our own? And if doing it manually is the only way, what's the best approach? Rewriting the template?

Simplest way is to specify a template for the collection:
$showMapper->add('comments', 'collection', [
'template' => 'YourBundle:SomePath:SubPath/show_comment_collection.html.twig',
]);
Look to SonataAdminBundle:CRUD:base_show_field.html.twig for a template to use as an example. And, in that template, you can loop over the value variable. For example:
{% block field %}
<ul>
{% for comment in value %}
<li><a href="{{ path('some_route', {'id': comment.id}) }}">
{{ comment.id }} - {{ comment.otherProperty}}</a>
</li>
{% else %}
<li>No comments</li>
{% endfor %}
</ul>
{% endblock %}

Related

Symfony Twig Path function parameters, key defined by string

In Twig with Symfony, to generate a path in a template, it wants to use
{{ path('mypathname', {parameterName: parameterValue}) }}
In my project I have an inherited template, and one block loops through a chunk of data and spits out a list of URL's. What I want to do, is from my new template, is allow me to {% set %} the keyname of the first parameter
so basically like this
template.html.twig
<ul>
{% for thing in things %}
<li>{{ thing.name }}</li>
{% endfor %}
</ul>
otherthings.html.twig
{% extends 'template.html.twig' %}
{% set path_name = "thingDetail" %}
{% set path_param = "id" %}
and see the output like
<ul>
<li>The 20th Thing</li>
</ul>
It may sound ridiculous, but across this app, the naming and pathing styles doesn't always match up with the different kinds of content that goes into that blocks loop, so some type of override needs to happen and the last thing I want to do right now, is overwrite the block for that for each type, when the last thing that needs to be done is URL generation
Thank you!
If you enclose the hash key in parenthesis, Twig will treat it as an expression:
{%- set field = 'foo' -%}
{%- set arr = { (field): 'bar' } -%}
will set arr to
{ foo: 'bar' }
(This is mentioned in the Twig documentation, but it's easy to see how it could be overlooked.)

How to dynamically modify child form of sonata_type_collection?

I have a field of 'sonata_type_collection' in a form which is defined in a Sonata Admin class. I need to modify the children of that form based on child's position.
In my particular case, each row in the collection has a 'Delete' checkbox, and I'd like to disable that checkbox only for the first element of the collection.
Any idea how to achieve this?
The only way that I found is by overriding the form_admin_fields.html.twig and add your own blocks.
You can override the template by modifying the related configuration file : https://sonata-project.org/bundles/doctrine-orm-admin/2-2/doc/reference/configuration.html#full-configuration-options
or use the SonataEasyExtendsBundle to extend SonataDoctrineOrmBundle.
You have to create 2 block one for you collection and one for your relation type (OneToMany or ManyToMany).
The annoying part is to find the name of your block, it's formed by your admin service name + field name + 'sonata_type_collection_widget'.
It depends of your Sonata version but here is a collection block example that I use :
{% block sonata_admin_challenge_organizers_sonata_type_collection_widget %}
{% if sonata_admin.field_description.mappingtype == constant('Doctrine\\ORM\\Mapping\\ClassMetadataInfo::ONE_TO_MANY') %}
{{ block('sonata_admin_challenge_organizers_orm_one_to_many_widget') }}
{% elseif sonata_admin.field_description.mappingtype == constant('Doctrine\\ORM\\Mapping\\ClassMetadataInfo::MANY_TO_MANY') %}
{{ block('sonata_admin_orm_many_to_many_widget') }}
{% else %}
INVALID MODE : {{ id }} - type : sonata_type_collection - mapping : {{ sonata_admin.field_description.mappingtype }}
{% endif %}
{% endblock %}
Once your collection block is done you have to add a new block for the oneToMany or ManyToMany, you simply copy the template used in your Sonata version and customize it to your need : https://github.com/sonata-project/SonataDoctrineORMAdminBundle/blob/master/Resources/views/CRUD/edit_orm_one_to_many.html.twig
In your case, you simply have to add an if statement based on the loop.index value to display or not the delete field : https://github.com/sonata-project/SonataDoctrineORMAdminBundle/blob/master/Resources/views/CRUD/edit_orm_one_to_many.html.twig#L26.

How To append parameters to KNP paginator url

I am using the KNP Paginator bundle for pagination. Does anyone know how to append parameters to the generated url?
Here is my set up:
{{ knp_pagination_sortable(supplierProducts, 'Product Sku', 'p.name') }}
I want to add &section=supplier to the end of the URL, I just have no clue how to do it. I looked through the docs but did not find any info.
Please help if you can.
Thanks.
According to the KnpPaginator documentation, you can append query parameters as follows:
$paginator = $this->get('knp_paginator');
...
$pagination->setParam('section', 'supplier');
You could extend the kn_pagination_sortable template. When you run "knp_pagination_sortable" behind the scenes, it will basically generate an HTML according to your specifications. However, you can extend that. Instead of using the bundle's generated HTML for that element, you can write your own template for that pagination_sortable. This is a snippet from the project I'm working on. This is at my pagination_sortable.html.twig:
<a id="table_sorteable_{{ options['title']|lower }}" {% for attr, value in options %} {{ attr }}="{{ value }}"{% endfor %}>
{{ title }}
<b class="caret {{ options['class'] }}"></b>
</a>
Get it? You can have a template like that and change it according to your needs.
You can find more information on the link below.
Overriding default pagination template
As of 2020, and KnpPaginatorBundle v5.3, the solution proposed by #likeitlikeit doesn't work because the setParam method doesn't exist any more.
But you can append parameters to the sort and pagination links directly in the knp helpers in twig:
{# 4th parameter for sortable helper #}
{{ knp_pagination_sortable(results, column.title, column.alias, {}, params) }}
{# 3rd parameter for pagination helper #}
{{ knp_pagination_render(results, '', params) }}
For example, if you want to include the query parameters in the sort and pagination links, you can do:
{# Sort - pass all query parameters except sort column and direction #}
{% set params=app.request.query.all | filter((v, k) => (k != 'direction' and k != 'sort'))%}
{% for column in ... %}
{{ knp_pagination_sortable(results, column.title, column.alias, {}, params) }}
{% endfor %}
{# Pagination #}
{{ knp_pagination_render(results, '', app.request.query.all) }}
To add parameters in the url, I proceeded like this:
in Front:
{{ knp_pagination_render(clients, ('annuaire/my_pagination.html.twig'), {"type": type ? type : '' ,"city": city ? city : ''}) }}

twig: pass variables from view to controller

Setup:
Twig 1.13.1
PHP 5.4.3
Problem:
I have 10,000 articles in my DB. I need a way to only allow X amount of stories to display on the page. I know I can limit the number in the controller before i call the template but that number will be different depending on the template that is used. I will have one controller to handles all the articles. I need a way to pass the number from the template to the controller to limit the array. I don't want to pull down all 10,000 articles then use twig "slice" filter/func.
I know in django you can use the below. That will only load the top 3 stories.
{% get_latest_stories 3 sports as story_list %}
{% for story in story_list %}
{{ story.title }}
{% endfor %}
Here is my current files.
Controller
<?php
$stories = news_stories::getStories("sports",5); //getStories(section,limit);
?>
<?=$twig->render("storyList.html", array('stories' => $stories))?>
View/Template
{% for story in story_list %}
{{ story.title }}
{% endfor %}
Summary
I would like a way to pass a number from the template to the controller so that i can limit the about of rows returned from the DB
Logically speaking, it would be impossible for the view to pass something to controller since the view is being processed at the end of the stack, after everything else.
You can however, pass a function into the view. You would want to create some sort of getViewStories function that you can access from your twig template. Since you have this already in your controller:
<?php
$stories = news_stories::getStories("sports",5); //getStories(section,limit);
?>
<?=$twig->render("storyList.html", array('stories' => $stories))?>
All you would need to do is change it around a bit, like this:
<?php
$function = new Twig_SimpleFunction('getViewStories', function (section, limit) {
return news_stories::getStories(section,limit);
});
$twig->addFunction($function);
?>
<?=$twig->render("storyList.html")?>
Now, from inside your template, you can call this function, like so:
{% set story_list = getViewStories('sports',5) %}
{% for story in story_list %}
{{ story.title }}
{% endfor %}
And change the getViewStories parameters around in each template.
And while you can use the slice filter, I would recommend against it in your case, as it makes for unnecessarily long database calls. This is the most optimized method (that I'm aware of).
You want to use the slice filter I think this should work for you
http://twig.sensiolabs.org/doc/filters/slice.html
{% for story in story_list|slice(1,5) %}
{{ story.title }}
{% endfor %}
should only return the elements 1 - > 5 of the loop then break loop. You can also do it like this
{% for story in story_list|[start:5] %}
{{ story.title }}
{% endfor %}
Disclaimer: I've never actually used twig though this was just a quick browse through its docs
You can embedded controllers ( or render other urls ) from inside a twig template. This means you could have a main layout template for your site, and keep your storyList.html template very plain - just to iterate over the stories and any markup they might need.
In your main layout you would render the action for the news stories:
<div id="stories">
{% render url('...') with { section: 'sports', limit: 5}, {'standalone': 'js'} %}
</div>
This way requires hindclude.js included on your page. Check these docs. If you are also using symfony ( you mention MVC but not the framework ) - even better. Scroll up a bit and look at Embedded controllers.
Otherwise, I believe this way essentially uses ajax.

Twig extend template on condition

I use Symfony 2 with Twig and my question is pretty straightforward:
In a view I want to extend one of the layouts based on a variable. If the variable is false I want to extend UdoWebsiteBundle::layout.html.twig and if it's true I want to extend UdoWebsiteBundle::layout_true.html.twig.
Here is the code I tried:
{% block layout_extender %}
{% if intro == 'false' %}
{% extends 'UdoWebsiteBundle::layout.html.twig' %}
{% else %}
{% extends 'UdoWebsiteBundle::layout_true.html.twig' %}
{% endif %}
{% endblock %}
I get this error:
Multiple extends tags are forbidden in "UdoWebsiteBundle:home:home.html.twig" at line 7
Is there any other way to achieve this?
Try this one:
{% extends intro == 'false'
? 'UdoWebsiteBundle::layout.html.twig'
: 'UdoWebsiteBundle::layout_true.html.twig' %}
Idea taken from here: http://jorisdewit.ca/2011/08/27/extending-different-layouts-for-ajax-requests-in-twig-symfony2/
To keep it neat you should use Twig dynamic inheritance support by using a variable, defined in your controller, as the base template:
{% extends parent_template_var %}
If the variable evaluates to a Twig_Template object, Twig will use it as the parent template.
Define parent_template_var in your controller:
if($intro == 'false')
$parent_template_var = 'UdoWebsiteBundle::layout.html.twig';
}else{
$parent_template_var = 'UdoWebsiteBundle::layout_true.html.twig';
}
return $this->render('::/action.html.twig', array('parent_template_var' => $parent_template_var ));
http://twig.sensiolabs.org/doc/tags/extends.html
Answer from the official documentation:
Conditional Inheritance
As the template name for the parent can be any valid Twig expression, it's possible to make the inheritance mechanism conditional:
{% extends standalone ? "minimum.html" : "base.html" %}
In this example, the template will extend the "minimum.html" layout template if the standalone variable evaluates to true, and "base.html" otherwise.
You cannot extends multiple template, that's why you've got the error, if you want to so, you need to push them in an array like below.
{% extends ['MyAppCustomBundle::Layout/layout.html.twig', 'FOSUserBundle::layout.html.twig'] %}
But you will need to use Twig version 1.2 to do it.
twig documentation
This all makes sense to do either this template or that template.
But let me describe another 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 seperate 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 did try
{% if not viewOnly %}
{% extends ... %}
{% endif %}
That gave problems because you can't use extends within a if. Like you suggested in other answers try using
{% extends viewOnly == true ? ... %}
This reolved the Twig issue up to the execution of the code when viewOnly is 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 == true ? ... %}
if viewOnly becomes false 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 true but nothing happens on the fail?

Categories