如何在ZF2中创建表单输入/元素


How to create form inputs/elements in ZF2

EDIT:我的主要问题现在变成了"我如何以某种干净的方式将带有条令实体管理器的ServiceManager交到我的表单、元素和输入类手中?"继续阅读以查看完整的帖子。

我将在这里尝试并举例说明,所以请耐心等待。让我知道我哪里错了/对了,或者我可以在哪里改进

我正在尝试创建一个注册表。我可以使用ZfcUser模块,但我想自己做。我也在Doctrine2中使用ZF2,所以这让我有点偏离了那个模块。

我的策略是,

  1. 创建一个名为注册表单的表单类

  2. 为每个元素创建单独的"元素"类,其中每个元素都有一个输入规范

  3. 由于每个元素都是表单中的一个独立类,因此我可以单独对每个元素进行单元测试。

一切似乎都很好,直到我想在我的username元素中添加一个验证器来检查用户名是否尚未使用。

这是迄今为止的代码

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

为什么这对我来说是个好主意?

  1. 这似乎是一个很好的可测试性/重用选择,因为如果需要的话,我可以在整个应用程序中单独重用这些元素

  2. 我可以对每个元素生成的每个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。发生这种情况的原因是,验证器只能作为ServiceManager访问ValidatorPluginManager,而不能访问应用程序范围的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;
                    }
                )

但这并没有奏效,因为它没有咨询应用程序级服务经理。

所以,总的来说,以下是我的问题:

  1. 这种分离形式和元素的策略好吗?我应该继续走这条路吗?什么是替代方案?(为了可测试性,我赞成把东西分解)我本来只打算用所有输入的组合来测试表单本身,但似乎我做得太多了。

  2. 如何解决上述问题?

  3. 我应该以我没有看到的其他方式使用Zend的Form/Element/Input部分吗?

这是我的验证器,使用静态方法注入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;
    }
}