ZF2 Validator Context when using a Fieldset or Collection - zend-framework2

Is it possible to pass the full form as the context to a validator?
I would like to create a conditional validator for element X in fieldset A which checks the value of element Y in a different fieldset B.
The problem is that the isValid function only receives the context for the fieldset it is in. This element X knows nothing about element Y.
All answers greatly received!

You can do this with collections and ZendCollectionInputFilter yeah.
There's not like loads of documentation for this, know the zend guys are sorting this out though (think the only mention of it is in http://framework.zend.com/apidoc/2.2/classes/Zend.InputFilter.CollectionInputFilter.html) but for now a resource that really helped me was this:
http://www.aronkerr.com/2013/11/zf2-form-collection-validation-unique.html
Very clever stuff once you get your head round these. Can't really give you much more help as your question isn't massively specific and there is no code for your form, fieldsets and input filters that you currently have implmented but hope this helps. If you get stuck at any point more than happy to run through more specific code

Let's say our fieldsets A and B belong to the form Sample. We need to add the validators from this parent form in order to access the context of this form when validating any child fieldsets:
<?php
namespace App\Form;
use Zend\Form\Form;
use Zend\InputFilter\InputFilterProviderInterface;
class Sample extends Form InputFilterProviderInterface
{
public function init()
{
$this->add([
'type' => 'App:Fieldset:A',
'name' => 'fieldsetA',
]);
$this->add([
'type' => 'App:Fieldset:B',
'name' => 'fieldsetB',
]);
$this->add([
'type' => 'submit',
'name' => 'submit',
'attributes' => [
'value' => 'Submit',
],
]);
}
public function getInputFilterSpecification()
{
return [
'fieldsetA' => [
'type' => 'InputFilter',
'X' => [
'required' => true,
'allow_empty' => true,
'continue_if_empty' => true,
'validators' => [
[
'name' => 'Callback',
'options' => [
'callback' => function ($value)
{
if ($this->data['fieldsetB']['Y'])
{
// do something
}
// do something else
},
],
],
],
],
],
];
}
}
Notice how we add validators to X in A from within Sample using the InputFilter type. Next we access $this->data directly and traverse it to get Y in B.

Related

How to use services for a form fieldset in Zend Framework 2?

