I've been reading a bit about form collections/fieldsets in ZF2 as I want to add some array fields to my form.
However as I understand - fieldsets/collections are build with domain entities in mind. In my situation this solution doesn't seem to be anywhere close to good idea.
My form is not related to any entity, it is just meant to pass params to pdf generator. These params are serialized and save in just one column in db.
Form itself is quite big and has a lot of fields like:
Question:
a) smth
b) smth
c) smth
d) other
a) other 1
b) other 2
...
x) other 22
So, I have multiple elements for extra input from user which have to be added dynamically.
Building a separated class for a fieldset containing ... 1 extra field each would result in atleast 20 extra classes per one form.
I thought I can make it simply by:
$element = new Element\Text('citiesOther[]');
$element->setAttributes(array(
'id' => 'citiesOther',
'placeholder' => 'Other city',
));
$this->add($element);
//view:
$this->formElement( $form->get( 'citiesOther[]' )
And then in the frontend there was a button "add another" which was just cloning whole input. It worked quite well, I could receive params from the post.
The thing is... I am unable to filter/validate these fields. If I pass a name "citiesOther[]" to inputFilter, it's not even validating one of these fields, not even mentioning whole array.
Collections in Zend Framework are a bit tricky to understand. Basically you need a basic fieldset for a collection. This fieldset implements the InputFilterProviderInterface for filtering and validation. With this in mind, you have to define all your repeatable input fields and the validation for this fields in a collection. It is not a good idea not validating this data. There are a few scenarios for injecting really bad data in a pdf. So please validate all data, which is coming from an form. Always!
Here 's a quick example how collection with Zend Framework are working.
namespace Application\Form;
use Zend\Form\Fieldset;
use Zend\Form\Element\Text;
use Zend\InputFilter\InputFilterProviderInterface;
class YourCollection extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'simple',
'type' => Text::class,
'attributes' => [
...
],
'options' => [
...
]
]);
}
public function getInputFilterSpecification()
{
return [
'simple' => [
'required' => true,
'filters' => [
...
],
'validators' => [
...
],
],
];
}
}
This is the basic fieldset class, which acts as collection of all input fields you want to repeat in your form. This basic fieldset filters and validates itself by implementing the InputFilterProviderInterface. By using the implemented method, you can place filters and validators for your inputs
If you have dependent input fields, you have to define them here in this fieldset.
For using it right, you need an entity, which is bound to the basic fieldset later in this post. Next, we create the entity for this fieldset.
namespace Application\Entity;
class YourCollectionEntity
{
protected $simple;
public function getSimple()
{
return $this->simple;
}
public function setSimple($simple)
{
$this->simple = $simple;
return $this;
}
}
This is a very simple entity. This will act as your data holder and will be bound to the collection later. Next we need a fieldset, that contains the collection and a hidden field for the collection count. Sounds a bit complecated using another fieldset. But in my eyes this is the best way.
namespace Application\Form;
class YourCollectionFieldset extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'collectionCounter',
'type' => Hidden::class,
'attributes' => [
...
],
]);
// add the collection fieldset here
$this->add([
'name' => 'yourCollection',
'type' => Collection::class,
'options' => [
'count' => 1, // initial count
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => YourCollection::class,
],
],
]);
}
public function getInputFilterSpecification()
{
return [
'collectionCounter' => [
'required' => true,
'filters' => [
[
'name' => ToInt::class,
],
],
],
];
}
}
This fieldset implements your collection and a hidden field, which acts as a counter for your collection. You need both when you want to handle collections. A case could be editing collection contents. Then you have to set the count of the collection.
As you can imagine, you need an entity class for this fieldset, too. So let 's write one.
namespace YourCollectionFieldsetEntity
{
protected $collectionCounter;
protected $yourCollection;
public function getCollectionCounter()
{
return $this->collectionCounter;
}
public function setCollectionCounter($collectionCounter)
{
$this->collectionCounter = $collectionCounter;
return $this;
}
public function getYourCollection()
{
return $this->yourCollection;
}
public function setYourCollection(YourCollectionEntity $yourCollection)
{
$this->yourCollection = $yourCollection;
return $this;
}
}
This entity contains the setYourCollection method, which takes an YourCollectionEntity instance as parameter. We will see later, how we will do that little monster.
Let 's wrap it up in a factory.
namespace Application\Form\Service;
YourCollectionFactory
{
public function __invoke(ContainerInterface $container)
{
$entity = new YourCollectionFieldsetEntity();
$hydrator = (new ClassMethods(false))
->addStrategy('yourCollection', new YourCollectionStrategy());
$fieldset = (new YourCollectionFieldset())
->setHydrator($hydrator)
->setObject($entity);
return $fieldset;
}
}
This factory adds a hydrator strategy to the hydrator. This will hydrate the entity bound to the repeatable fieldset.
How to use this in a form?
The above shown classes are for the needed collection only. The collection itself is not in a form yet. Let 's imagine we have a simple form, which implements this collection.
namespace Application\Form;
class YourForm extends Form
{
public function __construct($name = null, array $options = [])
{
parent::__construct($name, $options);
// add a few more input fields here
// add your collection fieldset
$this->add([
'name' => 'fancy_collection',
'type' => YourCollectionFieldset::class,
]);
// add other simple input fields here
}
}
Well, this is a simple form class, which uses the collection shown above. As you have written, you need a validator for validating all the data this form contains. No form validator without an entity. So first, we need an entity class for this form.
namespace Application\Entity;
class YourFormEntity
{
// possible other form members
...
protected $fancyCollection;
public function getFancyCollection()
{
return $this->fancyCollection;
}
public function setFancyCollection(YourCollectionFieldsetEntity $fancyCollection)
{
$this->fancyCollection = $fancyCollection;
return $this;
}
// other getters and setters for possible other form fields
}
... and finally the validator class for your form.
namespace Application\InputFilter;
class YourFormInputFilter extends InputFilter
{
// add other definitions for other form fields here
...
}
}
We don 't need to redefine filters and validators here for your collection. Remember, the collection itself implements the InputFilterProviderInterface which is executed automtically, when the validator for the form, which contains the collection, is executed.
Let 's wrap it up in a factory
namespace Application\Form\Service;
class YourFormFactory
{
public function __invoke(ContainerInterface $container)
{
$container = $container->getServiceLocator();
$entity = new YourFormEntity();
$filter = new YourFormInputFilter();
$hydrator = (new ClassMethods(false))
->addStrategy('fancy_collection', new FancyCollectionStrategy());
$form = (new YourForm())
->setHydrator($hydrator)
->setObject($entity)
->setInputFilter($filter);
return $form;
}
}
That 's all. This is your form containing your collection.
Hydrator strategies are your friend
Above two hydrator strategies are added to the hydrators you need for your form. Adding hydrator strategies to a hydrator works like a charme, if you have complex post data, which is ment to be pressed in enities.
namespace Application\Hydrator\Strategy;
class YourCollectionStrategy extends DefaultStrategy
{
public function hydrate($value)
{
$entities = [];
if (is_array($value)) {
foreach ($value as $key => $data) {
$entities[] = (new ClassMethods())->hydrate($data, new YourCollectionEntity());
}
}
return $entities;
}
}
This hydrator strategy will hydrate the entity four the repeated collection fieldset. The next strategy will hydrate the whole collection data into your form entity.
namespace Application\Hydrator\Strategy;
class FancyCollectionStrategy extends DefaultStrategy
{
public function hydrate($value)
{
return (new ClassMethods())
->addStrategy('yourCollection', new YourCollectionFieldsetEntity())
->hydrate($value);
}
}
This one will hydrate the collection count and the repeated fieldset. That 's all for the hydration of your data.
How does that look in a controller?
Well, that 's simple. Now, as we have all the classes we need for a complex form with collection, wie can go on with the controller.
namespace Application\Controller;
class YourController extends AbstractActionController
{
protected $form;
public function __consturct(Form $form)
{
$this->form = $form;
}
public function indexAction()
{
$request = $this->getRequest();
if ($request->isPost()) {
$this->form->setData($oRequest->getPost());
if ($this->form->isValid()) {
// get all data as entity
$data = $this->form->getData();
// iterate through all collection data
foreach ($data->getFancyCollection()->getYourCollection() as $collection) {
// get every simple field from every collection
echo $collection->getSimple();
}
}
}
}
}
Sure, this is way more complex than just rerieving the raw post data. But as a mentioned before, you should not use the raw data because of security reasons. Always validate and filter data, which was given by a user over a form. Or just to keep it simple: do not trust the user!
Related
The title might seem awkward, but the reason for doing is is the re-use of code.
Say I have a Form(type), an Entity and the validation on the entity as annotation.
For example :
the form:
$form = $this->createFormBuilder($entity)
->add('country', 'choice', [
'choices' => $entity->getLocations(),
'required' => true,
'multiple' => false,
'expanded' => false,
'label' => 'Are you located in:',
])
the entity :
/**
* #Assert\Choice(callback = "getLocations")
*/
protected $country;
#.....
public static function getLocations()
{
return array( 'en-wa' => 'England/Wales', 'sc' => 'Scotland');
}
Now of couse the validation will always fail because it works with values rather than with keys, so I was thinking of writing a custom validator called KeyChoice so I can still use the callback. but I can't seem to find any documentation about this, and reading the source didn't help out either.
Now I don't want judgement about best practice unless there is a way where I only have to define the options ones / form or even less, like for example in the form type, but then how do I use them in the callback in the validator ?
How about this:
/**
* #Assert/Choice(callback="getLocationChoices")
*/
...
public static function getLocationChoices(){
return array_keys(self::getLocations());
}
Update:
Or you can create a custom constraint for that:
// the constraint
namespace AppBundle\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class KeyChoice extends Constraint
{
public $message = 'the choice is not valid';
public $callback;
}
// the validator
namespace AppBundle\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
class KeyChoiceValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!is_callable($choices = array($this->context->getClassName(), $constraint->callback))
&& !is_callable($choices = $constraint->callback)
) {
throw new ConstraintDefinitionException('The Choice constraint expects a valid callback');
}
$choices = call_user_func($choices);
if(!in_array($value, $choices)){
$this->context->buildViolation($constraint->message)->addViolation();
}
}
}
You can then use this constraint as stated in the linked doc.However this will not provide any of the other functionality that the choice constraint provides, if you want to have those you have to extend both Symfony\Component\Validator\Constraints\Choice and Symfony\Component\Validator\Constraints\ChoiceValidator, don't override anything in the ChoiceConstraint but you have to override the ChoiceConstraintValidator::validate completely and copy paste it, then add a $choices = array_keys($choices); at this line
I have two entities: A and B. A is associated to B through a OneToMany association:
class A
{
// ...
/**
* #ORM\OneToMany(targetEntity="..\AcmeBundle\Entity\B", mappedBy="A", fetch="EXTRA_LAZY")
*/
private $things;
public function __construct()
{
$this->things = new ArrayCollection();
}
// ...
// addThing(), removeThing() and getThings() are properly defined
I have a Form class for the A entity:
class AThingsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('things', 'collection', array(
'type' => new BType()
))
->add('send', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => '..\AcmeBundle\Entity\A'
));
}
public function getName()
{
return 'a_things';
}
}
The BType class contains fields to define the forms of B, there is nothing particular in it.
And I use this form from my controller:
$a = $em->getRepository('AcmeBundle:A')->getAWithSubsetOnly($A_id);
$form = $this->createForm(new AThingsType (), $a /* instance of A*/);
Then the form is rendered in the Twig template.
It will add one BType for each element in A->things but I want to display only a subset of the A->strings values, because the current user doesn't have the rights to modify all the A->things, he can only update some of these B entities. I don't know how to do this, because the whole collection of A->things is loaded automatically (even if a do a ->innerJoin() to fetch only a subset of the things) so the form display all the BType whereas I only want to display some of them (based on a SQL query).
Now the fields are hidden by calling {% do form.contenu.setRendered %} in Twig but I hope there may be a better solution.
The problem is likely you're not explicitly selecting the entities you want in your DQL/QueryBuilder call in your getAWithSubsetOnly repository method.
For example, this will get a user and if I wanted to display all its logs as a collection in my form, will lazy load all logs regardless of my criteria:
$userData = $this->getEntityManager()
->createQueryBuilder()->select('u') // constraint ignored
->from('SomeUserBundle:User', 'u')
->join('u.logs', 'l')
->where('l.id < 20880')
->andWhere('u.id = 7')
->getQuery()->getSingleResult();
If you explicity tell doctrine to fetch the sub entity at join like below, it won't lazy load the collection & you will get your filtered collection subset:
$userData = $this->getEntityManager()
->createQueryBuilder()->select('u, l') // constraint taken into account as
// logs not lazy loaded
->from('SomeUserBundle:User', 'u')
->join('u.logs', 'l')
->where('l.id < 20880')
->andWhere('u.id = 7')
->getQuery()->getSingleResult();
You'll also save yourself a lot of lazy loading SQL queries with this approach.
I have a form with all field that map an object.
Of this, i would generate a subform within of the Form class.
I'm trying to do this by the displaygroup, but when call the "subform" in the controller, the tag form, is not generate.
how can I solve ?
Thanks
this is the code.
<?php
$username = new Zend_Form_Element_Text('user');
...//param for field
$password = new Zend_Form_Element_Password('pwd');
...//param for field
$name = new Zend_Form_Element_Text('name');
...//param for field
$submit = new Zend_Form_Element_Submit('submit');
...//param for field
$this->addElement(array($user,$password,$name,$submit));
$this->addDisplayGroup(array($user,$password,$submit),'login');
$this->addDisplayGroup(array($user,$password,$name, $submit),'create');
?>
A subform is something different than a display group. A subform is a Zend_Form_SubForm instance nested in Zend_Form instance. You can use this to embed one form into another. As an example, you might have a user profile form and a registration form. In the registration form you can enter profile values as well as some other details. So, you can use this profile form as subform embedded inside the registration form. A subform is mainly used for DRY (don't repeat yourself) principles or to create a multi-page form.
A display group is just a visual representation of some form elements grouped together. In html syntax this is called a fieldset. The main purpose is to create groups of elements which belong to each other. For example in a shopping cart you might have an invoice address group and a shipping address group. Such display group is mainly used for semantics and visual representation.
On of the largest differences is that for display groups, the form has awareness of those form elements, as with subforms the form has no awareness of the elements of the subforms. This said, I notice you want to create one form which contains two display groups: one when you login, one when you create (or register) a user. With the given from above you cannot use display groups for this. One option is to use two form instances:
class LoginForm extends Zend_Form
{
public function init ()
{
$this->addElement('text', 'user');
$this->addElement('password', 'pwd');
$this->addElement('submit', 'submit');
}
}
class RegisterForm extends Zend_Form
{
public function init ()
{
$this->addElement('text', 'user');
$this->addElement('password', 'pwd');
$this->addElement('text', 'name');
$this->addElement('submit', 'submit');
}
}
If you want to reuse the fields user and pwd you might want to use subforms for this:
class BaseForm extends Zend_Form_SubForm
{
public function init ()
{
$this->addElement('text', 'user');
$this->addElement('password', 'pwd');
}
}
class LoginForm extends Zend_Form
{
public function init ()
{
$subform = new BaseForm;
$this->addSubform($subform, 'base');
$this->addElement('submit', 'submit');
}
}
class RegisterForm extends Zend_Form
{
public function init ()
{
$subform = new BaseForm;
$this->addSubform($subform, 'base');
$this->addElement('text', 'name');
$this->addElement('submit', 'submit');
}
}
In both cases, you can simply instantiated one of those forms in your controller:
public function loginAction ()
{
$form = new LoginForm();
// More code here
$this->view->form = $form;
}
public function registerAction ()
{
$form = new RegisterForm();
// More code here
$this->view->form = $form;
}
Zend_Form_SubForm doesn't render <form> tags by default.
In order to get it to do so, you need to add the 'Form' decorator to your subform instance before you render it.
Try:
$mySubForm->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form'))
->addDecorator('Form');
and then, in your view script, you can do:
<?php echo $this->mySubForm; ?>
I have some Forms that for some reasons have to be instantiated in the different modules bootstraps. But in those forms I want to use
$this->setAction($this->getView()->url(array('controller' => 'foo', 'action' => 'bar')));
in the constructor. But since the viewhelper url is not accessible yet since I'm in the bootstrap, is there anyway around this? What I get now is
Fatal error: Uncaught exception 'Zend_Controller_Router_Exception' with message 'Route default is not defined'
on that line. I have NO custom routes so I only use the default router and routes.
If you really need to instantiate the forms so early, then perhaps one possibility is the order of the initializations in your Bootstrap.
In the Bootstrap method where you instantiate your forms, just make sure that you bootstrap the view first. Then grab the view object and pass it to the form during instantiation.
Something like:
protected function _initForms()
{
$this->bootstrap('view');
$view = $this->getResource('view');
$form = new My_Form($view);
// ...
}
Then in your form, you can do something like:
public function __construct($view)
{
$this->setView($view);
}
public function init()
{
// set the action
$this->setAction($this->getView()->url(array(
'controller' => 'foo',
'action' => 'bar',
)));
// create your form elements
// ...
}
Whaddya think?
I decided to change my approach and made the class that manages all the forms accept strings of forms instead of the forms.
static public function registerForm($formClassName, $formName) {
self::$_forms[$formName] = $formClassName;
}
Then I created function to get a form or all forms from the class like so
public function getForm($name) {
if(empty(self::$_forms[$name])) {
throw new Core_Form_Store_Exception;
}
// If the for is instanciated we return it
else if(self::$_forms[$name] instanceof Zend_Form) {
return self::$_forms[$name];
}
else {
// Instanciate the class and return it
$form = new self::$_forms[$name];
self::$_forms[$name] = $form;
return $form;
}
}
getForms() only calls getForm() for each available form. I tired to mimic the interface of the normal Zend_Form a bit when it comes to the names of functions and the order arguments are passed. Now I have it working and as a bonus I don't actually instantiate any form until I need it. And if I only request a specific form from the store class only that form is instantiate and saved for future access.
How about you set the action from the view when rendering the form, eg
<?php echo $this->form->setAction($this->url(array(
'controller' => 'foo',
'action' => 'bar'
))) ?>
I have separate db_table classes for books, book_sections and users (system end users). in book table has columns for book_title, section_id(book_section) , data_entered_user_id(who entered book info).
go to this url to see the image(I'm not allow to post images bacause I'm new to stackoverflow)
img685.imageshack.us/img685/9978/70932283.png
in the backend of the system I added a form to edit existing book (get book id from GET param and fill the relevant data to the form). form has elements for book_title, book_section and data_entered_user.
to get exciting book data to "edit book form" I join book_section and user tables with book table to get book_section_name and username(of data_entered_user: read only- display on side bar)
go to this url to see the image(I'm not allow to post images bacause I'm new to stackoverflow)
img155.imageshack.us/img155/2947/66239915.jpg
In class App_Model_Book extends Zend_Db_Table_Abstract
public function getBookData($id){
$select = $this->select();
$select->setIntegrityCheck(false);
$select->from('book', array('id','section_id','data_entered_user_id',...));
$select->joinInner('section','book.section_id = section.id',array('section_name' =>'section.name' ));
$select->joinInner(array('date_entered_user' => 'user'),'book.date_entered_user_id = date_entered_user.id',array('date_entered_user_name' =>'date_entered_user.user_name' ));
$select->where("book.id = ?",$id);
return $this->fetchRow($select);
}
public function updateBookData($id,$title,$section_id,...)
{
$existingRow = $this->fetchRow($this->select()->where('id=?',$id));
$existingRow->title = $title;
$existingRow->section_id = $section_id;
//....
$existingRow->save();
}
In Admin_BookController -> editAction()
$editForm = new Admin_Form_EditBook();
$id = $this->_getParam('id', false);
$bookTable = new App_Model_Book();
$book_data = $bookTable ->getBookData($id);
//set values on form to print on form when edit book
$editForm->book_title->setValue($book_data->title);
$editForm->book_section->setValue($book_data->section_name);
//........
//If form data valid
if($this->getRequest()->isPost() && $editForm->isValid($_POST)){
$bookTable = new App_Model_Book();
$bookTable ->updateBookData(
$id,
//get data from submitted form
$editForm->getValue('title'),
//....
);
when editing an exsiting book
get data from getBookData() method on App_Model_Book class
If form data is valid after submiting edited data, save data with updateBookData() method on App_Model_Book class
but I saw that if I created a custom
Db_Table_Row(extends Zend_Db_Table_Row)
class for book table with
book_section_name and
data_entered_user_name I can use it
for get existing book data and save it
after editing book data without
creating new Book(db_table) class and without calling updateBookData() to save updated data.But I don't know which code I should write on custom
Db_Table_Row(extends Zend_Db_Table_Row)
class.
I think you can understand my problem, in simple
how to write a custom db_table_row class to create a custom row with data
form 2 joined tables for a perticular db_table class ?
I'm new to zend framewok and to stackoverflow. forgive me if you confused with my first question.
Thanks again.
1) at your db_table class create field which contain row class, for example:
protected $_rowClass = 'App_Model_Db_Books_Row';
and add reference map for parent tables:
protected $_referenceMap = array(
'Section' => array(
'columns' => 'sectionId',
'refTableClass' => 'App_Model_Db_Sections',
'refColumns' => 'id'
),
'User' => array(
'columns' => 'userId',
'refTableClass' => 'App_Model_Db_Users',
'refColumns' => 'id'
)
);
2) at row class you must define variables for parent tables:
protected $section;
protected $user;
In such cases i create method called "load":
public function load()
{
$this->section = $this->findParentRow('App_Model_Db_Sections');
$this->user = $this->findParentRow('App_Model_Db_Users');
}
And in constructor i call this method:
public function __construct(array $config = array())
{
parent::__construct($config);
$this->load();
}
As a result after:
$book_data = $bookTable ->getBookData($id);
you can access data from reference table, for example:
$book_data->section->name = "Book Section";
$book_data->section->save();