Как создать входы/элементы формы в ZF2

EDIT: теперь мой главный вопрос: "Как я могу получить ServiceManager с менеджером сущностей доктрины в руки моего класса, элемента и классов ввода каким-то чистым способом?" Читайте дальше, чтобы увидеть полную запись.

Я собираюсь попробовать и попросить пример здесь, так что медведь со мной. Сообщите мне, где я ошибаюсь/прав, или где я мог бы улучшить

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

Моя стратегия была такой,

  • Создайте класс формы, называемый регистрационной формой

  • Создайте отдельные классы элементов для каждого элемента, где каждый элемент будет иметь спецификацию ввода

  • Поскольку каждый элемент является отдельным классом из формы, я могу unit test каждый отдельно.

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

Вот код до сих пор

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

}

Почему это показалось мне хорошей идеей?

  • Это казалось хорошим выбором для проверки/повторного использования, поскольку я мог бы повторно использовать элементы отдельно в моем приложении, если это необходимо.

  • Я мог unit test каждый ввод, сгенерированный каждым элементом, чтобы убедиться, что он правильно принимает/отклоняет ввод.

Это пример моего unit test для элемента

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.

Я попытался помещать factory в запись 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 каким-то другим способом, который я не вижу?

Ответ 1

это мой валидатор, используя статический метод для ввода 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\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;
    }
}