RSS
 

Archive for June, 2010

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

Drop this script into the /shells folder (either vendors or app).
Now 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”.

 
3 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.

Usage of those resources

For components, add it to the controller in order to use it in the corresponding actions:

var $components = array('MyC'); # file is in /app/controllers/components named my_c.php

And in one of the controller’s actions:

$this->MyC->foo();

For helpers, add it to the controller in order to use it in the corresponding views:

var $helpers = array('MyH'); # file is in /app/views/helpers named my_h.php

And in one of the controller’s views:

$this->MyH->foo();

Libs can be used everywhere – include them at runtime:

App::import('Lib', 'MyL'); # file is in /app/libs named my_l.php
$myL = new MyL();
$myL->foo();

Possible in controllers, components, behaviors, view, helpers, elements and everything else.

Behaviors and other elements are used similar to the above.

For Plugins simply add the plugin name: “Text” becomes “Plugin.Text” etc

Hacks for special use cases

Sometimes we need to break MVC in order to avoid redundance (and stay DRY).
A typical szenario is when we need a core helper in the controller (e.g. TextHelper).
We need to manually include it then at runtime:

App::import('Helper', 'Text');
$text = new TextHelper();
$myText = $text->truncate($myText);

I want to emphasize that this should only be done if not possible any other way.
You would also have to manually start and attach all helpers which are used inside the helper. Pretty annoying :)

I proposed a while ago that most of the core helpers should actually extend or at least use libs which contain the relevant functionality.
This way we can use the libs in the controller and we can use their methods in the view via helper wrappers.
But as of right now this is not yet planned for Cake2.0 or higher.

 
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 an uid (user_id) 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('Tools.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('Tools.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 choose 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.

Why not using hardcoded integer values like 0, 1, 2 etc?
The answer is simple: It is usually really bad style to do so.
What if you want to update your code a few weeks later – you probably don’t even know anymore what 1 or 2 stand for. Constants like TYPE_ACTIVE TYPE_PENDING, though, are way more descriptive.

Let’s get started

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

b) Put this in your app_model.php:

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

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

/*
 * static enum: Model::function()
 * @access static
 */
 public 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

[b]Thats it![/b]

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

further advantages
- can be used from anywhere (model, controller, view, behavior, component, …)
- 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)
- create multiple static functions for different views (“optionsForAdmins”, “optionsForUsers” etc). the overhead is minimal.

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 tips:
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”)

Combination with LazyLoading

This approach gets more complicated in combination with “Lazy Loading” of Models (if even implemented). In this case the related models are not imported until actually needed. So if you use constants from your model before this happens, you get a fatal error! You would need to App::import() all models which you need for static enum access. This can be done in the controller actions and adds not more than 1-2 lines of code.

I am working on a PHP5.3 ONLY solution right now which uses the brand new __callStatic() method and works well with LazyLoading. Stay tuned…

For Cake2.0, all you have to do is make sure that the classes are defined before you access their constants:

App::uses('MyModel', 'PluginName.Model');

You can do that right before you use it or define it globally in your Controller class for example.

UPDATE August 2011 – Enums in baking templates

Include your enums in your templates for “cake bake” in order to have them in your views out of the box.
All you need to do:
a) Add the pluralized static method of the enum field to the model (e.g. status => statuses):

/**
	 * @static
	 */
	public static function statuses($value = null) {
		$array = array(
			self::STATUS_INACTIVE => __('Inactive', true),
			self::STATUS_ACTIVE => __('Active', true),
		);
		return parent::enum($value, $array);
	}
	const STATUS_INACTIVE = 0;
	const STATUS_ACTIVE = 1;

b) Adjust your bake templates

foreach ($fields as $field) {
	if (strpos($action, 'add') !== false && $field == $primaryKey) {
		...
	} elseif ($schema[$field]['type'] == 'integer' && method_exists($modelClass, $enumMethod = lcfirst(Inflector::camelize(Inflector::pluralize($field))))) {
		echo "\t\techo \$this->Form->input('{$field}', array('options'=>".Inflector::camelize($modelClass)."::".$enumMethod."()));\n";
	} ...

this is the code for the form.ctp template

...
} elseif ($schema[$field]['type'] == 'integer' && method_exists($modelClass, $enumMethod = lcfirst(Inflector::camelize(Inflector::pluralize($field))))) {
	echo "\t\t<td>\n\t\t\t<?php echo ".$modelClass."::".$enumMethod."(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</td>\n";
}
...

