I'm looking for a way to extend Symfony 2 EntityType
Symfony\Bridge\Doctrine\Form\Type\EntityType
as in a new type extending this one, not creating a FormTypeExtension - and I can't figure it out. Does anyone know any proper way to do that?
I've tried simply extending it that way:
class NestedEntityType extends EntityType {
public function getName() {
return $this->getBlockPrefix();
}
public function getBlockPrefix() {
return 'nested_entity';
}
}
and then in sonata admin class I have:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('types', NestedEntityType::class, [
'label' => false,
'multiple' => true,
'expanded' => true,
'by_reference' => false
]);
}
but unfortunately it causes Fatal Error:
Catchable Fatal Error: Argument 1 passed to
Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must
implement interface Doctrine\Common\Persistence\ManagerRegistry, none
given, called in
I need to keep the whole functionality of EntityType, with one exception - the way it's presented. That's why I need to extend this type (I use it in other fields, so I can't just modify the template for it!).
I'm using Symfony 2.8 (just for the record).
You should not extend it directly but use parent option
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
So something like
class NestedEntityType extends AbstractType
{
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'nested_entity';
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
}
That way if FormType you're extending from has something in injected (or setted) into constructor, you don't need to care as symfony will do it for you.
So, if you need a create a reusable solution, for different Entities, you don't need a configureOptions in this case.
You need to create an elementType in you code like
$this->createForm(NestedEntityType::class, null, ['class' => YourEntity::class]);
And in this case, you will need to pass as an option a name of class Entity which is nested.
If you would go to EntityType, you'll see it's extending DoctrineType and needs dependencies in constructor -
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
So, your class NestedEntityType uses the same constructor and needs the same dependencies.
Actually, what you need is
class NestedEntityType extends AbstractType {
public function getParent() {
return EntityType::class;
}
public function getName() {
return $this->getBlockPrefix();
}
public function getBlockPrefix() {
return 'nested_entity';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => YourEntity::class
]);
}
}
UPD: Of course you need configure options, according to EntityType doc. See method I've added to code.
Related
I'm working on a TagField for EasyAdmin 4 (and Symfony 6) that will rely on a TagType. This TagType will have the native ChoiceType as a parent.
This field will be rendered as a multiple select, with these attributes to allow adding tags on the fly:
[ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ]
To do so, I created a TagListener. Its main goal is to prefill the options with the already existing tags (on other entities) to support tag suggestion. After reading the docs and many articles, I chose to listen to the FormEvents::PRE_SET_DATA event.
Unfortunately there does not seem to be an easy way to "override" the default options, and we're left with having to override the entire field.
Here's what the TagListener looks like:
<?php
// src/Form/EventListener/TagListener.php
namespace eduMedia\TagBundle\Form\EventListener;
use eduMedia\TagBundle\Entity\TaggableInterface;
use eduMedia\TagBundle\Form\Type\TagType;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TagListener implements EventSubscriberInterface
{
public function __construct(private TagService $tagService)
{
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents(): array
{
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
];
}
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$parentForm = $event->getForm()->getParent();
/** #var TaggableInterface $taggable */
$taggable = $parentForm->getData();
// We retrieve the existing options to override some of them
$options = $form->getConfig()->getOptions();
// if ($options['pre_set_data_called']) {
// return;
// }
// We prefill options with the existing tags for this resource type
$allTagNames = $this->tagService->getTypeTagNames($taggable->getTaggableType());
// They are our new choices
$options['choices'] = array_combine($allTagNames, $allTagNames);
// We also need to select the entity's tags
$options['data'] = $this->tagService->loadTagging($taggable)->getTagNames($taggable);
// We override the form field
// $options['pre_set_data_called'] = true;
$parentForm->add($form->getName(), TagType::class, $options);
}
}
Doing so seems to create an infinite loop, where onPreSetData is called when calling $parentForm->add(). Is that normal? Is PRE_SET_DATA dispatched again when adding a field in a listener? Is there a way to prevent this from happening?
I tried adding a pre_set_data_called form option, setting it to true when calling $parentForm->add() and exiting the listener when it is indeed true. It kind of works, but then I get this error:
An exception has been thrown during the rendering of a template ("Field "tags" has already been rendered, save the result of previous render call to a variable and output that instead.").
How can I manage to allow extra items in my custom field type?
For reference, here is my TagType class:
<?php
namespace eduMedia\TagBundle\Form\Type;
use eduMedia\TagBundle\Form\EventListener\TagListener;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function __construct(private TagService $tagService)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new TagListener($this->tagService));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'mapped' => false,
'multiple' => true,
// 'pre_set_data_called' => false,
]);
}
public function getParent()
{
return ChoiceType::class;
}
}
And my TagField class:
<?php
namespace eduMedia\TagBundle\Admin\Field;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use eduMedia\TagBundle\Form\Type\TagType;
class TagField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null)
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TagType::class)
->setFormTypeOption('attr', [ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ])
->setTemplatePath('#eduMediaTag/fields/tag.html.twig')
;
}
}
I ended up not using the ChoiceType as the parent (<select> element), but rather the TextType (<input type=text> element), and splitting/exploding a simple string.
The actual bundle is live on GitHub and even though it might not be perfect (yet 😉), the implementation is way simpler and the end-user behaviour is exactly what I expected.
I have an event that would dispatch information on multiple channels:
A channel for members
A channel for managers
I wrote this:
<?php
class ModelUpdated implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $model;
public function __construct(Model $model)
{
$this->model = $model;
}
public function broadcastWith($who)
{
if ($who == "model.{$this->model->id}")
return [$this->model->id];
else if ($who == "model.{$this->model->id}.managers")
return [$this->model];
else
return [];
}
public function broadcastOn()
{
return [
new PrivateChannel("model.{$this->model->id}"),
new PrivateChannel("model.{$this->model->id}.managers")
];
}
}
Unfortunately, the broadcastWith doesn't work with my $who magic. Is there an alternative way of doing it?
I would like to avoid having different events because mine is triggered in the model:
class MyModel extends Model
{
use Notifiable;
protected $dispatchesEvents = [
'saved' => ModelUpdated::class,
'updated' => ModelUpdated::class,
];
}
As far as I know broadcastWith does not accept any parameter, Laravel 8.x.
You need to pass all your data through the constructor, then, make decision on what you need to return as event's payload.
Make sure to return an array from the broadcastWith() method.
Ever since I started using Zend Framework 3, I had problems with testing my controllers. I'm trying to test my controllers with PhpUnit 5.7 and my controllers depend on Zend Form, which is hydrated with Doctrine's DoctrineObject.
I'm trying to put this as simple as possible, so here's a minimal example of a setup that's giving me headaches:
Controller:
class IndexController extends AbstractActionController {
private $form;
public function __construct(AlbumForm $form) {
$this->form = $form;
}
public function indexAction() {
return ['form' => $this-form];
}
}
ControllerFactory:
class IndexControllerFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, ...) {
$formManager = $container->get('FormElementManager');
return new IndexController($formManager->get(AlbumForm::class));
}
}
The corresponding view template in albums/index/index.phtml:
<?php
$this->form->prepare();
$this->form->setAttribute('action', $this->url(null, [], true));
$albumFieldset = $this->form->get('album');
?>
<?= $this->form()->openTag($this-form) ?>
<div class="form-group">
<?= $this->formRow($albumFieldset->get('name')) ?>
</div>
<?= $this->form()->closeTag() ?>
The form:
class AlbumForm extends Form {
public function init() {
$this->add([
'name' => 'albumFieldset',
'type' => AlbumFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
}
}
The fieldset:
class AlbumFieldset extends Fieldset {
public function init() {
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name of album',
],
]);
}
}
The FieldsetFactory:
class AlbumFieldsetFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, ...) {
$objectManager = $container->get(ObjectManager::class);
$fieldset = new AlbumFieldset();
$fieldset->setHydrator(new DoctrineObject($objectManager));
$fieldset->setObject(new Album());
return $fieldset;
}
}
Now, so far everything is working great.
However, when writing tests for this I run into troubles. Let me first show you what I have so far:
class IndexControllerTest extends AbstractHttpControllerTestCase {
protected function setUp() {
parent::setUp();
$this->configureServiceManager($this->getApplicationServiceLocator());
}
private function configureServiceManager(ServiceManager $services) {
$services->setAllowOverride(true);
$services->setService(ObjectManager::class, $this->mockObjectManager()->reveal());
$services->setService('FormElementManager', $this->mockFormManager()->reveal());
$services->setAllowOverride(false);
}
private $objectManager;
private function mockObjectManager() {
$this->objectManager = $this->prophesize(ObjectManager::class);
return $this->objectManager;
}
private $formManager;
private function mockFormManager() {
$this->formManager = $this->prophesize(FormElementManager::class);
$this->formManager->get(AlbumForm::class)->willReturn($this->mockForm()->reveal());
return $this->formManager;
}
private $form;
private function mockForm() {
$this->form = $this->prophesize(AlbumForm::class);
$this->form->prepare()->willReturn(null);
$this->form->setAttribute('action', Argument::type('string'))->willReturn(null);
$this->form->getAttributes()->willReturn([]);
$this->form->get('album')->willReturn($this->mockAlbumFieldset()->reveal());
return $this->form;
}
private $albumFieldset;
private function mockAlbumFieldset() {
$this->albumFieldset = $this->prophesize(AlbumFieldset::class);
$this->albumFieldset->get('name')->willReturn($this->mockName()->reveal());
return $this->albumFieldset;
}
private $name;
private function mockName() {
$this->name = $this->prophesize(Text::class);
$this->name->getLabel()->willReturn('label');
$this->name->getLabelAttributes()->willReturn(['for' => 'name']);
$this->name->getLabelOption('disable_html_escape')->willReturn(false);
$this->name->getLabelOption('always_wrap')->willReturn(false);
$this->name->getLabelOption('label_position')->willReturn('prepend');
$this->name->getName('album[name]');
$this->name->getAttribute('type')->willReturn('text');
$this->name->hasAttribute('id')->willReturn(true);
$this->name->getAttribute('id')->willReturn('name');
$this->name->getAttributes([])->willReturn([]);
$this->name->getValue()->willReturn(null);
$this->name->getMessages()->willReturn([]);
return $this->name;
}
}
This will eventually run without errors. However, I would like to draw your attention to the last few methods, especially mockName(). Most of those definitions are totally default and almost none of them are specified in AlbumFieldset in the beginning (only name is). It is very annoying to write them down for every form input I may have and writing this down actually introduces more errors than it solves. For example, I'm still not sure what the correct label option for always_wrap would be. I actually don't even care about that option, but I have to write something about it in my test, because otherwise the test fails with 'Prophecy\Exception\Call\UnexpectedCallException' with message 'Method call: - getLabelOption("always_wrap") on Double\Zend\Form\Element\Text\P245 was not expected, expected calls were: ....
Therefore, I'm asking you: is there any better way to go about this? A way that does not involve writing 20+ rows for every field I have in my fieldset. If it involves rewriting my controllers/fieldsets/view templates (etc.), that would totally be fine!
Any help is greatly appreciated! Also, this is my very first time asking something in a forum in over eight years of programming, so please bear with me if anything is unclear.
Yours
Steffen
PS: What I have already tried is to give the IndexController null instead of an actual form and simply abort the view template when it detects that the form is null. However, while that worked without that much setup, I was basically just avoiding the view template's logic. Because of that, I was not able to detect errors in the view template. That's not what I want.
edit IndexControllerTest: Change private as protected elsewhere and extends it for your new fields. Each new controller must overwrite methods calling parent::methodname($args) and add the needed code...
I get the following error when trying to create a form from another entity to pass through to my view.
I have two entities in this context CourseGuide and CourseGuideRow and I would like to pass through a form view of CourseGuideRowType to my view - how can I do this?
The form's view data is expected to be an instance of class
CRMPicco\CourseBundle\Entity\CourseGuide, but is an instance of class
CRMPicco\CourseBundle\Entity\CourseGuideRow. You can avoid this error
by setting the "data_class" option to null or by adding a view
transformer that transforms an instance of class
CRMPicco\CourseBundle\Entity\CourseGuideRow to an instance of
CRMPicco\CourseBundle\Entity\CourseGuide.
This is my controller:
// CourseGuideController.php
public function viewAction(Request $request)
{
if (!$courseId = $request->get('id')) {
throw new NotFoundHttpException('No Course ID provided in ' . __METHOD__);
}
$resource = $this->get('crmpicco.repository.course_guide_row')->createNew();
$form = $this->getForm($resource);
// ...
}
My Symfony FormBuilder class:
// CourseGuideRowType.php
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\Form\FormBuilderInterface;
class CourseGuideRowType extends AbstractResourceType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('channel', 'crmpicco_channel_choice', array('data_class' => null))
->add('name', 'text')
->add('courses', 'text')
;
}
/**
* #return string name
*/
public function getName()
{
return 'crmpicco_course_guide_row';
}
}
I have tried the data_class => null suggestion mentioned elsewhere, but this has no effect.
If I pass through the data_class like this:
$form = $this->getForm($resource, array('data_class' => 'CRMPicco\CourseBundle\Entity\CourseGuideRow'));
I then get this:
Neither the property "translations" nor one of the methods
"getTranslations()", "translations()", "isTranslations()",
"hasTranslations()", "__get()" exist and have public access in class
"CRMPicco\CourseBundle\Entity\CourseGuideRow".
Why is this? There are translations attached to the CourseGuide entity but not the CourseGuideRow.
try to add this function in your FormType:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'YourBundle\Entity\YourEntity',
));
}
And don't forget the specific use:
use Symfony\Component\OptionsResolver\OptionsResolver;
EDIT
In native Symfony (with the Form component):
public function showAction()
{
/.../
$entity = new YourEntity();
$form = $this->createForm('name_of_your_form_type', $entity);
# And the response:
return $this->render('your_template.html.twig', ['form' => $form->createView()]);
}
One of my classes currently extends the BaseController on the FOSUserBundle, and returns the parent action. However, due to project spec, I shouldn't have the need to edit the parent class. Is there a way of sending additional variables, for twig to render, through the child response?
Child Class:
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
return $response; // and 'myVariable' => $myVariable
}
}
Parent Class:
class ChangePasswordController extends ContainerAware
{
/**
* Change user password
*/
public function changePasswordAction(Request $request)
{
//lots of code.....
return $this->container->get('templating')
->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'
.$this->container->getParameter('fos_user.template.engine'),
array(
'form' => $form->createView()
//and 'myVariable' => $myVariable
)
);
}
}
So to summarise, is there a way of passing something to the parent class, without changing the parent class... whilst rendering the twig view with an additional variable.
-- Update --
Essentially I want to render a form using the FOSUserBundle changePassword action, therefore this works fine:
return $this->container
->get('templating')
->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array('form' => $form->createView())
);
However, I want to pass more variables to the view, just like the 'form' is passed as shown above, without altering the FosUserBundle ChangePassword Controller. Therefore I have a class which inherits the that controller, adds some additional functionality and returns the parent change password action:
class ChangePassController extends ChangePasswordController
{
public function changePasswordAction(Request $request)
{
// more code......
$response = parent::changePasswordAction($request);
return $response;
}
}
But, like with most applications, I want to add more than just the form variable to a view template. So is there a way of passing an additional variable to the view, without altering the parent controller / action? Like (but not like) pushing 'myVariable' => $myVariable to the parent changePasswordAction return statement?
There is a section in FOSUserBundle documentation that describes exactly how to do that, and from Symfony2's Cookbook, How to use Bundle Inheritance to Override parts of a Bundle.
In summary, create a Bundle class to override FOSUserBundle in src:
// src/Acme/UserBundle/AcmeUserBundle.php
<?php
namespace Acme\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeUserBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}
Then, override the ChangePasswordController class:
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
return $response; // and 'myVariable' => $myVariable
}
}
--UPDATE--
Ok I think I misread you question. Anyway what renderResponse() of the templating service does is essentially:
$response->setContent($this->render($view, $parameters));
You can see the Class of the templating service by running app/console container:debug which is actually the TwigEngine class.
So you can just re-invoke renderResponse() and supply you own extra parameters. eg:
return $this->container->get('templating')->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array(
'form' => $form->createView(),
'myVariable' => $myVariable', // There you go
),
$response // The previous response that has been rendered by the parent class, by this is not necessary
);
Think bottom up.
You can access your data without passing it through action, using Twig Extension http://symfony.com/doc/current/cookbook/templating/twig_extension.html
twig.extension.user_profile:
class: 'MyBundle\UserProfileExtension'
arguments:
- '#doctrine.orm.entity_manager'
tags:
- { name: twig.extension }
Extension class
class UserProfileExtension extends \Twig_Extension
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param UserProfileDataService $userProfileDataService
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #return array
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('get_my_custom_var', array($this, 'getMyCustomVar')),
);
}
/**
* #return array
*/
public function getMyCustomVar()
{
$var = $this->entityManager->getRepository('MyCustomRepository')->findOneBy(['id' => 1]);
return $var;
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'user_profile_extension';
}
Template usage
{dump(get_my_custom_var())}
if I am understanding your question correctly you should be able to set additional variables on the response like this:
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
$response['myVariable'] = $myVariable;
return $response;
}
}
Hope this helps!