ZF2 nested data validation - zend-framework2

I'm trying make to work my validation. I have data posted to controller in the format like this:
[
'property' => 'value',
'nested_property' => [
'property' => 'value',
// ...
]
]
I have divided fields/filters and form into different classes and just gather it together in the Form's controller that looks like that:
public function __construct($name, $options)
{
// ...
$this->add(new SomeFieldset($name, $options));
$this->setInputFilter(new SomeInputFilter());
}
But it doesn't work properly, looks like it just ignores nested array (or ignores everything). What have I missed?
Thank you.

You need to set up your inputfilter like the way you've setup your forms including the fieldsets if you use the InputFilter class.
So when you've got a structure like:
MyForm
1.1 NestedFieldset
1.2 AnotherFieldset
Your inputfilters need to have the same structure:
MyFormInputFilter
1.1 NestedFielsetInputFilter
1.2 AnotherFieldsetInputFilter
Some example code:
class ExampleForm extends Form
{
public function __construct($name, $options)
{
// handle the dependencies
parent::__construct($name, $options);
$this->setInputFilter(new ExampleInputFilter());
}
public function init()
{
// some fields within your form
$this->add(new SomeFieldset('SomeFieldset'));
}
}
class SomeFieldset extends Fieldset
{
public function __construct($name = null, array $options = [])
{
parent::__construct($name, $options);
}
public function init()
{
// some fields
}
}
class ExampleInputFilter extends InputFilter
{
public function __construct()
{
// configure your validation for your form
$this->add(new SomeFieldsetInputFilter(), 'SomeFieldset');
}
}
class SomeFieldsetInputFilter extends InputFilter
{
public function __construct()
{
// configure your validation for your SomeFieldset
}
}
So the important part of configuring your inputFilter for these situations is that you need to reuse the name of your fieldset when using: $this->add($input, $name = null) within your InputFilter classes.

Related

How to set the type of an element in a Fieldset child class in Zend Framework 2?

I have two very similar Fieldsets MyFooFieldset and MyBarFieldset. In order to avoid code duplication, I created an AbstractMyFieldset, moved the whole code there, and want to handle the differences in the init() methods of the concrete classes:
AbstractMyFooFieldset
namespace My\Form\Fieldset;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
abstract class AbstractMyFieldset extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add(
[
'type' => 'multi_checkbox',
'name' => 'my_field',
'options' => [
'label_attributes' => [
'class' => '...'
],
'value_options' => $this->getValueOptions()
]
]);
}
public function getInputFilterSpecification()
{
return [...];
}
protected function getValueOptions()
{
...
return $valueOptions;
}
}
MyFooServerFieldset
namespace My\Form\Fieldset;
use Zend\Form\Fieldset;
class MyFooServerFieldset extends AbstractMyFieldset
{
public function init()
{
parent::init();
$this->get('my_field')->setType('radio'); // There is not method Element#setType(...)! How to do this?
$this->get('my_field')->setAttribute('required', 'required'); // But this works.
}
}
I want to set the type and some other configurations for the element, e.g. the type and the required attribute. Setting attributes seems to be OK, at least I can set the required attribute. But I cannot set the type -- the Element#setType(...) is not there.
How to set the type of a Zend\Form\Element, after it has been added?
There is no way to set the type of an element as each element has its own type and element class defined. In your AbstractMyFieldset, see the "Type" key within your init(). You tell the form to add the MultiCheckbox element class and want to change the class to another one. So you need to either remove the default and copy it's attributes and options over to a newly added Zend Form element.
Another option is to use the base Zend\Form\Element class you can overwrite the attributes and set the type attribute. ->setAttribute('type', 'my_type') but ur missing all the benefits of the default Zend2 form classes. Especially as the default InArray validator for Zend\Form\Element\Radio or the Zend\Form\Element\MultiCheckbox.
Or you should just consider making an abstractFieldSet for the both fieldsets and define how they get their value options and reuse that. Like:
abstract class AbstractFieldSet extends Fieldset {
public function addMyField($isRadio = false)
{
$this->add([
'type' => $isRadio ? 'radio' : 'multi_checkbox',
'name' => 'my_field',
'options' => [
'value_options' => $this->getValueOptions()
]
]);
}
protected function getValueOptions()
{
// ..
return $valueOptions
}
}
class fieldSet1 extends AbstractFieldSet {
public function init()
{
$this->addMyField(false);
}
}
class fieldSet2 extends AbstractFieldSet {
public function init()
{
$this->addMyField(true);
}
}

How to pass parameter to FormType constructor from controller

