How to trigger fieldset factory in ZF3 - php

I need to use factory for fieldset. I know how to do it for form, but how to do it for fieldset?
The form code is:
namespace Application\Form;
use Application\Fieldset\Outline;
use Zend\Form\Element;
use Zend\Form\Form;
class Message extends Form
{
public function __construct()
{
parent::__construct('message');
$this->setAttribute('method', 'post');
$this->add([
'type' => Outline::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
$this->add([
'name' => 'submit',
'attributes' => [
'type' => 'submit',
'value' => 'Send',
],
]);
}
}
As one can see above the line 'type' => Outline::class,
tells parser to create fieldset object. But how to tell parser to create fieldset object with a custom fieldset factory?

FormElementManager is extending from ServiceManager so you have to config it same as service manager. Here's an example
class MyModule {
function getConfig(){
return [
/* other configs */
'form_elements' => [ // main config key for FormElementManager
'factories' => [
\Application\Fieldset\Outline::class => \Application\Fieldset\Factory\OutlineFactory::class
]
]
/* other configs */
];
}
}
With this config, when you call \Application\Fieldset\Outline::class, \Application\Fieldset\Factory\OutlineFactory::class will be triggered by FormElementManager. Everything same as ServiceManager. You will call your fieldset as via service manager;
$container->get('FormElementManager')->get(\Application\Fieldset\Outline::class);
Also you can call it in forms/fieldsets via getFormFactory method;
function init() { // can be construct method too, nothing wrong
$this->getFormFactory()->getFormElementManager()->get(\Application\Fieldset\Outline::class);
}
And of course you can use it's name in your factory-backed form extensions.
BUT if you create it via new keyword, your factory will not be triggered.

Related

Zend Form without binding object and access to fieldset data

I have Zend Framework 2 Form:
$form = new Form();
$form->add(
[
'name' => 'input1',
'type' => 'Text',
]
);
$fieldset1 = new Fieldset();
$fieldset1->setName('field1');
$fieldset1->add(
[
'name' => 'input2',
'type' => 'Text',
]
);
and controller for it:
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$data = $form->getData();
var_dump($this->params()->fromPost(),$data);
exit;
}
}
and problem is that when i dump values i get this:
array (size=3)
'input1' => string 'a' (length=1)
'input2' => string 'b' (length=1)
array (size=3)
'input1' => string 'a' (length=1)
'field1' =>
array (size=1)
'input2' => null
So what i do wrong? Because now in "field2" key i get "nulll". how i can get access to fieldset(s) data (after filters, validation etc) in that case?
Update: as i see, when i add to POST
<input name="field1[input2]" value="test" />
i get expected result. but why zendform not generate html like that, but (wrongly) generate:
<input name="input2" />
what i do wrong?
here is a complete more or less simple example with entities, input filters, hydrators and validators für zf2 form use with fieldsets.
First set up the fieldset class you want to use.
namespace Application\Form;
use Zend\Filter\StripTags;
use Zend\Form\Fieldset;
use Zend\Form\Element\Text;
use Zend\InputFilter\InputFilterProviderInterface;
class MyFieldset extends Fieldset implements InputFilterProviderInterface
{
/**
* #see \Zend\Form\Element::init()
*/
public function init()
{
$this->add([
'name' => 'input2',
'type' => Text::class,
'required' => true,
'attributes' => [
'id' => 'input2',
'required' => true,
],
'options' => [
'label' => 'The label for input2 of this fieldset',
],
]);
}
/**
* #see \Zend\InputFilter\InputFilterProviderInterface::getInputFilterSpecification()
*/
public function getInputFilterSpecification()
{
return [
'input2' => [
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
],
],
];
}
}
Your fieldset class defines all input elements within the fieldset. I encurage you to work with entity classes and factories. this is also the reason this example works with the init method. The init method is called after the constructor of the class. While using factories you can use the constructor for defining stuff you need for your fieldset or form class. For example depending input fields and so on.
Next you should write an entity for your fieldset.
namespace Application\Entity;
class MyFieldsetEntity
{
protected $input2;
public function getInput2()
{
return $this->input2;
}
public function setInput2($input2)
{
$this->input2 = $input2;
return $this;
}
}
This simple entity class will handle the data you have sent to your controller. One of the benefits of a entity class is, that you can define default values in it. If the post data should be empty for some reason, the entity can return default values. Let 's put it all together in a factory for your fieldset.
namespace Application\Form\Service;
class MyFieldsetFactory
{
public function __invoke(ContainerInterface $container)
{
$hydrator = new ClassMethods(false);
$entity = new MyFieldsetEntity();
return (new MyFieldset())
->setObject($entity)
->setHydrator($hydrator);
}
}
Why is using a factory smart? Because you can use all the favors of an object orientated environment. You can define all the stuff you need in a factory. for this purpose we create a fieldset instance with an entity and a hydrator. This will hydrate the fieldset with the filtered and validated data.
All that we need now is the form and an entity for the form.
namespace ApplicationForm;
use Zend\Form\Element\Text;
use Zend\Form\Form;
class MyForm extends Form
{
public function __construct($name = null, array $options = [])
{
parent::__construct($name, $options);
$this->setAttribute('method', 'post');
$this->add([
'name' => 'input1',
'type' => Text::class,
'required' => true,
'attributes' => [
'id' => 'input2',
'required' => true,
],
'options' => [
'label' => 'The label for input2 of this fieldset',
],
]);
// here goes your fieldset (provided, that your fieldset class is defined in the form elements config in your module.config.php file)
$this->add([
'name' => 'fieldset1',
'type' => MyFieldset::class,
]);
}
}
That 's all for your form. This form is implementing your fieldset. That 's all. Now we need a validator and an entity for this form.
namespace Application\Entity;
class MyFormEntity
{
protected $input1;
// we will hydrate this property with the MyFieldsetEntity
protected $fieldset1;
public function getInput1()
{
return $this->input1;
}
public function setInput1($input1)
{
$this->input1 = $input1;
return $this;
}
public function getFieldset1()
{
return $fieldset1;
}
public function setFieldset1($fieldset1)
{
$this->fieldset1 = $fieldset1;
return $this;
}
}
... and finally the input filter class for your form. An input filter filters and validates your form data. You should use always an input filter for security reasons and many more.
namespace Application\InputFilter;
use Zend\InputFilter\InputFilter;
use Zend\Filter\StripTags;
use Zend\Filter\StringTrim;
class MyFormInputFilter extends InputFilter
{
public function __construct()
{
$this->add([
'name' => 'input1',
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
[
'name' => StringTrim::class,
],
],
]);
}
}
Simple, hm? This input filter class just sets some input filters for your input 1 form element. The fieldset element is filtered by itself because it implements the InputFilterProviderInterface interface. You don 't hav to define more in the input filter class for your form.
Put it together in a factory ...
namespace Application\Form\Service;
class MyFormFactory
{
public function __invoke(ContainerInterface $container)
{
$entity = new MyFormEntity();
$inputFilter = new MyFormInputFilter();
$hydrator = (new ClassMethods(false))
->addStrategy('fieldset1', new Fieldset1Strategy());
$form = (new MyForm())
->setHydrator($hydrator)
->setObject($entity)
->setInputFilter($inputFilter);
return $form;
}
}
This is the factory for your form. This factory contains a special feature. It adds a hydrator strategy to your hydrator instance. this strategy will hydrate your entity with the fieldset data, if there is a 'fieldset1' key in your post array.
This will be the hydrator strategy class ...
namespace Application\Hydrator\Strategy;
use Zend\Hydrator\Strategy\DefaultStrategy;
use Zend\Hydrator\ClassMethods;
use Application\Entity\MyFieldsetEntity;
class Fieldset1Strategy extends DefaultStrategy
{
public function hydrate($value)
{
if (!$value instanceof MyFieldsetEntity) {
return (new ClassMethods(false))->hydrate($value, new MyFieldsetEntity());
}
return $value;
}
}
This strategy will add the MyFieldsetEntity to your form entity.
The last step is defining all that stuff in the config file module.config.php
// for the forms config provides the form elements key
'form_elements' => [
'factories' => [
YourForm::class => YourFormFactory::class,
YourFormFieldset::class => YourFormFactory::class,
]
],
// can be accessed with $container->get('FormElementsManager')->get(YourFormFieldset::class);
Usage Example
This is a small example how to use it in a controller.
class ExampleController extends AbstractActionController
{
protected $form;
public function __construct(Form $form)
{
$this->form = $form;
}
public function indexAction()
{
if ($this->getRequest()->isPost()) {
$this->form->setData($this->getRequest()->getPost());
if ($this->form->isValid()) {
$data = $this->form->getData();
\Zend\Debug\Debug::dump($data);
die();
// output will be
// MyFormEntity object
// string input1
// MyFieldsetEntity fieldset1
// string input2
// for fetching the filtered data
// $data->getInput1();
// $data->getFieldset1()->getInput2();
}
}
return [
'form' => $this->form,
];
}
}
In your view / template you can display the form with the different form view helpers zf2 provides.
$form = $this->form;
$form->setAttribute('action', $this->url('application/example'));
$form->prepare();
echo $this->form()->openTag($form);
// outputs the single text input element
echo $this->formRow($form->get('input1'));
// outputs the complete fieldset
echo $this->formCollection($form->get('fieldset1'));
Sure, this answer is a bit complex. But I encurage you to have a try. Once implemented in your application, this kind of form management is the easiest way you can use. Keep in mind, that just handling the raw post data can be insecure as hell. If you want the filtered data with the benefit of objects it is recommended using entities, input filters and all the other cool stuff zend framework comes with.
You have not added the fieldset to the form.
$form->add($fieldset1);
You forgot to prepare the form. It's in $form->prepare() that the names for the elements are changed to include the prefix for the fieldset.
If you use the "form" view helper, that will prepare the form for you. If you don't, you'll have to call "prepare" it yourself, for example in the view, just before you output the open tag:
$this->form->prepare();

