ZF2: How do I implement a custom filter? - filter

I've been working with Zend Framework 2 for a few weeks now, and, even though the documentation online is pretty incomplete, I managed to build up a first draft of my website.
Unfortunately, I got stuck while trying to implement a custom version of the Zend\Filter\File\Rename filter; here I sum up what I've done:
1) In my module called 'Library', created the file
src\Library\Filter\File\Rename.php
namespace Library\Filter\File;
use Traversable;
use Zend\Filter;
use Zend\Filter\Exception;
use Zend\Stdlib\ArrayUtils;
class Rename extends Filter\AbstractFilter {
static $uploadDir = '/srv/default/htdocs/public/uploads/';
public function filter($value) {
do {
$newname = md5(uniqid(time()));
} while(file_exists(self::uploadDir . $newname);
if (rename($value, self::uploadDir . $newname) !== true)
throw new Exception\RuntimeException(sprintf("File '%s' could not be renamed. An error occurred while processing the file.", $value));
return self::uploadDir . $newname;
}
}
As you can see, is pretty simple. Here's the module config:
module.config.php
<?php
return array(
'controllers' => array(
// Invokables don't support dependencies
'invokables' => array(
'myFileRename' => 'Library\Filter\File\Rename',
),
));
While the form is:
[...]
public function getInputFilterSpecification() {
return array(
'file' => array(
'required' => false,
'filters' => array(
// Custom Renamer
array('name' => 'myFileRename'),
),
)
);
}
}
?>
But this is not working, as I always get an exception:
Fatal error: Uncaught exception 'Zend\ServiceManager\Exception\ServiceNotFoundException' with message 'Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for myFileRename' in /srv/default/vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php:420
Stack trace:
#0 /srv/default/vendor/ZF2/library/Zend/ServiceManager/AbstractPluginManager.php(108): Zend\ServiceManager\ServiceManager->get('myfilerename', true)
#1 /srv/default/vendor/ZF2/library/Zend/Filter/FilterChain.php(179): Zend\ServiceManager\AbstractPluginManager->get('myfilerename', NULL)
#2 /srv/default/vendor/ZF2/library/Zend/InputFilter/Factory.php(263): Zend\Filter\FilterChain->attachByName('myfilerename', Array)
#3 /srv/default/vendor/ZF2/library/Zend/InputFilter/Factory.php(171): Zend\InputFilter\Factory->populateFilters(Object(Zend\Filter\FilterChain), Array)
#4 /srv/default/vendor/ZF2/library/Zend/InputFilter/Factory.php(237): Zend\InputFilter\Factory->createInput(Array)
#5 /srv/default/vendor/ZF2/library/Zend/Form/Form.php(672) in /srv/default/vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php on line 420
Unfortunately I found no article on the entire internet to help me with this, and I still get this error even when aliasing my filter as a factory :/ What do I have to do?

You load the filter as if it is a controller (hence, the controller key in your module config). The filter has a plugin manager, but for this filter you do not need the plugin manager. There is also no link in the filter plugin manager with the MVC configuration.
The most simple way is to just instantiate the filter yourself. It should be something like this that would work:
use Library\Filter\File\Rename as RenameFilter;
public function getInputFilterSpecification()
{
return array(
'file' => array(
'required' => false,
'filters' => array(
new RenameFilter,
),
)
);
}

just use:
'name' => 'file',
'required' => false,
'filters' => array(
array('name' => 'Library\Filter\File\Rename'),
),
had the same problem... this was the solution for me...

Related

ZF2 using inputFilter

I wonder what I've made wrong, I want to save in db some values whats not come form POST or GET:
public function saveAction()
{
$wikiTable = $this->getServiceLocator()->get('WikiTable');
$data = array('source' => $someVal);
$form = new WikiForm();
$inputFilter = new \MyApp\Form\WikiFilter();
$form->setInputFilter($inputFilter);
$form->setData($data);
$this->saveWiki($form->getData());
//$this->saveWiki($data);
}
WikiFilter:
$this->add(
array(
'name' => 'source',
'required' => false,
'filters' => array(
array('name' => 'StripTags'),
array('name' => 'StringTrim'),
)
)
);
And Form:
$this->add(array(
'name' => 'source',
'type' => 'Zend\Form\Element\Hidden',
'options' => array(
'label' => 'source',
)
));
In response I recive error:
Zend\Form\Form::getData cannot return data as validation has not yet
occurred
After this line:
$form->setData($data);
You need to put the rest of your code into something like this:
if($form->isValid()){
$this->saveWiki($form->getData());
}
Otherwise your form isn't validated and you won't get any data back from it by calling $form->getData()
So whenever you work with a form (not matter if the data come from a POST request or not) make sure to call the function isValid() on the form variable because otherwise you won't get the data back and you will get the error you wrote before

