Advanced Use Cases
Beginning with version 2.1, forms elements can be registered using a designated plugin manager, in the same way that view helpers, controller plugins, and filters are registered. This new feature has a number of benefits, especially when you need to handle complex dependencies in forms/fieldsets.
Short names
The first advantage of pulling form elements from the service manager is that now you can use short names to create new elements through the factory. Therefore, this code:
$form->add([
'type' => Element\Email::class,
'name' => 'email',
]);
can now be replaced by:
$form->add([
'type' => 'Email',
'name' => 'email'
]);
Each element provided out-of-the-box by laminas-form supports this natively.
Use the ::class constant
While using aliases leads to compact code, they're also can more easily result in typographic mistakes. We recommend using the
::class
constant in most situations, as these can be more easily scanned with static analysis tools for correctness.
Creating custom elements
laminas-form also supports custom form elements.
To create a custom form element, make it extend the Laminas\Form\Element
class,
or, if you have a more specific dependency, extend one of the classes in the
Laminas\Form\Element
namespace.
In the following, we will demonstrate creating a custom Phone
element for
entering phone numbers. It will extend Laminas\Form\Element
class and provide
some default input rules.
Our custom phone element could look something like this:
namespace Application\Form\Element;
use Laminas\Filter;
use Laminas\Form\Element;
use Laminas\InputFilter\InputProviderInterface;
use Laminas\Validator\Regex as RegexValidator;
class Phone extends Element implements InputProviderInterface
{
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* Get a validator if none has been set.
*
* @return ValidatorInterface
*/
public function getValidator()
{
if (null === $this->validator) {
$validator = new RegexValidator('/^\+?\d{11,12}$/');
$validator->setMessage(
'Please enter 11 or 12 digits only!',
RegexValidator::NOT_MATCH
);
$this->validator = $validator;
}
return $this->validator;
}
/**
* Sets the validator to use for this element
*
* @param ValidatorInterface $validator
* @return self
*/
public function setValidator(ValidatorInterface $validator)
{
$this->validator = $validator;
return $this;
}
/**
* Provide default input rules for this element
*
* Attaches a phone number validator.
*
* @return array
*/
public function getInputSpecification()
{
return [
'name' => $this->getName(),
'required' => true,
'filters' => [
['name' => Filter\StringTrim::class],
],
'validators' => [
$this->getValidator(),
],
];
}
}
By implementing Laminas\InputFilter\InputProviderInterface
interface, we are
hinting to our form object that this element provides some default input rules
for filtering and/or validating values. In this example, the default input
specification provides a Laminas\Filter\StringTrim
filter and a
Laminas\Validator\Regex
validator that validates that the value optionally has a
+
sign at the beginning, and is followed by 11 or 12 digits.
To use the new element in our forms, we can specify it by its fully qualified class name (FQCN):
use Application\Form\Element\Phone;
use Laminas\Form\Form;
$form = Form();
$form->add(array(
'name' => 'phone',
'type' => Phone::class,
));
Or, if you are extending Laminas\Form\Form
:
namespace Application\Form;
use Laminas\Form\Form;
class MyForm extends Form
{
public function __construct($name = null)
{
parent::__construct($name);
$this->add([
'name' => 'phone',
'type' => Element\Phone::class,
]);
}
}
If you don't want to use the custom element's FQCN, but rather a short name,
add an entry for it to Laminas\Form\FormElementManager
. You can do this by adding
an entry under the form_elements
configuration, or within your Module
class
via a getFormElementConfig()
method.
Configuration via a config file (e.g., module.config.php
) file looks like the
following:
use Laminas\ServiceManager\Factory\InvokableFactory;
return [
'form_elements' => [
'aliases' => [
'phone' => Application\Form\Element\Phone::class,
],
'factories' => [
Application\Form\Element\Phone::class => InvokableFactory::class,
],
],
];
The following demonstrates using your Module
class:
namespace Application;
use Laminas\ModuleManager\Feature\FormElementProviderInterface;
use Laminas\ServiceManager\Factory\InvokableFactory;
class Module implements FormElementProviderInterface
{
public function getFormElementConfig()
{
return [
'aliases' => [
'phone' => Form\Element\Phone::class,
],
'factories' => [
Form\Element\Phone::class => InvokableFactory::class,
],
];
}
}
If needed, you can define a custom factory for handling dependencies.
And now comes the first catch.
If you are creating your form class by extending Laminas\Form\Form
, you must
not add the custom element in the constructor (as we have done in the previous
example where we used the custom element's FQCN), but rather in the init()
method:
namespace Application\Form;
use Laminas\Form\Form;
class MyForm extends Form
{
public function init()
{
$this->add([
'name' => 'phone',
'type' => 'phone',
]);
}
}
The second catch is that you must not directly instantiate your form
class, but rather get an instance of it through Laminas\Form\FormElementManager
:
namespace Application\Controller;
use Application\Form\MyForm;
use Laminas\Mvc\Controller\AbstractActionController;
class IndexController extends AbstractActionController
{
private $form;
public function __construct(MyForm $form)
{
$this->form = $form;
}
public function indexAction()
{
return array('form' => $this->form);
}
}
This now requires a factory to inject the form instance:
namespace Application\Controller;
use Application\Form\MyForm;
use Laminas\Form\FormElementManager;
use Psr\Container\ContainerInterface;
class IndexControllerFactory
{
public function __invoke(ContainerInterface $container)
{
$formManager = $container->get(FormElementManager::class);
return new IndexController($formManager->get(MyForm::class));
}
}
Which in turn requires that you map the controller to the factory:
// In module.config.php
return [
/* ... */
'controllers' => [
'factories' => [
Application\Controller\IndexController::class => Application\Controller\IndexControllerFactory::class,
],
],
];
The biggest gain of this is that you can easily override any built-in form
elements if they do not fit your needs. For instance, if you want to create your
own Email
element instead of the standard one, create your custom element, and
add it to the form element config with the same key as the element you want to
replace:
namespace Application;
use Laminas\Form\Element\Email;
use Laminas\ModuleManager\Feature\FormElementProviderInterface;
use Laminas\ServiceManager\Factory\InvokableFactory;
class Module implements FormElementProviderInterface
{
public function getFormElementConfig()
{
return [
'aliases' => [
'email' => Form\Element\MyEmail::class,
'Email' => Form\Element\MyEmail::class,
],
'factories' => [
Form\Element\MyEmail::class => InvokableFactory::class,
],
];
}
}
Now whenever you create an element with a type
of 'email',
it will create the custom element instead of the built-in one.
Use the original?
If you want to be able to use both the built-in one and your own one, you can still provide the FQCN of the element, e.g.
Laminas\Form\Element\Email
.
In summary, to create your own form elements (or even reusable fieldsets!) and be able to use them in your form, you need to:
- Create your element (like you did before).
- Add it to the form element manager either via the
form_elements
configuration in your module, or by defining agetFormElementConfig()
in yourModule
class. - Make sure the custom form element is not added in the form's constructor,
but rather in its
init()
method, or after getting an instance of the form. - Retrieve your form through the form element manager instead of directly instantiating it, and inject it in your controller.
Handling dependencies
Dependency management can be complex. For instance, a very frequent use case is
a form that creates a fieldset, but itself need access to the database to
populate a Select
element. Retrieving forms from the FormElementManager
solves this issue, as factories it invokes have access to the application
service container, and can use it to provide dependencies.
For instance, let's say that a form create a fieldset called AlbumFieldset
:
namespace Application\Form;
use Laminas\Form\Form;
class CreateAlbum extends Form
{
public function init()
{
$this->add([
'name' => 'album',
'type' => AlbumFieldset::class,
]);
}
}
Let's now create the AlbumFieldset
, and have it depend on an AlbumTable
object that allows us to fetch albums from the database.
namespace Application\Form;
use Album\Model\AlbumTable;
use Laminas\Form\Fieldset;
class AlbumFieldset extends Fieldset
{
public function __construct(AlbumTable $albumTable)
{
// Add any elements that need to fetch data from database
// using the album table !
}
}
To enable this, we'll create a factory for our AlbumFieldset
as follows:
namespace Application\Form;
use Album\Model\AlbumTable;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
class AlbumFieldsetFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
return new AlbumFieldset($container->get(AlbumTable::class));
}
}
Compatibility
The above factory was written to work with both the v2 and v3 releases of laminas-servicemanager. If you know you will only be using v3, you can remove the
createService()
implementation.
You can now map the fieldset to the factory in your configuration:
// In module.config.php:
return [
'form_elements' => [
'factories' => [
Application\Form\AlbumFieldset::class => Application\Form\AlbumFieldsetFactory::class,
],
],
];
Inject your form into your controller, per the example in the previous section.
As a reminder, to use your fieldset in a view, you need to use the
formCollection
helper:
echo $this->form()->openTag($form);
echo $this->formCollection($form->get('album'));
echo $this->form()->closeTag();
Initialization
As noted in previous sections, and in the chapter on elements, we recommend
defining an init()
method for initializing your elements, fieldsets, and
forms. Where does this come from, and when exactly is it invoked in the object
lifecycle?
The method is defined in Laminas\Stdlib\InitializableInterface
, which
Laminas\Form\Element
implements. It is not, however, automatically invoked on
instantiation!
Within laminas-form, the FormElementManager
defines an
initializer
that is pushed to the bottom of the initializer stack, making it the last
initializer invoked. This initializer checks if the instance created implements
InitializableInterface
, and, if so, calls its init()
method.
This approach ensures that dependencies are fully injected prior to any methods
you call from your init()
method. As a result, when pulling items from the
FormElementManager
, you can be assured that all factories are correctly setup
and populated, and shared across all specifications you provide.