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

There are a lot of articles about that out there. Unfortunately, most of them are kind of crappy. Who wants to add the -app path all the time or use the full folder name with it? Its annoying!

Setup

Similar to Windows we need to add the path to the environment. In the case of “debian” systems in the .profile file in the home folder.

nano .profile

At the end of the file simply add your console folder of cake:

PATH=$PATH:/var/www/.../trunk/cake/console

Thats it. Now you need to re-login (..profile might work, too) and it should work.

Result

Now navigate to the app dir of your project: :.../app#. Then type “cake” or any other command:

cake bake ...

Everything else will work automatically.

Cronjobs

The documentation in the cookbook is pretty well written. Just use that cakeshell file as described.

CakePHP 2.x

See the updated windows post for the correct path here. Also note the way more comfortable quicklinks 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 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->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

For Cake2 I moved the current templates to my Setup plugin.

 
5 Comments

Posted in CakePHP