Symfony / Sonata Admin: List form on Edit form - php

I have a one (category) to many (product) relationship set up, and I'd like to have a list of products show up at the bottom of the edit category page.
It seems like this would be a common thing to do, but I haven't found any way to do it (or any examples of it). I have managed to get the product to display using sonata_type_collection but that gives me a whole edit form for the Product, when I really just want a list of products associated with the category.
Two questions here, really:
Is this possible?
Is it discouraged (which would explain the lack of examples)? If so, why?

The fastest way to do what you are looking for is overriding the edit template. At your admin serivce declaration you can do so:
services:
sonata.admin.mail:
class: %sonata.admin.category.class%
tags:
- { name: sonata.admin, manager_type: orm, group: "Categories", label: "Category" }
arguments:
- ~
- %skooli.category.class%
- ~
calls:
- [ setTemplate, ["edit", "AcmeAdminBundle:CategoryAdmin:edit.html.twig"] ]
Then, under AcmeBundle/Resources/views/CategoryAdmin/edit.html.twig you can have something like this:
{% extends 'SonataAdminBundle:CRUD:base_edit.html.twig' %}
{# Override any block from the parent view if necessary #}
{% block products %}
<ul>
{% for product in object.products%}
<li>{{ product.name }}</li>
{% endfor %}
</ul>
{% endblock products %}

In your original question you were talking about the edit template of the category. In your comment you want the list to appear in the show action.
The latter is easy. As soon as you add your relation to your showFields action they will be shown:
use Sonata\AdminBundle\Show\ShowMapper;
class CategoryAdmin extends Admin
{
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('products')
;
}
}
If you don't like the look you can create a custom template. This will work for show and edit:
https://sonata-project.org/bundles/admin/master/doc/reference/action_show.html#setting-up-a-custom-show-template-very-useful

Related

Sonata admin: list one to many in edit page

I have a many many to many relation in Sonata (two one to many relations to be exact), Brand and Retailer.
In the Brand admin Edit page, I want to display all the retailers as a list (so just a read only version), instead of having the normal edit (at the moment, on this brand edit page, I can manage the relationship between this brand and retailers - add a new one, delete an existing one).
I tried to explore two routes so far:
Edit page will load a custom twig
Using a custom field type for this field only
My issue is, with both options, I didn't manage to get to a solution
So here is what I have done:
1 - Loading a custom edit twig:
services:
xx_brand.admin.brand_brand:
calls:
- [ setTemplate, [edit, xxBrandBundle:Admin:base_edit.html.twig]]
On this case, base_edit is an exact copy of the sonata base_edit, but it loads my custom base_edit_form:
{% use 'xxBrandBundle:Admin:base_edit_form.html.twig' with form as parentForm %}
From here I can exclude the default rendering of the retailers, but can't find a way to then render it as I want, as I am not sure how the retailers entity is managed here:
{% if admin.formfielddescriptions[field_name] is defined and field_name != 'retailers' %}
{{ form_row(form[field_name])}}
{% else %}
<ul>
<li>retailer1</li>
<li>retailer2</li>
</ul>
{% endif %}
2 - For the approach of a custom field type, I tried to follow the documentation
Creating the Bundle/Form/Type/ListType.php
Creating the /BrandBundle/Resources/views/form/list.html.twig
Using the ListType in configureFormFields:
use XX\BrandBundle\Form\Type\ListType;
...
->add('retailers', 'ListType');
But I then get an error XX\BrandBundle\Form\Type\ListType
So basically, because I couldn't get it to work, are any of these two options good to solve my issue ?
If so, could anyone please advice on what I am missing there ??
Any help will be very much appreciated :)
You can use sonata_type_model_list: https://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/form_field_definition.html#example
Like so:
class BrandAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('retailer', 'sonata_type_model_list', array(
'btn_add' => false,
'btn_delete' => false,
));
}
}

Add custom Sonata page route to the navbar

I've created a custom Sonata page
Simple route
medapp_adminStreamCommands:
path: /admin/stream
defaults: { _controller: MedAppBundle:VideoChat/VideoChat:adminStreamCommands }
Controller that returns the admin pool
public function adminStreamCommandsAction(Request $request)
{
return $this->render('#MedApp/AdminSonata/Stream/stream_commands.html.twig', array(
'admin_pool' => $this->get('sonata.admin.pool')));
}
Plain view template
{% extends '#MedApp/AdminSonata/standard_layout.html.twig' %}
{% block content %}
foobar
{% endblock content
This works, I can access it on my website with /admin/foo and I get a page which has the Sonata admin template with my 'foobar' content.
My question is, how can I add this route to the left and top navbar without having to modify the default template?
That is because the left menu is rendered by a KNP menu:
{% block side_bar_nav %}
{% if app.user and is_granted('ROLE_SONATA_ADMIN') %}
{{ knp_menu_render('sonata_admin_sidebar', {template: admin_pool.getTemplate('knp_menu_template')}) }}
{% endif %}
{% endblock side_bar_nav %}
And I somehow need to add my new page to be rendered by this menu.
Normally, a page is added through a service, but these are built on top of an entity:
servicename:
class: Bundle\Class
arguments: [~, Bundle\Entity\Entityname, ~]
tags:
- { name: sonata.admin, manager_type: orm, group: admin, label: CustomName}
My page is not using an entity, though, just static content or content that is not dependant on an entity.
I know already that I can modify the blocks that generate the menus, but I was thinking that the best way would be to add my class as a service tagged as sonata.admin that doesn't have an orm manager_type, in other words, is not an Entity. How can that be done?
You should override standard_layout and modify content of side_bar_nav block. This is simple and fast way. Or you can dig into sonata code to find how to inject something into admin_pool.dashboardgroups - have fun :)
I don't think that's possible, you have to create a new layout, copy the sonata admin layout and customize it to your need.
You can change the layout used by changing the yml configuration for sonata_admin (templates -> layout) or extending the SonataAdmin bundle and creating your own layout.html.twig.

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.