I have a form (Zend\Form\Form) with some nested fieldsets (Zend\Form\Fieldset) in it. The construction is pretty similar to that in the Form Collections tutorial.
Storage\Form\MyForm
|_'Storage\Form\Fieldset\FooFieldset'
|_'Storage\Form\Fieldset\BarFieldset'
|_'Storage\Form\Fieldset\BazFieldset'
...
MyForm
class MyForm {
public function __construct()
{
...
$this->add(
[
'type' => 'Storage\Form\Fieldset\FooFieldset',
'options' => [
'use_as_base_fieldset' => true
]
]
);
}
}
FooFieldset
class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('foo');
$this->setHydrator(new ClassMethodsHydrator())->setObject(new Foo()); // dependencies!
}
}
It works, but the fieldset class has two dependencies in it. I want to inject them. In order to do it, I created a FooFieldsetFactory and extended the /module/MyModule/config/module.config.php by:
'service_manager' => [
'factories' => [
'Storage\Form\Fieldset\FooFieldset' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
The factory is simply being ignored. I guess, the service locator first tries to find the class by namespace and only if nothing is found, takes a look in the invokables and factories. OK. Then I created an alias:
'service_manager' => [
'factories' => [
'Storage\Form\Fieldset\FooFieldset' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
'aliases' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\FooFieldset',
],
... and tried to use it instead of Storage\Form\Fieldset\FooFieldset in my form class. But now I get an exception:
Zend\Form\FormElementManager::get was unable to fetch or create an instance for Storage\Form\Fieldset\Foo
I've also tried this directly:
'service_manager' => [
'factories' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
No effect, the same error.
And this also didn't work (the same error):
'form_elements' => [
'factories' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
So the referencing a service for a fieldset seems not to work. Or am I doing something wrong?
How to use services for form fieldsets?
UPDATE
With some debuggin I found out, that my Foo fieldset factory cannot be found, because it has not be added to the factories list of the Zend\Form\FormElementManager. Here is the place in the Zend\Form\Factory:
So my config
'form_elements' => [
'factories' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
is ignored. How to fix this?
UPDATE Additional information, how I'm creating my Form object.
/module/Foo/config/module.config.php
return [
'controllers' => [
'factories' => [
'Foo\Controller\My' => 'Foo\Controller\Factory\MyControllerFactory'
]
],
'service_manager' => [
'factories' => [
'Foo\Form\MyForm' => 'Foo\Form\Factory\MyFormFactory',
],
],
];
/module/Foo/src/Foo/Form/Factory/MyFormFactory.php
namespace Foo\Form\Factory;
use ...;
class MyFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$form = new MyForm();
$form->setAttribute('method', 'post')
->setHydrator(new ClassMethods())
->setInputFilter(new InputFilter());
return $form;
}
}
/module/Foo/src/Foo/Controller/Factory/MyControllerFactory.php
namespace Foo\Controller\Factory;
use ...;
class MyControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$fooPrototype = new Foo();
$realServiceLocator = $serviceLocator->getServiceLocator();
// $myForm = $realServiceLocator->get('Foo\Form\MyForm'); <-- This doesn't work correctly for this case. The FormElementManager should be used instead.
$formElementManager = $realServiceLocator->get('FormElementManager');
$myForm = $formElementManager->get('Foo\Form\MyForm');
return new MyController($myForm, $fooPrototype);
}
}
This issue is because you are adding your form elements in the forms __construct() method rather than init() as suggested in the documentation.
You can use a factory instead of an invokable in order to handle dependencies in your elements/fieldsets/forms.
And now comes the first catch.
If you are creating your form class by extending Zend\Form\Form, you must not add the custom element in the __construct-or (as we have done in the previous example where we used the custom element’s FQCN), but rather in the init() method:
The reason is that the new form's factory (which is used to create new elements using add()) must have the application's form element manager injected after the form's constructor has been called This form element manager instance contains all the references to your custom forms elements which are registered under the form_elements configuration key.
By calling add() in the form __construct the form factory will lazy load a new instance of the form element manager; which will be able to create all default form elements but will not have any knowledge of your custom form element.

Symfony3 get the value of a non mapped field in event listener

Trying to make a form that a user chooses an option and depending on their choice loads additional fields. So far I have a UserSignupType:
class UserSignupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('userType', ChoiceType::class, array(
'choices' => array(
"Subscriber" => "Subscriber",
"Friend" => "Friend"
),
'expanded' => true,
'mapped' => false
));
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
$usertype = $form->get('userType')->getData(); //updated per JBaffords answer
if($userType == "Subscriber")
{
$builder->add('agency', EntityType::class, array(
"class" => "\AppBundle\Entity\Agency",
"label" => "name"));
}
elseif($userType == "Friend")
{
$builder->add('phoneNumber', PhoneNumberType::class, array(
'default_region' => 'US',
'format' => PhoneNumberFormat::NATIONAL));
}
}
);
}
// ...
}
not sure if the getData method is the right method to use, and if it is, i need to somehow get the "userType" field out of it. I cant call getUserType because its not an actual mapped property and I don't want it to be. It simply decides the fields to show.
You can get the value for any form element (mapped or unmapped) by doing:
$form->get('fieldName')->getData();
get() returns a Form object, so if you have a nested form, you can continue to call ->get('nextFieldName') on each child until you get to the form element you need.
The value returned from getData for a form is going to depend on (amont other things) the mapping of its child elements. If the form has no children, then its value is its value; the mapping just determines whether that value is populated into its parent's data.
In your specific case, to get the data for the userType element, you would do:
$userType = $form->get('userType')->getData();

Yii2 : How to change URL in Action Column Grid View?

I want to change url in action view, from view to viewdaily, viewweekly and viewmonthly, what should i do?
[
'class' => 'yii\grid\ActionColumn',
],
The simplest way is not create/use ActionColumn, instead you can use raw-column and show there what you want:
[
'attribute' => 'some_title',
'format' => 'raw',
'value' => function ($model) {
return 'your Text';
},
],

