I'm using Symfony 4.3 and Sonata 3.x version.
I'm trying to create a custom route in a custom Page but I get the error :
An exception has been thrown during the rendering of a template ("unable to find the route `admin.photocall|admin.photocall_gallery.moderate`")
I have an entity X with a OneToMany relation to the Y entity.
Explanation with code :
class XAdmin extends AbstractAdmin
{
[...]
protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
$admin = $this->isChild() ? $this->getParent() : $this;
$id = $admin->getRequest()->get('id');
if ($this->isGranted('LIST')) {
$menu->addChild('Galerie', [
'uri' => $admin->generateUrl('admin.photocall_gallery.list', ['id' => $id])
]);
}
}
}
Then there is my YAdmin :
class YAdmin extends AbstractAdmin
{
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->add('_action', null, [
'actions' => [
'clone' => [
'template' => 'admin/PhotocallListAdmin/__list_action_moderate.html.twig'
]
]
])
;
}
protected function configureRoutes(RouteCollection $collection)
{
if ($this->isChild()) {
$collection->clearExcept(['list', 'moderate']);
$collection->add($collection->getBaseCodeRoute().'.moderate', 'moderate');
return;
}
}
}
So there, I add an action with a template which look like this :
<a class="btn btn-sm" href="{{ admin.generateObjectUrl('moderate', object) }}">
{% if not object.ismoderate %}
Moderate
{% else %}
Undo moderation
{% endif%}
</a>
So the error says that it's unable to find the route admin.photocall|admin.photocall_gallery.moderate. But when I dump the $collection in YAdmin after adding the moderate route, I have two elements :
admin.photocall|admin.photocall_gallery.list (the current page)
admin.photocall|admin.photocall_gallery.moderate
I searched but it looks like that nobody else did this.
Thanks for you help
I write Gaska's comment to close this issue.
I just had to add :
$collection->add('moderate', 'moderate'); and then clear the cache.
Thanks to him
Related
I am trying to manage multiple forms in the same page in Symfony 5 with the following function, but it seems that every time I try to submit a form, only the first form of the list is handled even if it is not the one that has been submitted:
class ContentController extends AbstractController
{
/**
* #Route("/category/edition/{content}", name="edit_category")
*/
public function edition(Request $request, Category $content): Response
{
$forms = [
"edit_category" => $this->createForm(EditCategoryType::class, $content),
"create_post" => $this->createForm(CreatePostType::class)
];
foreach($forms as $form) {
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
// Always prints edit_category
// even when that is the the create_post that is submitted
return var_dump($form->getName());
}
}
return $this->render(
'content/edition.html.twig',
[
'forms' => \array_map(
function($form) {
return $form->createView();
},
$forms
),
'content' => $content,
]
);
}
}
I have seen in other posts that the name of the forms can sometime raise an issue, but I have checked that the forms do have different names, and I have also tried to call handleRequest() on every form in a separate foreach loop because I have seen this done in some posts, but it (quite expectedly I must say) didn't change the behavior.
And I didn't seem to find any unanimous best practice tips about how to handle multiple forms in the same Controller in Symfony, so I was wondering what is the best way to do it, or if it would be cleaner to define a separate action route for each form in order to avoid this problem altogether.
If needed, the content/edition.html.twig file looks something like that:
{% set edit_category_form = forms['edit_category'] %}
{% set create_post_form = forms['create_post'] %}
{{ form_start(edit_category_form) }}
{{ form_errors(edit_category_form) }}
{{ form_end(edit_category_form) }}
{{ form_start(create_post_form) }}
{{ form_errors(create_post_form) }}
{{ form_end(create_post_form) }}
(Category is a classical Symfony entity, EditCategoryType is a form associated with the Category entity and CreatePostType is a form associated with another Symfony entity)
After some research, it seems for some reason like it works if (and only if?) the form is build just before handling the request:
class ContentController extends AbstractController
{
/**
* #Route("/category/edition/{content}", name="edit_category")
*/
public function edition(Request $request, Category $content): Response
{
$self = $this;
$formBuilders = [
"edit_category" => function() use ($self, $content) {
return $self->createForm(EditCategoryType::class, $content);
},
"create_post" => function() use ($self, $content) {
return $self->createForm(CreatePostType::class);
},
];
$forms = [];
foreach($formBuilders as $key => $formBuilder) {
$form = $formBuilder();
$forms[$key] = $form;
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
// Does print the name of the right form
return var_dump($form->getName());
}
}
return $this->render(
'content/edition.html.twig',
[
'forms' => \array_map(
function($form) {
return $form->createView();
},
$forms
),
'content' => $content,
]
);
}
}
It works but it doesn't feel like a proper way to handle this !
I am trying to "Update field dynamicly based on the user choice in another field in Symfony 5" from "https://symfony.com/doc/current/form/dynamic_form_modification.html#form-events-submitted-data" this side. But i faced this exceptions/error "Argument 2 passed to App\Form\SportMeetupType::App\Form{closure}() must be an instance of App\Form\Sport or null, instance of App\Entity\Sport given, called in C:\Apache24\htdocs\dynamicform\src\Form\SportMeetupType.php on line 58" when ajax called.
My Code:
Form :
class SportMeetupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('sport', EntityType::class, [
'class' => 'App\Entity\Sport',
'placeholder' => '',
])
;
$formModifier = function (FormInterface $form, Sport $sport = null) {
$positions = null === $sport ? [] : $sport->getPositions();
$form->add('position', EntityType::class, [
'class' => 'App\Entity\Position',
'placeholder' => '',
'choices' => $positions,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be your entity, i.e. SportMeetup
$data = $event->getData();
$formModifier($event->getForm(), $data->getSport());
}
);
$builder->get('sport')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
// It's important here to fetch $event->getForm()->getData(), as
// $event->getData() will get you the client data (that is, the ID)
$sport = $event->getForm()->getData();
// since we've added the listener to the child, we'll have to pass on
// the parent to the callback functions!
$formModifier($event->getForm()->getParent(), $sport); // Here line 58 and error show this line
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => SportMeetup::class,
]);
}
}
Controller:
/**
* #Route("/new", name="sport_meetup_new", methods={"GET","POST"})
*/
public function new(Request $request): Response
{
$sportMeetup = new SportMeetup();
$form = $this->createForm(SportMeetupType::class, $sportMeetup);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($sportMeetup);
$entityManager->flush();
return $this->redirectToRoute('sport_meetup_index');
}
return $this->render('sport_meetup/new.html.twig', [
'sport_meetup' => $sportMeetup,
'form' => $form->createView(),
]);
}
Twig/View:
{% extends 'base.html.twig' %}
{% block title %}New SportMeetup{% endblock %}
{% block body %}
<h1>Create new SportMeetup</h1>
{{ form_start(form) }}
{{ form_row(form.name)}}
{{ form_row(form.sport) }} {# <select id="sport_meetup_sport" ... #}
{{ form_row(form.position) }} {# <select id="sport_meetup_position" ... #}
{# ... #}
{{ form_end(form) }}
{% endblock %}
JavaScript code:
var $sport = $('#sport_meetup_sport');
// When sport gets selected ...
$sport.change(function() {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected sport value.
var data = {};
data[$sport.attr('name')] = $sport.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url : $form.attr('action'),
type: "POST",
data : data,
success: function(html) {
// Replace current position field ...
$('#sport_meetup_position').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#sport_meetup_position')
);
// Position field now displays the appropriate positions.
}
});
});
When changed Sport dropdown value then ajax called and show below exceptions/error
"Argument 2 passed to App\Form\SportMeetupType::App\Form{closure}() must be an instance of App\Form\Sport or null, instance of App\Entity\Sport given, called in C:\Apache24\htdocs\dynamicform\src\Form\SportMeetupType.php on line 58"
Please help me....
Make sure that in your SportMeetupType.php you have:
use App\Entity\Sport;
I think you are using wrong class.
I have a custom action, based on the editAction from Sonata. Only the form is different.
public function customAction(){
$id = $this->get('request')->get($this->admin->getIdParameter());
$object = $this->admin->getObject($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
}
if (false === $this->admin->isGranted('EDIT', $object)) {
throw new AccessDeniedException();
}
// On vérifie que l'annonce appartient bien à l'utilisateur connecté
if($this->getUser()->getId() !== $object->getUser()->getId()) {
throw new AccessDeniedException();
}
$em = $this->getDoctrine()->getManager();
$preparechoices = $em->getRepository('AcmeBundle:Entity')->findAll();
foreach($preparechoices as $c){
$choices[$c->getId()] = $c->getLibelle();
}
$form = $this->createFormBuilder(array('choix'=>1))
->add('choix','choice',array('choices'=>$choices))
->add('submit','submit')
->getForm();
$view = $form->createView();
$this->admin->setSubject($object);
$this->get('twig')->getExtension('form')->renderer->setTheme($view, $this->admin->getFormTheme());
return $this->render($this->admin->getTemplate('EDIT'), array(
'action' => 'edit',
'object' => $object,
'form' => $view,
));
}
But I got this error :
Impossible to access a key ("default") on a boolean variable ("")
The error come from this line in the twif file :
{{ form_helper.render_groups(admin, form, admin.formtabs['default'].groups, has_tab) }}
I can't find how to fix it, does anyone know ?
Thanks to Joshua, i was able to fix the error by adding this line:
$this->admin->setFormTabs(array('default'=>array('groups' => array())));
But now, i got a new error :
Impossible to access an attribute ("help") on a null variable
Form form_admin_fields.html.twig, this line, because sonata_admin.field_description is null :
{% if sonata_admin.field_description.help %}
title="{{ sonata_admin.admin.trans(sonata_admin.field_description.help, {}, sonata_admin.field_description.translationDomain)|raw }}"
{% endif %}
I don't know how to fix it, i tried several test, whitout success, in the form definition like :
$form = $this->createFormBuilder(array('choix'=>1))
->add('choix','choice',array('choices'=>$choices,'sonata_admin'=>array('field_description'=>array('help'=>'help_message'))))
->add('submit','submit')
->getForm();
Imagine you are developing a website using symfony2 and it's admin panel using Sonata Admin Bundle and of course imagine you have a class named Product which owns some Images. Image is also a class and you have set in Product a one to many relation-ship to Image class. So every image is owned by a product, so you want to manage Image Admin inside Product Admin classes.
So there are some problems you should to face with them. Would you please to tell how?
1. When you delete a product object, all images related to that product be deleted.
2. When you are in show product page or add new product page the picture of all of images display on page.
(Is there any solution without using sonata media bundle too?)
Thanks
I handle Image upload with Document class:
FstQst\WebBundle\Entity\Document:
type: entity
table: null
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
updated: # changed when files are uploaded, to force preUpdate and postUpdate to fire
type: datetime
nullable: true
format:
type: string
length: 25
nullable: true
lifecycleCallbacks:
prePersist: [ preUpload ]
preUpdate: [ preUpload ]
postPersist: [upload]
postUpdate: [upload]
postRemove: [removeUpload]
I have another class using Document for adding some features to image:
FstQst\WebBundle\Entity\Post:
type: entity
table: null
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
title:
type: string
length: 100
nullable: true
oneToOne:
document:
targetEntity: Document
joinColumn:
name: document_id
referencedColumnName: id
orphanRemoval: true
manyToOne:
site:
targetEntity: VisitablePoint
inversedBy: posts
joinColumn:
name: vPoint_id
referencedColumnName: id
lifecycleCallbacks: { }
And a class named VisitablePoint, which uses Post class:
FstQst\WebBundle\Entity\VisitablePoint:
type: entity
table: visitablePoint
id:
id:
type: integer
id: true
generator:
strategy: AUTO
oneToMany:
posts:
targetEntity: Post
mappedBy: site
orphanRemoval: true
lifecycleCallbacks: { }
I changed my classes names from Post to Image and from VisitablePoint to Product at my old post. Now I want when I go to VisitablePoint admin/show page I see pictures of Post object instead of their title. And of course in admin/edit page too.
And these are Admin Classes:
<?php
namespace FstQst\WebBundle\Admin;
use FstQst\WebBundle\Entity\Document;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class DocumentAdmin extends Admin
{
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add('updated')
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('file', 'file', $this->getFieldOptionForImagePreview())
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('id')
->add('updated')
->add('format')
;
}
public function prePersist($image) {
$this->manageFileUpload($image);
}
public function preUpdate($image) {
$this->manageFileUpload($image);
}
protected function manageFileUpload(Document $image) {
if ($image->getFile()) {
$image->refreshUpdated();
}
}
protected function getFieldOptionForImagePreview($maxSize = 200){
if($this->hasParentFieldDescription()) { // this Admin is embedded
// $getter will be something like 'getlogoImage'
$getter = 'get' . $this->getParentFieldDescription()->getFieldName();
// get hold of the parent object
$parent = $this->getParentFieldDescription()->getAdmin()->getSubject();
if ($parent) {
$document = $parent->$getter();
} else {
$document = null;
}
} else {
$document = $this->getSubject();
}
// use $fileFieldOptions so we can add other options to the field
$fileFieldOptions = array('required' => false);
if ($document && ($webPath = $document->getWebPath())) {
// get the container so the full path to the image can be set
$container = $this->getConfigurationPool()->getContainer();
$fullPath = $container->get('request')->getBasePath().'/'.$webPath;
//$fileFieldOptions['help'] = '<img src="/uploads/documents/10.png" class="admin-preview" style="max-height: 200px; max-width: 200px"/>';
$fileFieldOptions['help'] = <<<START
<img src="$fullPath" style="max-height: {$maxSize}px; max-width: {$maxSize}px"/>
START;
}
return $fileFieldOptions;
}
}
<?php
namespace FstQst\WebBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class VisitablePointAdmin extends Admin
{
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('posts', 'sonata_type_model', array('multiple' => true, 'property' => 'title', 'label' => 'Image', 'required' => false))
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('posts', 'sonata_type_collection', ['label' => 'Images',
'required' => false, 'cascade_validation' => true,
'by_reference' => false], ['edit' => 'inline', 'inline' => 'table'])
->end()
;
}
}
The One to Many relation
1. When you delete a product object, all images related to that product be deleted.
You can easily manage this kind of deletion with the orphanRemoval="true"
<?php
class Product{
[...]
/**
* #ORM\OneToMany(targetEntity="Event", mappedBy="day", orphanRemoval="true", cascade={"all"})
*/
private $images;
[...]
}
in yml, the configuration looks like:
oneToMany:
images:
targetEntity: Image
orphanRemoval: true
mappedBy: product
2. When you are in show product page or add new product page the picture of all of images display on page.
You have to use a sonata_type_collection in your configureFormFields or your configureShowFields method of your ProductAdmin class:
<?php
class ProductAdmin{
[...]
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->with('tab_images')
->add('images', 'sonata_type_collection', array(
'required' => false,
'cascade_validation' => true,
'by_reference' => false,
), array(
'edit' => 'inline',
'inline' => 'table',
))
->end()
;
}
[...]
}
Then provide a ImageAdmin with all you need to upload files.
You might have to change settings because i took it from a personal project and i don't know if it's totally adapted to your needs.
Show images in Sonata
first configure your ImageAdmin as follow:
class ImageAdmin extends Admin
{
[...]
protected function configureShowFields(ShowMapper $showMapper) {
$showMapper
->add('myImageAttr', 'image', array(
'prefix' => '/',
))
;
}
[...]
protected function configureListFields(ListMapper $listMapper) {
$listMapper
->add('myImageAttr', 'image', array(
'prefix' => '/',
'width' => 100
))
;
}
}
then create template for your list_image and you show_image types:
Resources/views/CRUD/show_image.html.twig
{% extends 'SonataAdminBundle:CRUD:base_show_field.html.twig' %}
{% block field %}
{% if value %}
<img src="{% if field_description.options.prefix %}{{ field_description.options.prefix }}{% endif %}{{ value }}" />
{% endif %}
{% endblock %}
Resources/views/CRUD/list_image.html.twig
{% extends admin.getTemplate('base_list_field') %}
{% block field%}
{% spaceless %}
{% set width = field_description.options.width is defined ? field_description.options.width : 50 %}
{% set height = field_description.options.height is defined ? field_description.options.height : 50 %}
{% if value %}
<img src="{% if field_description.options.prefix is defined %}{{ field_description.options.prefix }}{% endif %}{{ value }}"
style="max-width:{{ width }}px; max-height:{{ height }}px;" />
{% else %}
<div class="no-image" style="width:{{ width }}px; height:{{ height }}px;"></div>
{% endif %}
{% endspaceless %}
{% endblock %}
finally, add this configuration into your app/config/config.yml
sonata_doctrine_orm_admin:
templates:
types:
show:
image: YourBundle:CRUD:show_image.html.twig
list:
image: YourBundle:CRUD:list_image.html.twig
(http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/templates.html)
I got the following error when I add a custom action in Sonata Admin
FatalErrorException: Error: Class 'Symfony\Component\Debug\Exception\FlattenException' not found in /myproject_path/AppBundle/Admin/BalticsAdmin.php line 106
What is this problem please share me
HERE IS THE CODE
In admin class
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
................................
.................
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
'upload' => array('template' => 'SteelGuruBundle:CRUD:list__action_upload.html.twig'),
)
));
}
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('upload', $this->getRouterIdParameter().'/upload');
}
THEN I cREATE a controller class in src/.../.../Controller/CRUDController.php with following codes
namespace ...\AppBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as Controller;
class CRUDController extends Controller
{
public function uploadAction()
{
$id = $this->get('request')->get($this->admin->getIdParameter());
$object = $this->admin->getObject($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
}
}
}
Then I create a template in src/.../AppBundle/Resources/views/CRUD/list__action_upload.html.twig with the following code
<a class="btn btn-small" href="{{ admin.generateObjectUrl('upload', object) }}">Upload</a>
and then in services.yml I added the following
sg_app.admin.baltic:
class: ...\AppBundle\Admin\BalticsAdmin
tags:
- { name: sonata.admin, manager_type: orm, audit:false, group: Test, label: Upload}
arguments: [null, ...\AppBundle\Entity\Baltics, ...AppBundle:CRUD]
Thanks for your response
i had a similar problem and found out that i had to add
use Sonata\AdminBundle\Route\RouteCollection;
to the Admin (in your case BalticAdmin).