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

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.

Related

Zf2 - I am not able to define the form as shared one using its name as well as alias

I am working on ZendFramework 2. I have a form which I want to be used as a shared instance. But the shared key only accepts the actual class rather then the name allocated to it. Sharing some of the code snippet for better understanding of my problem:
SampleForm.php
namespace MyProject\Form;
use Zend\Form\Form;
class Sampleform extends Form
{
public function __construct()
{
parent::__construct('sampelname');
}
/**
* Initialize the form elements
*/
public function init()
{
$this->add(
[
'type' => 'Text',
'name' => 'name',
'options' => [
'label' => 'Enter your name',
]
]
);
}
}
Defining the SampleForm in Module.php:
namespace MyProject;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\FormElementProviderInterface;
use MyProject\Form\SampleForm;
class Module implements ConfigProviderInterface, FormElementProviderInterface
{
/**
* {#inheritDoc}
*/
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
/**
* {#inheritdoc}
*/
public function getFormElementConfig()
{
return [
'invokables' => [
'MyProject\Form\SharedSampleForm' => SampleForm::class,
],
'aliases' => [
'sharedSampleForm' => 'MyProject\Form\SharedSampleForm'
],
'shared' => [
'MyProject\Form\SharedSampleForm' => true
]
];
}
}
It throws me the error like:
Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\Form\FormElementManager\FormElementManagerV2Polyfill::setShared: A service by the name "MyProject\Form\SharedSampleForm" was not found and could not be marked as shared
But it works as expected when I define my getFormElementConfig in Module.php as follows:
public function getFormElementConfig()
{
return [
'invokables' => [
'MyProject\Form\SharedSampleForm' => SampleForm::class,
],
'aliases' => [
'sharedSampleForm' => 'MyProject\Form\SharedSampleForm'
],
'shared' => [
SampleForm::class => true
]
];
}
i.e. In the shared key I provide the reference to the actual Form class name.
If the same definitions are defined under getServiceConfig() then it works as expected without throwing any such error.
Can some one please suggest/help me out how can I be able to use the service name in the shared for forms then providing the actual class reference?
getFormElementConfig() is used for defining Form Element. Not used for defining Form as service. If you wanna define this form as Service, you should define it under getServiceConfig().
Another tips, if you have make an alias, just define the Service name using it's class name.
public function getServiceConfig()
{
return [
'invokables' => [
SampleForm::class => SampleForm::class,
],
'aliases' => [
'sharedSampleForm' => SampleForm::class
],
'shared' => [
SampleForm::class => true
]
];
}
You can call the Form using the alias name like this $this->getServiceLocator()->get('sharedSampleForm');

Zend Framework 2 - Controller instantiate Service via (modified?) Factory

