Вопрос по php, doctrine-orm, zend-framework2 – Как создать входы / элементы формы в ZF2

11

РЕДАКТИРОВАТЬ: мой основной вопрос теперь стал следующим: «Как мне каким-то чистым способом передать ServiceManager с менеджером сущностей доктрины в мои формы, элементы и входные классы?» Читайте дальше, чтобы увидеть полный пост.

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

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

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

Create a form class called registration form

Create separate 'element' classes for each element where each element will have an input specification

Since each element is a separate class from the form I can unit test each one separately.

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

Вот код на данный момент

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

}

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

It seemed like a good testability/re-use choice to make since I could re-use the elements separately across my application if need be.

I could unit test each Input generated by each element to make sure it correctly accepts/rejects input.

Это пример моего юнит-теста для элемента

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

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

Итак, в целом, вот мои вопросы:

Is this strategy of separating the form and elements a good one? Should I keep going this way? What are alternatives? ( I'm for breaking stuff up for the sake of testability ) I was going to test ONLY the form itself originally with a combination of ALL the inputs but it seemed like I'd be trying to do too much.

How do I resolve the issue I have above?

Should I be using the Form/Element/Input parts of Zend in some other way that I'm not seeing?

@DanielM Я посмотрел на документы, которые вы упомянули, но вопрос о том, "как я могу тестировать каждый вход отдельно?" остается. С помощью метода аннотаций я бы получил полностью реализованную форму, верно? Так я не буду вынужден тестировать, используя КАЖДУЮ комбинацию ввода? Jerry Saravia
Вы уже проверилиdocs о формах? Я особенно рекомендую использовать аннотации, так как, по моему мнению, это самый быстрый способ создания форм. Daniel M
Если вы создаете пользовательский класс валидатора, вы, конечно, должны также добавить для него контрольные примеры, да. Но я не понимаю твоей проблемы. Daniel M
@DanielM Я собирался следовать по маршруту без тестирования, но я хотел убедиться, что мой пользовательский валидатор был создан / использован должным образом, но он не сработал так хорошо, потому что валидаторы не получают общего менеджера сервисов. Это была еще одна причина для тестирования тоже. Полагаю, это больше как интеграционный тест. Jerry Saravia
Ну, я их не проверяю. Я не думаю, что здесь требуется тестирование, потому что тестируются сами классы элементов, тестируются валидаторы и входные фильтры. Конечно, если вы создаете новый класс Element для специальной цели, которая не соответствует существующим элементам, вы должны создать новые тестовые случаи. В противном случае это нормально. И да, формы, созданные с использованием аннотаций, могут отображаться полностью без какой-либо работы, кромеecho $this->formCollection($myForm); внутри представления, если вам не нужно настраивать визуализацию. Daniel M

Ваш Ответ

1   ответ
8

использующий статический метод для внедрения entityManager и работающий с любой сущностью доктрины.

<?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;
    }
}
Итак, когда вы устанавливаете статический entityManager? Я думаю, что это может быть путь, если я не планирую изменять сам ZendF2. Jerry Saravia
в приложении я устанавливаю его в файле module.php модуля, который содержит валидатор, а в тестах - в методе настройки тестового класса.
Я собираюсь подождать, чтобы узнать, публикует ли кто-нибудь какие-либо другие попытки, но я думаю, что это хороший вариант. Спасибо вам за вашу работу. Jerry Saravia

Похожие вопросы