and this for index.ctp (view.ctp works similar)

So if the bake script finds the static method “statuses” in the model it will then present the dropdown in the forms as well as the translated (+i18n) value in the views (index/view).

UPDATE Oktober 2011 – Regroup and Reorder

With a little trick it is now possible to return only a small subset (and in different order) for specific form elements. This is the improved enum method for your AppModel:

/**
 * @param string $value or array $keys or NULL for complete array result
 * @param array $options (actual data)
 * @return mixed string/array
 */
public static function enum($value, $options, $default = '') {
	if ($value !== null && !is_array($value)) {
		if (array_key_exists($value, $options)) {
			return $options[$value];
		}
		return $default;
	} elseif ($value !== null) {
		$newOptions = array();
		foreach ($value as $v) {
			$newOptions[$v] = $options[$v];
		}
		return $newOptions;
	}
	return $options;
}

Lets say, we have our status array from above. But for users we dont want them to be able to set the record to “deleted”. And lets say we are in the Message Model:

'options'=>Message::statuses(array(Message::STATUS_NEW, Message::STATUS_UNREAD, Message::STATUS_READ, Message::STATUS_ANSWERED))

We pass this on as options for the FormHelper and the deleted status is not available. No extra methods or configuration required. The order in which the keys are passed will decide the order of the translated enum values.

 
3 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 params “value”, “selected” or “checked” for the default value. But this field will then 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).

Default Values – hidden!

Many make the mistake to use hidden fields for some values that are not supposed to be edited by users. But they still can be modified using Firefug etc.
So what is the more correct approach here?

if (!empty($this->data)) {
	$this->Post->create();
	# add the content before passing it on to the model
	$this->data['Post']['status'] = '2';
	if ($this->Post->save($this->data)) {
		...
	}
}

We don’t even create hidden inputs in the form but add the hidden values in the controller “on demand”. Right before the model gets them and processes them. No tempering possible then. No overhead in the forms.

Disallowing some fields to be edited

Most beginners would make the (huge) mistake to simply mark the input fields as read-only or hidden field. Well, again, Firebug can easily modify those fields.
If you really want to use this strategy, make sure (!) that they are not passed on to the model. This can be accomplished with the third parameter in the save() method of the model:

