EDIT:我的主要问题现在变成了"我如何以某种干净的方式将带有条令实体管理器的ServiceManager交到我的表单、元素和输入类手中?"继续阅读以查看完整的帖子。
我将在这里尝试并举例说明,所以请耐心等待。让我知道我哪里错了/对了,或者我可以在哪里改进
我正在尝试创建一个注册表。我可以使用ZfcUser模块,但我想自己做。我也在Doctrine2中使用ZF2,所以这让我有点偏离了那个模块。
我的策略是,
-
创建一个名为注册表单的表单类
-
为每个元素创建单独的"元素"类,其中每个元素都有一个输入规范
-
由于每个元素都是表单中的一个独立类,因此我可以单独对每个元素进行单元测试。
一切似乎都很好,直到我想在我的username元素中添加一个验证器来检查用户名是否尚未使用。
这是迄今为止的代码
namepsace MyForm;
use ZendFormForm,
ZendFormElement,
ZendInputFilterInput,
ZendInputFilterInputFilter,
/**
* 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' => 'MyFormElementUsernameElement',
'attributes' => array(
'label' => 'Username',
'autofocus' => 'autofocus'
)
)
);
$this->add( array(
'name' => self::SUBMIT,
'type' => 'ZendFormElementSubmit',
'attributes' => array(
'value' => 'Submit'
)
) );
}
}
我去掉了很多我认为没有必要的东西。下面是我的用户名元素。
namespace MyFormRegistration;
use MyValidatorUsernameNotInUse;
use ZendFormElementText,
ZendInputFilterInputProviderInterface,
ZendValidatorStringLength,
ZendValidatorNotEmpty,
ZendI18nValidatorAlnum;
/**
*
*/
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' => 'MyValidatorUsernameNotInUse',
'options' => array(
'messages' => array(
UsernameNotInUse::ERROR_USERNAME_IN_USE => 'The usarname %value% is already being used by another user.'
)
)
)
)
);
}
}
现在这是我的验证器
namespace MyValidator;
use MyEntityHelperUser as UserHelper,
MyEntityRepositoryUser as UserRepository;
use ZendValidatorAbstractValidator,
ZendServiceManagerServiceManagerAwareInterface,
ZendServiceManagerServiceLocatorAwareInterface,
ZendServiceManagerServiceManager;
/**
*
*/
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 MyEntityRepositoryUser
*/
public function getUserHelper()
{
if( $this->userHelper == null )
{
$this->setUserHelper( $this->getServiceManager()->get( 'doctrine.entitymanager.orm_default' )->getObjectRepository( 'MyEntityUser') );
}
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;
}
}
为什么这对我来说是个好主意?
这似乎是一个很好的可测试性/重用选择,因为如果需要的话,我可以在整个应用程序中单独重用这些元素
我可以对每个元素生成的每个Input进行单元测试,以确保它正确地接受/拒绝输入。
这是我对元件进行单元测试的例子
public function testFactoryCreation()
{
$fac = new Factory();
$element = $fac->createElement( array(
'type' => 'MyFormRegistrationUsernameElement'
) );
/* @var $element MyFormRegistrationUsernameElement */
$this->assertInstanceOf( 'MyFormRegistrationUsernameElement',
$element );
$input = $fac->getInputFilterFactory()->createInput( $element->getInputSpecification() );
$validators = $input->getValidatorChain()->getValidators();
/* @var $validators ZendValidatorValidatorChain */
$expectedValidators = array(
'ZendValidatorStringLength',
'ZendValidatorNotEmpty',
'ZendI18nValidatorAlnum',
'MyValidatorUsernameNotInUse'
);
foreach( $validators as $validator )
{
$actualClass = get_class( $validator['instance'] );
$this->assertContains( $actualClass, $expectedValidators );
switch( $actualClass )
{
case 'MyValidatorUsernameNotInUse':
$helper = $validator['instance']->getUserHelper();
//HAVING A PROBLEM HERE
$this->assertNotNull( $helper );
break;
default:
break;
}
}
}
我遇到的问题是验证器无法正确获取UserHelper,这实际上是一个来自条令的UserRepository。发生这种情况的原因是,验证器只能作为ServiceManager访问ValidatorPluginManager,而不能访问应用程序范围的ServiceManager。
我在Validator部分得到了这个错误,尽管如果我在通用服务管理器上调用相同的get方法,它不会有任何问题。
1) TestMyFormRegistrationUsernameElementTest::testFactoryCreation
ZendServiceManagerExceptionServiceNotFoundException: ZendServiceManagerServiceManager::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(
'MyValidatorUsernameNotInUse' => function( $sm )
{
$validator = new MyValidatorUsernameNotInUse();
$em = $serviceManager->get( 'doctrine.entitymanager.orm_default' );
/* @var $em DoctrineORMEntityManager */
$validator->setUserHelper( $em->getRepository( 'MyEntityUser' ) );
return $validator;
}
)
但这并没有奏效,因为它没有咨询应用程序级服务经理。
所以,总的来说,以下是我的问题:
这种分离形式和元素的策略好吗?我应该继续走这条路吗?什么是替代方案?(为了可测试性,我赞成把东西分解)我本来只打算用所有输入的组合来测试表单本身,但似乎我做得太多了。
如何解决上述问题?
我应该以我没有看到的其他方式使用Zend的Form/Element/Input部分吗?
这是我的验证器,使用静态方法注入entityManager并处理任何doctine实体。
<?php
namespace BaseValidator;
use Traversable;
use ZendStdlibArrayUtils;
use ZendValidatorAbstractValidator;
use DoctrineORMEntityManager;
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 UserFormElement;
use ZendFormElementText;
use ZendInputFilterInputProviderInterface;
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 UserFormElement;
use ZendInputFilterInputProviderInterface;
use UserFormElementUsername;
class CreateUsername extends Username implements InputProviderInterface
{
public function getInputSpecification() {
$spec = parent::getInputSpecification();
$spec['validators'][] = array(
'name' => 'BaseValidatorEntityUnique',
'options' => array(
'message' => 'Der name %value% ist bereits vergeben.',
'entity' => 'UserEntityUser',
'attribute' => 'username',
),
);
return $spec;
}
}
编辑现有用户时
<?php
namespace UserFormElement;
use ZendInputFilterInputProviderInterface;
use UserFormElementUsername;
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' => 'BaseValidatorEntityUnique',
'options' => array(
'message' => 'Der name %value% ist bereits vergeben.',
'entity' => 'UserEntityUser',
'attribute' => 'username',
'exclude' => array(
'attribute' => 'id',
'value' => $this->_userId,
),
),
);
return $spec;
}
}