I'm trying to set the prefix path for Zend Form's validators and filters automatically. I've done a lot of research and found many solutions, but I want these to always take effect, automatically. I know I can do this in the form:
$this->addElementPrefixPath('My_Validate', 'My/Validate/', 'validate');
or set them on an element such as
$input->addValidatorPrefixPath('Other_Namespace', 'Other/Namespace');
$input->addFilterPrefixPath('Foo_Namespace', 'Foo/Namespace');
but is there any way for these to automatically look into what's already set in the autoloader and/or be set in the bootstrap (or elsewhere), without having to set it ever again?
Here's my autoloader:
// Autoload libraries
$autoloader = Zend_Loader_Autoloader::getInstance();
$autoloader->registerNamespace('Lib1_')
->registerNamespace('Lib2_')
->registerNamespace('Lib3_');
Now, when I add a validator using
->addValidator('CustomValidator', false, 1)
I would like it to respect the hierarchy listed in the autoloader, falling back to Zend after that. I just haven't been able to find how to automatically bootstrap this kind of autoloading for the validators and filters.
Thanks!
The way I get settings like this into all of my Zend forms without having to repeat the code all over for each form is to create a base form class that extends Zend_Form, which in turn all of my other forms extend.
In the base form's constructor, I set up different decorators for various types of elements or customize the decorators for my application, specify prefix paths for helpers and validators and other things.
The important thing to note about this method, is you must call parent::__construct() as the very last line if your base form __construct method. The reason for this is because the Zend_Form::init() method gets called by Zend_Form::__construct() and nothing else from the constructor runs after that.
Here is an example:
<?php
class Application_Form_Base extends Zend_Form
{
// decorator spec for form elements like text, select etc.
public $elementDecorators = array(
'ViewHelper',
'Errors',
array('Description', array('tag' => 'p', 'class' => 'description', 'escape' => false)),
array('HtmlTag', array('class' => 'form-div')),
array('Label', array('class' => 'form-label', 'requiredSuffix' => '*'))
);
// decorator spec for checkboxes
public $checkboxDecorators = array(
'ViewHelper',
'Errors',
array('Label', array('class' => 'form-label', 'style' => 'display: inline', 'requiredSuffix' => '*', 'placement' => 'APPEND')),
array('HtmlTag', array('class' => 'form-div')),
array('Description', array('tag' => 'p', 'class' => 'description', 'escape' => false, 'placement' => 'APPEND')),
);
// decorator spec for submits and buttons
public $buttonDecorators = array(
'ViewHelper',
array('HtmlTag', array('tag' => 'div', 'class' => 'form-button'))
);
public function __construct()
{
// set the <form> decorators
$this->setDecorators(array(
'FormElements',
array('HtmlTag', array('tag' => 'div', 'class' => 'form')),
'Form'));
// set this as the default decorator for all elements added to the form
$this->setElementDecorators($this->elementDecorators, array('submit', 'button'), true);
// add prefix paths for decorators and validators
$this->addElementPrefixPath('My_Decorator', 'My/Decorator', 'decorator');
$this->addElementPrefixPath('My_Validator', 'My/Validator', 'validate');
parent::__construct();
// parent::__construct must be called last because it calls $form->init()
// and anything after it is not executed
}
}
Related
I'm just starting out with Doctrine and was rewriting some code to use Doctrine entities in some Forms.
I have an Entity Business which has some 1:n relations with addresses, employees, emails etc. the Setup is really basic and working fine.
To add new Businesses i created a BusinessForm and Fieldsets for each of my entities. Here the constructor of the form:
public function __construct($scenario='create', $entityManager = null) {
parent::__construct('business_form');
$this->scenario = $scenario;
$this->entityManager = $entityManager;
$this->setAttribute('method', 'post');
$businessFieldset = new BusinessFieldset($this->entityManager);
$businessFieldset->setUseAsBaseFieldset(true);
$this->add($businessFieldset);
$hydrator = new DoctrineHydrator($this->entityManager, new Business());
$this->setHydrator($hydrator);
$this->addElements();
$this->addInputFilter();
}
addElements just adds a Submit and CSRF input.
And here the Controller action:
public function addAction(){
$form = new BusinessForm('create', $this->entityManager);
if ($this->getRequest()->isPost()) {
$data = $this->params()->fromPost();
$form->setData($data);
if($form->isValid()) {
// save Object
return $this->redirect()->toRoute('subcontractor', ['action'=>'index']);
}
}
return new ViewModel([
'form' => $form
]);
}
The form validates and i can get the Data from the form with $form->getData(). But i cant figure out how to get a populated Object from the form using the form's hydrator. When I use setObject(new Business()) at the start of the controller i get an error while $form->isValid() is running :
Zend\Hydrator\ArraySerializable::extract expects the provided object to implement getArrayCopy()
Isnt that the wrong hydrator being called ?
If i dont setObject() but instead use $form->bind(new Business()) after the validation i get an empty Object from $form->getObject(). If i get the data and hydrate a new Object with the form's hydrator like so : $form->getHydrator()->hydrate($data['business], new Business()) i do get the populated Object i was expecting. (Business being the name of the base fieldset)
So my question is, how to i get the result of the last call from just using $form->getObject() after the validation?
[EDIT]
The Problem seems to be with the Collections of Fieldsets used as sub-fieldsets in the businessfieldset. If i validate the form without using the collections i do get the expected Business Object from $form->getData()
Here an example how i add the collection (in the business fieldset):
$this->add([
'name' => 'emails',
'type' => 'Zend\Form\Element\Collection',
'attributes' => [
'id' => 'business_emails',
],
'options' => [
'label' => 'Emails',
'count' => 1,
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'target_element' => [
'type' => 'LwsSubcontractor\Form\EmailFieldset',
],
'target_class' => 'LwsSubcontractor\Entity\Email'
],
]);
and here the EmailFieldset:
public function __construct() {
parent::__construct('email');
$this->setObject(new Email());
$this->addElements();
}
protected function addElements() {
$this->add([
'name' => 'email',
'type' => 'Zend\Form\Element\Email',
'attributes' => [
'placeholder' => 'E-Mail (z.B. email#muster-email.de)',
'class' => 'form-control',
'required' => true,
'size' => 50,
],
'options' => [
'label' => 'Email',
],
]);
}
}
If using the Collections i get the Error message from above. So after adding a hydrator to each Fieldset i was fine.
Although i was under the impression that setting the hydrator for the form would result in the used fieldsets to inherit that hydrator.Or was this because of using the fieldsets as collections and not directly ?
You have to add the hydrator to all your fieldsets, personally I use DoctrineModule\Stdlib\Hydrator\DoctrineObject for doctrine entities.
I would also look at using the init() method to initialize your forms and add elements then register and retrieve your form and fieldsets through the FormElementManager, $serviceLocator->get('FormElementManager')->get(yourFieldsetorForm::class). The form can than be injected into your controller.
I hope this helps.
In Symfony admin, I have a form, where the second field type depends on the ChoiceField value selected. The second field can be of Symfony Url field type or sonata provided sonata_type_model_list field type.
I have created an ajax request to My Bundle Controller to return the form, which contains the needed field.
> /src/MyBundle/Controller/MyController.php
namespace MyBundle\Controller
use Sonata\AdminBundle\Controller\CRUDController;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Sonata\AdminBundle\Form\FormMapper;
class MyController extends CRUDController
{
public function getFieldAction()
{
//getting the value of choice field
$type = $this->get('request')->get('type');
//sonata.admin.reference is a service name of ReferenceBundle admin class
$fieldDescription = $this->admin->getModelManager()
->getNewFieldDescriptionInstance($this->admin->getClass(), 'reference');
$fieldDescription->setAssociationAdmin($this->container->get('sonata.admin.reference'));
$fieldDescription->setAdmin($this->admin);
$fieldDescription->setAssociationMapping(array(
'fieldName' => 'reference',
'type' => ClassMetadataInfo::ONE_TO_MANY,
));
// Getting form mapper in controller:
$contractor = $this->container->get('sonata.admin.builder.orm_form');
$mapper = new FormMapper($contractor, $this->admin->getFormBuilder(), $this->admin);
$form_mapper = $mapper->add('reference', 'sonata_type_model_list', array(
'translation_domain' => 'ReferenceBundle',
'sonata_field_description' => $fieldDescription,
'class' => $this->container->get('sonata.admin.reference')->getClass(),
'model_manager' => $this->container->get('sonata.admin.reference')->getModelManager(),
'label' => 'Reference',
'required' => false,
));
//#ToDo build $form from $form_mapper
return $this->render('MyBundle:Form:field.view.html.twig', array(
'form' => $form->createView(),
));
}
}
I cannot find any method in Sonata\AdminBundle\Form\FormMapper class to build a form (it seems to be possible with create() method, but it only works with common Symfony field types, not Sonata form field types, which are commonly generated in Block or Admin classes).
Is it possible to use Sonata\AdminBundle\Form\FormMapper in the Controller to build a form?
Or is there another way I can build a form with Sonata form field types in the Controller?
You should not use a controller, rather a Sonata Admin Service.
Sonata provides you with the 'sonata_type_choice_field_mask' type which allows you to change the fields displayed on the form dynamically depending on the value of this 'sonata_type_choice_field_mask' input.
Here is the doc where you can find everything about sonata types and the choice field mask.
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('reference', 'sonata_type_choice_field_mask', array(
'choices' => array(
//The list of available 'Reference' here
'choice1',
'choice2'
),
'map' => array(
//What you want to display depending of the selected option
'choice1' => array(
// List of the fields displayed if choice 1 is selected
'field1', 'field3'
),
'choice2' => array(
// List of the fields displayed if choice 2 is selected
'field2', 'field3'
)
),
'placeholder' => 'Choose an option',
'required' => false
))
->add('field1', 'sonata_type_model_list', array(/* Options goes here */))
->add('field2', 'url', array(/* Options goes here */))
->add('field3')
;
}
It looks like you have access to the admin which directly handles building the form with Sonata\AdminBundle\Admin\AbstractAdmin::getForm.
$form = $this->admin->getForm();
I want to get the selected schoolFilter form value and place in [SELECTED VALUE HERE].
$data = $this->Js->get('#SchoolFilter')->serializeForm(array('isForm' => true, 'inline' => true));
$this->Js->get('#SchoolSchoolId')->event(
'change', $this->Js->request(
array('action' => 'assign_school_ads/', [SELECTED VALUE HERE], array(
'update' => '#results',
'data' => $data,
'async' => true,
'dataExpression' => true,
'method' => 'POST'
)
)
);
// School Filter
$schoolFilter = $this->Form->create('School', array('id' => 'SchoolFilter');
$schoolFilter .= $this->Form->input('school_id', array('label'=>'Schools', 'empty'=>'- select -');
$schoolFilter .= $this->Form->end();
I have seen variations on this question but without any clear answer, except to just forget using JS Helper. Is it possible within the context of JS Helper? And if not, can I get the value using regular JQuery, then inject it into JS Helper.
Use following code :-
$this->Js->get('#SchoolFilter');
$this->Js->event('change', $this->Js->request(array('controller'=>'put-ur-controller-ame-here','action' => 'assign_school_ads'),array('async' => true,'update' => '#results','method' => 'post','dataExpression'=>true,'data'=> $this->Js->serializeForm(array('isForm' => true,'inline' => true)))));
the serialize() function sends the form data to the php action so we can see which option was selected and decide what to update in the ajax call.
the form data will be found in $this->data in the action (just like after a form has been submited).
Don't forget to add $this->Js->writeBuffer(); in your layout just before the body closing tag. Otherwise all the ajax code will not be added to your page.
I am currently working on a custom module add-on and I wanted to be able to use sorting and filtering on the a table in my control panel admin. I am using the EE table class and form helper. I'm trying to follow the documentation here for setting it up, but when I call try to call the '_datasource' method in my class I get this error
Fatal error: Call to undefined method Content_publish::_datasource() in /home/public_html/system/expressionengine/libraries/EE_Table.php on line 162
I have a feeling it's a scoping issue, but in the table class '$this->EE->table->datasource()' method you are supposed to just pass a string value with the name of your datasource function which is what I'm doing.
I don't seem to be the only one with this issue. There are more details and code examples on this EE Discussion forum thread
The documentation is not really clear. I also tried looking at EE's own comments module to see if i could figure it out, but no luck. Anyone have experience with this?
Here is the method I'm calling:
$data = $this->EE->table->datasource('_datasource');
And this is my function in my class:
function _datasource()
{
// ....
// $query comes from DB result set code above.
// I have omitted it here for brevity
$datarows = array();
foreach ($query->result_array() as $key => $row)
{
$datarows[] = array(
'entry_id' => $row['entry_id'],
'date' => date('Y-m-d',$row['entry_date']),
'author' => $row['screen_name'],
'payment' => $payment_amount,
'status' => $status,
'title' => $edit_href.$row['title']."</a>"
);
}
return $datarows;
}
Your datasource callback function must be on your Module_mcp class (looking at your forum thread you are trying to use it on a plugin which would explain the error).
If you want to put the datasource method on a different class, then just add this line right before you call datasource() to trick the table library into using the correct class:
// ensure table callbacks use this class rather than our MCP file
$this->EE->_mcp_reference =& $this;
$data = $this->EE->table->datasource('_datasource');
The table and form_validation libraries are the only two which use the special _mcp_reference variable, so I can't see any side effects to changing it, and have successfully done this in at least two modules.
On a side note, if you want a good example of how to use the built in tablesorter, take a look at system/expressionengine/controllers/cp/members.php. The documentation is pretty bad, but the source code always tells the truth :)
I've been having issues too and have a mixed solution of generate() and datasource working. Here it is here:
In my mcp file:
public function index()
{
$this->EE->cp->set_variable('cp_page_title', lang('my_module_name'));
$data = $this->EE->table->datasource('_datasource');
return $this->EE->load->view('index', $data, TRUE);
}
public function _datasource()
{
$headers = array(
'name' => array('header' => 'Name'),
'color' => array('header' => 'Color'),
'size' => array('header' => 'Size')
);
$rows = array(
array('name' => 'Fred', 'color' => 'Blue', 'size' => 'Small'),
array('name' => 'Mary', 'color' => 'Red', 'size' => 'Large'),
array('name' => 'John', 'color' => 'Green', 'size' => 'Medium'),
);
return array(
'rows' => $rows,
'headers' => $headers
);
}
In my index view file:
$this->table->set_columns($headers);
$this->table->set_data($rows);
echo $this->table->generate();
Seems to be working at the moment and I've not tried pagination yet, but sorting works.
I'm trying to make a form in Drupal, so more than one element can be added to a form. For example, a page might contain data for an event, then the event might have multiple dates. So I have a form that looks like:
/**
* Implements hook_form_alter().
*/
function addextra_form_alter(&$form, &$form_state) {
if ($form['#form_id'] == 'test_content_node_form' ) {
$form['elements_table'] = array(
'#theme' => 'table',
'#title' => 'Elements already added',
'#header' => array('Item', 'Remove'),
'#empty' => 'No elements',
'#prefix' => '<div id="elements-table">',
'#suffix' => '</div>',
);
$form['add_elements'] = array(
'#title' => 'Add another element',
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['add_elements']['add_content'] = array(
'#type' => 'textfield',
'#description' => t('Add an element to the table'),
'#title' => t('Add another item'),
'#size' => '12',
'#maxlength' => '60',
'#prefix' => '<div id="addextra_content">',
'#suffix' => '</div>',
);
$form['add_elements']['add_another_btn'] = array(
'#type' => 'button',
'#name' => 'add_another',
'#button_type' => 'submit',
'#executes_submit_callback' => FALSE,
'#value' => 'Add another',
'#ajax' => array(
'callback' => 'addextra_element_to_table',
),
);
}
}
When 'add_another_btn' gets clicked, it will run the ajax callback 'addextra_element_to_table.
That callback is:
function addextra_element_to_table(&$form, &$form_state) {
$form['elements_table']['#rows'][] = array($form_state['values']['add_content'], l('Remove Item', '#'));
drupal_add_js(drupal_get_path('module', 'addextra') . '/addextra.js');
return array(
'#type' => 'ajax',
'#commands' => array(
ajax_command_replace('#elements-table', render($form['elements_table'])),
),
);
}
The js file called replaces the val of the input field to ''
(function ($) {
$('#edit-add-content').val('');
})(jQuery);
But this callback only gets called one time. I believe this is because the behaviour has to be attached again once it's been called. Sorry for my ignorance - I'm not sure how to achieve this. Can anyone help me out? It would be much appreciated. Thanks in advance.
The problem is that render(), which basically just calls drupal_render() , does not process the #ajax element, it's simply ignored. You might want to try and pass the element through ajax_pre_render_element() before the call to render().
That said, I personally don't have good experience with trying to reuse Drupal functions outside their normal calling sequence, especially not with forms. I prefer to stick to the very top level functions, such as drupal_get_form. I have followed those functions many times in my debugger, and they do a bunch of things in a precise order that is hard to get a hold of when you want to reuse pieces of that.
To alter a form with AJAX callbacks, I would always prefer one of two strategies:
In the callback, adjust the content of the $form argument and do return $form. This requires that you set #ajax['wrapper'] to the id (the value of the id attribute in the markup, as used for CSS) of the form on the originating element (the button in your case). Then Drupal gets to do its shpiel with the whole form, and the browser replaces the whole thing. Drupal takes care of preserving values already entered etc.
Alternatively, you can have the callback return a set of commands that do very specific modifications on the DOM. In your case that would be commands which create and append new rows. Keep in mind that with ajax_command_invoke(), you have the entire jQuery arsenal at your disposal.
From those two strategies, I usually prefer the second one, because it seems more elegant for little tweaks. However, if you want to build on Drupal's rendering, or if you have more massive changes to the form, you should use the first one.
As a side note, did you know that there is drupal.stackexchange.com? Drupal can be quite peculiar, and on that site, you'll find more experts on the specifics.