ZF3 FormElementManager:如何获取ServiceManager

时间:2018-12-09 21:34:20

标签: php zend-framework3

我正在将代码从ZF2移植到ZF3。

在ZF2中,当我通过FormElementManager创建表单时,可以访问init方法上的servicelocator并配置如下内容:

public function init()
{
    $this->serviceLocator = $this->getFormFactory()->getFormElementManager()->getServiceLocator();
    $this->translator = $this->serviceLocator->get('translator');
}

这在很大的应用程序中很方便。实际上,我所有的表单都继承自BaseForm类。

在ZF3中,这很糟糕,不建议使用serviceLocator。 获得相同结果的最佳方法是哪一种? 一种方法是向ControllerFactory或ServiceFactory中的每个表单中注入所需的内容,但这很繁琐。

感谢您的帮助。

1 个答案:

答案 0 :(得分:1)

首先,您不应该在Form对象中使用ServiceManager和/或它的子级(如FormElementManager)。

使用Factory模式,您应该创建功能齐全的独立Form,Fieldset和InputFilter对象。

正如您所说的那样,肯定会有一些乏味的工作,但是您只需要做一次。

假设您要创建一个位置。位置由name属性和一个OneToOne单向Address引用组成。这就产生了以下需求:

  • LocationForm(-InputFilter)
  • LocationFieldset(-InputFilter)
  • AddressFieldset(-InputFilter)
  • 配置以上
  • 上述6个类别的工厂

在这个答案中,我将所有内容降至最低限度并使用我自己的存储库中的类和示例,因此对于完整的代码,您可以使用here和示例here

在创建了类本身之后,我将向您展示此用例所需的配置以及将所有用例捆绑在一起的工厂。


AbstractFieldset

abstract class AbstractFieldset extends Fieldset
{
    public function init()
    {
        $this->add(
            [
                'name'     => 'id',
                'type'     => Hidden::class,
                'required' => false,
            ]
        );
    }
}

AbstractInputFilter

abstract class AbstractFieldsetInputFilter extends AbstractInputFilter
{
    public function init()
    {
        $this->add([
            'name' => 'id',
            'required' => false,
            'filters' => [
                ['name' => ToInt::class],
            ],
            'validators' => [
                ['name' => IsInt::class],
            ],
        ]);
    }
}

AddressFieldset

class AddressFieldset extends AbstractFieldset
{
    public function init()
    {
        parent::init();

        $this->add([
            'name' => 'street',
            'required' => true,
            'type' => Text::class,
            'options' => [
                'label' => 'Address',
            ],
        ]);
    }
}

AddressInputFilter

class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    public function init()
    {
        parent::init();

        $this->add([
            'name' => 'street',
            '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,
                    ],
                ],
            ],
        ]);
    }
}

到目前为止,很容易。现在,我们需要创建LocationFieldset和LocationFieldsetInputFilter。这些将利用Address(-Fieldset)类。

LocationFieldset

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',
            ],
        ]);
    }
}

LocationFieldsetInputFilter

class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    /**
     * @var AddressFieldsetInputFilter
     */
    protected $addressFieldsetInputFilter;

    public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter) 
    {
        $this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
    }

    public function init()
    {
        parent::init();

        $this->add($this->addressFieldsetInputFilter, 'address');

        $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,
                        ],
                    ],
                ],
            ]
        );
    }
}

好的,所以这还不是很令人兴奋。请注意,LocationFieldset使用AddressFieldset作为类型。相反,在InputFilter类中,应该使用完整的类对象(一个InputFilter实例)。

表格。我还使用AbstractForm(在您的情况下为BaseForm)来处理一些默认值。在我完整的文章中(链接回购中),还有更多内容,但是在这里就足够了。这将CSRF保护添加到表单,如果表单没有表单,则会添加一个提交按钮。仅当在调用init时Form类没有一个类时,此操作才能完成,因此您可以覆盖这些设置。

AbstractForm

abstract class AbstractForm extends \Zend\Form\Form implements InputFilterAwareInterface
{
    protected $csrfTimeout = 900; // 15 minutes

