RSS
 

Archive for the ‘CakePHP’ Category

New year, new cake version – 2.1

09 Jan

The new version 2.1 is almost fully backwards compatible.

The migration guide from 2.0 to 2.1 can be found at
book.cakephp.org/2.0/en/appendices/2-1-migration-guide.html

New stuff for the upgrade shell

I used this chance to enhance my version of the upgrade shell and added – besides some minor fixes – a command `cake21`. It should take care of all api standard updates for 2.1: git commit1 and 2
The Upgrade Plugin can be found here.

It currently updates Auth::allow() and layout stuff ($content_for_layout, $scripts_for_layout etc).

Feel free to add missing replacements.

 
No Comments

Posted in CakePHP

 

Tools Plugin – Part 2: Contact Form

15 Dec

I want to show how easy it is to make a solid and universal contact form in Cake(2).
The full code can be found in the github rep of the Tools plugin.

Model

The most important part first: We need a solid validation for the form. How many forms are out there that do not have any validation. The first thing I always do on about any public contact form: Hit the submit button and smile it if tells me that my empty form “has just been successfully sent”. We can do better than that :)

class ContactForm extends ToolsAppModel {
 
	protected $_schema = array(
		'name' => array('type' => 'string' , 'null' => false, 'default' => '', 'length' => '30'),
		'email' => array('type' => 'string' , 'null' => false, 'default' => '', 'length' => '60'),
		'subject' => array('type' => 'string' , 'null' => false, 'default' => '', 'length' => '60'),
		'message' => array('type' => 'text' , 'null' => false, 'default' => ''),
	);
 
	public $useTable = false;
 
	public $validate = array(
		'name' => array(
			'notEmpty' => array(
				'rule' => array('notEmpty'),
				'message' => 'valErrMandatoryField',
				'last' => true
			)
		),
		...
	);

The _schema var mocks a database table so that we don’t really need one. This helps the FormHelper to generate the inputs (maxlength, type, …).
The validation rules will make sure the email is valid and the user actually entered some text.

Controller

The logic cakes care of the validation on POST.
/Controller/ContactController.php

<?php
class ContactController extends AppController {
 
	public $uses = array('Tools.ContactForm');
 
	public function index() {
		if ($this->request->is('post') || $this->request->is('put')) {
			if (!$this->Session->check('Auth.User.id')) {
				$this->ContactForm->Behaviors->attach('Tools.Captcha');
			}
			$this->ContactForm->set($this->request->data);
			if ($this->ContactForm->validates()) {
				$name = $this->request->data['ContactForm']['name'];
				$email = $this->request->data['ContactForm']['email'];
				$message = $this->request->data['ContactForm']['message'];
 
				//send email with CakeEmail
			} else {
				$this->Common->flashMessage(__('formContainsErrors'), 'error');
			}
 
		}
		$this->helpers = array_merge($this->helpers, array('Tools.Captcha'));
	}
}

As you can see it only sends emails after successfully validating.
For public contact forms I usually like some easy captcha behavior attached so that spam doesnt reach me. You can omit that, of course.
Also note: you should use your own setFlash() method instead of mine!

Last but not least: View

/View/Contact/index.ctp

<?php echo $this->Form->create('ContactForm');?>
	<fieldset>
		<legend><?php echo __('contactLegend');?></legend>
	<?php
		echo $this->Form->input('name');
		echo $this->Form->input('email');
 
		echo $this->Form->input('subject');
		echo $this->Form->input('message', array('rows'=>15));
 
		if (!$this->Session->read('Auth.User.id')) {
			echo $this->Captcha->input('ContactForm');
		}
	?>
	</fieldset>
<?php echo $this->Form->submit(__('Submit')); ?>
<?php echo $this->Form->end();?>

Result

Browse to /contact/
That should display the form right away.
After the successful POST you should redirect back to the contact form (emptied then) or to another custom page.

Final notes

Currently, the Model is in the Tools.Plugin. You could put it into your normal app model folder, as well.
But I use it in many projects and therefore I want to keep it dry. Feel free to adjust any of the code to your own needs. Same goes for my own custom methods like $this->Common->flashMessage() etc.

 
No Comments

Posted in CakePHP

 

Unit-Testing Tips for 2.0 and PHPUnit

04 Dec

Quite some time ago I wrote about Unit testing. But that was still in 1.3 and with SimpleTest.
A lot has changed since then.

Execution Order

The documentation wasn’t all that clear about it. So I tried it with the following test file:

App::uses('MyCakeTestCase', 'Tools.Lib');
 
class TestCaseExecutionOrderTest extends MyCakeTestCase {
 