# we dont want the "read-only" fields to be edited (like e.g. 'approved')
if ($this->Post->save($this->data, true, array('name', 'title', 'content'))) {

Now only those 3 fields get modified, no matter what. See my security post for details how to secure forms.

A cleaner approach usually is to simply echo the content (without any inputs). For this to work it is important NOT to just pass it along $this->data. After posting the form those fields are not in the data array anymore and will cause errors. The correct approach (I even baked my templates this way) would be to pass the record itself to the view as well:

function edit($id = null) {
	if (empty($id) || !($post = $this->Post->find('first', array('conditions'=>array('Post.id'=>$id))))) {
		//ERROR and redirect
	}
	if (!empty($this->data)) {
		$this->data['Post']['id'] = $post['Post']['id'];
		if ($this->Post->save($this->data, true, array('id', 'title', 'content', ...))) {
			//OK and redirect
		} else {
			//ERROR + validation errors without redirect
		}
	}
	if (empty($this->data)) {
		$this->data = $post;
	}
 
	$this->set(compact('post'));
}

Now we have full access to the record even after posting the form: echo $post['Post']['approved']; etc.

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 necessary (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

 

Cake Bake: Custom Templates

22 Jun

The Problem

The default templates of the “baking” process are not very useful for a productive environment.
They are meant to rapidly produce working code for development purposes. This is actually great for that matter. But as soon as you want to get your code online, you will face serious issues.
With CakePHP1.3 you can now use your own templates – easier than ever before.
So why not customizing it first and saving a lot of time in the long run?

The Solution

In your /vendors/ folder (either globally in /root/vendors or locally in /root/app/vendors) create a new folder with your desired template name (e.g. “custom”) inside “shells”:
/vendors/shells/custom
Tip: Copy all files from the cake default template (/cake/console/templates/default)
There should be 3 subfolders now:
/custom/actions
/custom/classes
/custom/views

A quick guide

Views:

If you need access to field types, you may use $field in combination with $schema:

if ($schema[$field]['type'] == 'datetime') {}

Possible values: boolean (tinyint 1), integer, date, datetime, time, string (varchar/char), text (textarea)

Other information:
$schema[$field]['key'] for example tells you if the field is “primary” (“id” usually).

Actions:
If you want to display the “displayField” instead of the id in the flash message for every add/edit/delete action:

$var = $this->data['<?php echo $currentModelName; ?>']['<?php echo $displayField; ?>'];

Quite handy. Now it can be “‘Test-Record’ deleted” instead of “ID 1 deleted” etc.

How I did it (your templates may differ from it!)

Controllers (/custom/actions/controller_actions.ctp)

My template for “view” actions looks like this:

function <?php echo $admin ?>view($id = null) {
		if (empty($id) || !($<?php echo $singularName; ?> = $this-><?php echo $currentModelName; ?>->find('first', array('conditions'=>array('<?php echo $currentModelName; ?>.id'=>$id))))) {
<?php if ($wannaUseSession): ?>
			$this->Common->flashMessage(__('invalid record', true), 'error');
			$this->Common->autoRedirect(array('action' => 'index'));
<?php else: ?>
			$this->flash(__('invalid record', true), array('action' => 'index'));
<?php endif; ?>
		}
		$this->set(compact('<?php echo $singularName; ?>'));
	}

This way it will NOT display empty views (due to invalid ids).
autoRedirect() is optional – i have a controller method that decides whether to redirect to index or to referer (if available). Note: This function is not part of the core!

My template for “delete” actions is similar:

function <?php echo $admin; ?>delete($id = null) {
		if (empty($id) || !($<?php echo $singularName; ?> = $this-><?php echo $currentModelName; ?>->find('first', array('conditions'=>array('<?php echo $currentModelName; ?>.<?php echo $primaryKey; ?>'=>$id), 'fields'=>array('<?php echo $primaryKey; ?>'<?php echo ($displayField!=$primaryKey?', \''.$displayField.'\'':'')?>))))) {
<?php if ($wannaUseSession): ?>
			$this->Common->flashMessage(__('invalid record', true), 'error');
			$this->Common->autoRedirect(array('action'=>'index'));
<?php else: ?>
			$this->flash(__('invalid record', true), array('action' => 'index'));
<?php endif; ?>
		}
		if ($this-><?php echo $currentModelName; ?>->delete($id)) {
<?php if ($wannaUseSession): ?>
			$var = $<?php echo $singularName; ?>['<?php echo $currentModelName; ?>']['<?php echo $displayField; ?>'];
			$this->Common->flashMessage(sprintf(__('record del %s done', true), h($var)), 'success');
			$this->redirect(array('action' => 'index'));
<?php else: ?>
			$this->flash(__('record del done', true), array('action' => 'index'));
<?php endif; ?>
		}
<?php if ($wannaUseSession): ?>
		$this->Common->flashMessage(sprintf(__('record del %s not done exception', true), h($var)), 'error');
<?php else: ?>
		$this->flash(__('record del not done', true), array('action' => 'index'));
<?php endif; ?>
		$this->Common->autoRedirect(array('action' => 'index'));
	}

Usually it is more useful to display the title/name (displayField) instead of the id in the flash message.

The edit actions also need some checking. The default templates would just display an empty form (like add) – which is not what edit is intended to be.

function <?php echo $admin; ?>edit($id = null) {
		if (empty($id) || !($<?php echo $singularName; ?> = $this-><?php echo $currentModelName; ?>->find('first', array('conditions'=>array('<?php echo $currentModelName; ?>.id'=>$id))))) {
<?php if ($wannaUseSession): ?>
			$this->Common->flashMessage(__('invalid record', true), 'error');
			$this->Common->autoRedirect(array('action' => 'index'));
<?php else: ?>
			$this->flash(__('invalid record', true), array('action' => 'index'));
<?php endif; ?>
		}
		if (!empty($this->data)) {
			if ($this-><?php echo $currentModelName; ?>->save($this->data)) {
<?php if ($wannaUseSession): ?>
				$var = $this->data['<?php echo $currentModelName; ?>']['<?php echo $displayField; ?>'];
				$this->Common->flashMessage(sprintf(__('record edit %s saved', true), h($var)), 'success');
				$this->redirect(array('action' => 'index'));
<?php else: ?>
				$this->flash(__('record edit saved', true), array('action' => 'index'));
<?php endif; ?>
			} else {
<?php if ($wannaUseSession): ?>
				$this->Common->flashMessage(__('formContainsErrors', true), 'error');
<?php endif; ?>
			}
		}
		if (empty($this->data)) {
			$this->data = $<?php echo $singularName; ?>;
		}
<?php
		foreach (array('belongsTo', 'hasAndBelongsToMany') as $assoc):
			...
		endforeach;
		if (!empty($compact)):
			echo "\t\t\$this->set(compact(".join(', ', $compact)."));\n";
		endif;
	?>
	}

You have more useful feedback after updating the record (displayField instead of id).

The add actions are pretty much the same.

function <?php echo $admin ?>add() {
		if (!empty($this->data)) {
			$this-><?php echo $currentModelName; ?>->create();
			if ($this-><?php echo $currentModelName; ?>->save($this->data)) {
<?php if ($wannaUseSession): ?>
				$var = $this->data['<?php echo $currentModelName; ?>']['<?php echo $displayField; ?>'];
				$this->Common->flashMessage(sprintf(__('record add %s saved', true), h($var)), 'success');
				$this->redirect(array('action' => 'index'));
<?php else: ?>
				$this->flash(__('record add saved', true), array('action' => 'index'));
<?php endif; ?>
			} else {
<?php if ($wannaUseSession): ?>
				$this->Common->flashMessage(__('formContainsErrors', true), 'error');
<?php endif; ?>
			}
		}
<?php
	foreach (array('belongsTo', 'hasAndBelongsToMany') as $assoc):
		...
	endforeach;
	if (!empty($compact)):
		echo "\t\t\$this->set(compact(".join(', ', $compact)."));\n";
	endif;
?>
	}

Models (/custom/classes/model.ctp)

after declaring $primaryKey I find it useful to automatically add:

<?php
...
if ($primaryKey !== 'id'): ?>
	var $primaryKey = '<?php echo $primaryKey; ?>';
<?php endif;
 
/** new **/
if ($displayField && $displayField != 'name' && $displayField != 'title'): ?>
	var $displayField = '<?php echo $displayField; ?>';
<?php endif; ?>
	var $recursive = -1;
	var $order = array();
 
<?php
/** new end **/
 
...
?>

recursive should always be -1 by default (you could also define $recursive globally in app_model), and $order is the default order if none is specified.

Views (/custom/views/…)

Now the files with the most changes

The CakePHP Team some time ago changed to sprintf(__(‘Add %s’, true), __(x, true)) instead of __(‘Add x’, true) and then changed back due to incompatibilites with some languages (not sure which ones).
But in most cases the %s makes it easier to translate.
You only need the “action” names translated + singular and plural “Model” name.
Without it, you will need every possible pairing (cross product) – which can easily be 1000s of translations which is quite some overhead. and totally unnecessary.

So here’s how it works:

//inside the actions block
echo "\t\t<li><?php echo \$this->Html->link(sprintf(__('Edit %s', true), __('{$singularHumanName}', true)), array('action' => 'edit', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?> </li>\n";
	echo "\t\t<li><?php echo \$this->Html->link(sprintf(__('Delete %s', true), __('{$singularHumanName}', true)), array('action' => 'delete', \${$singularVar}['{$modelClass}']['{$primaryKey}']), null, sprintf(__('Are you sure you want to delete # %s?', true), \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?> </li>\n";
	echo "\t\t<li><?php echo \$this->Html->link(sprintf(__('List %s', true), __('{$pluralHumanName}', true)), array('action' => 'index')); ?> </li>\n";

Now the security related part.
If users can enter data they usually can add js injection code which is executed as soon it is displayed (echoed) on the page. It is necessary to h() all text fields.

Extract of the view.ctp template:

<?php
...
foreach ($fields as $field) {
	/** CORE-MOD: prevents id fields to be displayed (not needed!) **/
	if ($field == 'id' || !empty($schema[$field]['key']) && $schema[$field]['key'] == 'primary') {
		continue;
	}
    /** CORE-MOD END **/
 
	$isKey = false;
	if (!empty($associations['belongsTo'])) {
		foreach ($associations['belongsTo'] as $alias => $details) {
			if ($field === $details['foreignKey']) {
				$isKey = true;
				echo "\t\t<dt<?php if (\$i % 2 == 0) echo \$class;?>><?php __('" . Inflector::humanize(Inflector::underscore($alias)) . "'); ?></dt>\n";
				echo "\t\t<dd<?php if (\$i++ % 2 == 0) echo \$class;?>>\n\t\t\t<?php echo \$this->Html->link(\${$singularVar}['{$alias}']['{$details['displayField']}'], array('controller' => '{$details['controller']}', 'action' => 'view', \${$singularVar}['{$alias}']['{$details['primaryKey']}'])); ?>\n\t\t\t&nbsp;\n\t\t</dd>\n";
				break;
			}
		}
	}
	if ($isKey !== true) {
 
		if ($field == 'modified' && !empty($fieldCreated)) {
			echo "<?php if (\${$singularVar}['{$modelClass}']['created'] != \${$singularVar}['{$modelClass}']['{$field}']) { ?>\n";
		}
 
		echo "\t\t<dt<?php if (\$i % 2 == 0) echo \$class;?>><?php __('" . Inflector::humanize($field) . "'); ?></dt>\n";
 
		/** CORE-MOD (datetime) **/
		if ($field == 'created' || $field == 'modified' || $schema[$field]['type'] == 'datetime') {
			if ($field == 'created') {
				$fieldCreated = true;
			}
 
			echo "\t\t<dd<?php if (\$i++ % 2 == 0) echo \$class;?>>\n\t\t\t<?php echo ";
			echo "\$this->Datetime->niceDate(\${$singularVar}['{$modelClass}']['{$field}'])";
			echo "; ?>\n\t\t\t&nbsp;\n\t\t</dd>\n";
 
			if ($field == 'modified' && !empty($fieldCreated)) {
				echo "<?php } ?>\n";
			}
		/** CORE-MOD END **/
 
		/** CORE-MOD (date) **/
		} elseif($schema[$field]['type'] == 'date') {
			echo "\t\t<dd<?php if (\$i++ % 2 == 0) echo \$class;?>>\n\t\t\t<?php echo ";
			echo "\$this->Datetime->niceDate(\${$singularVar}['{$modelClass}']['{$field}'], FORMAT_NICE_YMD)";
			echo "; ?>\n\t\t\t&nbsp;\n\t\t</dd>\n";
		/** CORE-MOD END **/
 
		/** CORE-MOD (yes/no) **/
		} elseif ($schema[$field]['type'] == 'boolean') {
			echo "\t\t<dd>\n\t\t\t<?php echo \$this->Common->yesNo(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</dd>\n"; // display an icon (green yes / red no)
		/** CORE-MOD END **/
 
		/** CORE-MOD (nl2br + h) **/
		} elseif ($schema[$field]['type'] == 'text') {
			echo "\t\t<dd>\n\t\t\t<?php echo nl2br(h(\${$singularVar}['{$modelClass}']['{$field}'])); ?>\n\t\t</dd>\n";
 
		/** CORE-MOD (protection against js injection by using h() function) **/
		} else {
			echo "\t\t<dd<?php if (\$i++ % 2 == 0) echo \$class;?>>\n\t\t\t<?php echo h(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t\t&nbsp;\n\t\t</dd>\n";
		}
		/** CORE-MOD END **/
	}
}
...
?>

As you can see I also have a Datetime helper method niceDate() which automatically localizes the date. The primary key is ommited because its absolutey useless.

In the index.ctp I don’t like the inline styling of the table – so I added a class “list” instead and used css to style it:

<table class="list">

I also applied the above %s and h() changes as well as ommiting the primary key:

Extract of the index.ctp template:

<?php
...
if ($isKey !== true) {
	/** CORE-MOD (no id) **/
	if ($field == 'id' || !empty($schema[$field]['key']) && $schema[$field]['key'] == 'primary') {
		# no output!
	/** CORE-MOD END **/
 
	/** CORE-MOD (datetime) **/
	} elseif ($field == 'created' || $field == 'modified' || $schema[$field]['type'] == 'datetime') {
		echo "\t\t<td>\n\t\t\t<?php echo \$this->Datetime->niceDate(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</td>\n";
	/** CORE-MOD END **/
 
	/** CORE-MOD (date) **/
	} elseif ($schema[$field]['type'] == 'date') {
		echo "\t\t<td>\n\t\t\t<?php echo \$this->Datetime->niceDate(\${$singularVar}['{$modelClass}']['{$field}'], FORMAT_NICE_YMD); ?>\n\t\t</td>\n";
	/** CORE-MOD END **/
 
	/** CORE-MOD (yes/no) **/
	} elseif ($schema[$field]['type'] == 'boolean') {
		echo "\t\t<td>\n\t\t\t<?php echo \$this->Common->yesNo(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</td>\n";
	/** CORE-MOD END **/
 
	/** CORE-MOD (nl2br + h) **/
	} elseif ($schema[$field]['type'] == 'text') {
		# "unchanged" output?
		/* echo "\t\t<td>\n\t\t\t<?php echo \${$singularVar}['{$modelClass}']['{$field}']; ?>\n\t\t</td>\n"; */
		# no difference to normal output right now...
		echo "\t\t<td>\n\t\t\t<?php echo nl2br(h(\${$singularVar}['{$modelClass}']['{$field}'])); ?>\n\t\t</td>\n";
 
	} else {
		//$schema[$field]['type'] == 'string'
		# escape: h()
		echo "\t\t<td>\n\t\t\t<?php echo h(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</td>\n";
	}
	/** CORE-MOD END **/
}
...
?>
 
2 Comments

Posted in CakePHP

 

CakePHP bootstrap goodies

21 Jun

A list of some things quite handy for every cake app and therefore best placed in the bootstrap.php as they are not (yet) part of the cake core.

# Useful when putting a string together in PHP
define('LF', PHP_EOL);
define('NL', "\n"); // new line
define('CR', "\r"); // carriage return
define('TB', "\t"); // tabulator
define('BR', '<br />'); // line break
 
# Make the app and l10n play nice with Windows.
if (substr(PHP_OS, 0, 3) == 'WIN') {
	define('WINDOWS', true);
} else {
	define('WINDOWS', false);
}
 
define('FORMAT_DB_DATETIME','Y-m-d H:i:s');	// used in date(...)
define('FORMAT_DB_DATE','Y-m-d');
define('FORMAT_DB_TIME','H:i:s');
 
define('DEFAULT_DATETIME',  '0000-00-00 00:00:00');
define('DEFAULT_DATE',      '0000-00-00');
define('DEFAULT_TIME',      '00:00:00');
 
 
/**
 * convenience function to check on "empty()"
 * 2009-06-15 ms
 */
function isEmpty($var = null) {
	if (empty($var)) {
		return true;
	}
	return false;
}
 
 
/**
 * of what type is the specific value
 * @return type: NULL, array, bool, float, int, string, unknown
 * 2009-03-03 ms
 */
function returns($value) {
	if ($value === null) {
		return 'NULL';
	} elseif (is_array($value)) {
		return '(array)'.'<pre>'.print_r($value,true).'</pre>';
	} elseif ($value === true) {
		return '(bool)TRUE';
	} elseif ($value === false) {
		return '(bool)FALSE';
	} elseif (is_numeric($value) && is_float($value)) {
		return '(float)'.$value;
	} elseif (is_numeric($value) && is_int($value)) {
		return '(int)'.$value;
	} elseif (is_string($value)) {
		return '(string)'.$value;
	} elseif (is_object($value)) {
		return '(object)'.get_class($value);
	} else {
		return '(unknown)'.$value;
	}
}
 
 
/**
 * uses native PHP function to retrieve infos about a filename etc.
 * @param string type (extension/ext, filename/file, basename/base, dirname/dir)
 * @param string filename to check on
 * //TODO: switch parameters!!!
 * 2009-01-22 ms
 */
function extractPathInfo($type = null, $filename) {
	switch ($type) {
		case 'extension':
		case 'ext':
			$infoType = PATHINFO_EXTENSION; break;
		case 'filename':
		case 'file':
			$infoType = PATHINFO_FILENAME; break;
		case 'basename':
		case 'base':
			$infoType = PATHINFO_BASENAME; break;
		case 'dirname':
		case 'dir':
			$infoType = PATHINFO_DIRNAME; break;
		default:
			$infoType = null;
	}
	return pathinfo($filename, $infoType);
}
 
 
/**
 * Shows pr() messages, even with debug=0
 *
 * @param mixed $content
 * @param string $class (optional)
 * 2009-04-07 ms
 */
function pre($array, $class = null) {
	$pre_array='';
	$pre_class='';
	if (is_array($array)) {
		if (!empty($class)){ $pre_class=' class="'.$class.'"'; }
		$pre_array='<pre'.$pre_class.'>'.print_r($array,true).'</pre>';
	} else {
		$pre_array = '<pre'.$pre_class.'>'.$array.'</pre>';
	}
	return $pre_array;
}
 
/**
 * Checks if the string [$haystack] contains [$needle]
 * @param string $haystack  Input string.
 * @param string $needle Needed char or string.
 * @return boolean
 */
function contains($haystack, $needle, $caseSensitive = false) {
  return (!$caseSensitive ? stripos($haystack, $needle) : strpos($haystack, $needle)) !== false;
}
 
/**
 * Checks if the string [$haystack] starts with [$needle]
 * @param string $haystack  Input string.
 * @param string $needle Needed char or string.
 * @return boolean
 */
function startsWith($haystack, $needle, $caseSensitive = false) {
	if ($caseSensitive) {
		return (mb_strpos($haystack, $needle) === 0);
	}
	return (mb_stripos($haystack, $needle) === 0);
}
 
/**
 * Checks if the String [$haystack] ends with [$needle]
 * @param string $haystack  Input string.
 * @param string $needle Needed char or string
 * @return boolean
 */
function endsWith($haystack, $needle, $caseSensitive = false) {
	if ($caseSensitive) {
		return mb_strrpos($haystack, $needle) === mb_strlen($haystack)-mb_strlen($needle);
	}
	return mb_strripos($haystack, $needle) === mb_strlen($haystack)-mb_strlen($needle);
}
 
 
register_shutdown_function('shutdownFunction');
 
/**
 * own shutdown function
 */
function shutDownFunction() {
	$error = error_get_last();
	if ($error['type'] == 1 && class_exists('CakeLog')) {
		CakeLog::write('error', 'Fatal Error in '.$error['file']. ' (line '.$error['line'].'):' . $error['message']);
	}
}
 
/*** < PHP5.3 ***/
if (function_exists('lcfirst') === false) {
  	function lcfirst($str) {
  		return (string)(mb_strtolower(mb_substr($str,0,1)).mb_substr($str,1));
	}
}

TB and BR etc really help if you write PHP and don’t want to switch to HTML all the time:

echo $this->foo().BR.$this->foo2();

returns() helped me a lot to figure out the return value of functions. E.g. pr() doesn’t show if NULL or FALSE was returned.

echo returns($this->foo2());
//or
$res = returns($this->foo2();
die(returns($res));
 
No Comments

Posted in CakePHP

 

Validating multiple models at once

20 Jun

There are forms where you want to add/edit not only fields of the current model, but also of a related one (usually hasMany or belongsTo).

Example:
User and Post

$this->Post->set($this->data);
$this->Post->User->set($this->data);
 
$val1 = $this->Post->validates();
$val2 = $this->Post->User->validates();
 
if ($val1 && $val2) {
	// OK (save both models separatly in order to use the user_id)
	$this->Post->User->save(null, false);
	$this->data['Post']['user_id'] = $this->Post->User->id;
	$this->Post->save($this->data, false);
} else {
	//ERROR 
}

So whats going on here?
We first set the posted data to the models wie want to validate. This is important when using validates() instead of save(). Then we validate them and only if both pass we continue saving.

Why not directly? Why using $val1 and $val2?
Well, for that you have to know something about php as programming language and how conditions are processed.
If (condition1 && condition2 && ...) {} else {} stops checking conditions as soon as the first condition fails. this is “smart” because it saves time. If the first condition fails, it will jump to the else block no matter what the other conditions return. So why bothering to check them?
But we want to validate both models no matter what – so we have to process them before and only compare the return values! If you miss that you won’t see the validation errors of the second model in the view (if the first one failed, anyway).

Cake automatically passes the errors to the view, if the Model.field syntax is used there:

echo $this->Form->input('User.email');
echo $this->Form->input('User.username');
echo $this->Form->input('comment'); // or Post.comment

Thats it. You can extend this with as many models as you like.

Using PHP Tricks

If you read my other article you might have found another approach:

if ($this->Post->validates() & $this->Post->User->validates()) {}

This works because one & will first check all conditions before deciding what to do.

NEVER use two & (&&) in this situation – for validating multiple models at once. It’s wrong!

Using cake’s find(all)

If your data is in model + related models structure you can also use this approach. It is faster and shorter (but I sometimes like control over my form processing):

if ($this->Post->saveAll($data, array('validate'=>'only'))) {
	//saveAll with validate false or single set of saves with validate false
}

This will only validate all the inputs without saving it.
Either way you should then set validate to false afterwards (no need to re-validate the data on save).

 
1 Comment

Posted in CakePHP

 

CakePHP and Subversion

18 Jun

A Quick How-To about properly setting up a svn for your cake app.

First of all, we need to drop a “trunk” folder into our svn:
/trunk

inside this trunk, we now add
/app (actual app)
/vendors (global vendors)
/cake (cake core)

Now, before committing the first time, we want to make sure that only non-transient files are actually under svn. That means, we need to exclude temporary files as well as all config files containing passwords and other private settings.

a) database.php
You will need to exclude this file in the properties of the parent folder “/trunk/app/config/”.

With tortoise, its as easy as right-clicking on the folder and adding
svn:ignore database.php
But under linux it shouldn’t be that much harder.

b) tmp folder
Exclude all subfolders and files within “/trunk/app/tmp” with
svn:ignore *
But don’t forget to manually add the subfolders like “cache” as well their subfolders (for cache: “persistant”, “models”, “views”).
Now apply the same ignore property on those subfolders, too.

The Result:
/tmp
/tmp/cache
/tmp/logs
/tmp/cache/persistant
/tmp/cache/models
/tmp/cache/views
are all under svn – but any tmp file (either in /tmp directly or in one of the subfolders) will be not.

c) configs – the probably most difficult part

It is never a good idea to submit keys (Akismet, SMTP, FacebookAPI, …) to svn.
so we need to find a solution for this.
This is my advice:

Include both “config.php” and “config_private.php” in exact this order in your bootstrap (with file_exists() in order to prevent errors!). Now all settings in the private config override the public one.

One example:

# config.php
$config['Email'] = array(
	'use_smtp' => 0,
	'smtp_port' => 25,
	'smtp_timeout' => 20,
	'smtp_host' => '',
	'smtp_username' => '',
	'smtp_password' => '',
);
 
# config_private.php
Configure::write('Email.smtp_password', '123');
//...

This way every developer can use his own private keys and settings without publishing them in the svn.

Hot tips

With Tortoise you can export your properties and easily import it for other upcoming projects (for each folder separately). I save my /config/ exlusions in “config.svnprops” for example.

The same properties can be applied to multiple folders at once by selecting all folders and then right-click on them. This helps to quickly ignore any files in the 3 tmp-subfolders.

Outlook

Soon I will talk about how to manage several apps with the same libraries (like cake and vendors folder).

 
No Comments

Posted in CakePHP, SVN