    public function __construct($name = null, $options = [])
    {
        $csrfName = null;
        if (isset($options['csrfCorrector'])) {
            $csrfName = $options['csrfCorrector'];
            unset($options['csrfCorrector']);
        }

        parent::__construct($name, $options);

        if ($csrfName === null) {
            $csrfName = 'csrf';
        }

        $this->addElementCsrf($csrfName);
    }

    public function init()
    {
        if (!$this->has('submit')) {
            $this->addSubmitButton();
        }
    }

    public function addSubmitButton($value = 'Save', array $classes = null)
    {
        $this->add([
            'name'       => 'submit',
            'type'       => Submit::class,
            'attributes' => [
                'value' => $value,
                'class' => (!is_null($classes) ? join (' ', $classes) : 'btn btn-primary'),
            ],
        ]);
    }

    public function get($elementOrFieldset)
    {
        if ($elementOrFieldset === 'csrf') {
            // Find CSRF element
            foreach ($this->elements as $formElement) {
                if ($formElement instanceof Csrf) {
                    return $formElement;
                }
            }
        }

        return parent::get($elementOrFieldset);
    }

    protected function addElementCsrf($csrfName = 'csrf')
    {
        $this->add([
            'type'    => Csrf::class,
            'name'    => 'csrf',
            'options' => [
                'csrf_options' => [
                    'timeout' => $this->csrfTimeout,
                ],
            ],
        ]);
    }
}

LocationForm

class LocationForm extends AbstractForm
{
    public function init()
    {
        $this->add([
            'name' => 'location',
            'type' => LocationFieldset::class,
            'options' => [
                'use_as_base_fieldset' => true,
            ],
        ]);

        parent::init();
    }
}

现在,我们拥有制作表格的一切。我们仍然需要验证。让我们现在创建它们:

AddressFieldsetInputFilter

class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    public function init()
    {
        parent::init();

        $this->add([
            'name' => 'street',
            '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,
                    ],
                ],
            ],
        ]);
    }
}

LocationFieldsetInputFilter

class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
    protected $addressFieldsetInputFilter;

    public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter) 
    {
        $this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
    }

    public function init()
    {
        parent::init();

        $this->add($this->addressFieldsetInputFilter, 'address');

        $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,
                        ],
                    ],
                ],
            ]
        );
    }
}

LocationFormInputFilter

class LocationFormInputFilter extends AbstractFormInputFilter
{
    /** @var LocationFieldsetInputFilter  */
    protected $locationFieldsetInputFilter;

    public function __construct(LocationFieldsetInputFilter $filter) 
    {
        $this->locationFieldsetInputFilter = $filter
    }

    public function init()
    {
        $this->add($this->locationFieldsetInputFilter, 'location');

        parent::init();
    }
}

对,就是所有这些类本身。您看到它们将如何嵌套在一起吗?这将创建可重用的组件,这就是为什么我说您只需要做一次。下次需要地址或位置时,只需确保加载AddressFieldset并在工厂中设置InputFilter。后者通过设置工厂的Setter Injection来设置正确的InputFilter。如下所示。


AbstractFieldsetFactory

abstract class AbstractFieldsetFactory implements FactoryInterface
{

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $fieldset;

    /**
     * @var string
     */
    protected $fieldsetName;

    /**
     * @var string
     */
    protected $fieldsetObject;

    public function __construct($fieldset, $name, $fieldsetObject)
    {
        $this->fieldset = $fieldset;
        $this->fieldsetName = $name;
        $this->fieldsetObject = $fieldsetObject;

        $this->hydrator = new Reflection(); // Replace this with your own preference, either Reflection of ZF or maybe the Doctrine EntityManager...
    }

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $fieldset = $this->fieldset;
        $fieldsetObject = $this->fieldsetObject;

        /** @var AbstractFieldset $fieldset */
        $fieldset = new $fieldset($this->hydrator, $this->name ?: $this->fieldsetName);
        $fieldset->setHydrator(
            new DoctrineObject($this->hydrator)
        );
        $fieldset->setObject(new $fieldsetObject());

        return $fieldset;
    }
}

AddressFieldsetFactory

class AddressFieldsetFactory extends AbstractFieldsetFactory
{
    public function __construct()
    {
        parent::__construct(AddressFieldset::class, 'address', Address::class);
    }
}

LocationFieldsetFactory

class LocationFieldsetFactory extends AbstractFieldsetFactory
{
    public function __construct()
    {
        parent::__construct(LocationFieldset::class, 'location', Location::class);
    }
}

