I found this topic and answer: Change layout in the controller of Zend Framework 2.0 :: Answer
I am trying to do this:
public function loginAction() {
if ($this->zfcUserAuthentication()->hasIdentity()) {
return $this->redirect()->toRoute('zfcadmin');
}
$this->layout('layout/login');
return new ViewModel();
}
But it doesn't work.
Sure I have file MODULE_DIR/view/layout/login.phtml.
I tried to var_dump($this->layout()); before setting layout and after it and it shows, that layout is changed after $this->layout('layout/login'); line. But it is not.
How to set different layout in controller?
Also, why I don't get any messages if layout is changed? Why standart layout loaded, instead of error?
I think, I have to set up layout somewhere (like I set routes, for example). Possibly in config ['view_manager']['template_map'] by adding something like:
$config = array(
'view_manager' => array(
'template_path_stack' => array(
__DIR__ . '/../view'
),
'template_map' => array(
'layout/login' => __DIR__ . '/../view/layout/login.phtml',
),
),
);
— like said there:
Of course you need to define those layouts, too... just check
Application Modules module.config.php to see how to define a layout.
That didn't helped me :(
Update 1
Tried this:
public function loginAction() {
if ($this->zfcUserAuthentication()->hasIdentity()) {
return $this->redirect()->toRoute('zfcadmin');
}
$layout = $this->layout();
$layout->setTemplate('layout/login');
return new ViewModel();
}
as #alex suggested. Doesn't work :'(. Same result without return new ViewModel(); line.
You review files by yourself:
AdminController.php (loginAction)
module.config.php (to be sure I added layout/login correctly
Update 2
I tried to debug as you suggest.
I updated __invoke functioN:
public function __invoke($template = null)
{
var_dump($template);
die();
if (null === $template) {
return $this->getViewModel();
}
return $this->setTemplate($template);
}
There are some cases:
With code, you suggested:
$layout = $this->layout();
$layout->setTemplate('layout/login');
it displays NULL. So method is called, but $template is null variable.
With code, from post, I had given in the start of my post:
$this->layout('layout/login');
return new ViewModel();
It shows string(12) "layout/login".
Without any code (so layout layout/admin loaded (default for ZfcAdmin), it shows: string(12) "layout/admin".
If I load / of my site it page is loaded with standart layout (in both cases with or without layout/layout in module config.
Update 3
I tried this:
$layout = $this->layout();
var_dump($layout->getTemplate());
$layout->setTemplate('layout/login');
var_dump($layout->getTemplate());
die();
in controller. It shows: string(13) "layout/layout" string(12) "layout/login". So layout is changed. But standart layout layout/layout rendered instead of layout/login. :(
Because you're using ZfcAdmin and have the use_admin_layout option enabled in that module and the login route you're attempting to set a layout on is a child route of ZfcAdmin, the admin layout listener is kicking in and over-writing the template you're attempting to set in your controller action.
It's perhaps easiest to disable zfcadmin layout, write your own listener and handle the specific case of login layout there. You can do that using essentially the same method that ZfcAdmin uses in Module.php with a tweak or two ...
Be sure to disable ZfcAdmin layout
'zfcadmin' => array(
'use_admin_layout' => false,
),
then, using your module name as a config key, set up your own version of the same config ...
'myzfcadmin' => array(
'use_admin_layout' => true,
'admin_layout_template' => 'layout/admin',
// you could even define a login layout template here
'login_layout_template' => 'layout/login',
),
Next in MyZfcAdmin/Module.php add a listener, almost exactly like the one in ZfcAdmin only have it check your myzfcadmin config values instead ...
public function onBootstrap(MvcEvent $e)
{
$app = $e->getParam('application');
$em = $app->getEventManager();
$em->attach(MvcEvent::EVENT_DISPATCH, array($this, 'selectLayoutBasedOnRoute'));
}
public function selectLayoutBasedOnRoute(MvcEvent $e)
{
$app = $e->getParam('application');
$sm = $app->getServiceManager();
$config = $sm->get('config');
if (false === $config['myzfcadmin']['use_admin_layout']) {
return;
}
$match = $e->getRouteMatch();
$controller = $e->getTarget();
if (!$match instanceof \Zend\Mvc\Router\RouteMatch
|| 0 !== strpos($match->getMatchedRouteName(), 'zfcadmin')
|| $controller->getEvent()->getResult()->terminate()
) {
return;
}
if ($controller instanceof \MyZfcAdmin\Controller\AdminController
&& $match->getParam('action') == 'login'
) {
// if you'd rather just set the layout in your controller action just return here
// return;
// otherwise, use the configured login layout ..
$layout = $config['myzfcadmin']['login_layout_template'];
} else {
$layout = $config['myzfcadmin']['admin_layout_template'];
}
$controller->layout($layout);
}
As you can see, I added code to check the controller is your specific AdminController instance and login action, and if so, set the alternate template otherwise use the default, no need to worry about it in your controller now.
Add your layout in the template map of your view manager in the module.config.php
Like so:
// View file paths
'view_manager' => array(
'display_not_found_reason' => true,
'display_exceptions' => true,
'doctype' => 'HTML5',
'not_found_template' => 'error/404',
'exception_template' => 'error/index',
'template_map' => array
'layout/login' => 'path_to_layout_file'
)
)
Then, in your controller try setting the layout like this, using the setTemplate() method:
$layout = $this->layout();
$layout->setTemplate('layout/login');
EDIT, the following is code from the Zend library:
Inside Zend\Mvc\Controller\Plugin\Layout notice this method:
/**
* Invoke as a functor
*
* If no arguments are given, grabs the "root" or "layout" view model.
* Otherwise, attempts to set the template for that view model.
*
* #param null|string $template
* #return Model|Layout
*/
public function __invoke($template = null)
{
if (null === $template) {
return $this->getViewModel();
}
return $this->setTemplate($template);
}
If you don't provide a template it will call this method:
/**
* Retrieve the root view model from the event
*
* #return Model
* #throws Exception\DomainException
*/
protected function getViewModel()
{
$event = $this->getEvent();
$viewModel = $event->getViewModel();
echo '<pre>' . print_r($viewModel, true) . '</pre>';die;
if (!$viewModel instanceof Model) {
throw new Exception\DomainException('Layout plugin requires that event view model is populated');
}
return $viewModel;
}
Notice the print_r statement, if you look at it, it will show you this:
Zend\View\Model\ViewModel Object
(
[captureTo:protected] => content
[children:protected] => Array
(
)
[options:protected] => Array
(
)
[template:protected] => layout/layout
[terminate:protected] =>
[variables:protected] => Zend\View\Variables Object
(
[strictVars:protected] =>
[storage:ArrayObject:private] => Array
(
)
)
[append:protected] =>
)
Notice the [template:protected] => layout/layout that why I was saying I think Zend defaults to that layout.
So go into that file, in the __invoke method and do echo $template;die; when you are setting your layout with $this->setTemplate('layout/login') in your controller and see if its even getting passed there. Then you might be able to trace it better.
EDIT: Setting up multiple layouts.
Here is one way you could set up layouts for your modules in an effort to reduce the likelihood of a conflict or something being overwritten.
// where $sm is the service manager
$config = $sm->get('config');
$config = array_merge($config, include '/path_to_config/layouts.config.php');
if (isset($config['module_layouts'][$moduleNamespace]))
{
$controller->layout($config['module_layouts'][$moduleNamespace]);
}
And your layouts config could look something this:
'module_layouts' => array(
'_default' => 'layout/layout',
'admin' => 'layout/admin',
'foo' => 'layout/foo',
'login' => 'layout/login' // etc, etc
),
Related
I am attempting to install APIGILITY in my app. I have followed this tutorial:
https://apigility.org/documentation/recipes/apigility-in-an-existing-zf2-application
When I attempt to access the apigility admin: www.myapp.dev/apigility I get a "The requested URL could not be matched by routing" error.
My config is as follows:
'modules' => array(
'DoctrineModule',
'DoctrineORMModule',
'ZfcRbac', //Keep this at the top
'Application', //The applications main functions run from this module
//APIGILITY
'ZF\Apigility',
'ZF\Apigility\Provider',
'AssetManager',
'ZF\ApiProblem',
'ZF\MvcAuth',
'ZF\OAuth2',
'ZF\Hal',
'ZF\ContentNegotiation',
'ZF\ContentValidation',
'ZF\Rest',
'ZF\Rpc',
'ZF\Versioning',
'ZF\DevelopmentMode',
'ZF\Apigility\Admin',
'ZF\Configuration',
I have enabled developer mode.
Typically if a route exists and ZfcRbac is blocking the route, I am re-directed. In this case when the route is not accessible I get the error.
Is there a simple way to test this?
To follow up on HappyCoder's own answer, you can match all routes in zf-apigility module with
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$e->getApplication()->getEventManager()->attach(
MvcEvent::EVENT_ROUTE, function(MvcEvent $e) {
// Route matched
$route_name = $e->getRouteMatch()->getMatchedRouteName();
// If apigility - set correct layout
if(preg_match('/^zf-apigility/', $route_name)) {
$e->getViewModel()->setTemplate('layout/api-layout');
}
}
);
}
When doing this way - it will set appropriate layout for all apigility views, including /apiligity (welcome screen)
I solved this issue by doing the following:
The tutorial makes no mention of copying the ApiGility template to your app. You need to do this. What I did was to add the template to my application/config/module.config.php file.
return [
'view_manager' => [
'display_not_found_reason' => true,
'display_exceptions' => true,
'doctype' => 'HTML5',
'not_found_template' => 'error/404',
'exception_template' => 'error/exception',
'template_map' => [
'customer/layout' => __DIR__ . '/../view/layout/customer-layout.phtml',
'api/layout' => __DIR__ . '/../view/layout/api-layout.phtml',
'layout/layout' => __DIR__ . '/../view/layout/admin-layout.phtml',
In the Application module I check routing and switch the template accordingly:
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$e->getApplication()->getEventManager()->attach(
MvcEvent::EVENT_ROUTE, function(MvcEvent $e) {
//Set the customer layout
$needle = $e->getRouteMatch()->getParam('controller');
$haystack = [
/* Customer template routes */
];
if (in_array( $needle , $haystack )) {
$e->getViewModel()->setTemplate('customer/layout');
}
//Apigility route
$haystack = [
'zf-apigility/ui'
];
if (in_array( $needle , $haystack )) {
$e->getViewModel()->setTemplate('api/layout');
}
}
);
}
To access the apigility pages, I now access via: http://www.myapp.com/apigility/ui#/
Hope this helps someone...
Is it possible to hook up (ideally in the controller) to add an additional parameter to routing?
I know that sounds unclear and at first glance it may sounds ridiculous - because to reach the controller we already must have routing. But I want to change only default variables.
I'll try to explain what I want to achieve:
Config:
return [
'router' => [
'routes' => [
'some' => [
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => [
'route' => '/some/:project',
'defaults' => [
'__NAMESPACE__' => 'Some\Controller',
'controller' => 'Some\Controller\Some',
'action' => 'some',
'extra' => 'default-value'
],
],
]
]
]
];
Controller:
class SomeController extends AbstractActionController {
protected $project = null;
public function setEventManager(EventManagerInterface $events)
{
parent::setEventManager($events);
$controller = $this;
$events->attach(
'dispatch', function (\Zend\Mvc\MvcEvent $e) use ($controller) {
$params = $e->getRouteMatch()->getParams();
$this->project = $params['project'] ;
// and there should be something that I want to
// achieve but do not know how (and if it is possible)
if ($this->project == 1) {
// magic action which modify config default param
// "extra" from "default-value" to "changed-value"
}
return;
}, 50
);
}
protected function attachDefaultListeners()
{
parent::attachDefaultListeners();
$eventManager = $this->getEventManager();
$eventManager->attach(
\Zend\Mvc\MvcEvent::EVENT_DISPATCH,
function(\Zend\Mvc\MvcEvent $event) {
$ViewModel = $event->getResult();
if ($ViewModel instanceof \Zend\View\Model\ViewModel) {
$ViewModel->setVariable('project',$this->project);
}
},
-99);
}
public function someAction() {
echo $this->params()->fromRoute("extra"); // return "default-value";
// but i want
echo $this->params()->fromRoute("extra"); // return "changed-value";
return new ViewModel();
}
}
View
<?php
echo "project: ".$this->project;
echo $this->url('some',['project'=>1]); // result: "/some/1"
I know this seems very strange. But for some reason (readable links, seo) is necessary to me.
Are you sure, you want to change the default param?
if ($this->project == 1) {
$e->getRouteMatch()->setParam('extra', 'changed-value');
}
You can set default params globally for assembling:
$serviceLocator->get('router')->setDefaultParam('extra', 'changed-value');
There is no way to change the defaults-Property of Zend\Mvc\Router\Http\Segment
If you really need it you must extend this class (but I would not recommend that, because I think your approach is already wrong)
I'm using a jQuery plugin that takes the text from labels associated with form elements and puts them as default text for the fields themselves. (You can find the plugin here.)
Here's the catch: it can only do this if the label has the class "inline". Now, I know I can use the following code to do this:
$this->add(array (
'name' -> 'name',
....
'options' => array (
'label' => 'Name',
'label_attributes' => array (
'class' => 'inline'
)
)
));
This will work fine, and if it has to be done item by item, then so be it. But I was wondering if there's some way I can add the class to ALL labels associated with text and text area form elements without using JavaScript. I'm thinking this would either done by a plugin, or by looping through all the elements in the form, but I don't know how to do either.
You could extend the FormRow view helper.
Here is a little example:
use Zend\Form\View\Helper\AbstractHelper;
use Zend\Form\View\Helper\FormRow;
class CustomFormRow extends FormRow
{
public function render(ElementInterface $element) {
...
$label = $element->getLabel();
if (isset($label) && '' !== $label) {
// Translate the label
if (null !== ($translator = $this->getTranslator())) {
$label = $translator->translate(
$label, $this->getTranslatorTextDomain()
);
}
$label->setAttribute('class', 'inline');
}
...
if ($this->partial) {
$vars = array(
'element' => $element,
'label' => $label,
'labelAttributes' => $this->labelAttributes,
'labelPosition' => $this->labelPosition,
'renderErrors' => $this->renderErrors,
);
return $this->view->render($this->partial, $vars);
}
...
}
You could probably leave the rest as it is and you should be good to go once you add some configuration in your Module.php for your view helper.
public function getViewHelperConfig() {
return array(
'factories' => array(
'CustomFormRow' => function($sm) {
return new \Application\View\Helper\CustomFormRow;
},
)
);
}
In your template files you now have to use your viewHelper instead.
<?php echo $this->CustomFormRow($form->get('yourelement')); ?>
I'm actualy a beginner in ZF2
I managed to use multiple BDD on the same application and it works.
(I'm talking about this : configure multiple databases in zf2 ).
Though, I'd have a little question...
Is it ok to declare my custom factory in global.php ? (in the service_manager thing).
Or do I need to declare it inside each module ? (in module.php)
Declaring it into global.php actualy works, but I was wondering if it's not breaking the spirit of the framework or something...
Thanks for your time !
Tounu
Store your connection settings in a local config:
config/autoload/local.php
this is in case you have multiple environments with different databases/connection credentials etc. for example, you may gave a staging setup, and a live setup, both using a separate database.
You can also then use multiple connections inside your application this way too.
there's nothing to stop you setting up multiple connections in here, and using them as needed in your database adapters etc.
local.php
return array(
/**
* Database Connection One
*/
'db' => array(
'driver' => 'pdo',
'dsn' => 'mysql:dbname=dbnamehere;host=localhost',
'username' => 'root',
'password' => '',
),
/**
* Database Connection Two
*/
'db_two' => array(
'driver' => 'pdo',
'dsn' => 'mysql:dbname=anotherdb;host=localhost',
'username' => 'root',
'password' => '',
),
If you are using version control (you should be!) this also allows you to exclude the .local config files from your repository to avoid storing password etc in there, and allows for easier deployment to multiple environments.
You can setup multiple adapters to use different connections too:
global.php
return array(
/**
* Database Adapter(s)
*/
'service_manager' => array(
'factories' => array(
/**
* Adapter One - this factory will use the default 'db' connection
*/
'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory',
/**
* Adapter Two - use the second connection
*/
'Application\Db\AdapterTwo' => function($sm) {
$config = $sm->get('Config');
return new Adapter($config['db_two']);
},
),
),
);
To connect multiple database at a time, follow these steps:
Step 1:
Create /module/MyModule/ and add into application.config.ini to access.
Step 2:
Create Module.php in /module/MyModule/ directory with the following scripts
<?php
namespace MyModule;
use MyModule\MyAdapterFactory;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
class Module implements ServiceProviderInterface{
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__.'/Db/Adapter/',
),
),
);
}
public function getServiceConfig()
{
return array(
'factories' => array(
'adapter1' => new MyAdapterFactory('db_adapter1'),
'adapter2' => new MyAdapterFactory('db_adapter2'),
),
);
}
}
Step 3:
Create MyAdapterFactory.php in the path: /module/MyModule/src/MyModule/Db/Adapter/ with the following scripts.
<?php
namespace MyModule;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Db\Adapter\Adapter;
class MyAdapterFactory implements FactoryInterface
{
protected $configKey;
public function __construct($key)
{
$this->configKey = $key;
}
public function createService(ServiceLocatorInterface $serviceLocator)
{
$config = $serviceLocator->get('Config');
return new Adapter($config[$this->configKey]);
}
}
?>
Step 4:
Add the following scripts in your getServiceConfig().
'YourModule\Model\YourTable' => function($sm) {
$tableGateway = $sm->get('YourTableGateway');
$table = new YourTable($tableGateway);
return $table;
},
'YourTableGateway' => function ($sm) {
$adapter1 = $sm->get('adapter1');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new YourModel());
return new TableGateway('tbl_name', $adapter1, null, $resultSetPrototype);
},
Step 5:
Add method into your controller to access your table as below:
Declare this on start of the class:
protected $this->yourTable;
public function getYourTable()
{
if (!$this->yourTable) {
$sm = $this->getServiceLocator();
$this->yourTable = $sm->get('YourModule\Model\YourTable');
}
return $this->yourTable;
}
Then, You can call your Model methods for Select, Update, Insert using this function (getYourTable()) in your controller.
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.