ZendFramework 2 - Problems with AbstractFactoryInterface; Param doesn't contain route/controller Namespace - zend-framework2

I've got a module with one route and an abstract controller factory.
The route
namespace BaseApi;
use BaseApi\Factory\Controller\AbstractApiControllerFactory;
use Zend\Mvc\Router\Http\Segment;
return [
'router' => [
'routes' => [
'v2a-api' => [
'type' => Segment::class,
'options' => [
'route' => '/api/:controller',
'defaults' => [
'__NAMESPACE__' => 'BaseApi\Controller',
],
],
],
],
],
'controllers' => [
'abstract_factories' => [
AbstractApiControllerFactory::class,
],
],
];
The abstract controller factory
namespace BaseApi\Factory\Controller;
use BaseApi\Controller\ApiController;
use BaseApi\Exception\ApiAbstractControllerFactoryException;
use Zend\Filter\Inflector;
use Zend\Filter\StringToLower;
use Zend\Filter\Word\CamelCaseToDash;
use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* Class AbstractApiControllerFactory
*
* #package BaseApi\Factory\Controller
*/
class AbstractApiControllerFactory implements AbstractFactoryInterface
{
/**
* #param \Zend\ServiceManager\ServiceLocatorInterface $controllerManager
* #param $name
* #param $requestedName
*
* #return bool
*/
public function canCreateServiceWithName(ServiceLocatorInterface $controllerManager, $name, $requestedName)
{
dump('api', $name, $requestedName);
$serviceManager = $controllerManager->getServiceLocator();
$configKey = $this->getConfigKey($requestedName);
$config = $serviceManager->get('Config');
return (
strpos($requestedName, 'BaseApi\Controller') === 0
&& key_exists(
$configKey,
!empty($config['d3-api']) ? $config['d3-api'] : []
)
);
}
/**
* #param \Zend\ServiceManager\ServiceLocatorInterface $controllerManager
* #param $name
* #param $requestedName
*
* #return \BaseApi\Controller\ApiController
* #throws \BaseApi\Exception\ApiAbstractControllerFactoryException
*/
public function createServiceWithName(ServiceLocatorInterface $controllerManager, $name, $requestedName)
{
$serviceManager = $controllerManager->getServiceLocator();
$configKey = $this->getConfigKey($requestedName);
$config = $serviceManager->get('Config');
if (empty($config['d3-api'][$configKey]['service'])) {
throw ApiAbstractControllerFactoryException::missingServiceKey($configKey);
}
if (!$serviceManager->has($config['d3-api'][$configKey]['service'])) {
throw ApiAbstractControllerFactoryException::missingServiceException(
$config['d3-api'][$configKey]['service']
);
}
return new ApiController(
$serviceManager->get($config['d3-api'][$configKey]['service'])
);
}
/**
* Transforms Controller-Name + Namespace $requestedName in the related Config-Key
* :: BaseApi\Controller\GameLists => game-lists
*
* #param $requestedName
*
* #return string
*/
protected function getConfigKey($requestedName)
{
$controllerName = substr($requestedName, strrpos($requestedName, '\\') + 1);
$inflector = new Inflector(':classname');
$inflector->setFilterRule(
':classname',
[
CamelCaseToDash::class,
StringToLower::class,
]
);
return $inflector->filter(['classname' => $controllerName]);
}
}
Additional config (doesn't matter for this problem, only for completeness sake)
<?php
return [
'd3-api' => [
'customers' => [
'service' => 'V2aCustomerDb\Service\Customers',
],
],
];
Now my problem: I've got 2 ZF-MVC-Apps. In one, everything works fine; I see my objects when I start a request under /api/customers. In the other one, the parameter $requestedName of AbstractApiControllerFactory::canCreateServiceWithName doesn't contain the namespace. I only receive customers instead of BaseApi\Controller\customers
The project-setup is the same. Everything installed over composer has the same version. So it has to be a config-bug, but after several hours, I've got no clue, what it might be...
someone any suggestions?

After two days...
Module.php has to look like this (add BootstrapListenerInterface)
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* #link http://github.com/zendframework/ZendSkeletonApplication for the canonical source repository
* #copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
* #license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace CustomerDatabase;
use Zend\EventManager\EventInterface;
use Zend\ModuleManager\Feature\BootstrapListenerInterface;
use Zend\Mvc\ModuleRouteListener;
/**
* Class Module
*
* #package CustomerDatabase
*/
class Module implements BootstrapListenerInterface
{
public function onBootstrap(EventInterface $event)
{
$eventManager = $event->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
}
[...]
}

Related

Symfony form eventlistener string error

Entity\Profile.php
class Profile
{
...
/**
* #var string
*
* #ORM\Column(name="country", type="string", length=100, nullable=true)
*/
private $country;
/**
* #var string
*
* #ORM\Column(name="province", type="string", length=100, nullable=true)
*/
private $province;
...
}
MyProfileTypeForm.php:
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
->add('country', CountryType::class, array(
'label' => 'form.profile.country',
'preferred_choices' => array(
'US'
)
))
...
$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) {
$form = $event->getForm();
$country = $form->get('country')->getData();
$form->add('province', EntityType::class, array(
'class' => 'UserBundle:LocationProvince',
'choice_label' => 'name',
'choice_value' => 'id',
'query_builder' => function (EntityRepository $er) use ($country) {
return $er
->createQueryBuilder('l')
->where('l.countryCode = :cc')
->setParameter(':cc', $country);
},
'label' => 'form.profile.province',
));
});
}
Error Code:
An exception occurred while executing 'UPDATE profile SET province = ? WHERE id = ?' with params [{}, 1]:
Catchable Fatal Error: Object of class Panel\UserBundle\Entity\LocationProvince could not be converted to string
Description:
The Entity of getting the country code. Provincial list drawn by the country code. But it does not record.
in your province choice drop-down list you've specified:
'choice_value' => 'id',
But I believe "id" is an integer.
You probably need to change this to:
'choice_value' => 'province',
Try that - I think it should work.