AbstractFieldsetInputFilterFactory

abstract class AbstractFieldsetInputFilterFactory implements FactoryInterface
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var HydratorInterface
     */
    protected $hydrator;

    /**
     * @var InputFilterPluginManager
     */
    protected $inputFilterManager;

    /**
     * Use this function to setup the basic requirements commonly reused.
     *
     * @param ContainerInterface $container
     * @param string|null $className
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     */
    public function setupRequirements(ContainerInterface $container, $className = null)
    {
        $this->inputFilterManager = $container->get(InputFilterPluginManager::class);
    }
}

AddressFieldsetInputFilterFactory

class AddressFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        parent::setupRequirements($container, Address::class);

        return new AddressFieldsetInputFilter($this->hydrator);
    }
}

LocationFieldsetInputFilterFactory

class LocationFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        parent::setupRequirements($container, Location::class);

        /** @var AddressFieldsetInputFilter $addressFieldsetInputFilter */
        $addressFieldsetInputFilter = $this->inputFilterManager->get(AddressFieldsetInputFilter::class);

        return new LocationFieldsetInputFilter(
            $addressFieldsetInputFilter,
            $this->hydrator
        );
    }
}

这负责FieldsetInputFilterFactory类。就在表格左边。

在我的情况下,我使用与Fieldset类相同的抽象工厂类。


LocationFormInputFilterFactory

class LocationFormInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        parent::setupRequirements($container);

        /** @var LocationFieldsetInputFilter $locationFieldsetInputFilter */
        $locationFieldsetInputFilter = $this->getInputFilterManager()->get(LocationFieldsetInputFilter::class);

        return new LocationFormInputFilter(
            $locationFieldsetInputFilter,
            $this->hydrator
        );
    }
}

所以,所有的类都完成了。这是一个完整的设置。在我修改自己的代码以删除吸气剂/设置器,代码注释/提示,错误,属性和变量检查而不进行测试时,您可能会遇到一些错误。但这应该有效;)

但是,我们快完成了。我们仍然需要:

  • config
  • 在控制器中使用
  • 在视图中打印/使用表单

配置很简单:

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

就是这样。这一点将上述所有类联系在一起。


要将表单放入控制器,您可以在工厂中执行以下操作:

class EditControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $hydrator = new Reflection(); // or $container->get('hydrator') or $container->get(EntityManager::class), or whatever you use

        /** @var FormElementManagerV3Polyfill $formElementManager */
        $formElementManager = $container->get('FormElementManager');

        /** @var LocationForm $form */
        $form = $formElementManager->get(LocationForm::class); // See :) Easy, and re-usable

        return new EditController($hydrator, $form);
    }
}

典型的“编辑”操作如下(注意,此操作使用Doctrine的EntityManager作为水化器):

public function editAction()
{
    $id = $this->params()->fromRoute('id', null);

    /** @var Location $entity */
    $entity = $this->getObjectManager()->getRepository(Location::class)->find($id);

    /** @var LocationForm $form */
    $form = $this->form;
    $form->bind($entity);

    /** @var Request $request */
    $request = $this->getRequest();
    if ($request->isPost()) {
        $form->setData($request->getPost());

        if ($form->isValid()) {
            /** @var Location $object */
            $object = $form->getObject();

            $this->getObjectManager()->persist($object);

            try {
                $this->getObjectManager()->flush();
            } catch (Exception $e) {
                // exception handling
            }

            return $this->redirect()->toRoute('route/name', ['id' => $object->getId()]);
        }
    }

    return [
        'form'               => $form,
        'validationMessages' => $form->getMessages() ?: '',
    ];
}

“查看部分”将如下所示(基于上述操作中的return):

form($ form)?>

就是这样。完全成熟,可重用的类。单一设置。最后在控制器的工厂中只有一行。

请注意:

  • 表单,字段集和InputFilter使用“地址”输入名称。始终保持相同是非常重要的,因为Zend会根据名称进行一些魔术操作以将Fieldset与InputFilter匹配。

如果您对它的工作方式还有其他疑问,请先阅读我所链接的回购协议中的文档,然后再问这个问题。还有更多可以为您提供更多帮助的信息,例如集合处理。