zf2/zf3 register custom validator and injecting it into fieldset

I have a custom validator:
class CustomValidator extends RecordExists
{
public function isValid($value, array $context = null)
{
// some behaviour
return parent::isValid($value);
}
}
This validator has a factory:
class CustomValidatorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$validator = new CustomValidator();
// preparations for CustomValidator
return $validator;
}
}
CustomValidator and its factory are registered in module.config.php under
'validators' => [
'factories' => [
CustomValidator::class => CustomValidatorFactory::class,
],
],
CustomValidator is attached to a fieldset's input in getInputFilterSpecification() as
'validators' => [
['name' => CustomValidator::class],
]
The fieldset is called via init() method of the form, and the form is called in the controller factory as $container->get('FormElementManager')->get('TheNeededForm').
The problem is that for some reason, CustomValidator gets instantiated not through the CustomValidatorFactory, but getInputFilterSpecification() of the fieldset just creates an instance of new CustomValidator().
How to make the custom validator instantiation through the factory?
PS: I already surfed the internet, and found a similar problem, but the solution given there didn't help: it suggested adding 'abstract_factories' => [FormAbstractServiceFactory::class] in module.config.php, under 'form_elements' directive.
I already answered this question here
https://stackoverflow.com/a/61287283/4685379
Here it is:
In ZF3/Laminas, if a validator is registered as an invokable, you can call the validator in the getInputFilterSpecification() of your form, and no problem. If a validator is instantiated using a factory, you get into trouble. If I understand correctly, even if your form is registered like this
'form_elements' => [
'factories' => [
SomeForm::class => SomeFormFactory::class,
]
]
and your validator:
'validators' => [
'factories' => [
SomeValidator::class => SomeValidatorFactory::class,
]
]
you won't be instantiating the validator via factory. The reason is that the form factory (the one you get like $form->getFormFactory()) has an input filter factory and in there sits default validator chain. And this validator chain has no ValidatorManager attached. And without the ValidatorManager, the default chain cannot map the validator name to the validator factory.
To solve all this headache, in your controller factory do this:
$form->('FormElementManager')->get(SomeForm::class);
$form->getFormFactory()->getInputFilterFactory()
->getDefaultValidatorChain()->setPluginManager($container->get('ValidatorManager'));
and your troubles are over.

