RSS
 

Posts Tagged ‘Bake’

Cake Bake: Custom Templates “Deluxe”

24 Apr

You might have read my almost two year old article about custom bake templates.
Much changed since then. In Cake as well in my user-land code.

Note: The "setup" bake theme is written for 2.x but can easily be backported to 1.3 manually.

Outline

We probably all know how xxxxxxx powerful the Bake tool is. It is all about speed for the first outline of a project.
So the faster the first CRUD controllers are baked the sooner we can dig into the details.
But speed itself is not the only crucial part. If we have a model/table with a lot of meta fields that should not get baked we would have to remove them manually from index/view/form every time we re-bake. That’s not really something you want to waste your time with.

Skip Fields

With some custom template like mine you can skip certain fields for baking. Some like 'password', 'slug', 'lft', 'rght' are automatically skipped by my template. But you can also define additional fields yourself:

class Deadline extends AppModel {
	public $scaffoldSkipFields = array('note');
}

So the textarea field "note" will not be outputted in your ctp files.

An additional feature I want to implement is that you can specify the actions it should skip:

public $scaffoldSkipFields = array('note'=>array('prefix'=>'', 'actions'=>array('index')));

This would mean that it skips this field for all non-prefixed ctps as well as all indexes.

public $scaffoldSkipFields = array('note'=>array('prefix'=>'admin')); // skip for all admin views

This will be included shortly.

Default Values

class User extends AppModel {
	public $scaffoldDefaultValues = array('role_id' => 'ROLE_USER', 'status' => 'User::STATUS_ACTIVE', 'level'=>1, 'text'=>'\'foo\'');
}

Which would result in this inside the controller action add:

if ($this->request->is('post')) {
    ...
} else {
    $this->request->data['User']['role_id'] = ROLE_USER;
    $this->request->data['User']['status'] = User::STATUS_ACTIVE;
    $this->request->data['User']['level'] = 1;
    $this->request->data['User']['text'] = 'foo'; 
}

This will have the above default values set up for adding a new user.

PS: Why not in the form itself? It is always better to put default values as a part of the logic into the controller and outside of the view layer.

Enum support

This article describes how to setup enums in CakePHP the easy way. Do this after you bake the model and BEFORE you bake controller and views. Then you will end up with ctps including all the Enum things automatically like a dropdown box for forms and translated values for index and view.

Smarter theme

We already covered a few basic field types before:

} elseif ($schema[$field]['type'] == 'date') {
	// formatted date
} elseif ($schema[$field]['type'] == 'boolean') {
	// yesNo image
} elseif ($schema[$field]['type'] == 'text') {
	// nl2br(h())
}

I also added decimals for currencies and normal float values:

} elseif ($schema[$field]['type'] == 'float' && strpos($schema[$field]['length'], ',2') !== false) {
	echo "\t\t<dd>\n\t\t\t<?php echo \$this->Numeric->money(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</dd>\n";
} elseif ($schema[$field]['type'] == 'float' && strpos($schema[$field]['length'], ',') !== false) {
	echo "\t\t<dd>\n\t\t\t<?php echo \$this->Numeric->format(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</dd>\n";
}

Switch Numeric with Number if you use your own helper. This will print a nicely formatted currency value instead of the default decimal value.

Usage

The files are in the Templates folder of my setup plugin.

Since my plugin theme might contain stuff you cannot and don’t wont to use you can cherrypick the above "tricks" for your own custom bake theme.

Now you know how you can further customize your themes to make baking even more fun as it already is 🙂

 
1 Comment

Posted in CakePHP

 

All new CakePHP Tips and Tricks

26 Mar

This is supposed to be a list of useful tricks gathered over many months.

First Templates, then Bake, then Adjustments

The usual workflow for a new project should be

This way you speed up your development while having all the advantages of custom templates.
Follow the link to read more about the topic.

Don’t sanitize

Sanitizing is not always bad (see later on). But most of the times we don’t have to sanitize every bit of input.
Its overhead and usually makes more harm then good.
On save to database nothing is needed as Cake properly escapes data itself.
What we DO need is some protection in the view.
Use h($var) in the views to make sure all potential dangerous strings are now harmless. This is called "escaping".
Note: You would have to do this with any string that gets in contact with the user. So if you use database content in flash messages, you would have to escape before you call setFlash() – or escape the content in the session flash element (but if you want to be able to use HTML output, the second option is not going to work). Alternatively you could use BBCode for flash messages. That would allow you to use h() and any HTML markup together.
To sum it up: Try to be less restrictive on the input but still make sure your site is safe. This makes the user happier and keeps the efforts for security as low as possible but as high as necessary.

BAD Example:
Firstname/Lastname input fields validated with regexp or [A-Za-zäöüÄÖÜß]
We can see that all major German signs are accepted. But what if a French guy wants to sign up. His name might be Aimé. He would be quite frustrated with the website and leave!

GOOD Example:
Simply validate the length and escape the name on output. Every possible name is accepted 🙂

echo h($user['User']['first_name']);

So follow these tips and you will be fine!

Careful with the super-automatic methods

Methods like updateAll() and deleteAll() are a little bit different from the normal save() or find() methods.
They accept expressions and therefore don’t automatically escape the content. You should only use them with own "controlled" input or after sanitizing the data thoroughly. Otherwise your SQL queries might break or can even be used in harmful ways against you and your site.

Those methods like updateAll() can be pretty handy or even necessary if you need atomic DB updates.
Example:

$this->Model->updateAll(array('Post.view_count' => 'Post.view_count + 1'), array('Post.id' => $post['Post']['id']));

Even if two users trigger this the exact same moment, it will raise the count twice. If you use find() and saveField() you might end up overriding each other and raising it only once.
But in this example the input is not from the user and can therefore be considered safe.

If you use user-input you should cast (string to int if applicable) or strictly sanitize to assert that sql injections are not possible!

Working with dates

I created some date constants in my bootstrap:

define('DEFAULT_DATE', '0000-00-00');
define('DEFAULT_TIME', '00:00:00');
define('FORMAT_DB_DATE','Y-m-d');
define('FORMAT_DB_TIME','H:i:s');
define('FORMAT_DB_DATETIME', FORMAT_DB_DATE . ' ' . FORMAT_DB_TIME);
// now I can use it everywhere (controller, models, views, ...)
$today = date(FORMAT_DB_DATE);
$yesterday = date(FORMAT_DB_DATE, time()-DAY);
$exactlySevenDaysAgo = date(FORMAT_DB_DATETIME, time()-7*DAY);

A very clean and readable approach.

Always try to use the SECOND, HOUR, DAY .. constants.
Note: Everything above WEEK gets fuzzy (MONTH is always 30 days). So for everything from MONTH up you should use the PHP5 DateTime object to correctly add months/years.

Using debug mode 1

Right now you can usually switch between debug mode 0 (no debug) and 2 (3 is not used anymore). With debug mode 2 you usually display the debug bar or debug plugin etc containing the SQL queries and other stuff.
My idea quite some time ago: Why not using 1 as well? 1 could mean debug output is on but no debug bar is displayed. It simply triggers all debug warnings/errors.
Simply make sure that your debug level is > 1 at the bottom of your layout:

$debug = (int)Configure::read('debug');
if ($debug > 1 && Configure::read('Debug.helper')) {
	// display debug tabs with SQL queries etc
}

For debug 1 it will still display all debug messages. And with debug 0 it is still debug-free.

 
1 Comment

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