ZF2 + Doctrine2 Annotation Form Required & AllowEmpty

I use Annotation in a Doctrine Entity Class. Annotation for a field are :
/**
* #var integer
*
* #ORM\Column(name="duree", type="integer", nullable=true)
*
* #Form\Type("Zend\Form\Element\Number")
* #Form\Attributes({"required":false, "placeholder":"Durée", "min":"1", "max":"20"})
* #Form\Required(false)
* #Form\AllowEmpty()
* #Form\Options({"label":"Durée :"})
* #Form\Filter({"name": "Int"})
* #Form\Validator({"name":"IsInt"})
*/
private $duree;
So the DB column can be Empty (nullable), and in the form i wan't the same (ie user can leave input empty). I've both annotation Required(false) and allowEmpty, but the form never valid (always got isEmpty for this field).
If i set #Form\Type to "Text", it is working fine (form is valid event if input is empty). But with the class Number, it'is not the same.
I've the same pb with a Select element (correspondinf to a relationship). Annotations are :
/**
* #var \Application\Entity\CcCategorie
*
* #ORM\ManyToOne(targetEntity="Application\Entity\CcCategorie")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="categorie", referencedColumnName="id", nullable=true, onDelete="SET NULL")
* })
*
* #Form\Type("DoctrineModule\Form\Element\ObjectSelect")
* #Form\Attributes({"type":"select", "required":false})
* #Form\Options({"label":"Catégorie :"})
* #Form\Required(false)
* #Form\AllowEmpty()
* #Form\Options({
* "label":"Catégorie :",
* "empty_option": "---",
* "target_class": "Application\Entity\CcCategorie",
* "property": "label"
* })
*/
private $categorie;
But, with this the field has error (isEmpty) if the Select is set to empty option when validating Form.
The only workaround i've found is to set the annotation
* #Form\Type("\Application\Form\Entity\CcRepriseFieldset")
at the top of the entity class. The class CcRepriseFieldset extend Fieldset, and implement InputFilterProviderInterface. Then i specify the function in this class :
public function getInputFilterSpecification()
{
return array(
array(
"name" => "duree",
'required' => false,
'allow_empty' => true,
),
array(
"name" => "categorie",
'required' => false,
'allow_empty' => true,
),
);
}
With this it works... But it's not annotations.
I don't understand why annotation not work
thanks
Ok i found what is the probleme.
Annotation Builder made a array with all fields spec, like this :
array (size=7)
'name' => string 'reprise' (length=7)
'attributes' => array (size=0)
'elements' =>
array (size=8)
1 =>array (size=2)
...
'fieldsets' => array (size=0) empty
'type' => string '\Application\Form\Entity\CcRepriseFieldset'
(length=42)
'input_filter' =>
array (size=8)
'name' =>
array (size=4)
'name' => string 'name' (length=4)
'required' => boolean true
'filters' =>
array (size=3)
...
'validators' =>
array (size=2)
...
But this array is the Array for the FieldSet attached to the Entity.
So the Zend\Form\Factory never parse this because 'input_filter' are only parse for ZendForm Element not for fieldset (of course because Fieldset doesn't have SetInputFilter method)...
Ok i've a workaround but i'm not very statisfied of it .
First i've create a new FormFactory :
namespace Application\Form;
use Zend\Form\FieldsetInterface;
Class EntityFormFactory extends \Zend\Form\Factory {
public function configureFieldset(FieldsetInterface $fieldset, $spec)
{
$fieldset=parent::configureFieldset($fieldset, $spec);
$fieldset->input_filter_factory= $spec['input_filter'];
return $fieldset;
}
}
So this factory add the "input_filter" to a input_filter_factory variable of the fieldset.
Then in the fieldset class is :
class CcRepriseFieldset extends Fieldset implements \Zend\InputFilter\InputFilterProviderInterface
{
public function getInputFilterSpecification()
{
if (isset($this->input_filter_factory)){
return $this->input_filter_factory;
}
}
}
And finally, when i use annotationbuilder i change the formfactory :
$builder = new AnnotationBuilder($this->_em);
$builder->setFormFactory(new EntityFormFactory());
With this all is working fine... I'm not sur it's the best way to do it.

symfony 3: trying to transform the value of field using DataTransform

I have added this model transformer following the docs:
->add('subtotal', MoneyType::class, array(
'attr' => array('readonly' => true),
))
->addModelTransformer(new CallbackTransformer(
// transform <br/> to \n so the textarea reads easier
function ($originalDescription) {
return $originalDescription;
},
function ($submittedDescription) {
var_dump($submittedDescription); //<<<<<<<<<<<<
// remove most HTML tags (but not br,p)
$cleaned = strip_tags($submittedDescription, '<br><br/><p>');
// transform any \n to real <br/>
return str_replace("\n", '<br/>', $cleaned);
}
))
The problem: the var_dump() you can see is outputting the whole entity as you can see below. I expected just the content of subtotal submitted field.
object(DefaultBundle\Entity\Bill)[682]
protected 'id' => null
private 'client' =>
object(Proxies\__CG__\DefaultBundle\Entity\Client)[788]
public '__initializer__' => null
public '__cloner__' => null
public '__isInitialized__' => boolean true
protected 'id' => int 1
private 'bills' (DefaultBundle\Entity\Client) =>
object(Doctrine\ORM\PersistentCollection)[436]
private 'snapshot' =>
array (size=0)
...
private 'owner' =>
&object(Proxies\__CG__\DefaultBundle\Entity\Client)[788]
private 'association' =>
array (size=15)
...
private 'em' =>
object(Doctrine\ORM\EntityManager)[151]
...
private 'backRefFieldName' => string 'client' (length=6)
private 'typeClass' =>
object(Doctrine\ORM\Mapping\ClassMetadata)[465]
...
private 'isDirty' => boolean false
protected 'collection' =>
object(Doctrine\Common\Collections\ArrayCollection)[801]
...
protected 'initialized' => boolean false
protected 'name' => string 'Jose Manuel Fernandez Fernandez' (length=31)
protected 'nif' => string '03113434P' (length=9)
protected 'address' => string 'Bulevar' (length=39)
protected 'phone' => string '633553423' (length=9)
protected 'email' => string 'me#gmail.com' (length=21)
protected 'numberPlate' => string 'fasdfdasf' (length=9)
protected 'createdAt' =>
object(DateTime)[453]
public 'date' => string '2016-02-28 00:00:00.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Madrid' (length=13)
protected 'serialNumber' => string '5' (length=1)
private 'servicesPerformed' =>
array (size=1)
0 =>
array (size=4)
'description' => string 'fasdf' (length=5)
'quantity' => string '4234' (length=4)
'unitPrice' => float 4234
'price' => float 17926756
protected 'subtotal' => null
protected 'taxRate' => float 0.21
protected 'tax' => float 3764618.76
protected 'total' => float 21691374.76
Here you have the entity and the form type:
class BillType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$em = $options['em'];
$builder
->add('serialNumber', TextType::class, array(
'label' => 'Nº serie',
'attr' => array('readonly' => true),
))
->add('createdAt', DateType::class, array(
'label' => 'Fecha',
'data' => new \DateTime("today"),
))
->add('client', null, array('label' => 'Cliente'))
->add('numberPlate', null, array(
'label' => 'Número matrícula',
//'data' => 'prueba'
))
->add('servicesPerformed', CollectionType::class, array(
'label' => false,
'entry_type' => ServicePerformedType::class,
'allow_add' => true,
'allow_delete' => true,
'attr' => array('data-prototype' => 'jander'),
))
->add('subtotal', MoneyType::class, array(
'attr' => array('readonly' => true),
//'grouping' => true
))
->addModelTransformer(new CallbackTransformer(
// transform <br/> to \n so the textarea reads easier
function ($originalDescription) {
return $originalDescription;
},
function ($submittedDescription) {
var_dump($submittedDescription);
die("jlfs");
// remove most HTML tags (but not br,p)
$cleaned = strip_tags($submittedDescription, '<br><br/><p>');
// transform any \n to real <br/>
return str_replace("\n", '<br/>', $cleaned);
}
))
->add('tax', MoneyType::class, array(
'label' => 'I.V.A.',
'attr' => array('readonly' => true),
'grouping' => true
))
->add('total', MoneyType::class, array(
'label' => 'Total',
'attr' => array('readonly' => true),
'grouping' => true
))
->add('Guardar', SubmitType::class)
->addEventListener(FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($em) {
$repository = $em->getRepository('DefaultBundle:Bill');
$result = count($repository->findAll()) + 1;
$form = $event->getForm();
$data = $event->getData();
if (!$data) {
$form->add('serialNumber', TextType::Class, array(
'data' => $result,
'label' => 'Nº serie',
'attr' => array('readonly' => true)
));
} else {
$form->add('serialNumber', TextType::Class, array(
'label' => 'Nº serie',
'attr' => array('readonly' => true)
));
}
}
)
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'DefaultBundle\Entity\Bill',
'em' => ''
));
}
}
/**
* DefaultBundle\Entity\Bill
*
* #ORM\Table
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Bill
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="bills")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id", nullable=false)
**/
private $client;
/**
* #ORM\Column(type="string")
*/
protected $numberPlate;
/**
* #ORM\Column(type="date")
*/
protected $createdAt;
/**
* #ORM\Column(type="string")
*/
protected $serialNumber;
/**
* Features of the product.
* Associative array, the key is the name/type of the feature, and the value the data.
* Example:<pre>array(
* 'size' => '13cm x 15cm x 6cm',
* 'bluetooth' => '4.1'
* )</pre>.
*
* #var array
* #ORM\Column(type="array")
*/
private $servicesPerformed = array();
/**
* #ORM\Column(type="float")
*/
protected $subtotal;
/**
* #ORM\Column(type="float")
*/
protected $taxRate = TaxRate::TAX_RATE;
/**
* #ORM\Column(type="float")
*/
protected $tax;
/**
* #ORM\Column(type="float")
*/
protected $total;
public function __toString()
{
return $this->numberPlate;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set numberPlate
*
* #param string $numberPlate
*
* #return Bill
*/
public function setNumberPlate($numberPlate)
{
$this->numberPlate = $numberPlate;
return $this;
}
/**
* Get numberPlate
*
* #return string
*/
public function getNumberPlate()
{
return $this->numberPlate;
}
/**
* Set servicesPerformed
*
* #param array $servicesPerformed
*
* #return Bill
*/
public function setServicesPerformed($servicesPerformed)
{
$this->servicesPerformed = $servicesPerformed;
return $this;
}
/**
* Get servicesPerformed
*
* #return array
*/
public function getServicesPerformed()
{
return $this->servicesPerformed;
}
/**
* Set subtotal
*
* #param string $subtotal
*
* #return Bill
*/
public function setSubtotal($subtotal)
{
$this->subtotal = $subtotal;
return $this;
}
/**
* Get subtotal
*
* #return string
*/
public function getSubtotal()
{
return $this->subtotal;
}
/**
* Set taxRate
*
* #param string $taxRate
*
* #return Bill
*/
public function setTaxRate($taxRate)
{
$this->taxRate = $taxRate;
return $this;
}
/**
* Get taxRate
*
* #return string
*/
public function getTaxRate()
{
return $this->taxRate;
}
/**
* Set tax
*
* #param string $tax
*
* #return Bill
*/
public function setTax($tax)
{
$this->tax = $tax;
return $this;
}
/**
* Get tax
*
* #return string
*/
public function getTax()
{
return $this->tax;
}
/**
* Set total
*
* #param string $total
*
* #return Bill
*/
public function setTotal($total)
{
$this->total = $total;
return $this;
}
/**
* Get total
*
* #return string
*/
public function getTotal()
{
return $this->total;
}
/**
* Set client
*
* #param \DefaultBundle\Entity\Client $client
*
* #return Bill
*/
public function setClient(\DefaultBundle\Entity\Client $client)
{
$this->client = $client;
return $this;
}
/**
* Get client
*
* #return \DefaultBundle\Entity\Client
*/
public function getClient()
{
return $this->client;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return Bill
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set serialNumber
*
* #param string $serialNumber
*
* #return Bill
*/
public function setSerialNumber($serialNumber)
{
$this->serialNumber = $serialNumber;
return $this;
}
/**
* Get serialNumber
*
* #return string
*/
public function getSerialNumber()
{
return $this->serialNumber;
}
}
EDIT: after the answer of #ejuhjav this is my buildForm() function:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$em = $options['em'];
$builder
->add('serialNumber', TextType::class, array(
'label' => 'Nº serie',
'attr' => array('readonly' => true),
'data' => 6
))
->add('createdAt', DateType::class, array(
'label' => 'Fecha',
'data' => new \DateTime("today"),
))
->add('client', null, array('label' => 'Cliente'))
->add('numberPlate', null, array(
'label' => 'Número matrícula',
//'data' => 'prueba'
))
->add('servicesPerformed', CollectionType::class, array(
'label' => false,
'entry_type' => ServicePerformedType::class,
'allow_add' => true,
'allow_delete' => true,
'attr' => array('data-prototype' => 'jander'),
))
->add('subtotal', MoneyType::class, array(
'attr' => array('readonly' => true),
'data' => 6
))
->add('tax', MoneyType::class, array(
'label' => 'I.V.A.',
'attr' => array('readonly' => true),
'grouping' => true,
'data' => 6
))
->add('total', MoneyType::class, array(
'label' => 'Total',
'attr' => array('readonly' => true),
'grouping' => true,
'data' => 6
))
->add('Guardar', SubmitType::class)
;
$builder->get('subtotal')
->addModelTransformer(new CallbackTransformer(
// transform <br/> to \n so the textarea reads easier
function ($originalDescription) {
return $originalDescription;
},
function ($submittedDescription) {
//var_dump($submittedDescription);
die("IT IS _NOT_ ENTERING HERE!!! :)");
// remove most HTML tags (but not br,p)
$cleaned = strip_tags($submittedDescription, '<br><br/><p>');
// transform any \n to real <br/>
return str_replace("\n", '<br/>', $cleaned);
}
));
}
You are connecting your model transformer to the whole form. To add the transformer into single field use either (from the referenced docs):
$builder->add('description', TextareaType::class);
$builder->get('description')
->addModelTransformer(new CallbackTransformer(
...
or
$builder->add(
$builder->create('description', TextareaType::class)
->addModelTransformer(...)
);

PHPUnit ZF2 InputFilter with Custom Validator

I have the following InputFilter:
<?php
namespace Login\InputFilter;
use Zend\InputFilter\InputFilter;
/**
* Class Login
*
* #package Login\InputFilter
*/
class Login extends InputFilter
{
/**
* Construct
*/
public function __construct()
{
/**
* Password
*/
$this->add(
[
'name' => 'password',
'required' => true,
'filters' => [
[
'name' => 'stringtrim'
]
],
'validators' => [
[
'name' => 'stringlength',
'options' => [
'min' => '5',
'max' => '128'
],
'break_chain_on_failure' => true
],
[
'name' => 'regex',
'options' => [
'pattern' => '/^[^\\\' ]+$/'
],
'break_chain_on_failure' => true
]
]
]
);
}
/**
* Init
*/
public function init()
{
/**
* Employee ID
*/
$this->add(
[
'name' => 'employeeId',
'required' => true,
'filters' => [
[
'name' => 'stringtrim'
]
],
'validators' => [
[
'name' => 'stringlength',
'options' => [
'min' => '1',
'max' => '20'
],
'break_chain_on_failure' => true
],
[
'name' => 'digits',
'break_chain_on_failure' => true
],
[
'name' => 'Login\Validator\EmployeeId',
'break_chain_on_failure' => true
]
]
]
);
}
}
Attached to the employeeId is a custom validator I've created to check if the Employee ID actually exists in a database. It has a constructor for Doctrine Entity Manager. This works fine when testing via the web, so no worries there.
However now I would like to test via PHPUnit and I've created the following test:
<?php
namespace LoginTest\InputFilter;
use Login\InputFilter\Login;
/**
* Class LoginTest
*
* #package LoginTest\InputFilter
*/
class LoginTest extends \PHPUnit_Framework_TestCase
{
/**
* #var Login $inputFilter
*/
protected $inputFilter;
public function setUp()
{
$this->inputFilter = new Login();
$this->inputFilter->init();
parent::setUp();
}
public function testFormHasElements()
{
$inputs = $this->inputFilter->getInputs();
$this->assertArrayHasKey(
'employeeId',
$inputs
);
$this->assertArrayHasKey(
'password',
$inputs
);
}
}
When the test runs the following error is produced:
1) LoginTest\InputFilter\LoginTest::testFormHasElements
Argument 1 passed to Login\Validator\EmployeeId::__construct() must be an instance of Doctrine\ORM\EntityManager, none given, called in /vhosts/admin-application/vendor/zendframework/zendframework/library/Zend/ServiceManager/AbstractPluginManager.php on line 180 and defined
I'm not certain how I can get passed this particular error. I assume I need to use Mockery but I'm not certain.
The validator has a Factory which supplies the Doctrine Entity Manager from the Service Locator.
I am still very new to PHPUnit but I've been trying to do my research before asking here.
Any ideas?
You're getting this error because you directly instantiate you input filter and it isn't then aware of your custom validator factory.
In real application InputFilter is using Zend\Validator\ValidatorPluginManager for getting validators from service manager.
I see two ways how to solve this problem:
1.) You can setup real service manager from application configuration, like it's described in documentation and then pull the input filter from service manager:
$inputFilter = Bootstrap::getServiceManager()->get(\Login\InputFilter\Login::class); // change the service name if you have another
This solution is good if you want to write some kind of integration tests.
2.) You can mock your custom validator and inject into ValidatorPluginManager in setup method:
protected function setUp()
{
$validator = $this->getMockBuilder(\Login\Validator\EmployeeId::class)->getMock();
$inputFilter = new Login();
$inputFilter->getFactory()
->getDefaultValidatorChain()
->getPluginManager()
->setService(\Login\Validator\EmployeeId::class, $validator);
$inputFilter->init();
$this->inputFilter = $inputFilter;
parent::setUp();
}
This solution is good if you want to write unit tests for Login input filter.