I have an abstract service class SAbstract which is inherited by ConcreteServiceA and ConcreteServiceB. Now I am instantiating ConcreteServiceA in the factory class of my controller and inject the service in my controller.
In a specific action in my controller I want to exchange ConcreteServiceA with ConcreteServiceB to change behavior. Because they have same interface (abstract class SAbstract) I could inject it in my controller as well (the services are a Strategy-Pattern).
But I don't want to instantiate ConcreteServiceB directly in my controller to keep my code clean (for easy refactoring and exchanging behavior).
A possible solution is to create a second factory for my controller which injects ConcreteServiceB instead of ConcreteServiceA but then I have duplicated lots of code which is not good...
Another solution would be to inject both services in my controller (but this "smells" like bad code).
Is a delegator factory the right way to do this? Then I have to implement setters in my controller...
Is there a better way?
I tried to schematically visualize my class relationships.
AbstractService <|--<inherit>- ConcreteServiceA
AbstractService <|--<inherit>- ConcreteServiceB
Controller -<use>--> AbstractService
Controller:ActionA -<use>--> ConcreteServiceA:exportAction()
Controller:ActionB -<use>--> ConcreteServiceB:exportAction()
In a specific action in my controller I want to exchange ConcreteServiceA with ConcreteServiceB to change behavior. Because they have same interface.
You can configure the route to use a different controller service name for each action; then configure a controller factory to inject the required service using configuration.
The route config could look like this.
'router' => [
'routes' => [
'foo' => [
'type' => 'literal',
'options' => [
'route' => '/foo',
'defaults' => [
'controller' => 'MyControllerWithFooService',
'action' => 'actionThatNeedsFooService',
],
],
],
'bar' => [
'type' => 'literal',
'options' => [
'route' => '/bar',
'defaults' => [
'controller' => 'MyControllerWithBarService',
'action' => 'actionThatNeedsBarService',
],
],
],
],
]
Then add the config for the services and controllers.
'app_config' => [
'MyControllerWithFooService' => [
'service_name' => 'FooService',
],
'MyControllerWithFooService' => [
'service_name' => 'BarService',
],
],
'service_manager' => [
'factories' => [
'FooService' => 'FooServiceFactory'
'BarService' => 'BarServiceFactory'
],
],
'controllers' => [
'factories' => [
'MyControllerWithFooService' => 'MyControllerServiceFactory'
'MyControllerWithBarService' => 'MyControllerServiceFactory'
],
]
The MyControllerServiceFactory could be very simple.
class MyControllerServiceFactory
{
public function __invoke($controllerManager, $name, $requestedName)
{
$sm = $controllerManager->getServiceLocator();
$config = $sm->get('config');
if (empty($config['app_config'][$requestedName])) {
throw new ServiceNotCreatedException('No config set!');
}
$serviceName = $config['app_config'][$requestedName]['service_name'];
$service = $sm->get($serviceName);
return new MyController($service);
}
}

PHPUnit ZF2 InputFilter with Custom Validator

I have the following InputFilter:
<?php
namespace Login\InputFilter;
use Zend\InputFilter\InputFilter;
/**
* Class Login
*
* #package Login\InputFilter
*/
class Login extends InputFilter
{
/**
* Construct
*/
public function __construct()
{
/**
* Password
*/
$this->add(
[
'name' => 'password',
'required' => true,
'filters' => [
[
'name' => 'stringtrim'
]
],
'validators' => [
[
'name' => 'stringlength',
'options' => [
'min' => '5',
'max' => '128'
],
'break_chain_on_failure' => true
],
[
'name' => 'regex',
'options' => [
'pattern' => '/^[^\\\' ]+$/'
],
'break_chain_on_failure' => true
]
]
]
);
}
/**
* Init
*/
public function init()
{
/**
* Employee ID
*/
$this->add(
[
'name' => 'employeeId',
'required' => true,
'filters' => [
[
'name' => 'stringtrim'
]
],
'validators' => [
[
'name' => 'stringlength',
'options' => [
'min' => '1',
'max' => '20'
],
'break_chain_on_failure' => true
],
[
'name' => 'digits',
'break_chain_on_failure' => true
],
[
'name' => 'Login\Validator\EmployeeId',
'break_chain_on_failure' => true
]
]
]
);
}
}
Attached to the employeeId is a custom validator I've created to check if the Employee ID actually exists in a database. It has a constructor for Doctrine Entity Manager. This works fine when testing via the web, so no worries there.
However now I would like to test via PHPUnit and I've created the following test:
<?php
namespace LoginTest\InputFilter;
use Login\InputFilter\Login;
/**
* Class LoginTest
*
* #package LoginTest\InputFilter
*/
class LoginTest extends \PHPUnit_Framework_TestCase
{
/**
* #var Login $inputFilter
*/
protected $inputFilter;
public function setUp()
{
$this->inputFilter = new Login();
$this->inputFilter->init();
parent::setUp();
}
public function testFormHasElements()
{
$inputs = $this->inputFilter->getInputs();
$this->assertArrayHasKey(
'employeeId',
$inputs
);
$this->assertArrayHasKey(
'password',
$inputs
);
}
}
When the test runs the following error is produced:
1) LoginTest\InputFilter\LoginTest::testFormHasElements
Argument 1 passed to Login\Validator\EmployeeId::__construct() must be an instance of Doctrine\ORM\EntityManager, none given, called in /vhosts/admin-application/vendor/zendframework/zendframework/library/Zend/ServiceManager/AbstractPluginManager.php on line 180 and defined
I'm not certain how I can get passed this particular error. I assume I need to use Mockery but I'm not certain.
The validator has a Factory which supplies the Doctrine Entity Manager from the Service Locator.
I am still very new to PHPUnit but I've been trying to do my research before asking here.
Any ideas?
You're getting this error because you directly instantiate you input filter and it isn't then aware of your custom validator factory.
In real application InputFilter is using Zend\Validator\ValidatorPluginManager for getting validators from service manager.
I see two ways how to solve this problem:
1.) You can setup real service manager from application configuration, like it's described in documentation and then pull the input filter from service manager:
$inputFilter = Bootstrap::getServiceManager()->get(\Login\InputFilter\Login::class); // change the service name if you have another
This solution is good if you want to write some kind of integration tests.
2.) You can mock your custom validator and inject into ValidatorPluginManager in setup method:
protected function setUp()
{
$validator = $this->getMockBuilder(\Login\Validator\EmployeeId::class)->getMock();
$inputFilter = new Login();
$inputFilter->getFactory()
->getDefaultValidatorChain()
->getPluginManager()
->setService(\Login\Validator\EmployeeId::class, $validator);
$inputFilter->init();
$this->inputFilter = $inputFilter;
parent::setUp();
}
This solution is good if you want to write unit tests for Login input filter.