Form with more than one collection

I want to realize a form, which is quite simple. The only thing that makes things complicated is that I 'm using two collections in my form. Displaying two collections in the view works like a charme. The problem is the validation and the associated hydration of the bound entity of the form. If all is validated and no errors occur the form instance tries to hydrate the bound entity and ends up with an exception:
Zend\Hydrator\ArraySerializable::hydrate expects the provided object to implement exchangeArray() or populate()
But first the example code ...
The form classes
namespace Application\Form;
use Zend\Form\Element\Collection;
use Zend\Form\Element\Text;
use Zend\Form\Form;
class MyForm extends Form
{
public function __construct($name = '', $options = [])
{
parent::__construct($name, $options);
$this->setAttribute('method', 'post');
$this->setAttribute('id', 'my-form');
}
public function init()
{
$this->add([
'name' => 'my-text-field',
'type' => Text::class,
'attributes' => [
...
],
'options' => [
...
],
]);
// The first collection
$this->add([
'name' => 'first-collection',
'type' => Collection::class,
'options' => [
'count' => 2,
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => FieldsetOne::class,
],
],
]);
// the second collection
$this->add([
'name' => 'second-collection',
'type' => Collection::class,
'options' => [
'count' => 2,
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => FieldsetTwo::class,
],
],
]);
}
}
The metioned Fieldset classes which are bound to the collections look pretty much the same.
namespace Application\Form;
use Zend\Form\Element\Number;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
class FieldsetOne extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'my-number',
'type' => Number::class,
'options' => [
...
],
'attributes' => [
...
],
]);
}
public function getInputFilterSpecification()
{
return [
'my-number' => [
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
[
'name' => ToInt::class,
],
],
'validators' => [
[
'name' => NotEmpty::class,
],
[
'name' => IsInt::class,
'options' => [
'locale' => 'de_DE',
],
],
],
],
];
}
}
Summed up the form got two collections of number elements. All data which is provided over the form should end up in the following entity.
The input filter class
The form gets filtered and validated by the following input filter. The input filter will be bound to the form via a factory. The factory will be shown later.
class MyFormInputFilter extends InputFilter
{
public function init()
{
$this->add([
'name' => 'my-text-field',
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
[
'name' => StringTrim::class,
],
],
]);
}
}
The input filter contains only settings for the my-text-field element. The collections will be validated with the implemented InputFilterProviderInterface in the fieldsets set as target elements. The input filter class is created over a factory and notated in the input_filters section in the module.config.php.
The form entity
The entity will be bound as an object to the form in a factory it looks like the following example.
namespace Application\Entity;
class MyFormEntity
{
protected $myTextField;
protected $firstCollection;
protected $secondCollection;
public function getMyTextField()
{
return $this->myTextField;
}
public function setMyTextField($myTextField)
{
$this->myTextField = $myTextField;
return $this;
}
public function getFirstCollection()
{
return $this->firstCollection;
}
public function setFirstCollection(array $firstCollection)
{
$this->firstCollection = $firstCollection;
return $this;
}
public function getSecondCollection()
{
return $this->secondCollection;
}
public function setSecondCollection(array $secondCollection)
{
$this->secondCollection = $secondCollection;
return $this;
}
}
This entity will be bound as object to the form. The form will be hydrated be zend 's own ClassMethods hydrator class. For the collections two hydrator strategies are added to the hydrator. The hydrator strategies for the collections look like this.
namespace Application\Hydrator\Strategy;
class FirstCollectionStrategy extends DefaultStrategy
{
public function hydrate($value)
{
$aEntities = [];
if (is_array($value)) {
foreach ($value as $key => $data) {
$aEntities[] = (new ClassMethods(false))->hydrate($data, new CollectionOneEntity());
}
}
return $aEntities;
}
}
This strategy will hydrate the data from collection one to the corresponding entity.
All wrapped up in a factory
This is the factory which creates the form instance.
class MyFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $serviceLocator->getServiceLocator();
$filter = $parentLocator->get('InputFilterManager')->get(MyFormInputFilter::class);
$hydrator = (new ClassMethods())
->addStrategy('first-collection', new FirstCollectionStrategy())
->addStrategy('second-collection', new SecondCollectionStrategy());
$object = new MyFormEntity();
$form = (new MyForm())
->setInputFilter($filter)
->setHydrator($hydrator)
->setObject($object);
return $form;
}
}
This factory is mentionend in the form_elements section in the module.config.php file.
The problem
Everything works fine. The input element and also the collections are rendered in the view. If the form is submitted and the $form->isValid() method gets called in the controller all ends up in a BadMethodCallException.
Zend\Hydrator\ArraySerializable::hydrate expects the provided object to implement exchangeArray() or populate()
I have not bound the collection entities to the form in the controller because the hydrator strategies are added to the form hydrator that should hydrate the form entity. This makes sense for me, because zend form can only bind one object. If i call the bind method twice in the controller, the first bound object will be overwritten.
Is it possible to add more than one object with the bind method of the form so two collections can be handled? What could alternatives look like? What I 'm doing wrong?