How can I add a violation to a collection?

My form looks like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$factory = $builder->getFormFactory();
$builder->add('name');
$builder->add('description');
$builder->add('manufacturers', null, array(
'required' => false
));
$builder->add('departments', 'collection', array(
'type' => new Department
));
}
I have a class validator on the entity the form represents which calls:
if (!$valid) {
$this->context->addViolationAtSubPath('departments', $constraint->message);
}
Which will only add a 'global' error to the form, not an error at the sub path. I assume this is because departments is a collection embedding another FormType.
If I changed departments to one of the other fields it works fine.
How can I get this error to appear in the right place? I assume it would work fine if my error was on a single entity within the collection, and thus rendered in the child form, but my criteria is that the violation occur if none of the entities in the collection are marked as active, thus it needs to be at the parent level.
By default, forms have the option "error_bubbling" set to true, which causes the behavior you just described. You can turn off this option for individual forms if you want them to keep their errors.
$builder->add('departments', 'collection', array(
'type' => new Department,
'error_bubbling' => false,
));
I have been wrestling with this issue in Symfony 3.3, where I wished to validate an entire collection, but pass the error to the appropriate collection element/field. The collection is added to the form thus:
$form->add('grades', CollectionType::class,
[
'label' => 'student.grades.label',
'allow_add' => true,
'allow_delete' => true,
'entry_type' => StudentGradeType::class,
'attr' => [
'class' => 'gradeList',
'help' => 'student.grades.help',
],
'entry_options' => [
'systemYear' => $form->getConfig()->getOption('systemYear'),
],
'constraints' => [
new Grades(),
],
]
);
The StudentGradeType is:
<?php
namespace Busybee\Management\GradeBundle\Form;
use Busybee\Core\CalendarBundle\Entity\Grade;
use Busybee\Core\SecurityBundle\Form\DataTransformer\EntityToStringTransformer;
use Busybee\Core\TemplateBundle\Type\SettingChoiceType;
use Busybee\Management\GradeBundle\Entity\StudentGrade;
use Busybee\People\StudentBundle\Entity\Student;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StudentGradeType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* StaffType constructor.
*
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('status', SettingChoiceType::class,
[
'setting_name' => 'student.enrolment.status',
'label' => 'grades.label.status',
'placeholder' => 'grades.placeholder.status',
'attr' => [
'help' => 'grades.help.status',
],
]
)
->add('student', HiddenType::class)
->add('grade', EntityType::class,
[
'class' => Grade::class,
'choice_label' => 'gradeYear',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('g')
->orderBy('g.year', 'DESC')
->addOrderBy('g.sequence', 'ASC');
},
'placeholder' => 'grades.placeholder.grade',
'label' => 'grades.label.grade',
'attr' => [
'help' => 'grades.help.grade',
],
]
);
$builder->get('student')->addModelTransformer(new EntityToStringTransformer($this->om, Student::class));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults(
[
'data_class' => StudentGrade::class,
'translation_domain' => 'BusybeeStudentBundle',
'systemYear' => null,
'error_bubbling' => true,
]
);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'grade_by_student';
}
}
and the validator looks like:
namespace Busybee\Management\GradeBundle\Validator\Constraints;
use Busybee\Core\CalendarBundle\Entity\Year;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class GradesValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (empty($value))
return;
$current = 0;
$year = [];
foreach ($value->toArray() as $q=>$grade)
{
if (empty($grade->getStudent()) || empty($grade->getGrade()))
{
$this->context->buildViolation('student.grades.empty')
->addViolation();
return $value;
}
if ($grade->getStatus() === 'Current')
{
$current++;
if ($current > 1)
{
$this->context->buildViolation('student.grades.current')
->atPath('['.strval($q).']') // could do a single atPath with a value of "[".strval($q)."].status"
->atPath('status') // full path = children['grades'].data[1].status
->addViolation();
return $value;
}
}
$gy = $grade->getGradeYear();
if (! is_null($gy))
{
$year[$gy] = empty($year[$gy]) ? 1 : $year[$gy] + 1 ;
if ($year[$gy] > 1)
{
$this->context->buildViolation('student.grades.year')
->atPath('['.strval($q).']')
->atPath('grade')
->addViolation();
return $value;
}
}
}
}
}
This results in the error being added to the field in the element of the collection as per the attach image.
Craig
I have a case very similar. I have a CollectionType with a Custom Form (with DataTransformers inside, etc...), i need check one by one the elements and mark what of them is wrong and print it on the view.
I make that solution at the ConstraintValidator (my custom validator):
The validator must target to CLASS_CONSTRAINT to work or the propertyPath doesnt work.
public function validate($value, Constraint $constraint) {
/** #var Form $form */
$form = $this->context->getRoot();
$studentsForm = $form->get("students"); //CollectionType's name in the root Type
$rootPath = $studentsForm->getPropertyPath()->getElement(0);
/** #var Form $studentForm */
foreach($studentsForm as $studentForm){
//Iterate over the items in the collection type
$studentPath = $studentForm->getPropertyPath()->getElement(0);
//Get the data typed on the item (in my case, it use an DataTransformer and i can get an User object from the child TextType)
/** #var User $user */
$user = $studentForm->getData();
//Validate your data
$email = $user->getEmail();
$user = $userRepository->findByEmailAndCentro($email, $centro);
if(!$user){
//If your data is wrong build the violation from the propertyPath getted from the item Type
$this->context->buildViolation($constraint->message)
->atPath($rootPath)
->atPath(sprintf("[%s]", $studentPath))
->atPath("email") //That last is the name property on the item Type
->addViolation();
}
}
}
Just i validate agains the form elements in the collection and build the violation using the propertyPath from the item in the collection that is wrong.

Resources