ZF2 set static dbAdapter in config loader

I have following setup in my development.global.php:
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter'
=> 'Zend\Db\Adapter\AdapterServiceFactory',
'dbAdapter' => function($sm) {
$config = $sm->get('config');
$config = $config['db'];
$dbAdapter = new Zend\Db\Adapter\Adapter($config);
return $dbAdapter;
},
),
),
An then, I'm loading static adapter in onBootstrap() of one of Module's Model class:
$dbAdapter = $e->getApplication()->getServiceManager()->get('dbAdapter');
\Zend\Db\TableGateway\Feature\GlobalAdapterFeature::setStaticAdapter($dbAdapter);
Is there any possibility to set that just once in config autoloader ? Currrently, if I do that, I still need to call setStaticLOader somewhere in the Module code.
UPDATE: as stated below, that's imposible - at least by standard way.
You can not avoid calling it explicitly in onBootstrap.
General rule is for global/static state to be avoided. Explicitly inject db adapter in factories for you TDG objects instead.
If you still insist on using it, suggest to use delegator factory to make it a bit more flexible. See blogpost for more info about delegators.
Add this to your module config:
'service_manager' => array(
'aliases' => array(
'globalDbAdapter' => 'Zend\Db\Adapter\Adapter',
),
'delegators' => array(
// Use alias to make it easier to chose which adapter to set as global
'globalDbAdapter' => array(
'YourModule\Factory\GlobalDbAdapterDelegator',
),
),
)
and then delegator factory:
namespace YourModule\Factory;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Db\TableGateway\Feature\GlobalAdapterFeature;
class GlobalDbAdapterDelegator implements DelegatorFactoryInterface
{
public function createDelegatorWithName(
ServiceLocatorInterface $serviceLocator,
$name,
$requestedName,
$callback
) {
$dbAdapter = $callback();
GlobalAdapterFeature::setStaticAdapter($dbAdapter);
return $dbAdapter;
}
}
and finally in onBootstrap method
// Force creation of service
$e->getApplication()->getServiceManager()->get('globalDbAdapter');

ZF2 Validator Context when using a Fieldset or Collection

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.

Resources