EDIT: Мой главный вопрос теперь стал «Как я могу получить ServiceManager с менеджером сущностей доктрины в руки моей формы, элемента и классов ввода каким-то чистым способом?» Читайте дальше, чтобы увидеть полный пост.
Я собираюсь попробовать и попросить пример здесь, так что медведь со мной. Сообщите мне, где я ошибаюсь / прав, или где я мог бы улучшить
Я пытаюсь создать регистрационную форму. Я мог бы использовать модуль ZfcUser, но я хочу сделать это самостоятельно. Я также использую ZF2 с Doctrine2, чтобы немного отвести меня от этого модуля.
Моя стратегия заключалась в том,
Создать класс формы, называемый регистрационной формой
Создайте отдельные классы «element» для каждого элемента, где каждый элемент будет иметь спецификацию ввода
Поскольку каждый элемент представляет собой отдельный класс из формы, я могу отдельно тестировать каждый.
Все выглядело прекрасно, пока я не захотел добавить валидатор к элементу имени пользователя, который будет проверять, что имя пользователя НЕ используется.
Вот код до сих пор
namepsace My\Form; use Zend\Form\Form, Zend\Form\Element, Zend\InputFilter\Input, Zend\InputFilter\InputFilter, /** * Class name : Registration */ class Registration extends Form { const USERNAME = 'username'; const EMAIL = 'email'; const PASSWORD = 'password'; const PASS_CONFIRM = 'passwordConfirm'; const GENDER = 'gender'; const CAPTCHA = 'captcha'; const CSRF = 'csrf'; const SUBMIT = 'submit'; private $captcha = 'dumb'; public function prepareForm() { $this->setName( 'registration' ); $this->setAttributes( array( 'method' => 'post' ) ); $this->add( array( 'name' => self::USERNAME, 'type' => '\My\Form\Element\UsernameElement', 'attributes' => array( 'label' => 'Username', 'autofocus' => 'autofocus' ) ) ); $this->add( array( 'name' => self::SUBMIT, 'type' => '\Zend\Form\Element\Submit', 'attributes' => array( 'value' => 'Submit' ) ) ); } }
Я удалил много, что, я думаю, не нужно. Ниже приведено мое имя пользователя.
namespace My\Form\Registration; use My\Validator\UsernameNotInUse; use Zend\Form\Element\Text, Zend\InputFilter\InputProviderInterface, Zend\Validator\StringLength, Zend\Validator\NotEmpty, Zend\I18n\Validator\Alnum; /** * */ class UsernameElement extends Text implements InputProviderInterface { private $minLength = 3; private $maxLength = 128; public function getInputSpecification() { return array( 'name' => $this->getName(), 'required' => true, 'filters' => array( array( 'name' => 'StringTrim' ) ), 'validators' => array( new NotEmpty( array( 'mesages' => array( NotEmpty::IS_EMPTY => 'The username you provided is blank.' ) ) ), new AlNum( array( 'messages' => array( Alnum::STRING_EMPTY => 'The username can only contain letters and numbers.' ) ) ), new StringLength( array( 'min' => $this->getMinLength(), 'max' => $this->getMaxLength(), 'messages' => array( StringLength::TOO_LONG => 'The username is too long. It cannot be longer than ' . $this->getMaxLength() . ' characters.', StringLength::TOO_SHORT => 'The username is too short. It cannot be shorter than ' . $this->getMinLength() . ' characters.', StringLength::INVALID => 'The username is not valid.. It has to be between ' . $this->getMinLength() . ' and ' . $this->getMaxLength() . ' characters long.', ) ) ), array( 'name' => '\My\Validator\UsernameNotInUse', 'options' => array( 'messages' => array( UsernameNotInUse::ERROR_USERNAME_IN_USE => 'The usarname %value% is already being used by another user.' ) ) ) ) ); } }
,namespace My\Form\Registration; use My\Validator\UsernameNotInUse; use Zend\Form\Element\Text, Zend\InputFilter\InputProviderInterface, Zend\Validator\StringLength, Zend\Validator\NotEmpty, Zend\I18n\Validator\Alnum; /** * */ class UsernameElement extends Text implements InputProviderInterface { private $minLength = 3; private $maxLength = 128; public function getInputSpecification() { return array( 'name' => $this->getName(), 'required' => true, 'filters' => array( array( 'name' => 'StringTrim' ) ), 'validators' => array( new NotEmpty( array( 'mesages' => array( NotEmpty::IS_EMPTY => 'The username you provided is blank.' ) ) ), new AlNum( array( 'messages' => array( Alnum::STRING_EMPTY => 'The username can only contain letters and numbers.' ) ) ), new StringLength( array( 'min' => $this->getMinLength(), 'max' => $this->getMaxLength(), 'messages' => array( StringLength::TOO_LONG => 'The username is too long. It cannot be longer than ' . $this->getMaxLength() . ' characters.', StringLength::TOO_SHORT => 'The username is too short. It cannot be shorter than ' . $this->getMinLength() . ' characters.', StringLength::INVALID => 'The username is not valid.. It has to be between ' . $this->getMinLength() . ' and ' . $this->getMaxLength() . ' characters long.', ) ) ), array( 'name' => '\My\Validator\UsernameNotInUse', 'options' => array( 'messages' => array( UsernameNotInUse::ERROR_USERNAME_IN_USE => 'The usarname %value% is already being used by another user.' ) ) ) ) ); } }
Теперь вот мой валидатор
namespace My\Validator; use My\Entity\Helper\User as UserHelper, My\EntityRepository\User as UserRepository; use Zend\Validator\AbstractValidator, Zend\ServiceManager\ServiceManagerAwareInterface, Zend\ServiceManager\ServiceLocatorAwareInterface, Zend\ServiceManager\ServiceManager; /** * */ class UsernameNotInUse extends AbstractValidator implements ServiceManagerAwareInterface { const ERROR_USERNAME_IN_USE = 'usernameUsed'; private $serviceManager; /** * * @var UserHelper */ private $userHelper; protected $messageTemplates = array( UsernameNotInUse::ERROR_USERNAME_IN_USE => 'The username you specified is being used already.' ); public function isValid( $value ) { $inUse = $this->getUserHelper()->isUsernameInUse( $value ); if( $inUse ) { $this->error( UsernameNotInUse::ERROR_USERNAME_IN_USE, $value ); } return !$inUse; } public function setUserHelper( UserHelper $mapper ) { $this->userHelper = $mapper; return $this; } /** * @return My\EntityRepository\User */ public function getUserHelper() { if( $this->userHelper == null ) { $this->setUserHelper( $this->getServiceManager()->get( 'doctrine.entitymanager.orm_default' )->getObjectRepository( 'My\Entity\User') ); } return $this->userHelper; } public function setServiceManager( ServiceManager $serviceManager ) { echo get_class( $serviceManager ); echo var_dump( $serviceManager ); $this->serviceManager = $serviceManager; return $this; } /** * * @return ServiceManager */ public function getServiceManager( ) { return $this->serviceManager; } }
Почему это показалось мне хорошей идеей?
Это казалось хорошим выбором для проверки / повторного использования, поскольку я мог бы повторно использовать элементы отдельно в моем приложении, если это необходимо.
Я мог бы тестировать каждый вход, генерируемый каждым элементом, чтобы убедиться, что он правильно принимает / отклоняет ввод.
Это пример моего модульного теста для элемента
public function testFactoryCreation() { $fac = new Factory(); $element = $fac->createElement( array( 'type' => '\My\Form\Registration\UsernameElement' ) ); /* @var $element \My\Form\Registration\UsernameElement */ $this->assertInstanceOf( '\My\Form\Registration\UsernameElement', $element ); $input = $fac->getInputFilterFactory()->createInput( $element->getInputSpecification() ); $validators = $input->getValidatorChain()->getValidators(); /* @var $validators \Zend\Validator\ValidatorChain */ $expectedValidators = array( 'Zend\Validator\StringLength', 'Zend\Validator\NotEmpty', 'Zend\I18n\Validator\Alnum', 'My\Validator\UsernameNotInUse' ); foreach( $validators as $validator ) { $actualClass = get_class( $validator['instance'] ); $this->assertContains( $actualClass, $expectedValidators ); switch( $actualClass ) { case 'My\Validator\UsernameNotInUse': $helper = $validator['instance']->getUserHelper(); //HAVING A PROBLEM HERE $this->assertNotNull( $helper ); break; default: break; } } }
,public function testFactoryCreation() { $fac = new Factory(); $element = $fac->createElement( array( 'type' => '\My\Form\Registration\UsernameElement' ) ); /* @var $element \My\Form\Registration\UsernameElement */ $this->assertInstanceOf( '\My\Form\Registration\UsernameElement', $element ); $input = $fac->getInputFilterFactory()->createInput( $element->getInputSpecification() ); $validators = $input->getValidatorChain()->getValidators(); /* @var $validators \Zend\Validator\ValidatorChain */ $expectedValidators = array( 'Zend\Validator\StringLength', 'Zend\Validator\NotEmpty', 'Zend\I18n\Validator\Alnum', 'My\Validator\UsernameNotInUse' ); foreach( $validators as $validator ) { $actualClass = get_class( $validator['instance'] ); $this->assertContains( $actualClass, $expectedValidators ); switch( $actualClass ) { case 'My\Validator\UsernameNotInUse': $helper = $validator['instance']->getUserHelper(); //HAVING A PROBLEM HERE $this->assertNotNull( $helper ); break; default: break; } } }
Проблема, с которой я столкнулась, заключается в том, что валидатор не может правильно загрузить UserHelper, что действительно является UserRepository из доктрины. Причина, по которой это происходит, заключается в том, что валидаторы получают доступ к ValidatorPluginManager как ServiceManager, а не к доступу к средству ServiceManager приложения.
Я получаю эту ошибку для части Validator, хотя, если я вызываю тот же метод get в общем сервис-менеджере, он работает без проблем.
1) Test\My\Form\Registration\UsernameElementTest::testFactoryCreation Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for doctrine.entitymanager.orm_default
Var_dump ($ serviceManager) в валидаторе показывает, что это класс ValidatorPluginManager.
Я попробовал поставить фабрику в записи service_manager, например, так
'service_manager' => array( 'factories' => array( 'My\Validator\UsernameNotInUse' => function( $sm ) { $validator = new \My\Validator\UsernameNotInUse(); $em = $serviceManager->get( 'doctrine.entitymanager.orm_default' ); /* @var $em \Doctrine\ORM\EntityManager */ $validator->setUserHelper( $em->getRepository( '\My\Entity\User' ) ); return $validator; } )
но это не сработало, потому что он не консультируется с менеджером сервисов уровня приложений.
Итак, в целом, вот мои вопросы:
Является ли эта стратегия разделением формы и элементов хорошей? Должен ли я продолжать идти этим путем? Какие альтернативы? (Я за то, чтобы разбить вещи ради проверки). Я собирался проверить ТОЛЬКО сама форма изначально с комбинацией ВСЕХ входов, но казалось, что я попытаюсь сделать слишком много.
Как решить проблему, которая у меня выше?
Должен ли я использовать части формы / элемента / ввода Zend каким-то другим способом, который я не вижу?
это мой валидатор, используя статический метод для внедрения entityManager и работы с любым объектом doctine.
<?php namespace Base\Validator; use Traversable; use Zend\Stdlib\ArrayUtils; use Zend\Validator\AbstractValidator; use Doctrine\ORM\EntityManager; class EntityUnique extends AbstractValidator { const EXISTS = 'exists'; protected $messageTemplates = array( self::EXISTS => "A %entity% record already exists with %attribute% %value%", ); protected $messageVariables = array( 'entity' => '_entity', 'attribute' => '_attribute', ); protected $_entity; protected $_attribute; protected $_exclude; protected static $_entityManager; public static function setEntityManager(EntityManager $em) { self::$_entityManager = $em; } public function getEntityManager() { if (!self::$_entityManager) { throw new \Exception('No entitymanager present'); } return self::$_entityManager; } public function __construct($options = null) { if ($options instanceof Traversable) { $options = ArrayUtils::iteratorToArray($token); } if (is_array($options)) { if (array_key_exists('entity', $options)) { $this->_entity = $options['entity']; } if (array_key_exists('attribute', $options)) { $this->_attribute = $options['attribute']; } if (array_key_exists('exclude', $options)) { if (!is_array($options['exclude']) || !array_key_exists('attribute', $options['exclude']) || !array_key_exists('value', $options['exclude'])) { throw new \Exception('exclude option must contain attribute and value keys'); } $this->_exclude = $options['exclude']; } } parent::__construct(is_array($options) ? $options : null); } public function isValid($value, $context = null) { $this->setValue($value); $queryBuilder = $this->getEntityManager() ->createQueryBuilder() ->from($this->_entity, 'e') ->select('COUNT(e)') ->where('e.'. $this->_attribute . ' = :value') ->setParameter('value', $this->getValue()); if ($this->_exclude) { $queryBuilder = $queryBuilder->andWhere('e.'. $this->_exclude['attribute'] . ' != :exclude') ->setParameter('exclude', $this->_exclude['value']); } $query = $queryBuilder->getQuery(); if ((integer)$query->getSingleScalarResult() !== 0) { $this->error(self::EXISTS); return false; } return true; } }
то есть. я использую его для этих элементов формы, которые также проверены и работают нормально:
<?php namespace User\Form\Element; use Zend\Form\Element\Text; use Zend\InputFilter\InputProviderInterface; class Username extends Text implements InputProviderInterface { public function __construct() { parent::__construct('username'); $this->setLabel('Benutzername'); $this->setAttribute('id', 'username'); } public function getInputSpecification() { return array( 'name' => $this->getName(), 'required' => true, 'filters' => array( array( 'name' => 'StringTrim' ), ), 'validators' => array( array( 'name' => 'NotEmpty', 'break_chain_on_failure' => true, 'options' => array( 'messages' => array( 'isEmpty' => 'Bitte geben Sie einen Benutzernamen ein.', ), ), ), ), ); } }
Авторизация<?php namespace User\Form\Element; use Zend\Form\Element\Text; use Zend\InputFilter\InputProviderInterface; class Username extends Text implements InputProviderInterface { public function __construct() { parent::__construct('username'); $this->setLabel('Benutzername'); $this->setAttribute('id', 'username'); } public function getInputSpecification() { return array( 'name' => $this->getName(), 'required' => true, 'filters' => array( array( 'name' => 'StringTrim' ), ), 'validators' => array( array( 'name' => 'NotEmpty', 'break_chain_on_failure' => true, 'options' => array( 'messages' => array( 'isEmpty' => 'Bitte geben Sie einen Benutzernamen ein.', ), ), ), ), ); } }
При создании нового пользователя
<?php namespace User\Form\Element; use Zend\InputFilter\InputProviderInterface; use User\Form\Element\Username; class CreateUsername extends Username implements InputProviderInterface { public function getInputSpecification() { $spec = parent::getInputSpecification(); $spec['validators'][] = array( 'name' => 'Base\Validator\EntityUnique', 'options' => array( 'message' => 'Der name %value% ist bereits vergeben.', 'entity' => 'User\Entity\User', 'attribute' => 'username', ), ); return $spec; } }
при редактировании существующего пользователя
<?php namespace User\Form\Element; use Zend\InputFilter\InputProviderInterface; use User\Form\Element\Username; class EditUsername extends Username implements InputProviderInterface { protected $_userId; public function __construct($userId) { parent::__construct(); $this->_userId = (integer)$userId; } public function getInputSpecification() { $spec = parent::getInputSpecification(); $spec['validators'][] = array( 'name' => 'Base\Validator\EntityUnique', 'options' => array( 'message' => 'Der name %value% ist bereits vergeben.', 'entity' => 'User\Entity\User', 'attribute' => 'username', 'exclude' => array( 'attribute' => 'id', 'value' => $this->_userId, ), ), ); return $spec; } }