In Symfony2.7 i was able to pass parameter to Form Type constructor directly from controller while creating the form, however in Symfony3 i'm not able to do it!
Before in Symfony2.7
$postedBy = $this->getUser()->getFullname();
$form = $this->createForm(new NewsType($postedBy));
After in Symfony3
$form = $this->createForm(NewsType::class); // no idea how to pass parameter?
Update:
I also wanted to access it from:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
// how to access posted_by_name here which is sent from controller
}
Any help will be highly appreciated..
Thanks for your time! i resolved this myself:
I removed parameter from NewsType constructor and added data to postedBy form field using $options array, and passed data to $options array from controller, please check following:
NewsType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('postedBy', HiddenType::class, array(
'data' => $options['postedBy']
)
)
;
}
// WARNING: this is a MANDATORY block! Only options described here will be allowed to be passed.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'postedBy' => null,
));
}
Controller
$form = $this->createForm(NewsType::class, $news, array(
'postedBy' => $this->getUser()->getFullname(),
);
UPDATE:
Please use below code if you want to access $options array from addEventListener:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$postedBy = $event->getForm()->getConfig()->getOptions()['postedBy'];
}
Hope it helps somebody!
You need to define your form as service.
namespace AppBundle\Form\Type;
use App\Utility\MyCustomService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class NewsType extends AbstractType
{
private $myCustomService;
private $myStringParameter;
public function __construct(MyCustomService $service, $stringParameter)
{
$this->myCustomService = $service;
$this->myStringParameter = $stringParameter;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Your code
}
}
Add to your service configuration:
#src/AppBundle/Resources/config/services.yml
services:
app.form.type.task:
class: AppBundle\Form\Type\NewsType
arguments:
- "#app.my_service"
- "posted_by_name"
tags:
- { name: form.type }
You are both right.
#Muzafar and #jkucharovic, the question is when to use which...
As Bernard Schussek shows in Symfony Forms 101:
1 Don't pass Dynamic Data to constructor..
2 ... but use Custom Options instead
3 Do pass Global Settings to constructor (or services)

How to use a custom FormType in Symfony 3

I m training but I'm under symfony 3
i have problem i get this error
Expected argument of type "string",
"Test\FrontBundle\Form\Type\SheetType" given
the code on SheetType.php is
<?php
namespace Test\FrontBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
class SheetType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name',null,array('label'=>'Titre de l\'album'))
->add('type')
->add('artist')
->add('duration')
->add('released', DateType::class)
;
}
}
and on my SheetController.php i do that form my controller
i dont know how i can solve this all time i try else i got error
public function createAction(Request $request)
{
$form = $this->createForm(new SheetType());
$form->handleRequest($request);
if($request->isMethod('post') && $form->isValid()){
$em = $this->getDoctrine()->getManager();
$em->persist($form->getData());
$em->flush();
return $this->redirect($this->generateUrl('test_front_sheet_list'));
}
return $this->render('TestFrontBundle:Sheet:create.html.twig', array('form' => $form->createView()));
}
Since symfony 2.8 you have to pass a full qualified class name instance as argument when create a form or form builder, it does not take an instance of FormTypeInterface anymore.
see https://github.com/symfony/symfony/blob/2.8/UPGRADE-2.8.md
So you should use $form = $this->createForm(SheetType::class); instead.

how to inject class dependency in Yii2 configuration?

I am learning Yii2. Here is one situation I have not googled the answer.
I register a component called scraper in config/console.php's $config['components'] array,
this scraper class has a public property $_client which is a Goutte\Client class.
I tried to use the following way to set up scraper component, but it is not working, Yii2 did not instantiate $_client as a Goutte\Client object.
$config = [
'scraper' => [
'class' => 'app\models\Scraper',
'_pageSize' => 10,
'_client' => [ //not working. can not instantiate this property as an object
'class' => 'Goutte\Client'
],
],
//...
]
Question: What would be working way to inject the dependency in the configuration?
Yii2 will not instantiate objects beyond the first level in your config array. In other words, scraper will get instantiated as an object, but it's property _client will be instantiated as an array ['class' => 'Goutte\Client'].
You should implement this logic yourself:
class Service extends Component
{
private $_client = null;
public $clientClass;
public function getClient()
{
if (null !== $this->_client) {
return $this->_client;
}
$this->_client = new $clientClass;
return $this->_client;
}
}
Alternatively, you can register Goutte\Client as a separate component, then Yii will properly instantiate it.
UPDATE:
To clarify, instantiating objects from config is done with yii\base\Configurable interface which is implemented in yii\base\Object class. Eventually, this implementation executes Yii::configure:
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}
As you see, all properties will be assigned their respective values, so _client will become an array, not an object.
Found another approach in the guide itself: The property targets of the class yii\log\Dispatcher can be initialized with a class names or an objects. To make it working as one expects the init method is overwritten:
/**
* {#inheritdoc}
*/
public function init()
{
parent::init();
foreach ($this->targets as $name => $target) {
if (!$target instanceof Target) {
$this->targets[$name] = Yii::createObject($target);
}
}
}
This allows configuration/initialization of the log component like this:
'log' => [
'class' => 'yii\log\Dispatcher',
'targets' => [
[
'class' => 'yii\log\FileTarget',
],
],
],
Note: targets is an array here. But it can be done with a single class/object as well.
So in your case this should be a solution:
namespace app\models;
class Scraper extends ActiveRecord // or extends from anything that actually implements yii\base\Configurable
{
public $_client;
/**
* {#inheritdoc}
*/
public function init()
{
parent::init();
if (!$this->_client instanceof Goutte\Client) {
$this->_client = Yii::createObject($this->_client);
}
}
}
btw: usually underscore prefix in variable names is used for private properties.

