How to add a class to all labels in a ZF2 form - zend-framework2

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')); ?>

Related

Declaring Symfony Form options (Sym 2.8/3.0)

I am looking for the best method for creating/adding dynamic options in a form. By options, I mean things like choice value pairs, or maybe even default values. I can see at least three options:
1) add the options to the $options array when adding the form type. For this, it appears that I must first declare a default value and then add them in the add method and in the controller:
controller:
$choices = [];
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$options = ['pages' => $choices];
$form = $this->createForm('MyBundle\Form\Type\PageType', $data, $options);
FormType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $options['pages'],
'label' => __('Page')
]);
}
...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'pages' => []
]);
}
}
2) If the values are not dependent on controller values, it seems I could create them in the OptionsResolver (assuming access to the source data)
FormType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $options['pages'],
'label' => __('Page')
]);
}
...
public function configureOptions(OptionsResolver $resolver)
{
$choices = [];
$pages = $this->getPages();
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$resolver->setDefaults([
'pages' => $choices
]);
}
3) Finally, I can also add in the buildForm method (again assuming access to source data):
FormType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [];
$pages = $this->getPages();
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $choices,
'label' => __('Page')
]);
}
Obviously, there is the most flexibility in the first option, but If I do not require that flexibility, or do not want to manage the options in the controller for some reason, does it make more sense to do the work in the buildForm or configureOptions methods?
If you require flexibility you can't use solution 3. But if you want to avoid flexibility, solution 3 is the best.
Solution 1 and 2 are OK, it really depend of what you need :
If you use your form in several actions with different choices: use solution 1, but add a requirement on this option to prevent the form to be called without choices
If your choices are often the same, but you want to override them only sometimes: chose solution 2
Personally I prefer the solution 1, because it's always better if your form relies on the less possible external objects ($this->pages in your example).
Regards
If you work with Doctrine Entities, you should use this:
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
// ...
$builder->add('pid', EntityType::class, array(
'class' => 'AppBundle:Page',
'choice_label' => 'title',
));
For working with another type of objects this one:
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use AppBundle\Entity\Page;
// ...
$builder->add('pid', ChoiceType::class, [
'choices' => [
new Page('Page 1'),
new Page('Page 2'),
new Page('Page 3'),
new Page('Page 4'),
],
'choices_as_values' => true,
'choice_label' => function($page, $key, $index) {
/** #var Page $page */
return $page->getTitle();
}
]);
More information you can read in blog post here.

How to change layout in controller in ZendFramework2?

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
),

Zend2: Specify currency prefix?

In Zend2 you can do this:
<?php echo $this->currencyFormat(120, 'ZAR'); ?>
This will result in:
ZAR 120.00
However, I want to end up with:
R 120.00
How can I set the prefix to rather be the currency symbol, as apposed to the code? The following doesn't work (obviously):
<?php echo $this->currencyFormat(120, 'R'); ?>
Figured it out myself. Easy as this:
$helper->setCurrencyPattern('R #0.#');
So the complete code which allows me to control everything in one place (Module.php) is as follows:
class Module
{
public function getConfig()
{
return array(
'view_helpers' => array(
'factories' => array(
'currencyFormat' => function($sm)
{
$helper = new \Zend\I18n\View\Helper\CurrencyFormat;
$helper->setCurrencyCode("ZAR");
$helper->setLocale('us_ZA');
$helper->setCurrencyPattern('R #0.#');
return $helper;
},
)
),
);
}
}
Enjoy...

Use a conditional statement when creating a form

I would like to use a conditional statement when creating a form in Symfony.
I am using a choice widget in general case. If the user selects the option "Other", I would like to display an additional text box widget. I suppose this can be done in javascript, but how can I still persist the data from 2 widgets into the same property in my entity?
I have this so far:
$builder->add('menu', 'choice', array(
'choices' => array('Option 1' => 'Option 1', 'Other' => 'Other'),
'required' => false,
));
//How to add text box if choice == Other ????
I was planing to use a DataTransfomer, but on 2 widgets??
I recommend to build a custom type for that, for example ChoiceOrTextType. To this type you add both the choice (named "choice") and the text field (named "text").
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ChoiceOrTextType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('choice', 'choice', array(
'choices' => $options['choices'] + array('Other' => 'Other'),
'required' => false,
))
->add('text', 'text', array(
'required' => false,
))
->addModelTransformer(new ValueToChoiceOrTextTransformer($options['choices']))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array('choices'));
$resolver->setAllowedTypes(array('choices' => 'array'));
}
}
As you already guessed, you also need a data transformer, which can be quite simple:
use Symfony\Component\Form\DataTransformerInterface;
class ValueToChoiceOrTextTransformer implements DataTransformerInterface
{
private $choices;
public function __construct(array $choices)
{
$this->choices = $choices;
}
public function transform($data)
{
if (in_array($data, $this->choices, true)) {
return array('choice' => $data, 'text' => null);
}
return array('choice' => 'Other', 'text' => $data);
}
public function reverseTransform($data)
{
if ('Other' === $data['choice']) {
return $data['text'];
}
return $data['choice'];
}
}
Now only make the "menu" field a field of that type.
$builder->add('menu', new ChoiceOrTextType(), array(
'choices' => array('Option 1' => 'Option 1', 'Option 2' => 'Option 2'),
'required' => false,
));

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