How can I access Database Adapter in ZF2 Field Set?

I have followed an example and would like to pass the Database adapter to a fieldset to create a drop down menu.
The code below is how i call the fieldset.
How can i access the database adapter in the BrandFieldset class?
$this->add(array(
'type' => 'Application\Form\BrandFieldset',
'name' => 'brand',
'options' => array(
'label' => 'Brand of the product',
),
));
Instantiating a fieldset is responsibility of the FormElementManager. When you try to access a form, form element or fieldset, the FormElementManager knows where to find and how to create it. This behaviour summerized in Default Services section of the framework.
Since the proper way of accessing form elements is retrieving them from FormElementManager, I would write a BrandFieldsetFactory to inject that DB adapter or further dependencies to fieldset on construction to achieve this.
A ZF3 friendly fieldset factory would look like:
<?php
namespace Application\Form\Factory;
use Application\Form\BrandFieldset;
use Interop\Container\ContainerInterface;
class BrandFieldsetFactory
{
/**
* #return BrandFieldset
*/
public function __invoke(ContainerInterface $fem, $name, array $options = null)
{
// FormElementManager is child of AbstractPluginManager
// which makes it a ContainerInterface instance
$adapter = $fem->getServiceLocator()->get('Your\Db\Adapter');
return new BrandFieldset($adapter);
}
}
At this point, BrandFieldset should extend the Zend\Form\Fieldset\Fieldset and it's constructor may look like following:
private $dbAdapter;
/**
* {#inheritdoc}
*/
public function __construct(My/Db/Adapter $db, $options = [])
{
$this->dbAdapter = $db;
return parent::__construct('brand-fieldset', $options);
}
Finally, in module.config.php file I'd have a configuration to tell FormElementManager about this factory:
<?php
use Application\Form\BrandFieldset;
use Application\Form\Factory\BrandFieldsetFactory;
return [
// other config
// Configuration for form element manager
'form_elements' => [
'factories' => [
BrandFieldset::class => BrandFieldsetFactory::class
],
],
];
HINT: The BrandFieldset::init() method will be called automatically by FormElementManager after construction. You can put any post-initialization logic into this method.
Based of these docs I was able to find a solution.
https://framework.zend.com/manual/2.1/en/modules/zend.form.advanced-use-of-forms.html
'form_elements' => array(
'invokables' => array(
'fieldset' => BrandFieldsetFactory::class
)
)
I needed to call the form using the service locator in the controller like below.
$sl = $this->getServiceLocator();
$form = $sl->get('FormElementManager')->get('Application\Form\CreateForm');
In addition I changed the __construct to init.