	public function setUp() {
		parent::setUp();
 
		$this->out('setUp');
	}
 
	public function tearDown() {
		$this->out('tearDown');
 
		parent::tearDown();
	}
 
	public function startTest() {
		$this->out('startTest');
	}
 
	public function endTest() {
		$this->out('endTest');
	}
 
 
	public function testFoo() {
		$this->out('* foo *');
	}
 
	public function testBar() {
		$this->out('* bar *');
	}
 
}

Note the parent calls for setUp and tearDown. Those need to be set. The other 2 methods don’t require this.

The result was pretty obvious:

setUp
startTest
* foo *
endTest
tearDown
 
setUp
startTest
* bar *
endTest
tearDown

Debug Output with PHPUnit >= 3.6

On the release notes of CakePHP2.0.3 you could find the note

A big difference people will notice when writing unit tests is that all output is swallowed by PHPUnit and not presented in either the web tester page nor in the CLI tester. To overcome this annoyance use the--debug modifier if you are using the CLI interface

Well, that broke a lot of existing debug code, of course.
So I tried to come up with a solution. The Shell uses a specific output(). Why not use sth like that in test cases, too?

/**
 * outputs debug information during a web tester (browser) test case
 * since PHPUnit>=3.6 swallowes all output by default
 * this is a convenience output handler since debug() or pr() have no effect
 * @param mixed $data
 * @param bool $pre should a pre tag be enclosed around the output
 * @return void
 * 2011-12-04 ms
 */
public function out($data, $pre = true) {
	if ($pre) {
		$data = pre($data);
	}
	echo $data;
	if (empty($_SERVER['HTTP_HOST'])) {
		# cli mode / shell access: use the --debug modifier if you are using the CLI interface
		return;
	}
	ob_flush();
}

As you probably noticed, I use it in the above “execution tryout”.
You can make yourself a custom “MyCakeTestCase” class with then extends the core test case class and put it in there.

UPDATE January 2012

The core (>=2.0) now contains a native way to enable debugging output for your tests.
You need to append &debug=1 to the url. Not ideal, but it works.

 
 

Serving views as files in Cake2

21 Nov

Actually, its not that different in Cake1.3. But as I just played around with it in 2.0, I will stick to that version for examples.

How to start

Skip this, if you want to cut to the chase.

In your routes.php you need to add Router::parseExtensions(); (or only specific ones).
That tells cake that urls ending with “.xyz” will be served as files (either inline or as download attachment).

Setup

Let’s say you want to display an invoice as pdf. The normal url is /invoices/view/1.
Now, we set a link in the view to the file like so:

$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1));

Since cake automatically detects that this will be a pdf file, it will
a) set the correct header (application/pdf)
b) will try to find the specific pdf view in /View/Invoices/pdf/index.ctp and the pdf layout in /View/Layouts/pdf/default.ctp

That’ all :)

Download it right away

Files like pdf can be displayed inline. So the browser will usually not force you to download it.
If you want this, though, you need to call $this->request->download($filename); in your controller action or in your layout.

Note: If your browser does not understand the file format (in this case pdf) it will probably trigger the download right away.

Browser bugs and easter eggs

Well, you could also call it a bug. But during my tryouts I found out that files served inline (Content-Disposition: inline; filename=”…”) will not use the given filename on save. They will be saved with the name in the url instead. In the above example it would be “1.pdf”. I did some research: Thats a well known browser deficiency that nobody yet fixed. Or so it seems.
Ok, but nobody wants his invoice to be “1.pdf”. So what can we do about it?

I found a pretty well working workaround:

$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1, 'invoice-2011-11-01_some_customer_tag'));