When using Symfony2.1 forms, at what point should I bind custom data (an object) based on form values to my form's object?

Given the following scenario, where should I put logic to bind Department to Review:
Entities:
Dealership (has many departments)
Department (has one type)
DepartmentType
Review (has one dealership and one department)
On my ReviewForm I need the user to be able to select a Dealership and a DepartmentType, and then in some form of callback or pre/post bind, work out from them which Department to bind to the Review.
I also need this to happen prior to validation so that I can validate that the Department is child of the Dealership.
Note: Review relates to both Dealership and Department when it could just relate to Department to ease traversal and other logic I have going on.
There are two approaches I've tried so far but reached deadends / confusion.
DataTransformer on the DepartmentType on the form, not sure I understand this properly, my transform / reverseTransform methods were getting passed in the Review object, not the field object.
PRE_BIND, happens before validation but I only have raw data to work with, no objects
POST_BIND, happens after validation :(
For the final step of validation of the relationship I have a relatively simple validator that should do the job, but I'm not sure at what point I am meant to bind data to the object like this. Any pointers?
As validation is also done in a POST_BIND listener, you could simply add your POST_BIND listener with a higher priority than the validation listener (i.e. anything > 0).
If you're writing a listener:
$builder->addEventListener(FormEvents::POST_BIND, $myListener, 10);
and if you're writing a subscriber:
public static function getSubscribedEvents()
{
return array(
FormEvents::POST_BIND => array('postBind', 10),
);
}
public function postBind(FormEvent $event)
{
...
}
I would go with a standard (ie: non-Doctrine) choice type containing a choice to represent each DepartmentType.
Then use a DataTransformer to turn the selected option into the relevant type, and vice versa.
Your custom FormType should end up looking something like this:
class Department extends AbstractType
{
private $em;
public function __construct(EntityManager $em) {
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new DepartmentToTypeTransformer($this->em);
$builder->addViewTransformer($transformer, true);
$builder->getParent()->addEventListener(FormEvents::PRE_BIND, function($event) use ($transformer) {
$data = (object) $event->getData();
$transformer->setDealership($data->dealership);
});
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'department';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choices = array();
foreach ($this->em->getRepository('AcmeDemoBundle:DepartmentType')->findAll() as $type) {
$choices[$type->getId()] = (string) $type;
}
$resolver->setDefaults(array(
'choices' => $choices,
'expanded' => true
));
}
}
Note the passing of the Dealership into the DataTransformer for use in the transformation.
And DataTransformer something like this:
class DepartmentToTypeTransformer implements DataTransformerInterface
{
private $em;
private $dealership;
public function __construct($em)
{
$this->em = $em;
}
public function transform($department)
{
if (null === $department) {
return $department;
}
return $department->getType()->getId();
}
public function reverseTransform($type)
{
if (null === $type) {
return $type;
}
return $this->em->getRepository('AcmeDemoBundle:Department')->findOneBy(array(
'dealership' => $this->getDealership(),
'type' => $type
));
}
public function getDealership() {
return $this->dealership;
}
public function setDealership($dealership) {
$this->dealership = $dealership;
return $this;
}
}
Your confusion regarding what is being passed to your transformer is most likely caused by the transformer you're binding being appended to pre-existing behaviour, try adding true as the second parameter to addViewTransformer:
$transformer = new DepartmentToTypeTransformer($this->em);
$builder->addViewTransformer($transformer, true);
From the docs:
FormBuilder::addViewTransformer(
DataTransformerInterface $viewTransformer,
Boolean $forcePrepend = false
)
Appends / prepends a transformer to the view transformer chain.

Resources