RSS
 

Posts Tagged ‘Console’

Useful CakePHP shell scripts

01 Aug

Today I want to present some useful shell scripts I often use.
Hopefully you find them useful, as well 🙂

Where to put them

Drop shells in a vendor folder of your choice (either app, core, or plugin), for instance:
/app/vendors/shells/ or like me /app/plugins/tools/vendors/shells/

Password Reset

A quick and easy shell script to reset all passwords for local development.
Usage:

cake pwd_reset [pwd]

The password is optional (will be prompted otherwise)

The code:

<?php
# enhancement for plugin user model
if (!defined('CLASS_USER')) {
	define('CLASS_USER', 'User');
}
/**
 * reset user passwords
 */
class PwdResetShell extends Shell {
	var $tasks = array();
	//var $uses = array('User');
	var $Auth = null;
	/**
	 * reset all pwds to a simply pwd (for local development)
	 * 2011-08-01 ms
	 */
	function main() {
		$components = array('AuthExt', 'Auth');
		foreach ($components as $component) {
			if (App::import('Component', $component)) {
				$component .='Component';
				$this->Auth = new $component();
				break;
			}
		}
		if (!is_object($this->Auth)) {
			$this->out('No Auth Component found');
			die();
		}
		$this->out('Using: '.get_class($this->Auth).' (Abort with STRG+C)');

		if (!empty($this->args[0]) && mb_strlen($this->args[0]) >= 2) {
			$pwToHash = $this->args[0];
		}
		while (empty($pwToHash) || mb_strlen($pwToHash) < 2) {
			$pwToHash = $this->in(__('Password to Hash (2 characters at least)', true));
		}
		$this->hr();
		$this->out('pwd:');
		$this->out($pwToHash);
		$pw = $this->Auth->password($pwToHash);
		$this->hr();
		$this->out('hash:');
		$this->out($pw);
		$this->hr();
		$this->out('resetting...');
		$this->User = ClassRegistry::init(CLASS_USER);
		if (!$this->User->hasField('password')) {
			$this->error(CLASS_USER.' model doesnt have a password field!');
		}
		
		if (method_exists($this->User, 'escapeValue')) {
			$newPwd = $this->User->escapeValue($pw);
		} else {
			$newPwd = '\''.$pw.'\'';
		}
		$this->User->recursive = -1;
		$this->User->updateAll(array('password'=>$newPwd), array('password !='=>$pw));
		$count = $this->User->getAffectedRows();
		$this->out($count.' pwds resetted - DONE');
	}

	function help() {
		$this->out('-- Hash and Reset all user passwords with Auth(Ext) Component --');
	}
}

Useful if you just switched the salt, the hash method or imported users from another DB for testing 🙂

New User

How do you set up an admin account with a new project if you can’t login? Sure, allow(*) or other hacks.
But wouldnt a simple command like cake user be nicer? Check it out:

<?php
if (!defined('CLASS_USER')) {
	define('CLASS_USER', 'User');
}
class UserShell extends Shell {
	var $tasks = array();
	var $uses = array(CLASS_USER);
	function help() {
		$this->out('command: cake 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)) {
			$username = $this->in(__('Username (2 characters at least)', true));
		}
		while (empty($password)) {
			$password = $this->in(__('Password (2 characters at least)', true));
		}
		$schema = $this->User->schema();
		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;
		}
		if (!empty($schema['status']) && method_exists('User', 'statuses')) {
			$statuses = User::statuses();
			pr($statuses);
			while(empty($status)) {
				$status = $this->in(__('Please insert a status', true), array_keys($statuses));
			}
			$data['User']['status'] = $status;
		}
		if (!empty($schema['email'])) {
			$provideEmail = $this->in(__('Provide Email? ', true),array('y', 'n'), 'n');
			if ($provideEmail === 'y') {
				$email = $this->in(__('Please insert an email', true));
				$data['User']['email'] = $email;
			}
			if (!empty($schema['email_confirmed'])) {
				$data['User']['email_confirmed'] = 1;
			}
		}

		$this->out('');
		pr ($data);
		$this->out('');
		$this->out('');
		$continue = $this->in(__('Continue? ', true),array('y', 'n'), 'n');
		if ($continue != 'y') {
			$this->error('Not Executed!');
		}
		$this->out('');
		$this->hr();
		if ($this->User->save($data)) {
			$this->out('User inserted! ID: '.$this->User->id);
		} else {
			$this->error('User could not be inserted ('.print_r($this->User->validationErrors, true).')');
		}
	}
}

Remove the closing PHP tags

Since CakePHP1.3 the closing tags are omitted – for a good reason. You should also make sure your app does not contain any of those closing tags in PHP files. With this shell it’s done in seconds:

<?php
/**
 * removes closing php tag (?>) from php files
 * it also makes sure there is no whitespace at the beginning of the file
 */