ZF2 + Duplicate Form Validation on composite key

I have a Form having primary key on two fields (gid, bid). I need to add validation to block duplicate entries into database.
I have checked with ZF2 Solution for this . http://framework.zend.com/manual/2.2/en/modules/zend.validator.db.html#excluding-records . While this approach of handling composite keys is not look the ideal way, But still I am trying it because it look like only buil-in way. Now it require me to provide second field's value (value option in exclude), which is again a problem. As I am trying it
$inputFilter->add(array(
'name' => 'gid',
'required' => true,
'validators' => array(
array(
'name' => 'NotEmpty',
'options' => array(
'messages' => array(
'isEmpty' => 'required'
),
),
),
array (
'name' => 'Zend\Validator\Db\NoRecordExists',
'options' => array (
'table' => 'gtable',
'field' => 'gid',
'adapter' => $this->dbAdapter,
'messages' => array(
\Zend\Validator\Db\NoRecordExists::ERROR_RECORD_FOUND => 'The specified key already exists in database'
),
'exclude' => array(
'field' => 'bid',
'value' => [?],
),
)
),
)
));
How do I get this value, As Form is absolute separate Class/File than controller where I have the submitted form values. Is some better architecture solution of this problem exists Or Some hack to pass submitted field value to Form Class is only solution ?
Note : I am not in favor of Build My Validation Plugin for this task as short time is constraint for functionality.
You can do all the job in your form. To achieve that, you could define your forms as factories in your module Module.php.
Module.php
use MyNamespace\MyForm;
//NOTE THAT THE SERVICE MANAGER IS INJECTED. YOUR FORM COULD RECEIVE IT THROUGH THE CONSTRUCTOR
public function getServiceConfig()
{
return array(
'factories' => array(
'my_form' => function( $sm ) {
$form = new MyForm( $sm );
return $form;
},
),
);
}
When you want to use the form is as easy as use this code in your controller:
class MyController extends AbstractActionController
{
public function createAction() {
$form = $this->getServiceLocator()->get( 'my_form' ) );
(...)
}
}
And your MyForm.php
use Zend\Form\Form;
class MyForm extends Form
{
public $serviceManager, $request, $postData;
public function __construct( $serviceManager ) {
parent::__construct( null );
$this->serviceManager = $serviceManager;
$this->request = $serviceManager->get( 'Application')->getMvcEvent()->getRequest();
$this->postData = get_object_vars( $this->request->getPost() );
}
}
This way you can get advantage of the Service Manager within your form. And the public postData, where you'll find the bid value you're looking for to build your NoRecordExists filter.
You could add the parameters to the getInputFilter, like this :
getInputFilter($gid, $bid)
And then on the controller, when you set the filter you pass the 2 parameters, and then just check as $form->isValid(); ...
Alternative try this:
array(
'name' => 'Db\NoRecordExists',
'options' => array(
'table' => 'gtable',
'field' => 'gid',
'adapter' => $this->dbAdapter,
),
),
I'm unsure on your use case. If you were to add a database entry the primary keys for that table would not be known until you insert anyway - If you have foreign key constraints you could handle the exception from the database.
I am not in favor of Build My Validation Plugin for this task
The validator is also not designed to validate multiple fields as they are attached to a form element on a 1-1 basis. You will therefore need to create your own.
The below example has NOT been tested, so take it as an example of the approach rather than working code.
The key bit is the isValid method.
namespace MyModule\Validator\Db;
use Zend\Validator\Db\NoRecordExists;
class CompositeNoRecordExists extends NoRecordExists
{
protected $field2;
protected $field2Value;
public function __construct($options = null)
{
parent::__construct($options);
if (array_key_exists('field2', $options)) {
$this->setField2($options['field2']);
} else {
throw new \BadMethodCallException('Missing field2 option!');
}
}
protected function setField2Value(array $context)
{
if (! isset($context[$this->field2])) {
throw new \BadMethodCallException('Unable to find value for field 2');
}
$this->field2Value = $context[$this->field2];
}
public function isValid($value)
{
// The isValid() method is actually given a 2nd argument called $context
// Which is injected by the inputFilter, via the input and into the validator chain
// $context contains all of RAW form element values, keyed by thier element name.
// Unfortunately due to the ValidatorInterface you are unable to add this to the method
// signature. So you will need to be 'creative':
$args = func_get_args();
if (isset($args[1]) && is_array($args[1])) {
$this->setField2Value($args[1]);
} else {
throw new \BadMethodCallException('Missing validator context');
}
return parent::isValid($value);
}
public function getSelect()
{
$select = parent::getSelect();
$select->where->equalTo($this->field2, $this->field2Value);
return $select;
}
}
Then all you would need to do is update the validator config, adding the field2 field name.
array (
'name' => 'MyModule\Validator\Db\CompositeNoRecordExists',
'options' => array (
'table' => 'gtable',
'field' => 'gid',
'field2' => 'bid',
'adapter' => $this->dbAdapter,
'messages' => array(
\Zend\Validator\Db\NoRecordExists::ERROR_RECORD_FOUND => 'The specified key already exists in database'
),
)
),

