我正在将代码从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中的每个表单中注入所需的内容,但这很繁琐。
感谢您的帮助。
答案 0 :(得分:1)
首先,您不应该在Form对象中使用ServiceManager和/或它的子级(如FormElementManager)。
使用Factory模式,您应该创建功能齐全的独立Form,Fieldset和InputFilter对象。
正如您所说的那样,肯定会有一些乏味的工作,但是您只需要做一次。
假设您要创建一个位置。位置由name
属性和一个OneToOne单向Address
引用组成。这就产生了以下需求:
在这个答案中,我将所有内容降至最低限度并使用我自己的存储库中的类和示例,因此对于完整的代码,您可以使用here和示例here。
在创建了类本身之后,我将向您展示此用例所需的配置以及将所有用例捆绑在一起的工厂。
abstract class AbstractFieldset extends Fieldset
{
public function init()
{
$this->add(
[
'name' => 'id',
'type' => Hidden::class,
'required' => false,
]
);
}
}
abstract class AbstractFieldsetInputFilter extends AbstractInputFilter
{
public function init()
{
$this->add([
'name' => 'id',
'required' => false,
'filters' => [
['name' => ToInt::class],
],
'validators' => [
['name' => IsInt::class],
],
]);
}
}
class AddressFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'street',
'required' => true,
'type' => Text::class,
'options' => [
'label' => 'Address',
],
]);
}
}
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)类。
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',
],
]);
}
}
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类没有一个类时,此操作才能完成,因此您可以覆盖这些设置。
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,
],
],
]);
}
}
class LocationForm extends AbstractForm
{
public function init()
{
$this->add([
'name' => 'location',
'type' => LocationFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
parent::init();
}
}
现在,我们拥有制作表格的一切。我们仍然需要验证。让我们现在创建它们:
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,
],
],
],
]);
}
}
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,
],
],
],
]
);
}
}
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。如下所示。
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;
}
}
class AddressFieldsetFactory extends AbstractFieldsetFactory
{
public function __construct()
{
parent::__construct(AddressFieldset::class, 'address', Address::class);
}
}
class LocationFieldsetFactory extends AbstractFieldsetFactory
{
public function __construct()
{
parent::__construct(LocationFieldset::class, 'location', Location::class);
}
}
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);
}
}
class AddressFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
parent::setupRequirements($container, Address::class);
return new AddressFieldsetInputFilter($this->hydrator);
}
}
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类相同的抽象工厂类。
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
);
}
}
所以,所有的类都完成了。这是一个完整的设置。在我修改自己的代码以删除吸气剂/设置器,代码注释/提示,错误,属性和变量检查而不进行测试时,您可能会遇到一些错误。但这应该有效;)
但是,我们快完成了。我们仍然需要:
配置很简单:
'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
):
就是这样。完全成熟,可重用的类。单一设置。最后在控制器的工厂中只有一行。
请注意:
如果您对它的工作方式还有其他疑问,请先阅读我所链接的回购协议中的文档,然后再问这个问题。还有更多可以为您提供更多帮助的信息,例如集合处理。