Search code examples
zend-frameworkzend-framework3

Zend ServiceManager using setter injection


in symfony i can use the setter injection for services via call option (https://symfony.com/doc/current/service_container/calls.html)

The example from the symfony documentation:

class MessageGenerator
{
    private $logger;

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    // ...
}

service.yml

services:
    App\Service\MessageGenerator:
        # ...
        calls:
            - method: setLogger
              arguments:
                  - '@logger'

I need this behaviour for my zend project. i want to inject a InputFilter into my FormFieldSet.

I didn't find anything about this in the zend documentation. Can i use something like this or exist a better solution for my problem in zend?


Solution

  • Based on this question and your previous question about Forms, Fieldsets and InputFilters, I'm thinking you want to achieve something similar to the following use case.

    Use case

    You have a

    • Location Entity
    • Address Entity
    • Location has a OneToOne to an Address (required, uni-directional)

    Requirements

    To manage the Location, you'll need:

    • LocationForm (-Factory)
    • LocationFormInputFilter (-Factory)
    • LocationFieldset (-Factory)
    • LocationFieldsetInputFilter (-Factory)
    • AddressFieldset (-Factory)
    • AddressFieldsetInputFilter (-Factory)

    Configuration

    To configure this in ZF3, you'll have to do add the following

    'form_elements' => [
        'factories' => [
            AddressFieldset::class  => AddressFieldsetFactory::class,
            LocationForm::class     => LocationFormFactory::class,
            LocationFieldset::class => LocationFieldsetFactory::class,
        ],
    ],
    'input_filters' => [
        'factories' => [
            AddressFieldsetInputFilter::class  => AddressFieldsetInputFilterFactory::class,
            LocationFormInputFilter::class     => LocationFormInputFilterFactory::class,
            LocationFieldsetInputFilter::class => LocationFieldsetInputFilterFactory::class,
        ],
    ],
    

    Forms & Fieldsets

    In the LocationForm, add your LocationFieldset and what else your Form needs, such as CSRF and submit button.

    class LocationForm extends AbstractForm
    {
        public function init()
        {
            $this->add([
                'name'    => 'location',
                'type'    => LocationFieldset::class,
                'options' => [
                    'use_as_base_fieldset' => true,
                ],
            ]);
    
            //Call parent initializer. Adds CSRF & submit button
            parent::init();
        }
    }
    

    (Note: my AbstractForm does a bit more, I would suggest you have a look here, such as remove empty (child fieldsets/collections) Inputs so data is not attempted to be created in the DB)

    In the LocationFieldset, give add Inputs for the Location, such as a name, and the AddressFieldset:

    class LocationFieldset extends AbstractFieldset
    {
        public function init()
        {
            parent::init();
    
            $this->add([
                'name'     => 'name',
                'required' => true,
                'type'     => Text::class,
                'options'  => [
                    'label' => _('Name'),
                ],
            ]);
    
            $this->add([
                'type'     => AddressFieldset::class,
                'name'     => 'address',
                'required' => true,
                'options'  => [
                    'use_as_base_fieldset' => false,
                    'label'                => _('Address'),
                ],
            ]);
        }
    }
    

    In the AddressFieldset just add Inputs for the Address Entity. (Same as above, without the Fieldset type Input)

    InputFilters

    To validate the Form, you can keep it very simple:

    class LocationFormInputFilter extends AbstractFormInputFilter
    {
        /** @var LocationFieldsetInputFilter  */
        protected $locationFieldsetInputFilter;
    
        public function __construct(LocationFieldsetInputFilter $filter) 
        {
            $this->locationFieldsetInputFilter = $filter;
    
            parent::__construct();
        }
    
        public function init()
        {
            $this->add($this->locationFieldsetInputFilter, 'location');
    
            parent::init();
        }
    }
    

    (The AbstractFormInputFilter adds CSRF validator)

    Notice that we simply ->add() the LocationFieldsetInputFilter, but we give it a name (2nd parameter). This name is used later in the complete structure, so it's important to both keep it simple and keep it correct. Simplest is to give it a name that one on one matches the object of the Fieldset it's supposed to validate.

    Next, the LocationFieldsetInputFilter:

    class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
    {
        /**
         * @var AddressFieldsetInputFilter
         */
        protected $addressFieldsetInputFilter;
    
        public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter) 
        {
            $this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
    
            parent::__construct();
        }
    
        public function init()
        {
            parent::init();
    
            $this->add($this->addressFieldsetInputFilter, 'address'); // Again, name is important
    
            $this->add(
                [
                    'name'       => 'name',
                    'required'   => true,
                    'filters'    => [
                        ['name' => StringTrim::class],
                        ['name' => StripTags::class],
                        [
                            'name'    => ToNull::class,
                            'options' => [
                                'type' => ToNull::TYPE_STRING,
                            ],
                        ],
                    ],
                    'validators' => [
                        [
                            'name'    => StringLength::class,
                            'options' => [
                                'min' => 3,
                                'max' => 255,
                            ],
                        ],
                    ],
                ]
            );
        }
    }
    

    Factories

    Now, you must bind them together, which is where your question about Setter injection comes from I think. This happens in the Factory.

    A *FormFactory would do the following:

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $inputFilterPluginManager = $container->get('InputFilterManager');
        $inputFilter = $inputFilterPluginManager->get(LocationFormInputFilter::class);
    
        /** @var LocationForm $form */
        $form = new LocationForm();
        $form->setInputFilter($inputFilter); // The setter injection you're after
    
        return $form;
    }
    

    A *FieldsetFactory would do the following (do the same for Location- and AddressFieldsets):

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var LocationFieldset $fieldset */
        // name matters! Match the object to keep it simple. Name is used from Form to match the InputFilter (with same name!)
        $fieldset = new LocationFieldset('location'); 
        // Zend Reflection Hydrator, could easily be something else, such as DoctrineObject hydrator. 
        $fieldset->setHydrator(new Reflection()); 
        $fieldset->setObject(new Location());
    
        return $fieldset;
    }
    

    A *FormInputFilterFactory would do the following:

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $inputFilterPluginManager = $container->get('InputFilterManager');
    
        /** @var LocationFieldsetInputFilter $locationFieldsetInputFilter */
        $locationFieldsetInputFilter = $inputFilterPluginManager->get(LocationFieldsetInputFilter::class);
    
        // Create Form InputFilter
        $locationFormInputFilter = new LocationFormInputFilter(
            $locationFieldsetInputFilter
        );
    
        return $locationFormInputFilter;
    }
    

    A *FieldsetInputFilterFactory would do the following:

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var AddressFieldsetInputFilter $addressFieldsetInputFilter */
        $addressFieldsetInputFilter = $this->getInputFilterManager()->get(AddressFieldsetInputFilter::class);
        $addressFieldsetInputFilter->setRequired(true);
    
        return new LocationFieldsetInputFilter(
            $addressFieldsetInputFilter
        );
    }
    

    Note:

    • Setting an InputFilter as (not) required is something I've added here
    • If your InputFilter (such as AddressFieldsetInputFilter) does not have a child InputFilter, you can can skip getting the child and straight away return the new InputFilter.

    I think I covered it all for a complete picture. If you have any questions about this, please comment.