filter and CActiveDataProvider in CGridView

I have these code in index action of controller:
public function actionIndex()
{
$cid = #$_GET['cid'];
$country = Country::model()->findByPk($cid);
if($cid)
$dataProvider=new CActiveDataProvider('City', array(
'criteria'=>array(
'condition'=>'ci_co_id ='.$cid,
),
));
else
$dataProvider=new CActiveDataProvider('City');
$this->render('index',array(
'dataProvider'=>$dataProvider,
'country' => $country
));
}
and these in view/index.php file:
<?php
$this->widget('zii.widgets.grid.CGridView', array(
'id'=>'city-grid',
'dataProvider'=>$dataProvider,
'filter' => $dataProvider,
'columns'=>array(
array(
'name' => ' ',
'value' => '$row + 1',
),
'ci_name',
'ci_pcode',
array(
'class'=>'CButtonColumn',
),
)
));
?>
but Yii gives me this error:
CActiveDataProvider and its behaviors do not have a method or closure named "getValidators".
What is the problem?
The filter has to be a class that extends CModel. However, you don't seem to be doing any actual filtering, so you can just comment the filter line of your CGridView out.
As a side note, you have major security hole in your criteria. You are leaving yourself wide open to an SQL injection attack.
Specify your criteria as follows to let the database handler properly escape the input:
'criteria'=>array(
'condition'=>'ci_co_id =:cid',
'params'=>array(':cid'=>$cid),
),

Expression Engine Module tables and datasort

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.

Setting an Error Message located in language file

In a form I have a callback function that checks if a number is_available.
If it returns TRUE it shows an error message.
The callback is working but it displays : lang:shortcodes.not_unique instead of the content given in a separate file.
I can't figure out what is wrong and didn't find about that in the user guide.
Thank you for your help.
public function __construct()
{
parent::__construct();
// Load all the required classes
$this->load->model('shortcodes_m');
$this->load->library('form_validation');
$this->lang->load('shortcodes');
// Set the validation rules
$this->item_validation_rules = array(
array(
'field' => 'number',
'label' => 'lang:shortcodes.number',
'rules' => 'trim|max_length[100]|required|numeric'
),
array(
'field' => 'name',
'label' => 'lang:shortcodes.name',
'rules' => 'trim|max_length[100]|required|callback_shortcodes_check'
)
);
}
public function shortcodes_check($str)
{
if($this->shortcodes_m->is_available($str) == TRUE)
{
$this->form_validation->set_message('shortcodes_check','lang:shortcodes.not_unique');
return FALSE;
}
else
{
return TRUE;
}
}
You need to fetch the line from the language file. The docs don't make any mention of being able to use field name translation with the set_message() method. Use:
$this->lang->line('not_unique');

Resources