As you can see we simply add our filename to the url – after the id (!).

Since it is only “filename cosmetics” we dont need to add this second passed param to our method:

public function view($id = null) {}

It will be ignored in the action itself.

So the generated url is /invoices/view/1/invoice-2011-11-01_some_customer_tag.pdf and will result in a file saved as “invoice-2011-11-01_some_customer_tag.pdf”.
Job done.

Let me know what you think.

Example for PDFs

A pretty quick example how to output your content as pdf using DOMPDF.

In your layout (default.ctp in /pdf/):

App::import('Vendor', 'dompdf/dompdf.php');
$dompdf = new DOMPDF();
$dompdf->load_html(utf8_decode($content_for_layout), Configure::read('App.encoding'));
$dompdf->render();
echo $dompdf->output();

For me it seemed that without utf8_decode the DOMPDF lib seems to be buggy – although it claims to support utf8.

 
1 Comment

Posted in CakePHP

 

Freshly baked Cake2.0 tips

31 Oct

With 2.0 there are many new features available.
Some of them I want to introduce here.

Using the console for 2.0

I once wrote about how to use the Cake1.3 console.
The 2.0 shell is not so much different. The Console folder was only moved to /lib/Cake/Command/.
Make sure you use this new path. Also note, that it now seems to be important to have a “base app”.
You can’t just use the console anymore anywhere you like. It needs to have an app to get configuration and additional information on.
It also might be important which direction you are using the console – at least in 2.0 now. Being in the Command path with -app param might result in sth different than using the default way of using the console from within your current app directory.
So you should always go with the latter:

E:\...\trunk\app\>..\lib\Cake\Console\cake [command]

And if you put your “…/Console” path in your System Environment path (see the other article for details), you can use “cake” as standalone again:

E:\...\trunk\app\>cake [command]

Cronjobs now work this way (using crontab):

*/30 * * * * /srv/www/.../trunk/vendors/cakeshell app -cli /usr/bin -console /srv/www/.../trunk/lib/Cake/Console -app /srv/www/.../trunk/site >> /tmp/log.log

This runs a the AppShell (defined in your /App/Console/Command/ folder) every 30 minutes and logs errors/results to the given log file.

Note: cakeshell is a file containing the script from the cookbook.
Also note: If you use windows you might need to run “dos2unix /…/Cake/Console/cake” in order to make the cake console working.

Upgrade using the upgrade shell

cake upgrade all -params

I use an enhanced shell which provides more functionality.
For params I use –svn. It cakes care of the moving process if you use subversion. git is also supported.
Even if you already upgraded you should run it. Just in case you missed anything. The shell also auto-corrects some styling errors or deprecated syntax.

Rendering elements from helpers

This is now possible because the View is available in the helpers now.
So inside a helper method just do:

$elementContent = $this->_View->element('...');

Note: element() now has three params. The second still is the one where you can pass variables to the element scope. But the third param is now the options param (cache, plugin, callbacks, …).

It is even possible to alter the output from a helper directly:

$output = $this->_View->output; // contains the current rendered output

If you need any information on the current url or params:

$this->_View->request // contains all information about the current request

Overriding plugin views in the app

Another feature that finally arrived. Now plugins don’t have to cover all possible cases.
For specific apps you can override the plugin view locally.
Place the view in /app/View/Plugin/PluginName/Controller/action.ctp. The same goes for layouts, of course.

Enhancing objects and aliasing

public $helpers = array(
    'Html' => array(
        'className' => 'Tools.HtmlExt' // Tools Plugin - HtmlExtHelper
    )
);

Declared anywhere in the code the HtmlExt helper will then be used instead of the core Html helper.
The important change is, that it will use the old namespace, though. $this->Html is still the right way to use it. So you can enhance every helper, component, … to suite your needs without having to change the existing code itself.

This opens whole new possibilities. Until now you would have used a FormExt helper and manually called it with $this->FormExt->foo().
For 2.0 I use and recommend the following structure:
Add Ext to helpers/components etc from the core you want to Extend:
- FormExt(Helper)
- RequestHandlerExt(Component)
alias them as above to the original one.
Best to place them in a plugin all your apps have in common (Tools in my case).
Now if you want to go even further, you can override that one again specifically for this app by prefixing it with My:
- MyForm
- MyFormExt

All app specific helpers are not in a plugin but the app dir structure and start with My.
And using aliasing you can still call the helper with $this->Form->foo().
Same goes for components, behaviors, …

Working with exceptions

$post = $this->Post->read(null, $id);
if (!$post) {
	throw new NotFoundException();
}

The exception should then display a 404 error page.

Including scripts

Before 2.0 you always used App::import();
But with the switch to App::uses() the files do not get included anymore at startup due to the Super-lazy-loading. This should only be used for real classes. Also start to group by PackageName:

App::uses('GeolocationLib', 'Tools.Geo');  # in /Plugin/Tools/Geo

It will only be included by Cake if actually needed. One reason why Cake2 is so much faster.

If you have a file without a class (containing functions or constants or whatever) you still need to use the old method:

App::import('Lib', 'Tools.Bootstrap/MyBootstrap'); # in package "Bootstrap"

Using own classes instead of core ones

Let’s say, you want to apply a fix to a core file without overriding the core folder. Or you want to replace a file altogether. Simply use the same folder structure inside the /Lib folder.
For your own “FormAuthenticate”
/app/Lib/Controller/Component/Auth/FormAuthenticate.php

Cleanup!

If you are upgrading to 2.0 – or already have – you should get rid of old PHP4 chunk.
For starters, the &+object needs to get eradicated once and for all.

Old style:

public function setup(&$Model, $config = array()) {}

A correct piece of a behavior now looks like:

public function setup(Model $Model, $config = array()) {} # type Model and no &

Same goes for components, controllers, helpers, …
Note: You can do this for 1.3 already, as well. At least if you are using PHP5 (and you probably are).

You should also change all “var $x” to “public $x” and add “public” to all methods in classes which don’t have a visibility attribute yet.

I wrote a “CorrectShell” which extends the UpgradeShell. It does take care of almost all changes. I will publish it soon.

Things I did find out the hard way

Most of my libs and helpers which use the Xml class had to be modified quite a bit.
The class itself is not accessed statically, and more important does return only lowercase keys.
This was some piece of work to find and correct all those necessary changes.

If you make use of the internal HttpSocket class you might want to be interested in the fact, that it now returns an object as response. So you need to use $response->body to access the result content.

You cannot really set the headers and content type in your layouts anymore manually using “header()”. You need to do this using the response object:

$this->response->type('pdf'); //or as mimetype:
$this->response->type('application/pdf');

Either from the controller or the view/layout/helper.

The core.php, routes.php and bootstrap.php need new settings in order for your app to fully function under 2.0. Tip: Look at the original 2.0 app files and compare them to your upgraded files.
Add anything that is missing.

There seem to be dependency problems now with components and helpers. So the order in which you add them in your controller is now important (wasn’t in 1.3). If they depend on a class you should add them after that one to prevent the collection to fatal error. (UPDATE: Some if it seems to be fixed now).

Shell tips not mentioned in any upgrade guide

Shells are one of the most useful tools out there. I have dozens of them and those were the first classes I ported to 2.0. Therefore I ran into problems with those, first. So in case, you write your own ones, too:

The usage of shells has changed quite a lot. You don’t need a help method anymore. The consoleOptionParser object will take care of that using “-h”.
For using params/args you will now need to set up a “getOptionParser” method (see the core shells for examples).
For string options you should set an empty default value: 'default' => '' (prevents you from warnings). For boolean values set 'boolean' => true.

A main() command should always be called explicitly (“cake Shell main -f” instead of “cake Shell -f”) if you want to pass params or args. Otherwise it won’t run.

And there is more

Make sure you check all your forms. Especially the ones where you customized. Since 2.0 now adds HTML5 types automatically, it can get unwanted results when you use names for fields that are already matched in the schema. In my case many normal “text” fields suddenly ended up being “type”=>”number”. Which screwed up the whole form (could not validate anymore).
Some normal selects (for search etc) suddenly ended up being “multiple”=>”multiple”. Why? I am not yet sure :)

The FormHelper does not support some field type aliases anymore. So for textareas you have to use 'type'=>'textarea'. “textfield” doesn’t work anymore – if you have that still somewhere in your forms.

Custom Joins do not need the prefix anymore. In 1.3 you had to do:

array(
	'table'=>$this->Conversation->tablePrefix.'conversation_messages',
	'alias'=>'ConversationMessage',
	'type'=>'inner',
	'conditions'=>array('Conversation.last_message_id = ConversationMessage.id'),
), ...

In 2.0 it’s just:

'table'=>'conversation_messages'

Otherwise it will prepend the prefix twice which, of course, breaks the query!

The Routing has changed quite a bit. Some to the better – some to the worse (or I coudn’t figure out yet how it is done in 2.0):
In 1.3 this was working to connect an url and all its prefixes:

Router::connect('/shortcut', array('controller' => 'overview', 'action'=>'some_action'));

But this will not in 2.0 anymore. Here all prefixes get treated as normal named params.
A quick fix (which does not enable the prefixed urls, though!) would be:

Router::connect('/shortcut', array('admin'=>false, 'controller' => 'overview', 'action'=>'some_action'));

At least the normal user url is working, again.
Although this doesn’t seem to work flawlessy, either…

Custom joins don’t need a model prefix anymore:

'joins'=>array(
	array(
		'table'=>$this->tablePrefix . 'conversation_users',
		'alias'=>'ConversationUser',
		'type'=>'inner',
		'conditions'=>array('Conversation.id = ConversationUser.conversation_id')
	),
)

becomes

'joins'=>array(
	array(
		'table'=>'conversation_users',
		'alias'=>'ConversationUser',
		'type'=>'inner',
		'conditions'=>array('Conversation.id = ConversationUser.conversation_id')
	),
)

Cake is now capable of prefixing the table names automatically.

 
5 Comments

Posted in CakePHP

 

More great news: CakePHP 2.0 stable is out!

18 Oct

After 1.5 years of development, Cake catches up with other frameworks.
It is now as modern as most of the others while remaining the most powerful of all.
Almost none if its automatic got lost during the process of rewriting.

Some of the important aspects of 2.0

  • Faster (20-40% depending on the application)
  • Modern (>=PHP5.2 and state of the art functionality)
  • More Flexible and extensible (almost every class can be switched out with own ones)
  • Exceptions as error handling
  • PHPUnit as test suite
  • Hundreds of fixes which had to stay in 1.3 due to compatibility issues
  • Lots of enhancements and new functionality like “aliasing” – details

If you start a new project dive right into 2.0. That’s the future :)

For all those who like to assimilate the changes as online video:
tv.cakephp.org/video/CakeFoundation/2011/10/06/ch-ch-anges_cakephp_2_0_-_by_mark_story

Tips for upgrading

The migration guide will help to upgrade existing applications to the new version.
This will be quite a bit of work, though. Almost all classes are renamed, many methods and object variables have been dropped.

Your biggest help will be the cake shell script “ugrade” which is available in 2.0 now. It helps you to automatically upgrade some of the code. You can either select all or single tasks like renaming components etc.
Simply type “cake upgrade” in the console to list all available methods.
It really is a huge time safer. You should backup or commit everything before you attempt to run this script, though.

UPDATE:
I tried to upgrade a medium sized 1.3 app to 2.0. After finding several problems I opened a ticket.
Hopefully the upgrade process will be made smoother the next couple of weeks :)

Last Words

I am a little bit proud to see many of my tickets and proposed changes/fixes being now part of the framework.
Contributing to such a popular and large framework is sometimes easier than one might think. Sometimes it needed only hours to approve one of my tickets. OK, sometimes it needs months.
Providing a good reason and attaching proposed changes as diff or patch can really speed up things and should encourage you to involve yourself in cake core development, as well.

 
2 Comments

Posted in CakePHP

 

Working with virtual fields in Cake1.3

13 Oct

In earlier versions of CakePHP we had to use Behaviors like “MultipleDisplayFields” or whatever.
With $Model->virtualFields it is now possible to natively use find() methods combined with virtual fields.
The nice thing about it is, that the pagination can work with those fields, too. You can even sort on them in paginated views. A huge advantage over earlier Cake versions.

An example (User model):

$virtualFields = array('fullname'=>'CONCAT(User.firstname, " ", User.lastname)'); // "john doe"

It has several downsides, though.
Virtual fields do not work across models. They cannot be used in combination with “fields” in the find() options.
More on it can be found in the official cookbook.

I tried to come up with a solution that fixes some of those incapabilities.

Introducing the custom model method

$fields = am(array('username', 'email'), $this->User->virtualFields('fullname'));
$user = $this->User->find('first', array('fields'=>$fields, 'conditions'=>...));

This allows us to only fetch specific “fields” while including the virtualField “fullname”

$fields = $this->User->virtualFields(array('fullname', 'OtherUser.fullname'));
$fields = am(array('User.id', 'OtherUser.id'), $fields);
$user = $this->User->find('first', array('contain'=>array('OtherUser'), 'fields'=>$fields, ...));

Virtual fields of other models are also no problem. In this case I simply joined the model User to itself (hasOne OtherUser).
The result would be sth like:

array (
  'User' =>
  array (
    'id' => '2',
    'fullname' => 'First+Last',
  ),
  'Test' =>
  array (
    'id' => '2',
    'fullname' => 'First-Last',
  ),
);

Note the difference in the join part (+/-). It shows that the query works (OtherUser has – and User has + as joining piece).

A little bit trickier were the temporary virtual fields. Those could be passed along and would be used only for this query (TODO! right now they are added to virtualFields but not removed anymore).

$fields = $this->User->virtualFields(array('fullname', 'othername'=>'CONCAT('.$this->User->alias.'.firstname, "-", '.$this->User->alias.'.lastname)', 'OtherUser.othername'=>'CONCAT('.$this->OtherUser->alias.'.firstname, "*", '.$this->OtherUser->alias.'.lastname)'));
$fields = am(array('User.id', 'OtherUser.id'), $fields);
$user = $this->User->find('first', array('contain'=>array('Test'), 'fields'=>$fields, 'order'=>array()));

The result again – as expected:

array (
  'User' =>
  array (
    'id' => '2',
    'fullname' => 'First+Last',
    'othername' => 'First-Last',
  ),
  'OtherUser' =>
  array (
    'id' => '2',
    'othername' => 'First*Last',
  ),
)

Even plugin models should work (not tried yet) with

$this->User->virtualFields(array('Plugin.PluginModel.fullname'));

Although the “Plugin” part usually can be omitted if the relation from User to PluginModel is already established (and the User model can access the model property without initializing the foreign model).

Code

The code to be placed in your AppModel:

/**
 * combine virtual fields with fields values of find()
 * USAGE:
 * $this->Model->find('all', array('fields' => $this->Model->virtualFields('full_name')));
 * Also adds the field to the virtualFields array of the model (for correct result)
 * TODO: adding of fields only temperory!
 * @param array $virtualFields to include
 * 2011-10-13 ms
 */
public function virtualFields($fields = array()) {
	$res = array();
	foreach ((array)$fields as $field => $sql) {
		if (is_int($field)) {
			$field = $sql;
			$sql = null;
		}
		$plugin = $model = null;
		if (($pos = strrpos($field, '.')) !== false) {
			$model = substr($field, 0, $pos);
			$field = substr($field, $pos+1);
 
			if (($pos = strrpos($model, '.')) !== false) {
				list($plugin, $model) = pluginSplit($model);
			}
		}
		if (empty($model)) {
			$model = $this->alias;
			if ($sql === null) {
				$sql = $this->virtualFields[$field];
			} else {
				$this->virtualFields[$field] = $sql;
			}
		} else {
			if (!isset($this->$model)) {
				$fullModelName = ($plugin ? $plugin.'.' : '') . $model;
				$this->$model = ClassRegistry::init($fullModelName);
			}
			if ($sql === null) {
				$sql = $this->$model->virtualFields[$field];
			} else {
				$this->$model->virtualFields[$field] = $sql;
			}
		}
		$res[] = $sql.' AS '.$model.'__'.$field;
	}
	return $res;
}

If anyone sees a cleaner approach etc, let me know :)

 
1 Comment

Posted in CakePHP

 

Maximum power for your validation rules

07 Oct

I already postet an article about custom validation rules some time ago here.
In this post I introduce some of my custom rules which come in handy quite often.

For being able to use it in different projects I do not use the app model for it but a plugin lib.
My app model then extends this lib (/libs/my_model.php):
App::import(‘Lib’, ‘PluginName.MyModel’) and AppModel extends MyModel.

I do not want to post the methods here in the blog because they might change over time. You can find the current source code at my github rep (test case).

Lets get started.
The following code fragments are part of the $validate array of the specific model you want to validate.

Validating urls

The core rule does not validate “deep” – meaning it cannot check if the url is actually accessible/correct.
My custom rule works like this:

'field' => array(
	'validateUrl' => array(
		'rule' => array('validateUrl', array('autoComplete'=>true)),
		'message' => 'Not a valid url',
	),
),

By default, it will check deep and make sure the url actually exists.
With the autoComplete param you can be more flexible with urls that are missing “http://” etc (note/todo: it would be even better if it would save the autocompleted string). Especially in combination with strict=>true.

If we want to allow only links on the same domain we could say

'field' => array(
	'validate' => array(
		'rule' => array('validate', array('autoComplete'=>true, 'sameDomain'=>true)),
		'message' => 'Please provide a valid url on the same domain',
	),
),

Now /some/link as well as http://samedomain.com/some/link works.
With deep=>false we can disable the deep check for availability.

Validating dates and times

The main improvements are the before/after params:

'end_date' => array(
	'validate' => array(
		'rule' => array('validateDatetime', array('after'=>'start_date')),
		'message' => 'Please provide a valid date later than the start date',
	),
),

There are also date and time only versions like

'time' => array(
	'validate' => array(
		'rule' => array('validateTime', array('allowEmpty'=>true)),
		'message' => 'Please provide a valid time or leave the field empty',
	),
),

I also had to hack around problems regarding empty strings or partially invalid dates. thats why this method is quite long compared to others.

Validating keys (primary/foreign)

'id' => array(
	'validate' => array(
		'rule' => array('validateKey'),
		'message' => 'Invalid primary key',
	),
),
'foreign_id' => array(
	'validate' => array(
		'rule' => array('validateKey', array('allowEmpty'=>true)),
		'message' => 'Invalid foreign key',
	),
),

The id can be either aiid (int10) or uuid (char36), the method will always validate correctly.
Same goes for foreign_id. But due to the allowEmpty it can also be left empty (or 0 for aiids).

Validating enums

Sometimes the enums are “dynamically” generated. You cannot use the build in “inList” validation rule then.
If you have a method set up, you can call this from within the rule:

'field' => array(
	'validate' => array(
		'rule' => array('validateEnum', 'methodX'),
		'message' => 'Invalid value',
	),
),

Somewhere in the model define your custom enum value generator:

function methodX() {
  return array(...);
}

Validating uniqueness

With this enhanced method we can check on other fields – dependent uniqueness so to speak.

'field' => array(
	'validate' => array(
		'rule' => array('validateUnique', array('user_id')),
		'message' => 'You already have an entry',
	),
),

In this case the field will only invalidate if this specific user has already an entry.
Two users can have the same field content without interfering each other.

TODO: test cases with fixtures etc in order to simulate DB content to validate against…

Validating identical

'email_confirm' => array(
	'validate' => array(
		'rule' => array('validateIdentical', 'email'),
		'message' => 'The two fields do not match',
	),
),

As you can see this rule comes in handy if you need to confirm a string to another in the post data. Simply pass the field name along as second param.

Last Words

Hopefully many other cake developers find those enhanced rules useful and maybe they set the core team thinking about implementing some of the features in the future versions of cake.

Please note:
Some of the internal methods, constants etc used might be not available without stuff from my tools plugin.
Those validation methods above might have to be adjusted to your application. Same goes for the test case which uses some of my own methods/libs to work with.

 
3 Comments

Posted in CakePHP