Symfony2 fos login, register and forgot password in one view

I need to apply a purchased template to our dashboard. In this template, the login, register and forgot password forms are all under the same view, and switching between them using simple JQuery.
I have been looking for a nice, not-too-flashy way of combining all three forms into one, but I came up empty.
My standing options (as I see them), and why I don't like any of them:
Take the views from the fos bundle, copy them to /app/Resources/FOSUserBundle/views/, remove the {% extend %} part and {% include %} them in my own login view. Reason for dislike: to me this looks a little like a quick-n-dirty fix - "that part's not working? Let's break it off!" :)
Extend the fos bundle, accept an extra parameter in the LoginAction and RegisterAction, use {% render %} with parameters in my own login view. Reason for dislike: extending a whole bundle and modifying two different controllers just to change the way it renders feels like bad MVC.
XHR load everything. Reason for dislike: this approach makes sense when using inner pages, but for pages that reload anyway it just doesn't make sense.
TL;DR version: I'm looking for a non-hack way of including the login, register and forgot password form in one page.
Any help would be greatly appreciated!
I found a solution with which I am comfortable with for my current project. The advantages and disadvantages of the proposed solution upfront:
Advantages:
few LOC to implement
FOSUserBundle update proof (does not override the view scripts*)
Disadvantages:
performance overhead due to subrequests
only forms can be displayed, form submission (and subsequently error handling upon submission) will always go to the pages provided by FOSUserBundle
still feels like a quick-n-dirty fix, but better than other options
* only needs to override the layout.html.twig file
With that being said, here is what I have done:
Render the form in your template
Use embedded controllers to render the forms you need:
<div>
<h2>Login</h2>
{{ render(controller('FOSUserBundle:Security:login', { embeddedForm: true})) }}
</div>
<div>
<h2>Reset</h2>
{{ render(controller('FOSUserBundle:Resetting:request', { embeddedForm: true})) }}
</div>
Override FOSUserBundle layout
As I use the routes provided by the bundle, I had to override the FOSUserBundle layout template file to extend the standard layout of my application. As the overriden FOSUserBundle layout file extends the main applications layout file the layout would be repeated for each call {{ render ... }}. To prevent that, we need to dynamically disarm the extended layout file. Here is what the overriden layout file looks like:
{# app/Resources/FOSUserBundle/views/layout.html.twig #}
{% if app.request.get('embeddedForm') %}
{% set layout = 'AcmeBundle::layout-content.html.twig' %}
{% else %}
{% set layout = 'AcmeBundle::layout.html.twig' %}
{% endif %}
{% extends layout %}
{% block content %}
{% block fos_user_content %}{% endblock %}
{% endblock %}
Create the AcmeBundle::layout-content.html.twig file
This layout should only render the content block of the FOSUserBundle view scripts and is such short and simple:
{# src/Acme/DemoBundle/Resources/views/layout-content.html.twig #}
{% block content %}{% endblock %}
Now the forms will render nicely with all dependencies (CSRF and so forth). Submitting the form will however take you to the FOSUserBundle actions.
Alternative solution:
This answer describes how to manually implement the forms and link them to the FOSUserBundle controller.

How to get at runtime the route name in Symfony2 when using the yaml routes description?

Here you can find my n-th question on Symfony2.
I'm working with a pagination bundle that uses the route name provided in the routing.yml file.
From my perspective, this approach is not flexible and lead to a dirty code, since if I change the name of the route, then I have to look at all the Twig templates or PHP files to update the route name. This is ok for small Web applications, but will provide such a bug for larger applications and also need an high burden for the developer.
So, I was wondering to pass a string variable x to the Pager object provided by the above mentioned bundle. The string x should be initialized within the controller and has to provide the desired route name as given in the routing.yml file.
Let me give an example. The routing file is the following:
//routing.yml
AcmeTestBundle_listall:
pattern: /test/page/{page}
defaults: { _controller: AcmeTestBundle:List:listall, page: 1 }
requirements:
page: \d+
Then the related controller is:
//use something....
class ListController extends Controller
{
public function exampleAction($page)
{
$array = range(1, 100);
$adapter = new ArrayAdapter($array);
$pager = new Pager($adapter, array('page' => $page, 'limit' => 25));
return array('pager' => $pager);
}
}
Then, in the twig template, the $pager receives the route name that refer to the above bundle
{% if pager.isPaginable %}
{{ paginate(pager, 'AcmeTestBundle_listall') }}
{% endif %}
{% for item in pager.getResults %}
<p>{{ item }}</p>
{% endfor %}
Any idea of how to get the 'AcmeTestBundle_listall' string value at runtime just inside the controller?
You can use the app global variable that is available in twig to get the current route from the request.
{% if pager.isPaginable %}
{{ paginate(pager, app.request.get('_route') }}
{% endif %}
More about app here and here.

Categories