Cake Bake: Custom Templates

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.

0.00 avg. rating (0% score) - 0 votes

5 Comments

  1. Thanks for the article.
    Can you show us also the cli command to bake the custom actions and views?

  2. its not different from using the default bake templates
    "cake bake" automatically asks you which template to use. just select the custom one.

  3. Would you consider open sourcing the Format / DateTime Helpers that are referenced in the Baked views created by your 2.0 Tools plugin?

    Or am i being dense in not finding them online? 🙂

    Cheers

  4. Awesome. Always happy to be proven stupid when deservedly so! 🙂 Thanks for your help!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.