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; ?>
Related
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!
I have a project using some components from Symfony. Namely Twig, Doctrine, and Form.
I would like to modify all form field types to be able to take a new 'suffix' argument when they are created. It seems like this is usually simple in the full Symfony stack and could be done by extending the existing form type. However I'm not sure how to get the form component to load my custom extension when using the Form Component standalone.
Any help would be great, thank you!
Okay, if I understood your question correctly, what you basically want to do, is add new option to your form builder for given type.
index.php - Basically here, we will just create a FormBuilder instance and add one field for testing purpose.
<?php
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
$formFactory = Forms::createFormFactoryBuilder()
->getFormFactory()
->createBuilder()
->add('test', 'text', array('customAttribute' => true))
->getForm()
->createView();
If we open the browser right now, we will get nice and big error, telling us that "customAttribute" is unknown option.
So, let's create custom form type! As you saw I named it TextCustomType since I will extends "text" form type.
The Type class:
<?php
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class TextCustomType extends AbstractTypeExtension {
public function getName() {
return "text";
}
public function getExtendedType() {
return "text";
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setOptional( array('customAttribute') );
$resolver->setDefaults( array('customAttribute' => true) );
}
public function buildView(FormView $view, FormInterface $form, array $options) {
$view->vars['customAttribute'] = $options['customAttribute'];
}
}
Now, we created our custom type, so lets add it to the form factory:
$formFactory = Forms::createFormFactoryBuilder()
->addTypeExtension( new TextCustomType() ) // once the class is loaded simply pass fresh instance to ->addTypeExtension() method.
->getFormFactory()
->createBuilder()
->add('test', 'text', array('customAttribute' => true))
->getForm()
->createView();
Refresh your browser, and you should be good to go! Hope you got the idea.
Updated as per OP's suggestion.
The answer is pretty simple! It was just a matter of looking in the right place. The FormFactoryBuilder is the key:
use Symfony\Form\Component\Form\Forms;
$form = Forms::createFormFactoryBuilder()
->addTypeExtension(new MyExtension())
->getFormFactory()
->create();
This $form variable now knows about my new 'suffix' property.
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();
I have two Zend_Forms (form1 and form2). I would like to combine them so I have a third form (form3) consisting of all the elements from both forms.
What is the correct way to do this with the Zend Framework?
Here's how I ended up doing it...I didn't want to namespace each form, I just wanted all the elements in the form so I decided to just add all the elements individually instead of using subForms.
<?php
class Form_DuplicateUser extends Zend_Form
{
public function init()
{
$this->setMethod('post');
$form1 = new Form_ContactPrimaryInformationForm();
$this->addElements($form1->getElements());
$form2 = new Form_ContactAdditionalInformationForm();
$this->addElements($form2->getElements());
}
}
You can use subforms. The only difference between Zend_Form and Zend_Form_SubForm are the decorators:
$form1 = new Zend_Form();
// ... add elements to $form1
$form2 = new Zend_Form();
// ... add elements to $form2
/* Tricky part:
* Have a look at Zend_Form_SubForm and see what decorators it uses.
*/
$form1->setDecorators(array(/* the decorators you've seen */));
$form2->setDecorators(array(/* ... */));
$combinedForm = new Zend_Form();
$combinedForm->addSubForm('form_1', $form1);
$combinedForm->addSubForm('form_2', $form2);
Then in the controller you assign the form to the view:
$this->view->form = $combinedForm;
And you can acces the two subforms in the view by name:
// In the view
echo $this->form->form_1;
echo $this->form->form_2;