ZF3 FormElementManager: как получить ServiceManager

Я портирую код с ZF2 на ZF3.

В ZF2, когда я создаю форму через FormElementManager, я могу получить доступ к servicelocator с помощью метода init и настроить некоторые вещи, подобные этому:

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

Это удобно в очень больших приложениях. Фактически все мои формы наследуются от класса BaseForm.

В ZF3 это плохая практика и serviceLocator устарели.
Какой лучший способ получить тот же результат?
Один из способов — внедрить каждую форму в ControllerFactory или ServiceFactory с необходимыми материалами, но это очень утомительно.

Любая помощь приветствуется.

0

Решение

Во-первых, вы не должны иметь ServiceManager и / или его дочерних элементов (например, FormElementManager) в ваших объектах Form.

Используя шаблон Factory, вы должны создать полностью функциональные, автономные объекты Form, Fieldset и InputFilter.

Определенно, будет утомительная работа, как вы выразились, но вам нужно сделать ее только один раз.

Допустим, вы хотите создать местоположение. Местоположение состоит из name собственность и однонаправленный OneToOne Address ссылка. Это создает следующие потребности:

  • LocationForm (-InputFilter)
  • LocationFieldset (-InputFilter)
  • AddressFieldset (-InputFilter)
  • Конфиг для вышеупомянутого
  • Фабрика для каждого из 6 классов выше

В этом ответе я сведу все к минимуму и буду использовать классы и примеры из моих собственных репозиториев, так что полный код вы можете использовать Вот и для примера Вот.

После создания самих классов я покажу вам конфигурацию, необходимую для этого варианта использования, и фабрики, которые связывают все это вместе.


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 и добавляет кнопку отправки, если форма не имеет таковой. Это делается только в том случае, если у класса Form нет ни одного из них при вызове init, поэтому вы можете переопределить эти настройки.

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();
}
}

Да, это все сами классы. Вы видите, как они будут вложены вместе? Это создает повторно используемые компоненты, поэтому я сказал, что вам нужно будет сделать это только один раз. В следующий раз, когда вам понадобится Address или Location, вы просто должны загрузить AddressFieldset и установить InputFilter в Factory. Последнее, установка правильного InputFilter, осуществляется через Setter Injection the Factories. Показано ниже.


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
);
}
}

Итак, это все классы сделаны. Это полная настройка. Вы можете столкнуться с некоторыми ошибками, поскольку я изменил свой собственный код для удаления методов получения / установки, комментариев / подсказок кода, ошибок, проверки свойств и переменных без тестирования. Но это должно работать;)

Тем не менее, мы почти закончили. Нам все еще нужно:

  • конфиг
  • использование в контроллере
  • распечатать / использовать форму в представлении

Конфиг прост:

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

Типичное действие «Редактировать» будет выглядеть следующим образом (обратите внимание, что в этом случае используется EntityManager Doctrine в качестве гидратора):

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

И View Partial будет выглядеть следующим образом (на основе return в вышеуказанном действии):

form ($ form)?>


Итак, это все. Полноценные, многоразовые занятия. Одиночная настройка. И, в конце концов, всего одна строчка на фабрике для контроллера.

Пожалуйста, обратите внимание:

  • Form, Fieldset и InputFilter используют имя входа «address». Очень важно, чтобы они оставались неизменными, поскольку Zend использует магию, основываясь на именах, чтобы сопоставить Fieldset с InputFilter.

Если у вас есть еще вопросы о том, как это работает, пожалуйста, сначала прочтите документацию в репозитории, на которую я ссылался, прежде чем задавать вопрос ниже. Есть еще кое-что, что должно помочь вам больше, например, для обработки Коллекции.

1

Другие решения

Других решений пока нет …

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector