RSS
 

Extended core validation rules

19 Jul

I18n Translation

Some translate the rules in the view – but it usually creates redundancy. In some projects this might be intentional. I like to keep the error messages centralized, though.

For that, you can just override the core translation rule – add this to app_model.php:

/**
 * Overrides the Core invalidate function from the Model class
 * with the addition to use internationalization (I18n and L10n)
 * @param string $field Name of the table column
 * @param mixed $value The message or value which should be returned
 * @param bool $translate If translation should be done here
 */
function invalidate($field, $value = null, $translate = true) {
	if (!is_array($this->validationErrors)) {
		$this->validationErrors = array();
	}
	if (empty($value)) {
		$value = true;
	} else {
		$value = (array )$value;
	}
 
	//TODO: make more generic?
	if (is_array($value)) {
		$value[0] = $translate?__($value[0], true) : $value[0];
 
		if (count($value) > 3) { # string %s %s string, trans1, trans2
			$value = sprintf($value[0], $value[1], $value[2], $value[3]);
		} elseif (count($value) > 2) { # string %s %s string, trans1, trans2
			$value = sprintf($value[0], $value[1], $value[2]);
		} elseif (count($value) > 1) { # string %s string, trans1
			$value = sprintf($value[0], $value[1]);
		} else {
			$value = $value[0];
		}
	}
	$this->validationErrors[$field] = $value;
}

Usage (some examples):

var $validate = array(
	'username' => array(
		'notEmpty' => array(
			'rule' => array('notEmpty'),
			'message' => 'valErrEmpty', // some short form to translate
			//normal sentences would of course work, too
		),
	),
	'pwd' => array(
		'between' => array(
			'rule' => array('between', 6, 30),
			'message' => array('valErrBetweenCharacters %s %s', 6, 30), // short form string with variables
			// maybe resulting in something like "Between 6 and 30 chars" as defined in locale.po
		)
	),
	'code' => array(
		'maxLength' => array(
			'rule' => array('maxLength', 5),
			'message' => array('The code cannot be longer than %s chars', 5), // normal text with a variable
		)
	),
	...
);

I like the short forms like “valErrBetweenCharacters %s %s” – they don’t have a certain “grammar”, so if you want to change the translation result “Between %s and %s chars” to “Please insert between %s and %s chars” this can be done with one change in locale.po instead of changing all xxx places you used this rule.

Custom validation rules

Here is one example how to use a custom validation rule with one param (custom rules can be placed in the model for local validation or the app_model for global validation):

var $validate = array(
	'pwd_repeat' => array(
		'validateIdentical' => array(
			'rule' => array('validateIdentical', 'pwd'), // we want to compare this to "pwd"
			'message' => 'valErrPwdNotMatch',
		),
	)
);
 
// in app_model.php:
/**
 * checks if the content of 2 fields are equal
 * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
* //TODO: make it more generic with Model.field syntax
 */
function validateIdentical($field, $compareWith) {
	return ($this->data[$this->alias][$compareWith] === array_shift($field));
}

Multiple validation rules

The Cookbook states:
“By default CakePHP tries to validate a field using all the validation rules declared for it and returns the error message for the last failing rule”
I don’t really like this default behavior – what sense does it make to validate all if only the last error can be returned anyway. But as long as “last” is not true by default, we have to manually set it:

var $validate = array(
    'login' => array(
        'loginRule-1' => array(
            'rule' => 'alphaNumeric',
            'message' => '...',
            'last' => true
         ),
        'loginRule-2' => array(
            'rule' => array('minLength', 8),
            'message' => '...'
        )
    )
);

The rules are validated top down. So if the first one fails, it will now return the error right away. Usually the following validation rules rely on a positive result of the predecessor. At least, thats how you should arrange your rules.

One example:
[email] (in this order – each with a specific error message)
- notEmpty
- email
- undisposable (custom – per vendor)
- notBlocked (custom – per webservice)

As you can see, it would not make sense to check all of them every time. It really slows down the validation – especially if the email address is not even valid. So we first want to check the simple stuff and then move on to the advanced (and sometimes more time consuming) rules. All rules get “last”=>true to achieve that.

Other quite handy custom translation rules

They can be put into app_model.php:

/**
 * checks a record, if it is unique - depending on other fields in this table (transfered as array)
 * example in model: 'rule' => array ('uniqueRecord',array('belongs_to_table_id','some_id','user_id')),
 * if all keys (of the array transferred) match a record, return false, otherwise true
 * @param ARRAY other fields
 * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
 */
function validateUnique($arguments, $fields = array(), $options = null) {
	$id = (!empty($this->data[$this->alias]['id'])?$this->data[$this->alias]['id'] : 0);
 
	foreach ($arguments as $key => $value) {
		$fieldName = $key;
		$fieldValue = $value; // equals: $this->data[$this->alias][$fieldName]
	}
 
	if (empty($fieldName) || empty($fieldValue)) { // return true, if nothing is transfered (check on that first)
		return true;
	}
 
	$conditions = array($this->alias.'.'.$fieldName => $fieldValue, // Model.field => $this->data['Model']['field']
		$this->alias.'.id !=' => $id, );
 
	foreach ((array )$fields as $dependingField) {
		if (!empty($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
			$conditions[$this->alias.'.'.$dependingField] = $this->data[$this->alias][$dependingField];
 
		} elseif (!empty($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
			$conditions[$this->alias.'.'.$dependingField] = $this->data['Validation'][$dependingField];
 
		} elseif (!empty($id)) {
			# manual query! (only possible on edit)
			$res = $this->find('first', array('fields' => array($this->alias.'.'.$dependingField), 'conditions' => array($this->alias.'.id' => $this->data[$this->alias]['id'])));
			if (!empty($res)) {
				$conditions[$this->alias.'.'.$dependingField] = $res[$this->alias][$dependingField];
			}
		}
	}
 
	$this->recursive = -1;
	if (count($conditions) > 2) {
		$this->recursive = 0;
	}
	$res = $this->find('first', array('fields' => array($this->alias.'.id'), 'conditions' => $conditions));
	if (!empty($res)) {
		return false;
	}
 
	return true;
}
 
// validateUrl and validateUndisposable are to come

More

See the Cookbook for details on that matter.

 
No Comments

Posted in CakePHP

 

Redirect Root Domain to WWW Subdomain

13 Jul

The Problem

“When you have two different addresses pointing to the same page, like www.example.com/offers.html and example.com/offers.html, many search engines (or so we are led to believe) will treat those two URLs as two separate pages. When you, as a human, see those two pages and notice they are identical, you will automatically realise (correctly) that they are actually the same page. Apparently, the search engines do not make this assumption, and will regard those as different pages with duplicate content.”
(source: www.thesitewizard.com)

I personally think that apart from the SEO problem there shouldn’t be two different urls to the same content in the first place.

So what do we do?

The bad thing to do would be to disable one of the domains (root or www).
We want to select one (www.example.com) as default domain and if someone just enters “example.com” he will be automatically redirected to our default domain.
Again – the bad thing would be to use meta redirects. We want to use so called permanent redirects ( code 301) in order to notify search engines and browsers about the reason we want to redirect.

Using Mod Rewrite

For the cake app to be available per “www.example.com” (and a 301 redirect from mydomain.de) you just need to modify the htaccess file in the /app/webroot/ folder:

<IfModule mod_rewrite.c>
	RewriteEngine On
 
	RewriteCond %{HTTP_HOST} !^www\. [NC]
	RewriteCond %{HTTP_HOST} !^localhost [NC]
	RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]
 
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
</IfModule>

The “localhost” part is not neccessary – but it prevents your local htaccess file to redirect on your development computer. That is if you have your htaccess file in svn or git and therefore both versions (local and stage) are the same.

The reason why i did not lay out the other direction (www to root) is simple: I don’t think this makes sense. Most people not so comfortable with the internet usually type the complete address anyway – they just expect a “www” in front of the domain for the main website.
And with the internet getting more and more complex, some guidelines should just remain. One is that you don’t use root domains for websites. (I could go into details here – about other side effects like “technical” cookie problems occuring with root domain sites etc – but i will leave that out for now)

Using Apache Directives

This is way faster because the server doesn’t have to open the htaccess files for it. It can directly use what it has to have available anyway.

Inside /etc/apache2/sites-available/ there should be your domain file.
Between <Directory> and </Directory> you can add the above lines.
To actually increase the performance we now need to disable htaccess files for this domain.
Add AllowOverride None directly above your new lines. This prevents the server from looking for, opening and processing htaccess files.

Note: Usually this is only available on root servers.

 

User Add Console Script

04 Jul

If you have a fresh setup of your app and no users in the database, its not easy to “register” a new one (with the admin role and everything).
The other case would be if you changed the security salt. Now you need new passwords, as well.

The second part is not yet covered by my script – the first one is, though.
The idea is to insert an admin user in order to login for the first time. All you need is a name, an email and a password.

<?php
 
class UserShell extends Shell {
	var $tasks = array();
	var $uses = array('User');
 
	//TODO: refactor (smaller sub-parts)
	function main() {
		if (App::import('Component','AuthExt')) {
			$this->Auth = new AuthExtComponent();
		} else {
			App::import('Component','Auth');
			$this->Auth = new AuthComponent();
		}
 
		while (empty($username)) {
			$continue = $this->in(__('Set \'username\'?', true),array('y', 'n', 'q'), 'y');
			if ($continue == 'q') { die('Abort'); } elseif ($continue == 'n') { break; }
			$username = $this->in(__('Username (2 characters at least)', true));
		}
		while (empty($password)) {
			$password = $this->in(__('Password (2 characters at least)', true));
		}
 
 
 
		if (isset($this->User->Role) && is_object($this->User->Role)) {
			$roles = $this->User->Role->find('list');
 
			if (!empty($roles)) {
				$this->out('');
				pr ($roles);
			}
 
			$roleIds = array_keys($roles);
			while (!empty($roles) && empty($role)) {
				$role = $this->in(__('Role', true), $roleIds);
			}
		} elseif (method_exists($this->User, 'roles')) {
			$roles = User::roles();
 
			if (!empty($roles)) {
				$this->out('');
				pr ($roles);
			}
 
			$roleIds = array_keys($roles);
			while (!empty($roles) && empty($role)) {
				$role = $this->in(__('Role', true), $roleIds);
			}
		}
		if (empty($roles)) {
			$this->out('No Role found (either no table, or no data)');
			$role = $this->in(__('Please insert a role manually', true));
		}
 
		$this->out('');
		$pwd = $this->Auth->password($password);
 
		$data = array('User'=>array(
			'password' => $pwd,
			'active' => 1
		));
		if (!empty($username)) {
			$data['User']['username'] = $username;
		}
		if (!empty($email)) {
			$data['User']['email'] = $email;
		}
		if (!empty($role)) {
			$data['User']['role_id'] = $role;
		}
		$this->out('');
		pr ($data);
		$this->out('');
		$this->out('');
		$continue = $this->in(__('Continue? ', true),array('y', 'n'), 'n');
		if ($continue != 'y') {
			die('Not Executed!');
		}
 
		$this->out('');
		$this->hr();
		if ($this->User->save($data)) {
			$this->out('User inserted! ID: '.$this->User->id);
		} else {
			$this->out('User could not be inserted (email, nick duplicate!!!)');
		}
	}
}
?>

Notes

The code obviously needs some refactoring. And its written for my applications – it might not work out of the box with other cake apps. Maybe we could make it more generic, as well.

It assumes that you have either AuthExt or Auth running for Authentication, that you have a User and a Role model and that the fields are “username”, “email”, “password” and “role_id”. Additionally it sets “active” to 1 – this field is checked in my login procedure.
But this script should be fairly easily adjustable for your needs.

Ideas for further improvement are:
- user add (for adding)
- user edit (for editing specific user/password)
- user reset (to reset all passwords to 123 or whatever)

 
No Comments

Posted in CakePHP

 

Subversion and multiple CakePHP apps

02 Jul

A few days ago i already wrote about how to set up a svn for cake.
In this second part i will cover the aspect of managing several apps.

The Problem

We have several apps, each with its own cake and vendor folder. If you need to update from 1.3.1 to 1.3.2 you would need to do that for each of your cake cores. Woudn’t it be nice if you changed one and all others would be updated automatically?

The Solution

Lets say, our apps have the following svn rep. urls:
http://123.456.789.000/app1
and
http://123.456.789.000/app2

Both have the same structure:
/root/trunk/app/…
(/root/trunk/vendors/…)
(/root/trunk/cake/…)

Both vendor and cake folder we now want to substitute with our shared folders

Sharing Cake Core and Vendors

We now put our cake core in svn under the following url:
http://123.456.789.000/cake13

and vendors:
http://123.456.789.000/vendors

In both apps, we need to edit the properties for the /trunk folder with the following schema:
svn:externals svn_url local_folder
Result:
svn:externals http://123.456.789.000/cake13 cake
svn:externals http://123.456.789.000/vendors vendors

If you update the cake13 or vendors svn, both apps will get the updated version.
Especially if you have more than 2 apps, this will be very helpful.

Extending to other folders

/app/plugins is a very common folder where you should use svn:externals instead of just copying your plugins to all apps. One change in any plugin version will affect the other ones.

/app/libs can have subfolders, too. So /app/libs/lib/… could contain a “library of libs” for all apps.
The same goes for /app/views/helpers, /app/controller/components, /app/models/behaviors/ etc

Your test cases should be in “lib” folders then, too.
E.g:
/app/controller/components/lib/upload.php
/app/tests/cases/components/lib/upload.php
Both inside those shared lib folders which are attached with svn:externals (in this case parent folder “components”).

This might make sense in some cases. Most of the time, though, it’s better to create some kind of “tools” plugin where you can combine all your “library” files (common libs, helpers, components, behaviors, …).
And the test cases are easier managed, as well.

Final tips

You don’t necessarily need seperate svns for each external folder. They could all reside in one.
E.g with “common” svn:

svn:externals http://123.456.789.000/common/cake13 cake
svn:externals http://123.456.789.000/common/subfolder1/subfolder2/vendors vendors

Any svn subfolder is valid

NEW: Use github reps as external svns

Now you can include github reps in svn reps. Its as easy as including
svn:externals https://svn.github.com/[user]/[repository]
in your folder properties.

This is especially useful, if you want to include one of the many cake plugins available at github without manually including it (changes would not be adopted).
Thats how i include my tools plugin in all my apps:
svn:externals https://svn.github.com/dereuromark/tools tools
(in my /app/plugins folder properties)

Note: Currently it does not seem to work the other way around. Although planned to be available very soon, it didn’t work for me committing to such a git-svn rep externally included.
But most of the time, a simple checkout is more than enough. If you need to change the rep, you can do that with git itself and then update the svn rep (it will get the changes then anyway).

 
1 Comment

Posted in CakePHP

 

Code-Completion Console Script

28 Jun

Many good IDEs for Webdevelopment cannot automatically understand the structure of a framework and its classes/objects.
They need a small file initializing the objects so that they know in which context they appear.

E.g.:
$this->Session (Session Helper)
its impossible for the IDE to know that the the helpers are inside the View class.
Same goes for controller components and model behaviors

I wrote a little script which produces mockup code. My IDE “PHPDesigner” works very well with it.

Usage

Just type cake cc inside the cake shell.

Code

<?php
 
App::import('Core', 'Folder');
App::import('Core', 'File');
 
/**
 * Code Completion
 * 2009-12-26 ms
 */
class CcShell extends Shell {
	var $uses = array();
 
	private $content = '';
 
	function main() {
		$this->out('AutoComplete Dump');
 
		//TODO: ask for version (1.2 etc - defaults to 1.3!)
 
		$this->filename = APP.'code_completion__.php';
 
		# get classes
		$this->models();
		$this->components();
		$this->helpers();
		//TODO: behaviors
 
		# write to file
		$this->_dump();
 
		$this->out('...done');
	}
 
	/**
	 * @deprecated
	 * now use: Configure::listObjects()
	 */
	function __getFiles($folder) {
		$handle = new Folder($folder);
		$handleFiles = $handle->read(true, true);
		$files = $handleFiles[1];
		foreach ($files as $key => $file) {
			$file = extractPathInfo('file', $file);
 
			if (mb_strrpos($file, '_') === mb_strlen($file) - 1) { # ending with _ like test_.php
				unset($files[$key]);
			} else {
				$files[$key] = Inflector::camelize($file);
			}
		}
		return $files;
	}
 
 
	public function _getFiles($type) {
    $files = App::objects($type);
    # lib
    $paths = (array)App::path($type.'s');
    $libFiles = App::objects($type, $paths[0] . 'lib' . DS, false);
 
    $plugins = App::objects('plugin');
    if (!empty($plugins)) {
      foreach ($plugins as $plugin) {
         $pluginFiles = App::objects($type, App::pluginPath($plugin) . $type.'s' . DS, false);
          if (!empty($pluginFiles)) {
              foreach ($pluginFiles as $t) {
                  $files[] = $t; //"$plugin.$type";
              }
          }
      }
    }
    $files = array_merge($files, $libFiles);
    $files = array_unique($files);
 
		$appIndex = array_search('App', $files);
		if ($appIndex !== false) {
			unset($files[$appIndex]);
		}
 
		# no test/tmp files etc (helper.test.php or helper.OLD.php)
    foreach ($files as $key => $file) {
			if (strpos($file, '.') !== false || !preg_match('/^[\da-zA-Z_]+$/', $file)) {
				unset($files[$key]);
			}
		}
    return $files;
	}
 
 
	function models() {
		//$files = App::objects('component', null, false);
		$files = $this->_getFiles('model');
		//$files = $this->_getFiles(COMPONENTS);
 
		$content = LF.'<?php'.LF;
		$content .= '/*** model start ***/'.LF;
		$content .= 'class AppModel extends Model {'.LF;
		if (!empty($files)) {
			$content .= $this->_prepModels($files);
		}
		$content .= '}'.LF;
		$content .= '/*** model end ***/'.LF;
		$content .= '?>';
 
		$this->content .= $content;
	}
 
	function components() {
		$files = $this->_getFiles('component');
 
		$content = LF.'<?php'.LF;
		$content .= '/*** component start ***/'.LF;
		$content .= 'class AppController extends Controller {'.LF;
		if (!empty($files)) {
			$content .= $this->_prepComponents($files);
		}
		$content .= '}'.LF;
		$content .= '/*** component end ***/'.LF;
		$content .= '?>';
 
		$this->content .= $content;
	}
 
	function helpers() {
		$files = $this->_getFiles('helper');
 
		$content = LF.'<?php'.LF;
		$content .= '/*** helper start ***/'.LF;
		$content .= 'class AppHelper extends Helper {'.LF;
		if (!empty($files)) {
			$content .= $this->_prepHelpers($files);
		}
		$content .= '}'.LF;
		$content .= '/*** helper end ***/'.LF;
		$content .= '?>';
 
		$this->content .= $content;
	}
 
	function _prepModels($files) {
		$res = '';
		foreach ($files as $name) {
			$res .= '
	/**
	* '.$name.'
	*/
	public $'.$name.';
'.LF;
		}
 
		$res .= '	function __construct() {';
 
		foreach ($files as $name) {
			$res .= '
		$this->'.$name.' = new '.$name.'();';
		}
 
		$res .= '}'.LF;
		return $res;
	}
 
	function _prepComponents($files) {
		$res = '';
		foreach ($files as $name) {
			$res .= '
	/**
	* '.$name.'Component
	*/
	public $'.$name.';
'.LF;
		}
 
		$res .= '	function __construct() {';
 
		foreach ($files as $name) {
			$res .= '
		$this->'.$name.' = new '.$name.'Component();';
		}
 
		$res .= '}'.LF;
		return $res;
	}
 
	function _prepHelpers($files) {
		# new ones
		$res = '';
 
		foreach ($files as $name) {
			$res .= '
	/**
	* '.$name.'Helper
	*/
	public $'.$name.';
'.LF;
		}
 
		$res .= '	function __construct() {';
 
		foreach ($files as $name) {
			$res .= '
		$this->'.$name.' = new '.$name.'Helper();';
		}
 
		# old ones
		$res .= ''.LF;
		/*
		foreach ($files as $name) {
		$res .= '
		$'.lcfirst($name).' = new '.$name.'Helper();
		';
		}
		$res .= LF;
		*/
 
		$res .= '	}'.LF;
 
		return $res;
	}
 
 
	function _dump() {
		$file = new File($this->filename, true);
 
		$content = '<?php exit();'.LF;
		$content .= '//Add in some helpers so the code assist works much better'.LF;
		$content .= '//Printed: '.date('d.m.Y, H:i:s').LF;
		$content .= '?>'.LF;
		$content .= $this->content;
		return $file->write($content);
	}
}
 
?>

Result

The file will look like:

class AppModel extends Model {
	/**
	* Address
	*/
	public $Address;
	...
 
	function __construct() {
		$this->Address = new Address();
		...
	}
}
class AppController extends Controller {
	/**
	* AclComponent
	*/
	public $Acl;
	...
	function __construct() {
		$this->Acl = new AclComponent();
		...
	}
}
class AppHelper extends Helper {
	/**
	* AjaxHelper
	*/
	public $Ajax;
	...
	function __construct() {
		$this->Ajax = new AjaxHelper();
		...
	}
}

Final notes

Feel free to update the TODOs and send me the improved file. A test file would be awesome, too, i guess :)

For LF constants see my article about “bootstrap goodies”.

 
No Comments

Posted in CakePHP

 

Social Bookmark Helper

27 Jun

If you want to add social bookmarks to your cake projects, this helper will do the trick.

Usage

# Defaults:
$this->Bookmark->getBookmarks();
 
# All:
$this->Bookmark->getBookmarks(null, null, $this->Bookmark->availableBookmarks());
 
# Custom:
$custom = (array)Configure::read('Bookmarks');
//or
$custom = array('Twitter', 'Facebook', ...);
$this->Bookmark->getBookmarks(null, null, $custom);

Link to GIT-Rep

cakephp-bookmark-helper

Updates on bookmarks?

Are there any outdated bookmarks – or even some missing ones? Write me.

 
No Comments

Posted in CakePHP

 

Helper? Component? Lib?

26 Jun

Some ideas what to use if you want to add some additional feature.
Feel free to comment on this below.

Level of Independence

We need to ask ourselves if this feature needs to interact with other cake elements, like controller, some components, models, …

If it needs to save to the session, or if it needs some controller functionality, it will have to be a component.
With initialize(&$controllerReference) and startup(&$controllerReference) this is very easy to accomplish.

But with Cake13 libs have been introduced. Not every piece of “controller” code necessarily needs to be component anymore.
So if you retrieve an RSS feed or get the weather from a weather channel web service you could just make a clean and independent lib class. No need to extend the cake object or even pass the controller reference. Less memory and dependency is a good thing. And its easier to test, anyway.

Helpers are used if the result is in relation to the view – in other words if it gets printed/echoed right away. If you want to retrieve some web service information and save it to the database use a component instead.

Database related?

Often times we need to adjust some model data, we either use a component first and then pass it to the model or we use beforeValidate() and beforeSave() in the model. Same goes for the other direction (from model to controller): afterFind() or a component call afterwards.
This is fine for custom changes. As soon as it could be something useful for several models, it might make sense to build a behavior. The code gets cleaner and your models more powerful.

Examples would be:
“Last Editor/Last Change”, “Geocoding”, “Auto-Capitalize first letter of name/title”, “Format/Localize Date/Time”, …

Reducing code redundancy

Now that we have a vague understanding where to use what type of tool, we should think about cutting down the redundancy.
Lets say we use the vendor class “phpThump”. We would have to write two wrappers. one for the helper (display images in the view) and one for the component (uploading images and resizing), maybe even for some behavior (validating + uploading + resizing). This wrapper handles default values from Configure::read() and other cake related settings.
In this scenario we should build one single library in /libs, maybe called “phpthumb_lib.php”.
Here we put our wrapper with our custom functions.
Then we build a helper (view side) as well as a component or a behavior (controller side). They will import and use the library file. This is a cleaner approach because changes in the library class will be available in all files it is used in.
Bonus: The main library file is easier to test. And therefore testing the other classes afterwards is easier, too.

Generally speaking, all web services should be some kind of library file (some would make a data source out of it). It doesn’t matter then if we use it in components or helpers, because it will fit either way.
A helper in a controller, though, is not really a nice thing.

Thats – by the way – something i don’t like about the core helpers. They have functionality which is often useful in the controller (replacing a timestamp with localized date in session flash messages).
So there should be a library called “Time” or whatever which is then extended or used in the helper.
But, if needed, it can be used in the controller, as well. Same goes for “Text” and “Number”.

Plugin or not?

If your feature is not site-specific but very generic it probably makes sense to build a plugin.
This way all other apps can easily use the same plugin. Additionally, test cases, assets etc are all combined in one folder – clean and extendable.

Examples:
A bookmark helper usually can be used in several apps, whereas a custom helper with two functions for this one app will not be very useful anywhere else.

 
No Comments

Posted in CakePHP

 

Tools Plugin – Part1: CodeKey

25 Jun

In this first part of the presentation of my tools plugin I will show you how you can easily manage (store, retrieve, validate) “code keys”.
They are useful in the registration process of users, or if you want to send some double opt in confirmation emails, etc.

function newKey($type, $key = null, $uid = null, $content = null) {}

If you submit a uid it can increase security by validation only if the pair uid + key are both valid.
As 4th parameter any string content can be stored. This comes in handy if you use it for changing email addresses. The new one will be stored in content until validation is complete and will then replace the old one.

function useKey($type, $key, $uid = null) {}

Usage

register_account(){}

# get new code with key "activate" and specific user id bound to it
$this->CodeKey = ClassRegistry::init('CodeKey');
$cCode = $this->CodeKey->newKey('activate', null, $uid);

This code can be used to send an activation email with a link to click on.

activate_account($keyToCheck){}

...
$this->CodeKey = ClassRegistry::init('CodeKey');
$key = $this->CodeKey->useKey('activate', $keyToCheck);
 
if (!empty($key) && $key['CodeKey']['used'] == 1) {
	# warning flash message: already activated
} elseif (!empty($key)) {
	# continue with activation
} else {
	# error flash message: invalid key
}
...

Plugin Model Code

<?php
 
class CodeKey extends ToolsAppModel {
 
	var $name = 'CodeKey';
 
	var $displayField = 'key';
	var $order = array('CodeKey.created' => 'ASC');
 
	private $defaultLength = 22;
 
	var $validate = array(
		'type' => array(
			'notEmpty' => array(
				'rule' => array('notEmpty'),
				'message' => 'valErrMandatoryField',
			),
		),
		'key' => array(
			'isUnique' => array(
				'rule' => array('isUnique'),
				'message' => 'key already exists',
			),
			'notEmpty' => array(
				'rule' => array('notEmpty'),
				'message' => 'valErrMandatoryField',
			),
		),
		'content' => array(
			'maxLength' => array(
				'rule' => array('maxLength', 255),
				'message' => array('valErrMaxCharacters %s', 255),
				'allowEmpty' => true
			),
		),
		'used' => array('numeric')
	);
 
	//var $types = array('activate');
 
	/**
	 * stores new key in DB
	 * @param string type: neccessary
	 * @param string key: optional key, otherwise a key will be generated
	 * @param mixed user_id: optional (if used, only this user can use this key)
	 * @param string content: up to 255 characters of content may be added (optional)
	 * NOW: checks if this key is already used (should be unique in table)
	 * @return string key on SUCCESS, boolean false otherwise
	 * 2009-05-13 ms
	 */
	function newKey($type, $key = null, $uid = null, $content = null) {
		if (empty($type)) {		//  || !in_array($type,$this->types)
			return false;
		}
 
		if (empty($key)) {
			$key = $this->generateKey($this->defaultLength);
			$keyLength = $this->defaultLength;
		} else {
			$keyLength = mb_strlen($key);
		}
 
		$data = array(
			'type' => $type,
			'user_id' => (string)$uid,
			'content' => (string)$content,
			'key' => $key,
		);
 
		$this->set($data);
		$max = 99;
		while (!$this->validates()) {
			$data['key'] = $this->generateKey($keyLength);
			$this->set($data);
			$max--;
			if ($max == 0) { //die('Exeption in CodeKey');
			 	return false;
			}
		}
 
		$this->create();
		if ($this->save($data)) {
			return $key;
		}
		return false;
	}
 
	/**
	 * usesKey (only once!) - by KEY
	 * @param string type: neccessary
	 * @param string key: neccessary
	 * @param mixed user_id: needs to be provided if this key has a user_id stored
	 * @return ARRAY(content) if successfully used or if already used (used=1), FALSE else
	 * 2009-05-13 ms
	 */
	function useKey($type, $key, $uid = null) {
		if (empty($type) || empty($key)) {
			return false;
		}
		$conditions = array('conditions'=>array($this->alias.'.key'=>$key,$this->alias.'.type'=>$type));
		if (!empty($uid)) {
			$conditions['conditions'][$this->alias.'.user_id'] = $uid;
		}
		$res = $this->find('first', $conditions);
		if (empty($res)) {
			return false;
		} elseif(!empty($uid) && !empty($res[$this->alias]['user_id']) && $res[$this->alias]['user_id'] != $uid) {
			// return $res; # more secure to fail here if user_id is not provided, but was submitted prev.
			return false;
		} elseif ($res[$this->alias]['used'] == 1) {
			return $res;
		}
 
		# actually use key
		if ($this->spendKey($res[$this->alias]['id'])) {
			return $res;
		}
		$this->log('VIOLATION in CodeKey Model (method useKey)');
		return false;
	}
 
	/**
	 * sets Key to "used" (only once!) - directly by ID
	 * @param id of key to spend: neccessary
	 * @return boolean true on success, false otherwise
	 * 2009-05-13 ms
	 */
	function spendKey($id = null) {
		if (empty($id)) {
			return false;
		}
		$this->id = $id;
		if ($this->saveField('used', 1)) {
			return true;
		}
		return false;
	}
 
	/**
	 * remove old/invalid keys
	 * does not remove recently used ones (for proper feedback)!
	 * @return boolean success
	 * 2010-06-17 ms
	 */
	function garbigeCollector() {
		$conditions = array(
			$this->alias.'.created <'=>date(FORMAT_DB_DATETIME, time()-MONTH),
		);
		return $this->deleteAll($conditions, false);
	}
 
 
	/**
	 * @param length (defaults to defaultLength)
	 * @return string codekey
	 * 2009-05-13 ms
	 */
	function generateKey($length = null) {
		if (empty($length)) {
			$length = $defaultLength;
		} else {
			if ((class_exists('CommonComponent') || App::import('Component', 'Common')) && method_exists('CommonComponent', 'generatePassword')) {
				return CommonComponent::generatePassword($length);
			} else {
				return $this->_generateKey($length);
			}
		}
	}
 
	/**
	 * backup method - only used if no custom function exists
	 * 2010-06-17 ms
	 */
	function _generateKey($length = null) {
		$chars = "234567890abcdefghijkmnopqrstuvwxyz"; // ABCDEFGHIJKLMNOPQRSTUVWXYZ
		$i = 0;
		$password = "";
		$max = strlen($chars) - 1;
 
		while ($i < $length) {
			$password .= $chars[mt_rand(0, $max)];
			$i++;
		}
		return $password;
	}
 
}
?>
 
No Comments

Posted in CakePHP

 

Static Enums or “Semihardcoded Attributes”

24 Jun

There are many cases where an additional model + table + relation would be total overhead. Like those little “status”, “level”, “type”, “color”, “category” attributes.
Often those attributes are implemented as “enums” in sql – but cake doesn’t support them natively.

If there are only a few values to chose from and if they don’t change very often, you might want to consider this approach.
I use it a hundred times. Its very efficient and easily expandable.

Let’s get started.

a) Add tinyint(2) unsigned field called for example “status” (singular)
“Tinyint(2) unsigned” covers 0…255 – which should always be enough for enums. if you need more, you SHOULD make an extra relation as real table. dont use tinyint(1) as cake interprets this as a toggle field, which we dont want!

b) Put this in your app_model.php:

/**
   * static enums
   * @access static
   */
  function enum($value, $options, $default = '') {
      if ($value !== null) {
		if (array_key_exists($value, $options)) {
			return $options[$value];
		}
		return '';
	}
	return $options;
  }

c) Put something like this in any model where you want to use enums:

/*
 * static enum: Model::function()
 * @access static
 */
function statuses($value = null) {
	$options = array(
		self::STATUS_NEW => __('statusNew',true),
		self::STATUS_UNREAD => __('statusUnread',true),
		self::STATUS_READ => __('statusRead',true),
		self::STATUS_ANSWERED => __('statusAnswered',true),
		self::STATUS_DELETED => __('statusDeleted',true),
	);
	return parent::enum($value, $options);
}
 
const STATUS_NEW = 0; # causes sound, then marks itself as "unread"
const STATUS_UNREAD = 1;
const STATUS_READ = 2;
const STATUS_ANSWERED = 4;
const STATUS_DELETED = 5;
// add more - order them as you like

d) Use them in your controller logic, model functions, view forms, …:

//view form
...
echo $this->Form->input('status', array('options'=>Notification::statuses()));
//controller action
...
if ($this->data['Notification']['status'] == Notification::STATUS_READ)) {...}
//controller logic on find
...
$options = array('conditions'=>array('Notification.user_id'=>$uid, 'Notification.status <='=>Notification::STATUS_UNREAD));
$notifications = $this->Notification->find('all', $options);
//view index/view
...
<?php echo h($notification['Notification']['title']);?>
<?php echo Notification::statuses($notification['Notification']['status']); // returns translated text ?>

=> NOTE: example with “statuses”, could also be priorities, gender, types, categories, … etc
anything that is not often changed or extended

Thats it!

Conclusion: fast and easy to extend in the future if neccessary
it also saves a lot of overhead by using (tiny)ints instead of strings
AND it does not need any additional table joins! which makes it even more performant

futher advantages
- reorder them by just changing the order of the array values in the model
- auto-translated right away (i18n without any translation tables – very fast)

what you cant do:
- sort by the value of the keys (only by keys): small, medium, low => no sorting by their name ASC/DESC, only by key ASC/DESC

Final tipps:
if you use them in an index view many times repeatedly, if would make sense to write them into an array before using them. otherwise the translation will be transformed every time. caching would also work!

validation/emptyFields: use ‘empty’ attribute in form helper options array for default “blank” with either ‘empty’=>array(0=>’xyz’) to allow 0 values or ‘empty’=>’xyz’ to require one value (combined with validation rule “numeric”)

 
No Comments

Posted in CakePHP

 

Working with forms

23 Jun

The Cookbook only covers the real basics.
I want to go over some common issues in a more detailed way.

Dummy Values

Dummy values for select fields are usually added in the view:

/* (1) */
echo $this->Form->input('select_field', array('empty'=>'please select'));
/* (2) */
echo $this->Form->input('select_field', array('empty'=>array(0=>'please select')));

(1) is used if validation is “notEmpty” for this field (key = empty string) or if it is “numeric” and the user has to chose a value other than the dummy one.
(2) is used if validation is “numeric” and you want to allow the dummy value as default value.

Of course, there are other cases, too.
Note: We assume that the controller passes a variable named “selectField” with the options array to the view. You could also manually add options with the ‘options=>array(…)’ param.

Default Values

Many beginners make the mistake to use the options param “value” for the default value. But this field will be populated with the same default value every time after posting the form. This breaks the logic of most forms. The validation errors are supposed to show what is wrong and the posted content should remain in the form fields.
So how is it done right?

Use the controller for populating the form with default values

/* inside add/edit actions */
if (!empty($this->data)) {
	$this->Country->create();
	if ($this->Country->save($this->data)) {
		...
	} else {
		...
	}
} else {
	/* Now here you can put your default values */
	$this->data['Country']['active'] = 1;
	$this->data['Country']['lat'] = 0.0;
	...
}

It doesn’t matter what type they are (checkbox, select, radio, textarea, text). For checkboxes and radio buttons it should be either 0 (not checked – by default) or 1 (checked).

Validating first (manually)

This comes in handy, if you need to do something prior to saving the posted data:

# very important if you use validates()
$this->Post->set($this->data);
 
if ($this->Post->validates()) {
	// do something here
	# false = no need to validate again (already done in validates()!)
	$this->Post->User->save(null, false);
	...
} else {
	// ERROR 
}

This is also very useful if you don’t want to save the record. Maybe you want to write an email or something.

Note: In save() we do not pass $this->data again because callback functions inside the model could already have altered the passed data. We would override this. So we pass “null” instead.
If you actually need to alter $this->data here, be sure all your important modifications happen inside beforeSave() – as beforeValidate() is now not called anymore with “false” as second param.

Custom controller validations

This is useful if the result of a component matters for the validation process and you want to manually invalidate a field:

$this->Post->set($this->data);
 
if (!$this->MyComponent->foo()) {
	$this->Post->invalidate('field', 'error message');
}
 
# if foo() returns false, validates() will never return true
if ($this->Post->validates()) {
	$this->Post->User->save(null, false);
	...
} else {
	// ERROR 
}

Modifications prior to validation/saving

Sometimes you want to make sure, that your display field (e.g. “name”/”title”) is uppercase for the first letter, and lowercase for the rest. As any other modification as well you would want to put this in the model as beforeValidate() or beforeSave(). Usually the first one is the better choice. After validation there should only be necessary changes that don’t affect the validated fields if possible. And if so, make those don’t break the validation rules. In our case we even wont to auto-prefix urls in order to properly validate them. So here we have no choice other than using beforeValidate():

function beforeValidate() {
	if (!empty($this->data[$this->alias]['name'])) {
		$this->data[$this->alias]['name'] = ucfirst(mb_strtolower($this->data[$this->alias]['name']));
	}
	/* adding http:// if neccessary (if only www... was submitted) */
	if (!empty($this->data[$this->alias]['homepage'])) {
		$this->data[$this->alias]['homepage'] = CustomComponent::autoPrefixUrl($this->data[$this->alias]['homepage']);
	}
	/* very important! */
	return true;
}

If you don’t return true, the saving process will abort. This usually doesn’t make sense in beforeValidate() – and is mainly done in beforeSave().

Note: Most generic beforeSave() and beforeValidate() functionality could easily be transformed into a behavior.

 
No Comments

Posted in CakePHP