in symfony i can use the setter injection for services via call option (https://symfony.com/doc/current/service_container/calls.html)
The example from the symfony documentation:
class MessageGenerator
{
private $logger;
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
// ...
}
service.yml
services:
App\Service\MessageGenerator:
# ...
calls:
- method: setLogger
arguments:
- '#logger'
I need this behaviour for my zend project. i want to inject a InputFilter into my FormFieldSet.
I didn't find anything about this in the zend documentation. Can i use something like this or exist a better solution for my problem in zend?
Based on this question and your previous question about Forms, Fieldsets and InputFilters, I'm thinking you want to achieve something similar to the following use case.
Use case
You have a
Location Entity
Address Entity
Location has a OneToOne to an Address (required, uni-directional)
Requirements
To manage the Location, you'll need:
LocationForm (-Factory)
LocationFormInputFilter (-Factory)
LocationFieldset (-Factory)
LocationFieldsetInputFilter (-Factory)
AddressFieldset (-Factory)
AddressFieldsetInputFilter (-Factory)
Configuration
To configure this in ZF3, you'll have to do add the following
'form_elements' => [
'factories' => [
AddressFieldset::class => AddressFieldsetFactory::class,
LocationForm::class => LocationFormFactory::class,
LocationFieldset::class => LocationFieldsetFactory::class,
],
],
'input_filters' => [
'factories' => [
AddressFieldsetInputFilter::class => AddressFieldsetInputFilterFactory::class,
LocationFormInputFilter::class => LocationFormInputFilterFactory::class,
LocationFieldsetInputFilter::class => LocationFieldsetInputFilterFactory::class,
],
],
Forms & Fieldsets
In the LocationForm, add your LocationFieldset and what else your Form needs, such as CSRF and submit button.
class LocationForm extends AbstractForm
{
public function init()
{
$this->add([
'name' => 'location',
'type' => LocationFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
//Call parent initializer. Adds CSRF & submit button
parent::init();
}
}
(Note: my AbstractForm does a bit more, I would suggest you have a look here, such as remove empty (child fieldsets/collections) Inputs so data is not attempted to be created in the DB)
In the LocationFieldset, give add Inputs for the Location, such as a name, and the AddressFieldset:
class LocationFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'name',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Name'),
],
]);
$this->add([
'type' => AddressFieldset::class,
'name' => 'address',
'required' => true,
'options' => [
'use_as_base_fieldset' => false,
'label' => _('Address'),
],
]);
}
}
In the AddressFieldset just add Inputs for the Address Entity. (Same as above, without the Fieldset type Input)
InputFilters
To validate the Form, you can keep it very simple:
class LocationFormInputFilter extends AbstractFormInputFilter
{
/** #var LocationFieldsetInputFilter */
protected $locationFieldsetInputFilter;
public function __construct(LocationFieldsetInputFilter $filter)
{
$this->locationFieldsetInputFilter = $filter;
parent::__construct();
}
public function init()
{
$this->add($this->locationFieldsetInputFilter, 'location');
parent::init();
}
}
(The AbstractFormInputFilter adds CSRF validator)
Notice that we simply ->add() the LocationFieldsetInputFilter, but we give it a name (2nd parameter). This name is used later in the complete structure, so it's important to both keep it simple and keep it correct. Simplest is to give it a name that one on one matches the object of the Fieldset it's supposed to validate.
Next, the LocationFieldsetInputFilter:
class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/**
* #var AddressFieldsetInputFilter
*/
protected $addressFieldsetInputFilter;
public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter)
{
$this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
parent::__construct();
}
public function init()
{
parent::init();
$this->add($this->addressFieldsetInputFilter, 'address'); // Again, name is important
$this->add(
[
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]
);
}
}
Factories
Now, you must bind them together, which is where your question about Setter injection comes from I think. This happens in the Factory.
A *FormFactory would do the following:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$inputFilterPluginManager = $container->get('InputFilterManager');
$inputFilter = $inputFilterPluginManager->get(LocationFormInputFilter::class);
/** #var LocationForm $form */
$form = new LocationForm();
$form->setInputFilter($inputFilter); // The setter injection you're after
return $form;
}
A *FieldsetFactory would do the following (do the same for Location- and AddressFieldsets):
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var LocationFieldset $fieldset */
// name matters! Match the object to keep it simple. Name is used from Form to match the InputFilter (with same name!)
$fieldset = new LocationFieldset('location');
// Zend Reflection Hydrator, could easily be something else, such as DoctrineObject hydrator.
$fieldset->setHydrator(new Reflection());
$fieldset->setObject(new Location());
return $fieldset;
}
A *FormInputFilterFactory would do the following:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$inputFilterPluginManager = $container->get('InputFilterManager');
/** #var LocationFieldsetInputFilter $locationFieldsetInputFilter */
$locationFieldsetInputFilter = $inputFilterPluginManager->get(LocationFieldsetInputFilter::class);
// Create Form InputFilter
$locationFormInputFilter = new LocationFormInputFilter(
$locationFieldsetInputFilter
);
return $locationFormInputFilter;
}
A *FieldsetInputFilterFactory would do the following:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var AddressFieldsetInputFilter $addressFieldsetInputFilter */
$addressFieldsetInputFilter = $this->getInputFilterManager()->get(AddressFieldsetInputFilter::class);
$addressFieldsetInputFilter->setRequired(true);
return new LocationFieldsetInputFilter(
$addressFieldsetInputFilter
);
}
Note:
Setting an InputFilter as (not) required is something I've added here
If your InputFilter (such as AddressFieldsetInputFilter) does not have a child InputFilter, you can can skip getting the child and straight away return the new InputFilter.
I think I covered it all for a complete picture. If you have any questions about this, please comment.
What you need are Initializers from Zend Service Manager.
The initializer can be a class that is called whenever a service has been created.
In that class, you need to check the type of service that is created, and if it's appropriate type than inject whatever you want.
To register one Initializer add in config under service_manager key:
'service_manager' => [
'initializers' => [
MyInitializer::class
],
]
and then just create that class
class MyInitializer implements InitializerInterface
{
public function __invoke(ContainerInterface $container, $instance)
{
// you need to check should you inject or not
if ($instance instanceof MessageGenerator) {
$instance->setLogger($container->get('logger'));
}
}
}
You need to have registred MessageGenerator in zend-servicemanager also. In this way, when you try to retrive MessageGenerator from SM, after creation MyInitializer is called.
Related
For a simple blog manager where I want to associate some tags to some Post I have create a repository able to create a new entity when it's name can't be found in database:
public function requireByName(string $name): PostTag
{
$result = $this->findOneBy(['name' => [$name]]);
if (!$result instanceof PostTag) {
$result = $this->create($name);
$this->getEntityManager()->flush();
}
return $result;
}
I have managed to create a form TagType populating PostTag entity with the results of the PostTagRepository::requireByName method:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name', TextType::class, [
'setter' => function (PostTag &$tag, string $name) {
$tag = $this->tagRepository->requireByName($name);
},
]);
}
All of this is tested with the instruction provided in Symfony documentation How to unit test your form
public function testItSetsRepositoryResultToInboundModel(): void
{
$formTagName = 'my new tag';
$formData = ['name' => $formTagName];
$createdPostTag = new PostTag($formTagName);
$this->tagRepository->method('requireByName')
->with($formTagName)
->willReturn($createdPostTag);
$this->testedForm->submit($formData);
self::assertTrue($this->testedForm->isSynchronized());
$formEntity = $this->testedForm->getData();
self::assertInstanceOf(PostTag::class, $formEntity);
self::assertSame($createdPostTag, $formEntity);
}
public function testItMapsTagsWithItsNameInFormField(): void
{
$tagName = 'my new tag';
$formData = new PostTag($tagName);
$form = $this->factory->create(TagType::class, $formData);
self::assertTrue($form->has('name'));
self::assertEquals($tagName, $form->get('name')->getData());
}
My next step is to create a TypeTestCase for my PostType form where I can assume that results from PostTagRepository are populated in my Post entity once I submit an array of tag names. Here is the code I produced so far (I load some extensions - Validator, CKEditor - required from the legacy behavior) :
class PostTypeTest extends TypeTestCase
{
use ValidatorExtensionTrait;
private PostTagRepository&MockObject $tagRepository;
protected function setUp(): void
{
$this->tagRepository = $this->createMock(PostTagRepository::class);
parent::setUp(); // TODO: Change the autogenerated stub
}
/** #return FormExtensionInterface[]
*
* #throws Exception
*/
protected function getExtensions(): array
{
$tagType = new TagType($this->tagRepository);
return [
$this->getValidatorExtension(),
$this->getCKEditorExtension(),
new PreloadedExtension([$tagType], []),
];
}
private function getCKEditorExtension(): PreloadedExtension
{
$CKEditorType = new CKEditorType($this->createMock(CKEditorConfigurationInterface::class));
return new PreloadedExtension([$CKEditorType], []);
}
public function testItMapsRepositoryResultToTagCollection(): void
{
$model = new Post();
$form = $this->factory->create(PostType::class, $model);
$requiredTag = new PostTag('poo');
$this->tagRepository->method('requireByName')
->with('poo')
->willReturn($requiredTag);
$form->submit([
'title' => 'a title',
'content' => 'some content',
'isPublic' => true,
'tags' => [
['name' => 'poo'],
],
]);
self::assertTrue($form->isSynchronized());
self::assertNotEmpty($model->getTags());
}
}
And here is the PostType code :
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'label' => 'Titre',
'required' => true,
'constraints' => [new Assert\NotBlank(), new Assert\Length(['min' => 5, 'max' => 255])],
]
)
->add(
'content',
CKEditorType::class,
[
'required' => true,
'label' => 'Contenu',
'config' => [
'filebrowserBrowseRoute' => 'elfinder',
'filebrowserBrowseRouteParameters' => [
'instance' => 'default',
'homeFolder' => '',
],
],
]
)
->add('isPublic', CheckboxType::class, [
'label' => 'Le Post est public',
'required' => false,
])
->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'entry_options' => ['label' => false],
])
->add('submit', SubmitType::class, [
'label' => 'app.actions.validate',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Post::class,
]
);
}
}
All properties are populated with submitted values but the $model->getTags() Collection remains empty.
Help would be really appreciated if someone could tell me what I am doing wrong ?
I have come across a solution that filters fields from a resource collection in the Controller CompanyController.php
E.g The code below returns all the values except company_logo
CompanyResource::collection($companies)->hide(['company_logo']);
CompanyResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CompanyResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected $withoutFields = [];
public static function collection($resource)
{
return tap(new CompanyResourceCollection($resource), function ($collection) {
$collection->collects = __CLASS__;
});
}
// Set the keys that are supposed to be filtered out
public function hide(array $fields)
{
$this->withoutFields = $fields;
return $this;
}
// Remove the filtered keys.
protected function filterFields($array)
{
return collect($array)->forget($this->withoutFields)->toArray();
}
public function toArray($request)
{
return $this->filterFields([
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'telephone' => $this->telephone,
'company_logo' => $this->company_logo,
'social_links' => $this->social_links,
]);
}
}
Now from my UserResource I still want to specify fields I don't want returned from the same CompanyResource but it's not a collection anymore in the UserResource
UserResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'email' => $this->email,
'status' => $this->status,
'timezone' => $this->timezone,
'last_name' => $this->last_name,
'first_name' => $this->first_name,
'tags' => TagResource::collection($this->whenLoaded('tags')),
'company' => new CompanyResource($this->whenLoaded('company')),
];
}
So my idea is to be able to specify excluded fields on 'company' => new CompanyResource($this->whenLoaded('company')), Been stuck here for some time.
After researching I found a working solution for my problem
'company' => CompanyResource::make($this->whenLoaded('company'))->hide(['company_logo']),
Instead of the below which I could not use flexibly use:
'company' => new CompanyResource($this->whenLoaded('company')),
I am confronted to a problem that is driving me crazy for more than 3 days and I do not find any solutions. Nevertheless I found a post on stackoverflow that is EXCACTLY the problem I am facing. Unfortunately the person did manage to find a solution on his down but he or she did not shared it fully. As he explained it perfectly let just copy paste it here below:
By the way it seems that person who created that post only created his account for this problem and never came back since for other things. That it is why I allow myself to ask here again...
I have 2 entities (A and B) with a Many to One relationship between
them.
I create my form with the A entity and i use an entity field (dropdown
list) to display the rows in the B entity. I use a query builder to
filter them. If don't change the values in the list (ie. with ajax),
everything is working fine.
But if I change dynamicly the values in the dropdown, when I submit
the form I have this error "This value is invalid"
It's because the submitted value isn't included in the "array"
returned by the query builder.
It seems that this validation is automatic in symfony for entity field
(I don't use any asserts on this field). I'd like to get rid of this.
But how ?
It seems that I need to implement Form Events. Unfortunatally I do not get it. I read the documentation which is very poor on that subject, read a lot of posts, searched on the Internet but did not found anything.
Here below my personal form type. What I do is the following. I create the first entity field type with the mapped property set to false and filter the entity just to get the departements. Then I create another entity type called localisation. By default I filter the entity to get nothing (''). What I do then to populate it is to use Jquery. But unfortunatelly I am confro,ted to the same problem as the other buddy (see above).
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Doctrine\ORM\EntityRepository;
use Auth\GeoBundle\Form\LocalisationType;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('departement', 'entity', array(
'mapped' => false,
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'departement',
'query_builder' => function ($repository) {
return $repository
->createQueryBuilder('e')
->add('groupBy', 'e.departement')
;
},
));
$builder->add('localisation', 'entity', array(
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'formLabel',
'query_builder' => function ($repository) use ($dpt) {
return $repository
->createQueryBuilder('e')
->where('e.departement = :dpt')
->setParameter('dpt', '')
->add('orderBy', 'e.ville ASC')
;
},
));
//some other fields here...
}
public function getName()
{
return 'auth_user_registration';
}
}
I finally manage to find a solution using the form events. I played with the "tutorial" at http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html and got it working :) Here below the code I used in case somebody interested.
My formType:
<?php
//src/Auth/UserBundle/Form/Type/RegistrationFormType.php
namespace Auth\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Doctrine\ORM\EntityRepository;
use Auth\GeoBundle\Form\LocalisationType;
use Auth\UserBundle\Form\EventListener\IsAdminFieldSubscriber;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
//NE PAS EFFACER -> exempled e comment ajouter un champ qui n'a rien à voir avec nos entitys
//$builder->add("firstName", "text", array("mapped" => false));
$builder->add('departement', 'genemu_jqueryselect2_entity', array(
'mapped' => false,
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'departement',
'query_builder' => function ($repository) {
return $repository
->createQueryBuilder('e')
->add('groupBy', 'e.departement')
;
},
));
$dpt = "";
$builder->add('localisation', 'genemu_jqueryselect2_entity', array(
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'formLabel',
'query_builder' => function ($repository) use ($dpt) {
return $repository
->createQueryBuilder('e')
->where('e.departement = :dpt')
->setParameter('dpt', $dpt)
->add('orderBy', 'e.ville ASC')
;
},
));
$builder->add('sexe', 'genemu_jqueryselect2_choice', array(
'empty_value' => '',
'choices' => array(
'homme' => 'Homme',
'femme' => 'Femme',
),
'configs' => array(
'minimumResultsForSearch' => 5,
)
));
$builder->add('date_naissance', 'date', array(
'empty_value' => '',
'widget' => 'choice',
'attr' => array('class' => 'input-small'),
'years' => range(1900,2100),
'months' => range(1,12),
'days' => range(1,31),
));
$builder->add('petit_mot');
$subscriber = new IsAdminFieldSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
}
public function getName()
{
return 'auth_user_registration';
}
}
my EventListener:
<?php
//src/Auth/UserBundle/Form/EventListener/isAdminFieldSubscriber.php
namespace Auth\UserBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class IsAdminFieldSubscriber implements EventSubscriberInterface
{
/**
* #var FormFactoryInterface
*/
private $factory;
/**
* #param FormFactoryInterface $factory
*/
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_BIND => 'preBind',
);
}
/**
* Called before form data is set
*
* #param DataEvent $event
*/
public function preBind(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$dpt = $data['localisation'];
$form->add($this->factory->createNamed('localisation', 'entity', null, array(
'empty_value' => '',
'class' => 'AuthGeoBundle:Localisation',
'property' => 'formLabel',
'query_builder' => function ($repository) use ($dpt) {
return $repository
->createQueryBuilder('e')
->where('e.id = :dpt_id')
->setParameter('dpt_id', $dpt)
->add('orderBy', 'e.ville ASC')
;
},
)));
}
}
You explain the problem yourself:
"It's because the submitted value isn't included in the "array" returned by the query builder."
You can use data transformers to solve this issue.
I updated Symfony2 to 2.1 and when I trying submit form I am getting error:
The Choice constraint expects a valid callback
source code from form type class:
$builder->add('type', 'choice',
array(
'expanded' => true,
'multiple' => false,
'choice_list' => new TypeChoices(),
'required' => true,
)
)
TypeChoices class:
class TypeChoices implements ChoiceListInterface {
public static $choices = array(
'full-time' => 'Full time',
'part-time' => 'Part time',
'freelance' => 'Freelance',
);
public static function getChoiceNameByValue($value)
{
return self::$choices[$value];
}
public function getChoices()
{
return self::$choices;
}
public static function getTypeChoicesKeys()
{
return array_keys(self::$choices);
}
public static function getPreferredChoiceKey()
{
return 'full-time';
}
}
Could someone give me any advice?
Maybe you could try to extend the SimpleChoiceList class, this way:
ChoiceList code:
class TypeChoices extends SimpleChoiceList
{
public static $choices = array(
'full-time' => 'Full time',
'part-time' => 'Part time',
'freelance' => 'Freelance',
);
/**
* Constructor.
*
* #param array $preferredChoices Preffered choices in the list.
*/
public function __construct(array $preferredChoices = array()) // PASS MORE ARGUMENT IF NEEDED
{
parent::__construct(
static::$choices,
$preferredChoices
);
}
}
Form type code:
->add('type', 'choice', array(
'choice_list' => new TypeChoices(),
...
))
I have two entites User Role. Both are used in Symfony2 for authentication. The authentication process needs to have method 'getRoles' in class User which returns array.
On the other hand there is form builder which needs roles given as Collection.
How can i force form builder to use specific getter to get Collection of roles?
class UserType extends AbstractType{
//...
->add('roles', 'entity', array( 'class' => 'MyBundle:Role',
'property' => 'name',
'required' => false,
'multiple' => true,
'expanded' => true,
);
//...
}
class User implements UserInterface {
//...
public function getRoles() {
return $this->roles->toArray(); //This needs authentication mechanism
}
public function getRolesCollection() {
return $this->roles; //This needs form builder.
}
//...
}
Maybe you could try to invert it all :
class UserType extends AbstractType{
//...
->add('rolesCollection', 'entity', array( 'class' => 'MyBundle:Role',
'property' => 'name',
'required' => false,
'multiple' => true,
'expanded' => true,
);
//...
}
class User implements UserInterface {
protected $rolesCollection
//...
public function getRoles() {
return $this->rolesCollection->toArray(); //This needs authentication mechanism
}
public function getRolesCollection() {
return $this->rolesCollection; //This needs form builder.
}
//...
}