class PhpTagShell extends Shell {
	var $tasks = array();
	var $uses = array();
	var $autoCorrectAll = false;
	# each report: [0] => found, [1] => corrected
	var $report = array('leading'=>array(0, 0),'trailing'=>array(0, 0));
	function main() {
		if(isset($this->args[0]) && !empty($this->args[0])) {
			$folder = realpath($this->args[0]);
		} else {
			$folder = APP;
		}
		if(is_file($folder)) {
			$r = array($folder);
		} else {
			App::import('Core',array('Folder'));
			$App = new Folder($folder);
			$this->out("Find recursive *.php in [".$folder."] ....");
			$r = $App->findRecursive('.*\.php');
		}
		$folders = array();
		foreach($r as $file) {
			$error = array();
			$action = '';
			$c = file_get_contents($file);
			if(preg_match('/^[\n\r|\n\r|\n|\r|\s]+\<\?php/', $c)) {
				$error[] = 'leading';
			}
			if(preg_match('/\?\>[\n\r|\n\r|\n|\r|\s]*$/', $c)) {
				$error[] = 'trailing';
			}
			if (!empty($error)) {
				foreach($error as $e) {
					$this->report[$e][0]++;
				}
				$this->out('');
				$this->out('contains '.rtrim(implode($error, ', '), ', ').' whitespaces / php tags: '.$this->shortPath($file));
				if (!$this->autoCorrectAll) {
					$dirname = dirname($file);
					if (in_array($dirname, $folders)) {
						$action = 'y';
					}
					while (empty($action)) {
						//TODO: [r]!
						$action = $this->in(__('Remove? [y]/[n], [a] for all in this folder, [r] for all below, [*] for all files(!), [q] to quit', true), array('y','n','r','a','q','*'), 'q');
					}
				} else {
					$action = 'y';
				}
				if ($action == '*') {
					$action = 'y';
					$this->autoCorrectAll = true;
				} elseif ($action == 'a') {
					$action = 'y';
					$folders[] = $dirname;
					$this->out('All: '.$dirname);
				}
				if($action == 'q') {
					die('Abort... Done');
				} elseif ($action == 'y') {
					$res = $c;
					if(in_array('leading', $error)) {
						$res = preg_replace('/^[\n\r|\n\r|\n|\r|\s]+\<\?php/', '<?php', $res);
					}
					if(in_array('trailing', $error)) {
						$res = preg_replace('/\?\>[\n\r|\n\r|\n|\r|\s]*$/', "\n", $res);
					}
					file_put_contents($file, $res);
					foreach($error as $e) {
						$this->report[$e][1]++;
						$this->out('fixed '.$e.' php tag: '.$this->shortPath($file));
					}
				}
			}
		}
		# report
		$this->out('--------');
		$this->out('found '.$this->report['leading'][0].' leading, '.$this->report['trailing'][0].' trailing ws / php tag');
		$this->out('fixed '.$this->report['leading'][1].' leading, '.$this->report['trailing'][1].' trailing ws / php tag');
	}
}

You an either go through the complete app. Or you can pass a specific path like so:
cake php_tag C:\testfolder (custom folder) or cake php_tag config (/app/config).

UPDATE 2013-02-12 for 2.x

I put everything in my Tools plugin for easier use. It also contains the most recent (bug)fixes. Make sure you use those files instead.
And the path would now be APP/Console/Command/ – but it is better to use the Tools plugin as a whole.
Just call them as

cake Tools.ShellName command

now.

 
2 Comments

Posted in CakePHP

 

Cakephp Console on Linux systems

24 May

You could hard-wire the Cake path into the environment.
With multiple cake core versions on a single system, sometimes it is better not to do that, though.
So I just follow the following guidelines:

  • Only add the PHP path to the system environment path (not the cake one!)
  • Always navigate to your app folder.
  • Call your shells from there, either with Console/cake (if you have an app Console folder with cake file) or ../lib/Cake/Console/cake (core cake file).

Cronjobs

The documentation in the cookbook is pretty well written.

Here with a little bit more detailed explanation:

# m h dom mon dow command
*/5 *   *   *   * cd /full/path/to/app && Console/cake myshell myparam

The tool Crontab is used here. This config file contains an example with a cronjob that is executed every 5 minutes.
Basically, you just tell the script to navigate into your app folder and call your shell as you normally would as real user (always from inside your app folder).

If you don’t have an app cake file (I don’t for some older projects), you can also use the core cake file just as fine:

# m h dom mon dow command
*/5 *   *   *   * cd /full/path/to/app && ../lib/Console/cake myshell myparam

You can edit the crontab for the www-data user, for example, using

crontab -e -u www-data

Using the user "www-data" (or a user that is affiliated with it) is advised to prevent running into issues with file access when creating or modifying files.
It is also a security issue using a user with more or different rights than he should have or needs.

Also note that your "cake" file (either app or core) needs to be executable, so make sure you have set the appropriate rights for it.

CakePHP 2.x and windows

See the updated windows post for the correct path here.
Also note the way more comfortable quick-links if you need to use 1.x and 2.x parallel.

 
1 Comment

Posted in CakePHP

 

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

 

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.

This will create the code completion script in your APP folder. For projects created via PHPDesigner this file will automatically be parsed and the code completion should work out of the box (the parsing can take a few minutes depending on the project size).

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

Update 2012-12-23 ms

For Cake2.x take a look at the new version of it in my Tools Plugin.

 
3 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 CakePHP 1.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->Format->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->Format->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 **/
}
...
?>

Update 2012-02-26 ms

In CakePHP 2 the path for the custom templates is now Console/Templates/.

I also moved the current templates to my Setup plugin. See this repo for an example how to do things in 2.x.

 
5 Comments

Posted in CakePHP