ZF2 + DoctrineModule: Allow Doctrine Form Element ObjectSelect to be empty

I have a Zend\Form\Form for one of my entities that uses the DoctrineModule\Form\Element\ObjectSelect element to enable the user to select a referenced entity.
class MyEntityForm extends Zend\Form\Form
{
public function __construct()
{
// ...
$this->add([
'name' => 'referenced_entity',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => [
'object_manager' => $object_manager,
'target_class' => 'MyOtherEntity',
'property' => 'id',
'display_empty_item' => true
],
]);
// ...
}
}
The referenced entity may be empty (= the foreign key field in the database can be NULL). I just can't get the form to validate if no referenced entity has been selected. I would like my form to validate even if the given referenced_entity is empty (null or "") or not present at all (key referenced_entity missing from the data array).
I tried a variety of different input filter specifications, the last setup looked as follows
class MyEntityForm
extends Zend\Form\Form
implements Zend\InputFilter\InputProviderInterface
{
// ...
public function getInputSpecification()
{
return [
// ...
'referenced_entity' => [
'required' => false,
'allow_empty' => true,
'continue_if_empty' => false
],
// ...
}
// ...
}
But to no avail, the validation error stays the same (excerpt of var_dump of $form->getMessages() after $form->isValid())
'referenced_entity' =>
array (size=1)
'isEmpty' => string 'Value is required and can't be empty' (length=36)
Do I have to extend the ObjectSelect form element to change its input filter specification and remove the isEmpty validator or is there an easier solution?
If I remember well, if you want to provide input filter configuration into your Form class, then you must implement the InputFilterProviderInterface interface.
If you want to configure it at the element level then your Element class must implement the InputProviderInterface interface
So it would mean that your form class has to be like that:
class MyEntityForm
extends Zend\Form\Form
implements
// this...
// Zend\InputFilter\InputProviderInterface
// must be this!
Zend\InputFilter\InputFilterProviderInterface
{
// ...
public function getInputFilterSpecification()
{
return [
// ...
'referenced_entity' => [
'required' => false,
'validators' => [],
'filters' => [],
],
// ...
}
// ...
}
DoctrineModule\Form\Element\ObjectSelect inherits Zend\Form\Element\Select and it includes automaticatilly a input especification with a validator for itself.
I didn't test myself, but an way to solve this is remove this validator by adding 'disable_inarray_validator' key in options:
public function __construct()
{
// ...
$this->add([
'name' => 'referenced_entity',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => [
'object_manager' => $object_manager,
'target_class' => 'MyOtherEntity',
'property' => 'id',
'display_empty_item' => true,
'disable_inarray_validator' => true
],
]);
// ...
//or via method
$this->get('referenced_entity')->setDisableInArrayValidator(true);
}

Categories