I created a Validator for one of my form fields. To do that, I need the ServiceLocator so I would like to use a factory ...
Edit :
Here is my factory :
namespace Maintenance\Factory\Validator;
/* Zend */
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/* Controller */
use Maintenance\Validator\Echeancedebut;
class EcheancedebutFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$realServiceLocator = $serviceLocator->getServiceLocator();
$maiContratService = $realServiceLocator->get(
'Maintenance\Service\Model\FMaiContratService'
);
return new Echeancedebut($maiContratService);
}
}
My Validator :
class Echeancedebut extends AbstractValidator
{
const ERROR_DATEDEB = 'ERROR_DATEDEB';
protected $maiContratService;
protected $messageTemplates = array(
self::ERROR_DATEDEB => "Saisie inférieure à la date de début du contrat"
);
public function __construct($maiContratService) {
$this->maiContratService = $maiContratService;
}
public function isValid($value){
$this->setValue($value);
if (!$this->validatedate($value)) {
$this->error(self::ERROR_DATEDEB);
return false;
}
return true;
}
private function validatedate($date) {
return false;
}
}
Edit :
My InputFilter:
public function getInputFilter()
{
if (! $this->inputFilter) {
$inputFilter = new InputFilter();
$this->inputFilter = $inputFilter;
}
$inputFilter->add(
array(
'name' => 'dateDeb',
'required' => true,
'allow_empty' => false,
'validators' => array(
array(
'name' => 'Date',
'locale' => 'FR_fr',
'options' => array(
'format' => 'd/m/Y',
),
),
array(
'name' => 'Maintenance\Validator\Echeancedebut',
'options' => array(
'contratId' => $this->iMaiContratId,
)
);
return $this->inputFilter;
}
The problem is that it doesn't retrieve my error message, why ?
The name of your factory and the name you registered in your config are not corresponding:
Maintenance\Factory\Validator\EcheancedebutFactory (inside your config)
Maintenance\Factory\Validator\EcheancedebFactory (the full class name)
Echeancedebut vs Echeancedeb.
Change that and I think it should work.
Okay so injecting Sl in a validator is a bad idea. I will pass my options as arguments via the inputFilter, called by my controller. So the problem is solved. Thanks for your answers.
Related
Can implement dynamic validation on element level? I used this example to implement validation of one element dependent on the value of the other element. But for this purpose I'll need to implement this for every single form where I use this element (comment) with this validation. I have many forms like that. Is there way to do the following:
to take this filter/validation logic to the element level using some kind of "data-comment-for" attribute and retrieving the value of the element on which it depends from the parent form.
This is my current code (but I need to have it for every form now. It does not look elegant at all) :
class CompetencyAdvanceHumanRightsAndJusticeFormFilter extends InputFilter
{
public function isValid($context = null)
{
$figradeCommentName = 'applJusticeFIGrade'.'Comment';
$forGrade = $this->get('applJusticeFIGrade');
$gradeComment = $this->get($figradeCommentName);
$applJusticeFIGradeRawValue = $forGrade->getRawValue('applJusticeFIGrade');
if(is_numeric($applJusticeFIGradeRawValue)){
$gradeValue = intval($applJusticeFIGradeRawValue);
}else{
$gradeValue = $applJusticeFIGradeRawValue;
}
if ($gradeValue != 'na' && $gradeValue > 0) {
$gradeComment->setRequired(true);
$validatorChain = new Validator\ValidatorChain();
$validatorChain->attach(
new Validator\NotEmpty(),
true
);
$gradeComment->setValidatorChain($validatorChain);
}
return parent::isValid($context);
}
public function __construct(){
$this->add(array(
'name' => 'id',
'required' => true,
'filters' => array(
array('name' => 'Int'),
),
));
$this->add(array(
'name' => 'studEvalId',
'required' => true,
'filters' => array(
array('name' => 'Int'),
),
));
}
}
EDIT:
I added code for the custom element to the question. There are some "leftovers" of my attempts to place this logic to the element level.
Comment Element
class Comment extends Element implements InputProviderInterface
{
/**
* #var ValidatorInterface
*/
protected $validator;
// set its type
protected $attributes = array(
'type' => 'comment'
);
public function init()
{
if (null === $this->validator) {
$validator = new StringLength();
$validator->setMax(10);
$validator->setMessage('The comment should not exceed 1000 letters!', StringLength::INVALID);
$this->validator = $validator;
}
}
/**
* Get a validator if none has been set.
*
* #return ValidatorInterface
*/
public function getValidator()
{
return $this->validator;
}
/**
* #param ValidatorInterface $validator
* #return $this
*/
public function setValidator(ValidatorInterface $validator)
{
$this->validator = $validator;
return $this;
}
/**
* remove require and validator defaults because we have none
*
* #return array
*/
public function getInputSpecification()
{
// return array(
// 'name' => $this->getName(),
// 'required' => false,
// 'validators' => array(
// $this->getValidator(),
// ),
// 'filters' => array(
// new FIGradeCommentDynamicBufferFilter()
// ),
// );
return array(
'name' => $this->getName(),
'required' => false,
'filters' => array(
array('name' => 'Zend\Filter\StringTrim'),
),
'validators' => array(
$this->getValidator(),
),
);
}
// tell it where to find its view helper, so formRow and the like work correctly
public function getViewHelperConfig()
{
return array('type' => '\OnlineFieldEvaluation\View\Helper\FormComment');
}
}
You could make a base abstract input-filter class and an interface and make all your form filters extend the base class that implements the interface with the methods you expect inside your form classes to make the thing work correctly.
Make an interface with the methods:
interface GradeCommentFormFilterInterface()
{
protected function getGradeInput();
protected function getCommentInput();
}
Then you move the common code to your base class:
abstract class BaseGradeCommentFormFilter extends InputFilter implements GradeCommentFormFilterInterface
{
protected function getGradeInput()
{
return $this->get(static::GRADE_NAME);
}
protected function getCommentInput()
{
return $this->get(static::GRADE_NAME . 'Comment');
}
public function isValid($context = null)
{
$gradeInput = $this->getGradeInput();
$commentInput = $this->getCommentInput();
$rawValue = $this->getRawValue($gradeInput);
if(is_numeric($rawValue))
{
$gradeValue = intval($rawValue);
}
else
$gradeValue = $rawValue;
if ($gradeValue != 'na' && $gradeValue > 0) {
$commentInput->setRequired(true);
$validatorChain = new Validator\ValidatorChain();
$validatorChain->attach(
new Validator\NotEmpty(),
true
);
$commentInput->setValidatorChain($validatorChain);
}
return parent::isValid($context);
}
}
Now you can use your abstract class like this:
class CompetencyAdvanceHumanRightsAndJusticeFormFilter extends BaseGradeCommentFormFilter
{
const GRADE_NAME = 'applJusticeFIGrade';
//... other code
}
I quickly tried to make it work for your case, but this isn't tested, and probably there are ways to optimize this, but it gives you an idea of what you can do.
i am using zendframework 2 and doctrine 2. i want to populate the values of my MultiCheckbox from values in my database .
i got the technique from: https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md
namespace Users\Form;
use Zend\Form\Form;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Doctrine\Common\Persistence\ObjectManager;
class addForm extends form implements ObjectManagerAwareInterface
{
protected $objectManager;
public function setObjectManager(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function getObjectManager()
{
return $this->objectManager;
}
public function __construct($name = null)
{
parent::__construct('add');
$this->setAttribute('method', 'post');
$this->setAttribute('enctype','multipart/formdata');
$this->add(array(
'type' => 'DoctrineModule\Form\Element\ObjectMultiCheckbox',
'name' => 'option',
'options' => array(
'label' => 'Options Véhicule',
'object_manager' => $this->getObjectManager(),
'target_class' => 'Users\Entity\optionsvehicule',
'property' => 'property'
, )));
the error message i received:
No object manager was set.
I have tried and found similar error. After some search I found solution posted on https://github.com/doctrine/DoctrineModule/issues/175. Which works.
For implement you need to do some changes like that
In Module.php add method getFormElementConfig :
public function getFormElementConfig()
{
return array(
'invokables' => array(
'addForm' => 'Users\Form\addForm',
),
'initializers' => array(
'ObjectManagerInitializer' => function ($element, $formElements) {
if ($element instanceof ObjectManagerAwareInterface) {
$services = $formElements->getServiceLocator();
$entityManager = $services->get('Doctrine\ORM\EntityManager');
$element->setObjectManager($entityManager);
}
},
),
);
}
In Your Form Class addForm.php, replace constructor with init method :
namespace Users\Form;
use Zend\Form\Form;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Doctrine\Common\Persistence\ObjectManager;
class addForm extends form implements ObjectManagerAwareInterface
{
protected $objectManager;
public function setObjectManager(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function getObjectManager()
{
return $this->objectManager;
}
//public function __construct($name = null)
public function init()
{
$this->setAttribute('method', 'post');
$this->setAttribute('enctype','multipart/formdata');
$this->add(array(
'type' => 'DoctrineModule\Form\Element\ObjectMultiCheckbox',
'name' => 'option',
'options' => array(
'label' => 'Options Véhicule',
'object_manager' => $this->getObjectManager(),
'target_class' => 'Users\Entity\optionsvehicule',
'property' => 'property'
, )));
In Your Controller Class, Call form obejct through Service Locator :
//$form = new addForm();
$forms = $this->getServiceLocator()->get('FormElementManager');
$form = $forms->get('addForm');
The $objectManager property is undefined.
This is because you call the $this->getObjectManager() method immediately within the __construct() and before you set the variable.
The form depends on the object manager; so you could just add it as a constructor argument which would ensure it is set before the class is used.
Also, the constructor should only really be used for setting up the object's initial properties and state, use init() for modifying form elements.
class addForm extends Form
{
protected $objectManager;
public function __construct(ObjectManager $objectManager)
{
parent::__construct('add-form');
$this->objectManager = $objectManager;
}
// The form element manager will call `init()`
// on the form so we can add the elements in this method
public function init() {
//....
$this->setAttribute('method', 'post');
$this->setAttribute('enctype','multipart/formdata');
// $this->add(....
// more elements added here
}
}
Last thing is to register a factory that actually does the injection
class Module {
public function getFormElementConfig() {
return array(
'factories' => array(
'ModuleName\Form\FooForm' => function($formElementManager) {
$serviceManager = $formElementManager->getServiceLocator();
$objectManager = $serviceManager->get('ObjectManager');
$form = new Form\FooForm($objectManager);
return $form;
},
),
);
}
}
I have problem with creating custom translator from database in ZF2. I have a DB like this
and files:
1)Application/module.config.php
'service_manager' => array(
'abstract_factories' => array(),
'factories' => array(
'translator' => function (\Zend\ServiceManager\ServiceManager $serviceManager)
{
$pluginManager = new \Zend\I18n\Translator\LoaderPluginManager();
$pluginManager->setServiceLocator($serviceManager);
$pluginManager->setFactory('DatabaseTranslationLoaderFactory', function($serviceManager)
{
$translator = new \Zend\I18n\Translator\DatabaseTranslationLoaderFactory();
return $translator->createService($serviceManager);
});
$translator = new \Zend\I18n\Translator\Translator(array());
$translator->setFallbackLocale('en_US');
$translator->setPluginManager($pluginManager);
$translator->addRemoteTranslations('DatabaseTranslationLoaderFactory');
return $translator;
},
),
),
'translator' => array(
'locale' => 'en_US',
'translation_file_patterns' => array(
array(
'type' => 'Zend\I18n\Translator\Loader\Database',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
),
),
),
2) Zend/I18n/Translator/Loader/Database.php
<?php
namespace Zend\I18n\Translator\Loader;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Sql\Sql;
use Zend\I18n\Translator\Plural\Rule as PluralRule;
use Zend\I18n\Translator\TextDomain;
class Database implements RemoteLoaderInterface {
protected $dbAdapter;
public $dbAdapter;
public function __construct(Adapter $dbAdapter = null)
{
if ($dbAdapter === null)
{
$configArray = array('driver' => 'Pdo_Mysql',
'database' => 'dbname',
'username' => 'username',
'password' => 'pswd',
'hostname' => 'localhost',
'charset' => 'utf-8',
);
$dbAdapter = new Adapter($configArray);
}
$this->dbAdapter = $dbAdapter;
}
public function load($locale, $textDomain)
{
$sql = new Sql($this->dbAdapter);
$select = $sql->select('ic_var')->columns(array('value'))
->where(array('language' => $locale, 'name' => $textDomain));
$messages = $this->dbAdapter->query(
$sql->getSqlStringForSqlObject($select),
Adapter::QUERY_MODE_EXECUTE
);
$textDomain = new TextDomain();
foreach ($messages as $message) {
if (isset($textDomain[$message['name']])) {
if (!is_array($textDomain[$message['name']])) {
$textDomain[$message['name']] = array(
$message['plural_index'] => $textDomain[$message['name']]
);
}
$textDomain[$message['name']][$message['plural_index']] = $message['value'];
} else {
$textDomain[$message['name']] = $message['value'];
}
}
return $textDomain;
}
}
3) Zend/I18n/Translator/DatabaseTranslationLoaderFactory.php
<?php
namespace Zend\I18n\Translator;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\I18n\Translator\Loader\Database;
class DatabaseTranslationLoaderFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new Database($serviceLocator->get('Zend\Db\Adapter\Adapter'));
}
}
4) Application/Module.php
public function onBootstrap(MvcEvent $e)
{
$translator = $e->getApplication()->getServiceManager()->get('translator');
$translator->addTranslationFile(
'DatabaseTranslationLoader',
'text-domain',
'text-domain'
);
}
But translation doesn`t work, because db adapter not find in loader:
Fatal error: Uncaught exception 'Zend\I18n\Exception\RuntimeException' with message 'Specified loader is not a file loader'
Thanks for your answers!
First of all you shouldn't define your custom classes in the Zend namespace as this is reserved a namespace for the ZF2 library and you don't want to touch (or add) files in the vendor directory.
Just put the custom classes in your own namespace outside the vendor folder. i.e. MyI18n
You can register you custom remote loader to the pluginManager in module.config.php.
return array(
'translator' => array(
'loaderpluginmanager' => array(
'factories' => array(
'database' => 'MyI18n\Translator\DatabaseTranslationLoaderFactory',
)
),
'remote_translation' => array(
array(
'type' => 'database' //This sets the database loader for the default textDomain
),
),
)
);
You don't have to overwrite the Translator factory if you want to add a custom loader, so just remove that code in your Module.php.
Als remove the configuration under translation_file_patterns as this is only needed for file loaders.
EDIT
For the above to work you need to overwrite the TranslatorServiceFactory because ZF has no support to register custom loaders on the plugin manager.
namespace MyNamespace\Translator;
use Zend\Mvc\Service\TranslatorServiceFactory as BaseTranslatorFactory;
class TranslatorServiceFactory extends BaseTranslatorFactory
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return MvcTranslator
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$translator = parent::createService($serviceLocator);
$config = $serviceLocator->get('Config');
$pluginManagerConfig = isset($config['translator']['loaderpluginmanager']) ? $config['translator']['loaderpluginmanager'] : array();
$pluginManager = new LoaderPluginManager(new Config($pluginManagerConfig));
$pluginManager->setServiceLocator($serviceLocator);
$translator->setPluginManager($pluginManager);
return $translator;
}
}
Now register your custom factory in the service configuration:
class Module
{
public function getServiceConfig()
{
return array(
'factories' => array(
'MvcTranslator' => 'MyNamespace\Translator\TranslatorServiceFactory',
)
)
}
}
I register custom remote loader to the pluginManager in module.config.php like this
'translator' => [
'loaderpluginmanager' => [
'factories' => [
'database' => function($lpm){
$sm = $lpm->getServiceLocator();
$loader = new Zf2Translation\Loader\DatabaseTranslationLoader($sm);
return $loader;
},
],
],
'remote_translation' => [
[
'type' => 'database',
],
],
]
Next in Database Loader class
use Zend\I18n\Translator\Loader\RemoteLoaderInterface;
class DatabaseTranslationLoader implements RemoteLoaderInterface
{
protected $dbAdapter;
protected $sm;
public function __construct(ServiceManager $sm)
{
$this->sm = $sm;
$this->dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
}
}
I hope it helps.
EDIT : My main question has now become 'How do I get the ServiceManager with the doctrine entity manager into the hands of my form, element, and input classes in some clean way?' Read on to see the full post.
I'm going to try and ask by example here so bear with me. Let me know where I'm going wrong/right or where I could improve
I'm trying to create a registration form. I could use ZfcUser module but I want to do this on my own. I'm using ZF2 with Doctrine2 as well so that leads me away from that module a bit.
My strategy was this,
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.
All seemed fine until I wanted to add a validator to my username element that would check that the username is NOT is use yet.
Here is the code thus far
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'
)
) );
}
}
I removed a lot that I think isn't necessary. Here is my username element below.
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.'
)
)
)
)
);
}
}
Now here is my validator
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;
}
}
Why did this seem like a good idea to me?
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.
This is the example of my unit test for the element
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;
}
}
}
The problem I'm having is that the validator can't fetch the UserHelper properly, which is really a UserRepository from doctrine. The reason this is happening is because the validators only get access to the ValidatorPluginManager as a ServiceManager rather than having access to the application wide ServiceManager.
I get this error for the Validator portion, although if I call the same get method on the general service manager it works with no problems.
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
The var_dump( $serviceManager ) in validator shows me it is of the class ValidatorPluginManager.
I tried putting a factory in the service_manager entry like so
'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;
}
)
but that didn't work because it's not consulting the application level service manager.
So, overall, here are my questions :
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?
this is my validator, using a static method to inject the entityManager and working with any doctine entity.
<?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;
}
}
ie. i'm using it for theese form elements which are also tested and working fine:
<?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.',
),
),
),
),
);
}
}
When creating a new user
<?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;
}
}
when editin an existing user
<?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;
}
}
Hey there, I'm trying to validate a form with Zend_Validate and Zend_Form.
My element:
$this->addElement('text', 'username', array(
'validators' => array(
array(
'validator' => 'Db_NoRecordExists',
'options' => array('user','username')
)
)
));
For I use Doctrine to handle my database, Zend_Validate misses a DbAdapter. I could pass an adapter in the options, but how do I combine Zend_Db_Adapter_Abstract and Doctrine?
Is there maybe an easyer way to get this done?
Thanks!
Solved it with an own Validator:
<?php
class Validator_NoRecordExists extends Zend_Validate_Abstract
{
private $_table;
private $_field;
const OK = '';
protected $_messageTemplates = array(
self::OK => "'%value%' ist bereits in der Datenbank"
);
public function __construct($table, $field) {
if(is_null(Doctrine::getTable($table)))
return null;
if(!Doctrine::getTable($table)->hasColumn($field))
return null;
$this->_table = Doctrine::getTable($table);
$this->_field = $field;
}
public function isValid($value)
{
$this->_setValue($value);
$funcName = 'findBy' . $this->_field;
if(count($this->_table->$funcName($value))>0) {
$this->_error();
return false;
}
return true;
}
}
Used like that:
$this->addElement('text', 'username', array(
'validators' => array(
array(
'validator' => new Validator_NoRecordExists('User','username')
)
)
));