\Zend\Form\Element\Collection validation as a whole

I have form with collection. And i attach validation to whole collecion - i just want to check the existence of certain relations between elements of the collection.
And it works great. in the case of wrong data - form does not pass the "isValid()" test.
But there is one problem. formElementErrors / getMessages didnt return anything.
What i do wrong?
My form:
class Form implements InputFilterProviderInterface {
/**
* #return array
*/
public function getInputFilterSpecification()
{
return [
[
'name' => 'legend',
'required' => true,
'allowEmpty' => false,
'validators' => [
['name' => 'Callback', 'options' => [
'messages' => [
\Zend\Validator\Callback::INVALID_VALUE => 'Wrong',
],
'callback' => function ($values, $context=[]) {
return false;
},
]],
]
],
];
}
public function init()
{
$this->add(
[
'name' => 'legend',
'type' => 'Zend\Form\Element\Collection',
'options' => [
'label' => 'Legenda',
'count' => 2,
'should_create_template' => true,
'allow_add' => true,
'template_placeholder' => '__placeholder__',
'target_element' => [
'type' => 'Narzedzie\Form\Legenda\LegendyOpcjeFieldset',
],
],
]
);
}
}
And view:
$element = $NarzedzieForm->get('legend');
var_dump($element->getMessages()); // in case of error - empty array!
echo $this->formElementErrors($element); // in case of error - empty string
echo $this->formColleciton($element);
Maybe you need to add both messages?
'messages' => [
\Zend\Validator\Callback::INVALID_VALUE => 'Wrong VALUE',
\Zend\Validator\Callback::INVALID_CALLBACK => 'Wrong CALLBACK',
],
as perhaps the invalid callback message is being suppressed as you are only supplying one? I would hope it would fall back to the default. But then all this validator message stuff seems a bit stupid to me the way it is done.
Looks like you have an error in your callback, which might be throwing an exception and is being caught in the validator in the try catch statement maybe?
should be?
function ($values, $context=[]) {
foreach ($values as $value) {
if ($value['el'] == '1') return false;
}
return true;
},
as in $values not $value for the array in the foreach? Probably want to check that key is set too, with an isset($value['el'])?

How to add a class to all labels in a ZF2 form

I'm using a jQuery plugin that takes the text from labels associated with form elements and puts them as default text for the fields themselves. (You can find the plugin here.)
Here's the catch: it can only do this if the label has the class "inline". Now, I know I can use the following code to do this:
$this->add(array (
'name' -> 'name',
....
'options' => array (
'label' => 'Name',
'label_attributes' => array (
'class' => 'inline'
)
)
));
This will work fine, and if it has to be done item by item, then so be it. But I was wondering if there's some way I can add the class to ALL labels associated with text and text area form elements without using JavaScript. I'm thinking this would either done by a plugin, or by looping through all the elements in the form, but I don't know how to do either.
You could extend the FormRow view helper.
Here is a little example:
use Zend\Form\View\Helper\AbstractHelper;
use Zend\Form\View\Helper\FormRow;
class CustomFormRow extends FormRow
{
public function render(ElementInterface $element) {
...
$label = $element->getLabel();
if (isset($label) && '' !== $label) {
// Translate the label
if (null !== ($translator = $this->getTranslator())) {
$label = $translator->translate(
$label, $this->getTranslatorTextDomain()
);
}
$label->setAttribute('class', 'inline');
}
...
if ($this->partial) {
$vars = array(
'element' => $element,
'label' => $label,
'labelAttributes' => $this->labelAttributes,
'labelPosition' => $this->labelPosition,
'renderErrors' => $this->renderErrors,
);
return $this->view->render($this->partial, $vars);
}
...
}
You could probably leave the rest as it is and you should be good to go once you add some configuration in your Module.php for your view helper.
public function getViewHelperConfig() {
return array(
'factories' => array(
'CustomFormRow' => function($sm) {
return new \Application\View\Helper\CustomFormRow;
},
)
);
}
In your template files you now have to use your viewHelper instead.
<?php echo $this->CustomFormRow($form->get('yourelement')); ?>

Resources