RSS
 

Posts Tagged ‘Behavior’

CakePHP and NamedScope for DRY conditions

15 Feb

A behavior for CakePHP 2.x

Background

I stumbled upon this fork and SimpleScope.
The latter has the disadvantage of redundancy in the scope conditions when used in multiple find configs. The first was pretty much how Rails’ scopes work.
But amongst other small issues it lacked the possibility of using model attributes for configuration.
And both didn’t have test cases.
So I decided to combine both, test the hell out of them and get the best out of the basic implementation ideas.

Basic Usage

For the behavior there is a more detailed documentation in the wiki.
But here it goes.

First install/download and load the Tools plugin as documented in the cookbook or its readme file.

Attach the behavior to your AppModel:

App::uses('Model', 'Model');
class AppModel extends Model {
    public $actsAs = array('Tools.NamedScope');
}

Then define some scopes in your model:

App::uses('AppModel', 'Model');
class User extends AppModel {
    public $scopes = array(
        'active' => array('User.active' => 1),
        'admin' => array('User.role LIKE' => '%admin%'),
    );
}

Then you can use those scopes in any of your find queries:

$activeUsers = $this->User->find('all', array('scope' => array('active')));
$activeAdmins = $this->User->find('all', array('scope' => array('active', 'admin')));
$activeAdminList = $this->User->find('list', array('scope' => array('active', 'admin')));

Advanced Usage

If you also want to use scopedFind(), you will also get rid of all the many find wrappers around those scopes that will often be placed inside the models.

An example:

public function getActiveAdmins() {
    $this->virtualFields['fullname'] = "CONCAT(User.firstname, ' ', User.lastname)";
    $options = array(
        'fields' => array('User.id', 'User.fullname'),
        'conditions' => array('User.role LIKE' => '%admin%'),
        'order' => array('User.fullname'),
    );
    return $this->find('all', $options);
}

Now there will maybe also be a getActiveUsers() method and maybe a few dozen more, which all
contain the same condition – which is not really DRY and might be quite error-prone if the conditions have to be adjusted (easy to miss one of the many occurrences in and out of the model).

So what would be a smarter way to approach this?
Let’s try to use the above scopes here – and also use the single wrapper method.
Besides the above scopes, you also need to define some scopedFinds in your model:

App::uses('AppModel', 'Model');
class User extends AppModel {
    public $scopes = array(
        'activeAdmins' => array(
            'name' => 'Active admin users',
            'find' => array(
                'type' => 'all',
                'virtualFields' => array(
                    'fullname' => "CONCAT(User.firstname, ' ', User.lastname)"
                ),
                'options' => array(
                    'fields' => array('User.id', 'User.fullname'),
                    'scope' => array('active', 'admin'),
                    'order' => array('User.fullname'),
                ),
            ),
        ),
        'activeUsers' => array(
             ...
        )
    );
}

The scope itself will both contain active, and the config about this scope key will be stored in a single place. So if you have some very complex condition around published (> a && < b && != c && …) this will take the overhead from multiple definitions and reduce it to a single location.

Let’s execute it:

$activeAdmins = $this->User->scopedFind('activeAdmins');

In case we need to only get a list or the count, we can adjust the scopedFind:

$activeAdminList = $this->User->scopedFind('activeAdmins', array('type' => 'list'));
$activeAdminCount = $this->User->scopedFind('activeAdmins', array('type' => 'count'));

We can also overwrite the default options:

$config = array(
    'options' => array(
        'limit' => 2, 
        'order' => array('User.created' => 'DESC'))
);
$twoNewestActiveAdmins = $this->User->scopedFind('activeAdmins', $config);

You can also get a list of available scoped finds:

$scopedFinds = $this->User->scopedFinds();

Scoped finds:

  • require a name string
  • optionally use a find array

The find arrays:

  • optionally use a type string (defaults to all)
  • optionally use an options array
  • optionally use virtualFields

The options arrays:

  • can use the behaviors’ scope property
  • support all other find options (including contain, order, group, limit, …)

Tip: See the test cases for more complex examples.

Testing

You should test your scopes, even if it’s just something like this:

public function testScopes() {
    $scopes = $this->User->scopes;
    // Each on its own
    foreach ($scopes as $scope) {
        $this->User->find('first', array('scope' => $scope));
    }
    // All together
    $this->User->find('first', array('scope' => $scopes));
}

In case there was invalid SQL, missing fields, wrong contain statements, it would be noticeable right away.

If you use scopedFinds, don’t forget to also unit test them (regarding valid SQL).
This can easily be forgotten now as you don’t have the find wrapper methods anymore.
In case you are lazy, add this test case to any model test that uses custom scopedFinds:

public function testScopedFinds() {
    $scopedFinds = $this->User->scopedFinds();
    foreach ($scopedFinds as $key) {
        $this->User->scopedFind($key);
    }
}

This will at least execute each find and throw an error if the SQL is invalid.
It is advisable to have a more thorough test case for each find key, though, that includes the assert of the return value.

Outlook

I bet when this behavior is thoroughly used, there will be quite a few adjustments necessary. But all in all this already seems to cover most of the use cases.

With Cake3 and stackable custom finders much of the functionality we need here will be part of core functionality. Which will be awesome.
Never-the-less, until then this can be a solid solution to keep the scopes/conditions DRY.

 
No Comments

Posted in CakePHP

 

ResetBehavior and HazardableBehavior

29 Oct

ResetBehavior

Reset is a new behavior I recently had to write to update some geocoded entries as well as records with processed titles/slugs – via beforeSave() – that had to be re-saved with a corrected processing method. Basically, it is a batch update that works even with huge tables as it processes the data in steps.

Example: Resetting slugs

Either via shell or via controller action you can trigger an update of all slugs that have been generated via Tools plugin Slugged behavior:

// First we need to re-load the Slugged behavior to enable "overwrite" mode
$this->Post->Behaviors->load('Tools.Slugged', array('label' => 'title', 'overwite' => true));
// Load the Reset behavior with only the title and slug field to read and modify.
$this->Post->Behaviors->load('Tools.Reset', array('fields' => array('title', 'slug')));
$res = $this->Post->resetRecords();
// flash message with number of records modified in $res

You should make a shell command for this and execute this migration code once on deploy of the modified code or SQL schema.
If you are not using CLI, make sure you set the time limit in your controller action accordingly (HOUR for example).

Example: Retrigger/Init geocoding

If you got records that now need to be geocoded, you probably added a lat and lng field (decimal 6,2 etc) to your table.
You probably also attached the Tools plugin Geocoder behavior to this model.
In order to quickly geocode you just have to use Reset:

$this->Post->Behaviors->load('Tools.Reset', array('fields' => array('address', 'lat', 'lng'), 'timeout' => 3));
$res = $this->Post->resetRecords();

Since all lat/lng fields are still null it will geocode the records and populate those fields. It will skip already geocoded ones. If you
want to skip those completely (not even read them), just set the scope to 'NOT' => array('lat' => null) etc.

Note that in this case we also use a timeout to avoid getting a penalty by Google for geocoding too many records per minute.

Advanced example: Resetting composite cache field

In this case we added a new cache field to our messages in order to make the search faster with >> 100000 records. The data was containing
all the info we needed – in serialized format.
We needed a callback here as there was some logic involved. So we simply made a shell containing both callback method and shell command:

$this->Message->Behaviors->load('Tools.Reset', array(
	'fields' => array('data'), 'updateFields' => array('guest_name'),
	'scope' => array('data LIKE' => '{%'), 'callback' => 'UpdateShell::prepMessage'));
$res = $this->Message->resetRecords();
$this->out('Done: ' . $res);

The callback method (in this case just statically, as we didnt want to mess with the model itself):

public static function prepMessage(array $row) {
	if (empty($row['Message']['data_array']['GUEST_FIRST_NAME'])) {
		return array();
	}
	$row['Message']['guest_name'] = $row['Message']['data_array']['GUEST_FIRST_NAME'] . ' ' . $row['Message']['data_array']['GUEST_LAST_NAME'];
	return $row;
}

See the test cases for more ways to use callbacks – including adjusting the updateFields list.

So as you can see, everything that involves a complete "re-save" including triggering of important callbacks (in model and behaviors) of all or most records can leverage this behavior in a DRY, quick and reusable way.

HazardableBehavior

This behavior is a very useful tool to test vulnerability against XSS or unescaped html output (especially accidental one).
The basic idea is to test all views that output data from varchar or text fields for proper escaping. Even if it not user input, it is still vital to properly escape.
Admin input can have chars like <, > etc in there, as well. Without the use of h() it can destroy the layout or worse. So it is always a good idea to cover all
views.

Just attach it temporarily (!) to any of your models and quickly fill your table with hazardous strings. Those strings can potentially end up there via Form input, of course.
This just automates it.
Then you can browse your site and see if an alert or other strange behavior occurs. This tells you that you forgot to use h() or other measures to secure your output properly.

You can also apply this behavior globally to overwrite all strings in the find result.
This way you don’t need to modify the database. On output it will just inject the hazardous strings and you can browse your website just as if they were actually stored in your db.
Just add it to some models or even the AppModel (temporarily!) as $actsAs = array('Tools.Hazardable' => array('replaceFind' => true)).
A known limitation of Cake behaviors in 2.x, though, is, that this would only apply for first-level records (not related data). So it is usually better to insert some hazardous strings into all your tables and make your tests then as closely to the reality as possible.

You can use skipFields to blacklist certain stringish fields from being overwritten and populated with hazardous strings.

Note: In 3.x the behavior callback issue regarding non-primary records will be solved 🙂 I am really looking forward to that.

CakePHP 3

This article is 2.x only.
For CakePHP 3 please see the 3.0 Tools Plugin documentation on the Reset behavior.

 
No Comments

Posted in CakePHP

 

Localized number formats for forms

09 Dec

Often your language is not english and therefore the locale settings for decimals are not . for decimals and , for thousands, but the opposite, for example.
If you do not allow localized input/output in your CakePHP apps it often times confuses users. So it is wise to do so.

You want to convert the localized values to the internal number format on save() and vice versa on find() prior to populating the form on edit.

I use my NumberFormat behavior to accomplish all those things in a clean way.

Important: This is only for form fields. Do not use to generally format your find() results. That’s what the View helpers like Time and Number are for. You need to format and output those values in a localized form in the view as needed using their methods.

Setup

I assume you already got the Tools Plugin up and running.
You can either attach the behavior statically:

public $actsAs = array('Tools.NumberFormat' => array('output' => true, ...));

or you can dynamically load it at runtime for the specific add/edit actions with custom options (usually the better way for customization):

$this->ModelName->Behaviors->load('Tools.NumberFormat', array('output' => true, ...));

Next you should set a global default localization pattern. You can use Configure:

$config['Localization'] = array(
	'thousands' => '.',
	'decimals' => ',',
);

You can also use the system setting

setlocale(LC_NUMERIC, 'de_DE.utf8', 'german');

in combination with localeconv set to true. But this often times can have some side-effect else-where.

Basic usage

For add actions we don’t need any output, so we simply use

$this->ModelName->Behaviors->load('Tools.NumberFormat');

For edit actions we also need the localized output:

$this->ModelName->Behaviors->load('Tools.NumberFormat', array('output' => true, 'fields' => array('custom_float_field')));

We also want the behavior to convert the field custom_float_field here.

Advanced usage

Multiply

If you want your users to input percentages as interger values (0 … 100 instead of 0.0 … 1.0), you can use multiply:

$this->ModelName->Behaviors->load('Tools.NumberFormat', array('multiply' => 0.01));

This would convert 50 to 0.5 on save and undo it back to 50 on read (with output set to true, of course!).

Strict

My experience is, that too lose validation for decimals can result in a mess. Often times, the resulting price in our database was 100 times the inputted value just because the person used . or a invalid combination of multiple ., , or both. You can easily prevent this by expecting a valid localized value using strict:

$this->ModelName->Behaviors->load('Tools.NumberFormat', array('strict' => true));

This would not allow . (dot) as decimal if your localized decimal is currently , (comma) – among other things.

Before

Sometimes you do not need/want to validate your data, use before to delay the converting:

$this->ModelName->Behaviors->load('Tools.NumberFormat', array('before' => 'save'));

This will then execute on save() even if you set $validate to false.

Tips

If you already loaded your behavior statically, and you want to change the settings, use unload() prior to load():

public function edit() {
	$this->ModelName->Behaviors->unload('NumberFormat');
	$this->ModelName->Behaviors->load('Tools.NumberFormat', $myNewConfig);
}

Note: to unload a behavior, do not use the Plugin. prefixed syntax here.

For more examples and use cases see the test case.

Question

How do you manage to provide localized forms? Clearly CakeNumber itself is not enough here.

 
2 Comments

Posted in CakePHP

 

Typographic Behavior and Typography Helper

12 Aug

For CakePHP Apps.

What is the issue here?

Typography has quite a few regional differences. In some countries/languages you use quotation marks like so:

“Hello” // English, curly, often in texts (1)
«Hello» // French, brackets, often in texts (2)
‹Hello> // Single brackets, often in texts (3)
„Hello‟ // German, low/up, often in texts (4)
"Hello" // English, basic, unified (5)
...

If many people from different countries and languages contribute to your site and post their text or some snippets this will result in a mess pretty quick. Especially if there is a lot of copy-and-paste involved (texts from different websites) there will be many different chars involved. So if you have a blog or a social network for stories, articles and reports you might want to unify them for the database backend.

How can we solve it?

Wouldn’t it be a good idea to address this issue as soon as possible? On save() for instance. This is where the behavior comes into play. So after the save() the text will be clean and unified.

In a second step you can regional it again using the TypographyHelper. Either for your main country/language or on a personal user-defined basis. The latter is more difficult to work with if caching is involved. In most cases you would just want to display it in the main one – (1) for English, (4) for Germany etc.

Behavior usage

As simple as behaviors are there is only one line to attach to the model:

class Article extends AppModel {
	public $actsAs = array('Tools.Typographic'=>array('fields'=>array('title', 'short_content', 'content', 'annotations')));
}

The behavior needs to know the fields you want to transform. If you do not define any fields it will transform all textarea and text fields!

It usually fires on beforeSave() but you can make it fire prior to validation with 'before' => 'validate' as configuration option.

Helper usage

The helper is pretty straight-forward, as well:

echo $this->Typography->formatCharacter($content);

Don’t forget to add your helper to the public $helpers array of your controller.

The default will be the english style “double quotes”, but you can either pass the locale setting as second argument or globally set it in your configs:

Configure::write('Typography.locale', 'low'); // will produce sth like „double quotes‟ - e.g- for Germany

Feel free to contribute if something is missing so far.

The helper also contains some methods like autoTypography() to transform all kind of things automatically, including the character formatting.

Extra goodies

In case you attach your behavior afterwards (some records already exist uncleaned), you can use updateTypographie from the behavior. So if we create an admin action like so:

public function admin_typographic($dryRun = null) {
	$res = $this->Article->updateTypography((bool)$dryRun);
	$this->Session->setFlash(__('done %s', $res));
	$this->redirect(array('action'=>'index'));
}

we will be able to clean all those in one batch, as well.

Remarks

In combination those two are a good example how text can easily be modified on a modular and clean basis.

If you use a lot of typographic modification in your views you might want to think about caching the final html result. This can reduce server load and increase speed a lot for larger articles/texts.

 
No Comments

Posted in CakePHP

 

Geocoding with CakePHP

12 Jun

Geocoding is a very powerful tool if you want to work with lat/lng, distances or just want to display a small map for your addresses. I use it in combination with my GoogleMaps Helper to display a dynamic or a static map with those addresses (as small pins on the map) or to display how far away you currently are (in km or miles) from this point.

The full package consists of a Lib, a Behavior and their tests and is in my Tools plugin.

Basics first: The lib

I moved the actual querying to a lib for flexibility and better unit testing. It can be used standalone from anywhere in your app where you want to geocode an address (get lat/lng) or reverse-geocode coordinates etc.

Geocoding

App::uses('GeocodeLib', 'Tools.Lib');
$this->Geocode = new GeocodeLib();
$this->Geocode->setOptions(array('host' => 'de')); //optional - you can set it to your country's top level domain.
if ($this->Geocode->geocode('12345 Cityname', $settings)) {
	$result = $this->Geocode->getResult();
}

Thats all there is to it. You get a result array. Use debug() to display it if you are unsure what it contains.

Some of the available setting options are:

  • min_accuracy (see below – behavior)
  • allow_inconlusive
  • log (for debugging)
  • address (array of fields to map)
  • expect
  • overwrite (see below – behavior
  • before (validate/save)

For details see the class or the test cases (which do not cover all yet, though – feel free to help out).

Reverse geocoding

if ($this->Geocode->reverseGeocode($lat, $lng, $settings)) {
	$result = $this->Geocode->getResult();
}

will retrieve a matching address to your coordinates.

There are also some helper methods included already:

Distance

$result = $this->Geocode->distance($pointOne, $pointTwo); // array('lat' => ..., 'lng' => ...);

returns the distance between those two points.

Convert

$result = $this->Geocode->convert($value, $fromUnit, $toUnit);

helps to convert miles to km etc.

Blur

$coordinate = GeocodeLib::blur($coordinate, $level); // level=1...5

Helpful when saving geocoded user data and displaying them publicly (google map etc).
It would be dangerous to display the exact coordinates (if they entered their street name and number) of other users. In such a case you should modify the coordinates prior to displaying them. The above code snippet can be used in the view before you pass it on to the GoogleMap helper or before you print it out. The higher the level the more blurred the coordinate.

The Behavior for (automatic) geocoding on the fly

So if we have an address model, all we need to do is adding three fields: lat, lng (both float 9,4 for example) and formatted_address (optional if you want to capture/store the full address string).
Those will be filled by the behavior if we attach our behavior like so:

public $actsAs = array('Tools.Geocoder' => array('real' => false, 'override' => true));

Override means it always updates the coordinates if the address is provided. Real means the fields have to actually exist in the table – like street, postal_code and city etc.

There are many more options – please take a look at the behavior documentation in the class itself or the test cases for details.
One important configuration option is min_accuracy:

'min_accuracy' => GeocodeLib::ACC_SUBLOC

means that the geocoding response is only valid if we can find an address as exact as a sub-locality or better. This can help in validation addresses since incorrect addresses would fall back to pretty unspecific results (and therefore bad accuracy).

Advanced tips

If you have a country Model related to your addresses you probably want to include the country in your query (otherwise it might find the address in some other country).
Just passing the country_id will not do the trick, of course. You should manually add the field country to your data before passing it on to the model:

$this->request->data['Address']['country'] = $countryName; // as a string
 if ($this->Address->save($this->request->data) {}

My plan was to automatically extract the country name in the behavior if country_id is passed – but that might be a little bit too much magic.

Searching/Filtering

This is a little bit more complicated. Basically, if we want to retrieve results based on distance we need to pass one point (lat/lng) to the behavior and calculate the distance in the database at runtime. Since 2.x it is very easy to use virtual fields for this.
The behavior has support for the virtual fields needed to do this using setDistanceAsVirtualField():

$this->Address->Behaviors->attach('Tools.Geocoder');
$this->Address->setDistanceAsVirtualField($lat, $lng);
$options = array('order' => array('Address.distance' => 'ASC'));
$res = $this->Address->find('all', $options);

Using this in pagination is supported, as well.

The default unit is GeocodeLib::UNIT_KM but can easily be switched to miles, nautical miles, …

Note: If you do not have lat/lng fields in your table yet, you need to add them first. Then run a loop for all records using save() to geocode the address etc. After this you will be ready to use filtering here. You cannot geocode at runtime. It is too costly. So do it once on save and then you can use the lat/lng fields as often as you want.

Validation

The behavior adds two new rules you can use: validateLatitude and validateLongitude.
See the test cases for details on how to use it.

Dynamically adding them works as well, of course:

$this->Address->validator()->add('lat', 'validateLatitude', array('rule'=>'validateLatitude', 'message'=>'validateLatitudeError'));
$this->Address->validator()->add('lng', 'validateLongitude', array('rule'=>'validateLongitude', 'message'=>'validateLongitudeError'));
$data = array(
	'lat' => 44,
	'lng' => 190,
);
$this->Address->set($data);
$res = $this->Address->validates(); // will return false in this case

Todos

I want to cleanup and rewrite some of it – as soon as the test cases are complete.
Not just because this code is mainly from 1.2 and only "upgraded", but also because throwing Exceptions and improving the overall workflow would probably be a good idea.

 
26 Comments

Posted in CakePHP

 

Helper? Component? Lib? CakePHP2

03 Apr

Note: For 1.3 see the old article on this subject.

I want to outline some ideas on how to chose the appropriate class type if you want to add some additional feature to your cake app.
For a beginner it might be difficult to decide what to use in which case. Hope this article helps to clarify things.
Feel free to comment on this below.

Overview

The following generic levels and types for extending functionality are available, each for its own domain inside MVC:

  • bootstrap functions (most generic methods – no class context)
  • Lib (most generic class)
  • Helper (view level)
  • Component (controller level)
  • Behavior (model level)

And of course the base classes: Datasource, Model and Controller (and View) which you can extend (and provide further functionality this way).

Level of Independence

We need to ask ourselves if this feature needs to interact with other cake elements, like controller, some components, models, …

If it needs to save to the session, or if it needs some controller functionality, it will have to be a component.
With initialize(Controller $controllerReference) and startup(Controller $controllerReference) this is very easy to accomplish.

With Cake13 libs have been introduced. Not every piece of "controller" code necessarily needs to be component anymore.
So if you retrieve an RSS feed or get the weather from a weather channel web service you could just make a clean and independent lib class. No need to extend the cake object or even pass the controller reference. Less memory and dependency is a good thing. And its easier to test, anyway.

Helpers are used if the result is in relation to the view – in other words if its directly related to output or generation of markup (HTML usually). If you want to retrieve some web service information and save it to the database use a component instead.

Database related?

Often times we need to adjust some model data, we either use a component first and then pass it to the model or we use beforeValidate() and beforeSave() in the model. Same goes for the other direction (from model to controller): afterFind() or a component call afterwards.
This is fine for custom changes. As soon as it could be something useful for several models, it might make sense to build a behavior. The code gets cleaner and your models more powerful.

Examples would be:
"Last Editor/Last Change" (see my WhoDidBehavior), "Geocoding" (see my GeocoderBehavior), "Auto-Capitalize first letter of name/title", "Format/Localize Date/Time" (see my DecimalInput or NumberFormat behaviors), "Working with passwords", "Slugging", "Soft Delete", "Serializable input/output", …

Extending base classes

Once you decided what part of MVC is relevant for the functionality you can then proceed with coding such a helper class or method.
Sometimes, it does’nt have to a new class, it can also just be part of the App base class (AppHelper, AppController, AppModel, AppShell, …). And if it is generic enough
to be of use to more than this single application, consider putting it into a generic My base class and let your App base classes extend those:

class MyHelper extends Helper {}
// and
class AppHelper extends MyHelper {}

Don’t forget the appropriate App::uses() statements, though.

This is mainly useful, if your functionality is of use to all extending classes or if it overwrites/extends existing methods.

Reducing code redundancy

Now that we have a vague understanding where to use what type of tool, we should think about cutting down the redundancy.
Lets say we use the vendor class "phpThump". We would have to write two wrappers. one for the helper (display images in the view) and one for the component (uploading images and resizing), maybe even for some behavior (validating + uploading + resizing). This wrapper handles default values from Configure::read() and other cake related settings.
In this scenario we should build one single library in /Lib, maybe called PhpthumbLib.php.
Here we put our wrapper with our custom functions.
Then we build a helper (view side) as well as a component or a behavior (controller side). They will import and use the library file. This is a cleaner approach because changes in the library class will be available in all files it is used in.
Bonus: The main library file is easier to test. And therefore testing the other classes afterwards is easier, too.

Generally speaking, all web services should be some kind of library file (some would make a datasource out of it). It doesn’t matter then if we use it in components or helpers, because it will fit either way.

A helper in a controller, though, is not really a nice thing.
With Cake2.1 there is no need to use helpers in the controller or model code anymore. All text/number/time helper methods have been moved to the Utility package and can now by used from anywhere within your application in a dry and clean way:

App::uses('CakeNumber', 'Utility');
$myValueInPercent = CakeNumber::toPercentage(45.12, 0); // or dynamically - but statically is easier in this case

Plugin or not?

If your feature is not site-specific but very generic it probably makes sense to build a plugin.
This way all other apps can easily use the same plugin. Additionally, test cases, assets etc are all combined in one folder – clean and extendable.

Examples:
A bookmark helper usually can be used in several apps, whereas a custom helper with two functions for this one app will not be very useful anywhere else.

Usage of those resources

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

public $components = array('MyC'); # file is in /APP/Controller/Component named MyCComponent.php

And in one of the controller’s actions:

$this->MyC->foo();

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

public $helpers = array('MyH'); # file is in /APP/View/Helper named MyHHelper.php

And in one of the controller’s views:

$this->MyH->foo();

Libs can be used everywhere – include them at runtime:

App::uses('MyL', 'Lib'); # file is in /APP/Lib/ named MyL.php
$MyL = new MyL();
$result = $MyL->foo($input);

Possible in controllers, components, behaviors, view, helpers, elements and everything else.
They are the most generic classes you can create.

Sidenote: I like to keep those files appended with a suffix, as well (Lib to avoid collisions with other classes or core classes):

App::uses('MyLLib', 'Lib'); # file is in /APP/Lib/ named MyLLib.php
$MyL = new MyLLib();

But that is just my personal convention.

Also note that you are encouraged in 2.x to group your lib classes in packages. So if you have some Utility Helper of your own, you might want to create a subfolder for it (you can use the core folder names or your own naming scheme):

App::uses('MyUtilityLib', 'Utility'); # file is in /APP/Lib/Utility/ named MyUtilityLib.php
$MyL = new MyLLib();

Same with plugins:

App::uses('UrlCacheManagerLib', 'Tools.Routing'); # file is in /APP/Plugin/Tools/Lib/Routing/ named UrlCacheManagerLib.php
$UrlCacheManager = new UrlCacheManagerLib();

Behaviors and other elements are used similar to the above.

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

bootstrap functions:
If those functions are so generic that you want to use them like h() or env() you can define them in your bootstrap. But be aware that this can lead to chaos if you do that for too many things.

Hacks for special use cases

Sometimes we need to break MVC in order to avoid redundance (and stay DRY).
If a helper has to be available in a controller we need to manually include it then at runtime:

App::uses('MySpecialHelper', 'Helper');
App::uses('View', 'View');
$MySpecialHelper = new MySpecialHelper(new View(null));
$myText = $MySpecialHelper->foo($myText);

I want to emphasize that this should only be done if not possible any other way.
What I do in this case: Move the functionality to a Lib class and use it inside the helper. This way we can use the Lib in the controller/model level and the helper is still the convenience access for the view level.

 
2 Comments

Posted in CakePHP

 

Bitmasked – Using bitmasks in CakePHP

26 Feb

Introduction

Based on Mark Story’s Post about this and the need in one of my apps for this lead me to believe that a behavior would be most appropriate to handle this. So I gave it a try.

There already is a Bitmasked Behavior out there – but it uses a secondary table which will get joined to the primary one. I wanted something that works on the same table (and an existing field), though.
It is supposed to be simple and yet powerful.

Use cases

In some cases a boolean flag in your model is not enough anymore. It might need a second flag – or even a third. You could do that and add those new fields – or combine them all using a bitmask. You can work with quite a few flags this way using a single database field.

The theoretical limit for a 64-bit integer [SQL: BIGINT unsigned] would be 64 bits (2^64). But don’t use bitmasks if you seem to need more than a hand full. Then you obviously do something wrong and should better use a joined table etc.
I highly recommend using tinyint(3) unsigned which can hold up to 8 bits – more than enough. It still only needs 1 byte.

There are two different ways to check against those bitmasks.
The first one – in my implementation the default one – assumes that you are using them similar to a list of multiple selects/checkboxes. Those can be individually edited in a form and saved as this single field. On retrieval you usually want to get only those records with certain bit groups set (a kind of OR condition).
The second use case is on retrieving data based on the value of single flags. Where it doesn’t matter if other flags are also set or not. This case is covered by the last chapter and those "contains" methods of the behavior which create a SQL snippet to be used in the condition.

Bitmasked Behavior

Basically it encodes the array of bit flags into a single bitmask on save and vice versa on find.
I created it as an extension of my pretty well working Enum stuff. It can use this type of enum declaration for our bitmask, as well.
We use constants as this is the cleanest approach to define model based field values that need to be hardcoded in your application.

The code can be found in the Tools Plugin.

Basic Usage

We first want to attach the behavior via public $actsAs = array('Tools.Bitmasked' => array(...)) or at runtime:

$this->Comment->Behaviors->attach('Bitmasked', 
	array('mappedField'=>'statuses', 'field' => 'status'));

The mappedField param is quite handy if you want more control over your bitmask. It stores the array under this alias and does not override the bitmask key. So in our case status will always contain the integer bitmask and statuses the verbose array of it.

We then need to define the bitmask in our model:

const STATUS_ACTIVE = 1;
const STATUS_PUBLISHED = 2;
const STATUS_APPROVED = 4;
const STATUS_FLAGGED = 8;
public static function statuses($value = null) {
	$options = array(
		self::STATUS_ACTIVE => __('Active'),
		self::STATUS_PUBLISHED => __('Published'),
		self::STATUS_APPROVED => __('Approved'),
		self::STATUS_FLAGGED => __('Flagged'),
	);	
	return parent::enum($value, $options);
}

Please note that you need to define MyModel::enum by extending my MyModel or by putting it into your AppModel manually if you want to use that. You don’t have to, of course.

Either way, we need an array of bits that we want to work with.
They should start at 1 and can go as high as your database field is designed for (2, 4, 8, 16, 32, …) or common sense cries "stop".

The behavior tries to find the plural method of your field name automatically (statuses for status here). You can always override this by manually setting param bits to your method name. You may also directly assign the bits to this param.
The advantage of the static model method is that we can use it everywhere else, as well. For instance in our forms to display some multiple checkbox form field for those flags.

Now, in the add/edit form we can add the field:

echo $this->Form->input('statuses', array('options'=>Comment::statuses(), 'multiple'=>'checkbox'));

It will save the final bitmask to the field status.

Searching for a record can be done using the bitmask itself:

$conditions = array('status' => BitmaskedComment::STATUS_ACTIVE | BitmaskedComment::STATUS_APPROVED);
$comment = $this->Comment->find('first', array('conditions' => $conditions));

If you want to search for a specific record, you can also use the array structure, though:

$conditions = array('statuses' => array(BitmaskedComment::STATUS_ACTIVE, BitmaskedComment::STATUS_APPROVED));
$comment = $this->Comment->find('first', array('conditions' => $conditions));

Note that it uses the mappedField – in this case statuses – for the array lookup.

Retrieving the record will then transform the bitmask value back into an array of bits.
If you use a mappedField, you will find it there instead of an overwritten field value.

Extended Usage

In your model you should define a rule for it if you want at least one flag to be selected:

public $validate = array(
	'status' => array(
		'notEmpty' => array(
			'rule' => 'notEmpty',
			'last' => true
		)
	)
);

You can always add more rules manually – for example if you want to make sure only some combinations are valid etc.

There are cases where you want to get all records that have a specific bit set, no matter what the other bits are set to.
In this case you currently need to wrap it like so:

// contains BitmaskedComment::STATUS_PUBLISHED:
$conditions = $this->Comment->containsBit(BitmaskedComment::STATUS_PUBLISHED);
$res = $this->Comment->find('all', array('conditions' => $conditions));
// dos not contains BitmaskedComment::STATUS_PUBLISHED:
$conditions = $this->Comment->containsNotBit(BitmaskedComment::STATUS_PUBLISHED);
$res = $this->Comment->find('all', array('conditions' => $conditions));

Tip: Looking at the test cases is usually one of the best ways to figure out how a behavior is supposed to work.

Manual usage

In some cases you might want to control encoding/decoding yourself (or have to by using saveField() method which does not trigger the behavior).
Let’s also get the enums of a different model here (using a static method – see my enums for details) for testing purposes:

$this->UserSetting->Behaviors->attach('Tools.Bitmasked', 
	array('field' => 'choices', 'bits' => 'ProfileChoice::types'));
$choices = (int) $this->UserSetting->field('my_choices', array('id' => $uid)); // A numeric value (0 ... x)
$choicesArray = $this->UserSetting->decodeBitmask($choices); // Now the array for the checkboxes
if ($this->request->is(array('post', 'put'))) {
	$choicesArray = $this->request->data['UserSetting']['choices']; // our posted form array
	$choices = $this->UserSetting->encodeBitmask($choicesArray); // back to integer
	$this->UserSetting->id = $uid;
	$this->UserSetting->saveField('my_choices', $choices);
	// flash message and redirect
} else {
	$this->request->data['UserSetting']['choices'] = $choicesArray;
}
// in the view:
echo $this->Form->input('choices', array(
	'options' => ProfileChoice::types(), 'multiple' => 'checkbox'));

CakePHP 3.x

For CakePHP 3 this has only slightly changed regarding the syntax.
The full up-to-date documentation is now to be found inside the Tools plugin documentation.

 
5 Comments

Posted in CakePHP

 

Working with passwords in CakePHP

25 Aug

The basics

Usually, this is already well known. But.. there are still developers who actually store the password unhashed. You always have to store user passwords hashed. You can either use the still very common sha1 method (which is the cake default as for right now) or switch to more secure measures like bcrypt.
If you want to be on the safe side you not only use salts (which is also cake standard) but also a unique salt per user. In case someone actually hacks your DB the information he gets is then usually pretty useless. You can read more about this here.

That also means that the password is a one-way ticket. Due to the technical details on hashs you are not able to recover the password from this hash – hence you always need to display some empty password field which is not prepopulated (see the examples below on how to unset the field from the controller). Most websites also use a confirmation field (second password field below the first one) to make sure, there are no spelling mistakes – since the password fields shood be unreadable using the well-known stars (input type=password).

A clean approach for working with passwords

Most beginners make the mistake to use the raw baked templates and "password" as fieldname for the database field "password" in forms.
But this causes quite some issues. For starters it will also hash and save empty strings (no input) and mess up previously entered passwords.
So you should never use this field in the forms (except for login, of course).
Always use "pwd" or some other temporary field name for your forms which you will then map to the real field in your model’s beforeSave() method:

public function beforeSave($options = array()) {
	parent::beforeSave($options);
	if (!empty($this->data[$this->alias]['pwd'])) {
		$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['pwd']);
	}
	...
	return true;
}

Do not use isset() in this case! It’s bad to accidentally hash an empty string here (if for some reason the field was actually not null). It won’t be noticeable afterwards when the empty string is suddenly a long hash value. So better don’t do that in the first place. !empty() suffices.

Also make sure that you grab and use the password before it gets hashed if you plan on sending an email with the password to the user.
After the hashing there is no way in resolving it back to what it used to be (basic principle of hash algorithms).

The main advantage of ALWAYS using a different name (even with cake2) is that you cannot accidently write an unhandled/unhashed password to your database.
Also consider providing a "pwd_repeat" field to ensure the entered password was typed correctly – especially on the user side.
To keep this easy and extendable use the following behavior.

For your validation rules you will need a few new methods like validateIdentical (to ensure the password and the confirmation password match) as well some rules on min length etc (more on this and some code in the next section as well as the linked behavior code).

If you use passwords in multiple places (or just use password stuff in multiple apps) it might make sense to separate the code in a clean way. See the next chapter.

Note that in newer versions of CakePHP (>= 2.4) it is recommended to not use the AuthComponent, but the hasher classes directly:

// At the top of the file
App::uses('SimplePasswordHasher', 'Controller/Component/Auth');
// Inside your User model
public function beforeSave($options = array()) {
	parent::beforeSave($options);
	if (!empty($this->data[$this->alias]['pwd'])) {
		$PasswordHasher = new SimplePasswordHasher(); // or add array('hashType' => '...') to overwrite default "sha1" type
		$this->data[$this->alias]['password'] = $PasswordHasher->hash($this->data[$this->alias]['pwd']);
	}
	...
	return true;
}

For new apps you should at least consider using Blowfish, though.

Note: The most common mistake with more safe hashing algorithms is to use too short DB fields. Make sure your password field is of at least the length of the hash (for blowfish up to 123). So it is wise to simply use VARCHAR(255) and play it safe. This also leaves room for even "safer" hashes one day.

Introducing my Passwordable behavior

The code can be found on github (CakePHP2.x) [1.3 – careful, different name back then].
What it does is taking care of all the stuff that usually is done redundant in several places: hashing, validating, whitelisting, ensuring fields, …
Using the behavior the controller code stays slim (remember: fat model, slim controller). Especially for beginners this really helps to keep it clean and safe. But I use is everywhere to keep it DRY. In case I have to adjust I usually only have to touch the behavior code.

The most important fact the behavior automatically takes care of is the update process.
If you have the password field in your form and you don’t enter anything it will just skip the password changing part. As soon as you enter something the behavior assumes that you actually want to change it then. Usually this has to be covered in the controller or model at some point. This is also the point most beginners struggle with. The overhead is reduced to the behavior as responsible element.

You can override the default values with Configure::write('Passwordable.auth', 'MyAuth'); for example (config/bootstrap). It is also possible to change them at runtime – where you include the behavior.
It is important NOT to globally assign the behavior to the model – for security purposes.
Only use it in those actions that actually work with the password (update/reset/…). This way other actions cannot accidently use the behavior to update the password (or by tempering with the forms).

PWD_MIN_LENGTH and PWD_MAX_LENGTH are constants and can be set in the bootstrap. They are now deprecated, though, as you should be using Configure keys here, as well.

Note: Do NOT add the above beforeSave codesnippet with the manual password hashing as the behavior already does that. Or the password will be hashed twice on save (and thus be rendered unusable).

Register forms

The basic use case first. We just attach the behavior to the model and and the two password fields in the view.
It boils down to:

if ($this->request->is('post') || $this->request->is('put')) {
	$this->User->Behaviors->attach('Tools.Passwordable');
	if ($this->User->save($this->request->data, true, array('username', 'email', ..., 'pwd', 'pwd_repeat'))) {
		//SUCCESS flash/redirect
	}
	//ERROR flash
	unset($this->request->data['User']['pwd']);
	unset($this->request->data['User']['pwd_repeat']);
}

The register form then contains

echo $this->Form->input('pwd', array('type'=>'password', 'value'=>'', 'autocomplete'=>'off'));
echo $this->Form->input('pwd_repeat', array('type'=>'password', 'value'=>'', 'autocomplete'=>'off'));

Important: Be careful with your whitelisting (as it can go wrong quickly).
Either use the security component to make sure, only the right fields are passed, processed and saved, or use whitelisting appropriatly (including all form fields that should be saved!). Do NOT use it if you don’t know how.
The declaration of the save method is save($data, $validate, $whitelist) or just save($data, $options) (then including validate and whitelist as array keys in options).
The whitelist MUST contain all relevant fields coming from the controller, so for your registration form all fields including the password fields (as behaviors cannot add fields automatically to the whitelist).

In the above registration form example we add all fields like "username" or "email" – and also "pwd", "pwd_repeat" etc then, of course. The only field the behavior can add automatically, is "password", the final password field name going into the DB. Thus we can omit that here.

Admin Forms

if ($this->request->is('post') || $this->request->is('put')) {
	$this->User->Behaviors->attach('Tools.Passwordable', array('require' => false, 'confirm' => false));			
	if ($this->User->save($this->request->data)) {
		//YEAH
	} else {
		//OH NO
	}
}

Using require here is quite important. This makes the validation skip the password fields if you do not enter anything. The password will be left alone.
As admin I also might not want to retype the password. So confirm can possibly be set to false (although a confirmation field is always a good idea).

/users/edit view

Here is the most common scenario of a complete edit form for the user with only basic password input:

if ($this->request->is('post') || $this->request->is('put')) {
	$this->User->Behaviors->attach('Tools.Passwordable', array('require' => false));			
	if ($this->User->save($this->request->data)) {
		//YEAH
	} else {
		//OH NO
	}
}

You have your two input fields in your form again which can be left empty to not update the password. Simple, isn’t it?

On an edit form the user should not have to be forced to change his password. Leaving the field empty will just skip that part. As soon as the user enters a new password, though, validation will get triggered for this field as specified in your settings.

If you use whitelisting (which is a good idea) please make sure, that you set the whitelist to only those fields from your actual model that are allowed to be updated (or use Security component).

Separate password form for the user

Note: This is the form with only the password to be changed – see the other form above for a complete edit view

if ($this->request->is('post') || $this->request->is('put')) {
	// attach the behavior and force the user to enter the current password for security purposes
	$this->User->Behaviors->attach('Tools.Passwordable', array('current'=>true));
	$this->request->data['User']['id'] = $this->Session->read('Auth.User.id');
	if ($this->User->save($this->request->data, true, array('id',  'pwd', 'pwd_repeat'))) {
		//SUCCESS flash/redirect
	}
	//ERROR flash
	// pw should not be passed to the view again for security reasons
	unset($this->request->data['User']['pwd']);
	unset($this->request->data['User']['pwd_repeat']);
}

As user I have to first provide the old password and confirm (retype) the new password. Confirmation is active by default.
The advantage of such a separate view might be that you can reduce complexity for the user. Especially with confirmation of the current password or more.

Login

This is only for completeness. The login part matches the one in the documentation.
Here we use the field that matches the one in the database – so password in most cases:

echo $this->Form->input('password');

On important issue, though: For the login forms some forget to clear the password after an unsuccessful post.

if ($this->request->is('post') || $this->request->is('put')) {
	if ($this->Auth->login()) {
		...
	}
	// the important part after every post:
	$this->request->data['User']['password'] = '';
}

Now the password field stays empty. Which is usually a good thing to do in forms.
The Passwordable behavior is not needed here as the Auth component already takes care of the login procedure. That’s why "password" as fieldname is all right in this case.

Is there more?

Yes, there is. You can take a look at the behavior itself for more configuration or peek into its test case.
‘allowSame’ might be interesting. false requires the user to use a different password then the one previously entered. This can be useful on a separate "change password" page and if you want to force the user to change it to something than it was before.

Custom auth and hash types

If you derive from the default Form authenticate adapter or the default hashing type, you need to tell the behavior that. See the appropriate settings.
For cake2.4 there is now also the support for "passwordHasher" config.

Your own validation rules

If you do not provide any validation rules for your password fields, the behavior will automatically use the "best practice" ones of his own. But you can always define your own ones in the model to overwrite then (not recommended, though).

Letting users change their password

For users who want to change or have forgotten their password, check out "Tokens" and the reset functionality mentioned there.

I18n

Don’t forget to take the translation strings from the plugin’s Locale folder and translate them accordingly to your language (eng, deu, …) in your APP/Locale folder.

Common pitfalls

If some fields are not saved, make sure your whitelisting is in order or just drop it in favor of the Security component.

If you are creating a password change form and include the id in the form <?php echo $this->Form->input('id'); ?> and also check on it wrongly using is(post) only, it won’t work:

if ($this->request->is('post')) {} // Careful with this

You should always check on both put and post if you don’t know why (or use my quick Common->isPosted()).
Also, it is a good idea to keep the id out of the form and inject it prior to the save call as shown above.

Notes

In Cake1.3 the framework automatically hashes the field "password". that’s one of the reasons why you should use "pwd" or something similar and either let the behavior/model handle it or assign "pwd" to "password" manually on save.
In Cake2.0 no automatic hashing will be done anymore. But it will still be useful not to use "password". Especially because the validation rules would not apply to a sha1 string etc^^

For resetting local passwords for development purposes (or after salt/hash changes) you might want to look into my password reset shell script.

And if you want to create your first (admin) user, you might want to take a look at my User shell which saves you the trouble of manually hashing a password and running some custom sql query. just hit cake Tools.User and "bake yourself one".

Note: this article has been rewritten for 2.x (so $this->request->data instead of this->data is now being used inside the controller scope).

2013-08 Important fix

There has been an important correction to make handling of required input more stable. Please replace any 'allowEmpty' => true with the new 'require' => false.
Attaching the behavior, it will automatically require a password input for BC.

2013-10 Important update

Now, since CakePHP2.5, the whitelisting works as expected even from within behaviors. Only now we can omit all form fields that the behavior works with (pwd, pwd_repeat and pwd_current) in the whitelisting. The behavior will add them automatically, if needed.
For a password change form then, for example, it boils down to just the id now:

if ($this->User->save($this->request->data, array('fieldList' => array('id')))) {}

2014-04-02 Use Blowfish now if you can

If you are developing a new 2.x app, use the new 3.x standard right away. It is way safer and useful to use Blowfish or any of the "real" password hashing algos.
With the Passwordable behavior it is this config setting:

$config['Passwordable'] = array(
	'authType' => 'Blowfish'
);

All you then need to do is to adjust your AppController:

public function beforeFilter() {
	$this->Auth->authenticate = array(
		'Form' => array(
			'passwordHasher' => Configure::read('Passwordable.authType'))); // Or simply `Blowfish`
	parent::beforeFilter();
}

To migrate live applications with existing users and password this is a little bit more tricky.
There is an open ticket regarding that issue – and some MultiHasher class will probably serve best.
If would first try to match the new algo and if not possible will try the deprecated algos and maybe even rehash it to the new one if possible. Sooner or later all
active accounts should have a up-to-date blowfish password.

2014-07-08 Backported PHP5.5+ password_hash()

The new PHP5.5+ password_hash()/password_verify()/password_needs_rehash() functions are now available in 2.x via Shim.Modern PasswordHasher class. I backported the 3.x DefaultPasswordHasher class for this.
All that is needed, is to add this new hasher to the Auth and Passwordable configs:

'authType' => 'Blowfish',
'passwordHasher' => 'Shim.Modern'

See the test cases for details.

You can also pass in options (like "cost"):

'passwordHasher' => array('className' => 'Shim.Modern', 'cost' => 20)

And don’t forget to add the password_compat shim via composer or the included version of it via require if you are not yet on PHP5.5, but PHP5.4 etc:

// Without composer - in your bootstrap.php
require CakePlugin::path('Shim') . 'Lib/Bootstrap/Password.php';
// With composer
"require": {
	"ircmaxell/password-compat": "dev-master"
}

To complete the new "modern" password hashing, you can also make login() automatically rehash the passwords if needed:

// In your login action
if ($this->Auth->login()) {
	$this->Common->flashMessage(__('You have logged in successfully.'), 'success');
	
	$this->User->Behaviors->load('Tools.Passwordable', array('confirm' => false));
	$password = $this->request->data['User']['password'];
	$dbPassword = $this->User->field('password', array('id' => $user['id']));
	if ($this->User->needsPasswordRehash($dbPassword)) {
		$data = array(
			'id' => $user['id'],
			'pwd' => $password,
			'modified' => false
		);
		if (!$this->User->save($data)) {
			trigger_error('Could not store new pwd for user ' . $user['id'] . '.');
		}
	}
	return $this->redirect($this->Auth->redirectUrl());
}

Note how it uses the currently stored hash with needsPasswordRehash() to determine if an update is necessary. If so it will do that automatically.
The additional check on save() might not be necessary – but in case you changed your validation params you might need to loosen the validation here then in order for the password to be rehashed without errors.

In future versions (CakePHP3.x) this will be easier as the Auth component will directly provide wrapper methods for this ($this->Auth->loginProvider()->needsPasswordRehash()).
But until then this shim works quite well in 2.x.

For the code visit the Shim plugin.

CakePHP 3.x

For CakePHP 3.x it is still quite convenient to use the behavior, even for login and rehashing. See the 3.x compatible version of the Tools plugin .
The docs for it can directly be found there in Tools Plugin 3.0 documentation

 
36 Comments

Posted in CakePHP

 

Working with models

08 Mar

Since there is so much to talk about it here, I will cut right to the chase.

We all know about "Fat Models, Slim Controllers". So basically, as much as possible of the code should be in the model. For a good reason: If you need that code in another controller, you can now easily access it. Having it inside a controller does not give you this option. Thats another principle: "Don’t repeat yourself".

What if I use the same model in different contexts?

We sometimes need the "User" (class name!) Model to be more than just that. Imagine a forum with posts. We could have "User", "LastUser", "FirstUser" etc. For all those we have custom belongsTo relations in our models.

If we had something like this in our code, we could get in trouble:

function beforeValidate() {
	if (isset($this->data['User']['title'])) {
		$this->data['User']['title'] = ucfirst($this->data['User']['title']);	
	}
	return true;
}

So always use $this->alias:

function beforeValidate() {
	parent::beforeValidate();
	if (isset($this->data[$this->alias]['title'])) {
		$this->data[$this->alias]['title'] = ucfirst($this->data[$this->alias]['title']);
	}
	return true;
}

Now we can set up any additional relation without breaking anything:

var $belongsTo = array(
	'LastUser' => array(
		'className' 	=> 'User',
		'foreignKey'	=> 'last_user_id',
		'fields' => array('id', 'username')
	)
);

$this->alias is now "LastUser" for this relation.

Always try to outsource code, that repeats itself, into behaviors or the AppModel callbacks.
So if the "ucfirst()" code snippet from above is used in pretty much all models, you can either use beforeValidate() of the AppModel:

function beforeValidate() {
	parent::beforeValidate();
	if (isset($this->data[$this->alias][$this->displayField])) {
		$this->data[$this->alias][$this->displayField] = ucfirst($this->data[$this->alias][$this->displayField]);
	}
	return true;
}

or you can create a behavior:

class DoFancyBehavior extends ModelBehavior {
	function setup(Model $Model, $config = array()) {
		$this->config[$Model->alias] = $this->default;
		$this->config[$Model->alias] = array_merge($this->config[$Model->alias], $config);
		// [...]
		$this->Model = $Model;
	}
	function beforeValidate(Model $Model) {
		$this->_doFancyUcfirst($Model->data);
		return true;
	}
	
	function _doFancyUcfirst(&$data) {				
		if (isset($data[$this->Model->alias][$this->Model->displayField])) { 
			$data[$this->Model->alias][$this->Model->displayField] = ucfirst($data[$this->Model->alias][$this->Model->displayField]);
		}
	}
}

With the behavior you can define which models will and which won’t "behave" fancy.
But of course the same would be possible for the AppModel method using an internal variable ($this->behaveFancy: true/false) and checking on it in our callback.

 
No Comments

Posted in CakePHP

 

Behaviors in CakePHP

02 Oct

I didn’t use behaviors for the first 12 months with Cake. I regret it now, because the code is no much cleaner and easier to extend.
Let’s make a test with a simple task. We need some additional statistics to a specific model. We could do that in the controller (very messy!) or with a custom model function. Second option can be used in different views etc. But to cover all kinds of find operations it is likely to get messy as well.
The easiest and cleanest approach is a behavior which goes over every found record, does the calculation, maybe gets some related data and attaches the result to the current record before it reaches the controller.

Workflow

Controller: Model->Behaviours->attach(‘MyStatistics’); # one line of code
Controller: Model->find(all)
Model: find(all)
Behaviour: afterFind() # here we add the statistics
Controller: passes data to view
View: displays data + statistics

The nice byproduct: We won’t have any overhead if we don’t need the statistics. The Behavior will simply not be included.

$model->alias or $model->name?

If we want to use a behavior in many different places and models, we cannot hardcode the model names. We need to dynamically retrieve them.
In most cases it is $this->alias or $model->alias.
$model->name is the original name and can cause errors if you use some non-standard model relations.

example:

$hasMany = array('Receiver' => array('className' => 'User'));

$model->alias is "Receiver" (which the behavior needs)
$model->name still is "User"

Basic set up

class MyStatisticsBehavior extends ModelBehavior {
	var $default = array(
		'fields' => array(),
		'prefix' => 'stat_',
		'primary' => true
	);
	var $config = array();
	/**
	* adjust configs like: $model->Behaviors-attach('MyStatistics', array('fields'=>array('xyz')))
	*/
	function setup(&$model, $config = array()) {
		$this->config[$model->alias] = $this->default;
		$this->config[$model->alias] = array_merge($this->config[$model->alias], $config);
	}
	/**
	* main function
	*/
	function afterFind(&$model, $result, $primary = false) {
		//modify the result here
		return $result;
	}
}

Now we want to do some operation for each of the given fields.
First we need to check if the result exists. In this example i simplified "primary". Usually you can control this with the above settings. In our case we only want the statistics if the model is the primary model (the one which initialized the query).
After that we loop over every record.

function afterFind(&$model, $result, $primary = false) {
	if (!$result || !$primary) {
		return $result;
	}
	
	# check for single of multiple model
	$keys = array_keys($result);
	if (!is_numeric($keys[0])) {
		$this->stats($model, $result);
	} else {
		foreach ($keys as $index) {
			$this->stats($model, $result[$index]);
		}
	}
	return $result;
}

Note that we use "reference" for passing the data. this way we don’t need to return the data from the sub-methods. it is the very same record we modify.

The stats() method:

function stats(&$model, &$data = null) {
	if ($data === null) {
		$data = &$model->data;
	}
	if (empty($data[$model->alias]['id'])) {
		return $data;
	}
	
	if (isset($this->config[$model->alias])) {
		$fields = $this->config[$model->alias]['fields'];
		
		foreach ($fields as $column) {
			$count = $this->_countRecords($model, $data, $column);
			$column = $this->config[$model->alias]['prefix'] . $column;
			
			$data[$model->alias][$column] = $count;
		}
	}
	return $data;
}

Any finally, the _countRecords() method. it could be located in any other model, as well. To simplify again, everythings in the same place.

function _countRecords(&$model, &$data, $column) {
	$id = $data[$model->alias]['id'];
	$count = 0;
	switch ($column) {
		case 'images':
			return $model->Image->find('count', array('contain'=>array(), 'conditions'=>array('user_id'=>$id)));
		case 'friends':
			return $model->Friend->find('count', array('contain'=>array(), 'conditions'=>array('Role.user_id'=>$id)));
		... # of course there could be more complex queries here
	}
	return $count;
}

Other callbacks

Of course you can build more complex behaviors, as well.
Some that modifiy the record prior to saving it. Or doing something after saving a record.

Result

In the controller index (or view):

# We want the stats "images" and "friends"
$this->User->Behaviours->attach('MyStatistics', array('images', 'friends'));
$user = $this->User->find(all, array(...)); # paginated or limited to xx users!
pr($user);

Besides the real database fields the statistics now show up:

[0] => Array
        (
            [Gallery] => Array
                (
                    [id] => 4c979553-1500-4922-8866-07d452b0caef
                    [name] => 'Test'
                    [created] => 2010-09-20 19:09:39
                    [modified] => 2010-09-20 19:09:39
                    [stat_images] => 3
                    [stat_friends] => 1

Available is:

beforeFind()
beforeValidate()
beforeSave()
afterSave()
beforeDelete()
afterDelete()
onError()

Tip: Look at the cake core behaviors. You can learn a lot from those (as will all available code).

Naming conventions

Usually plugins are in adjective-form ("Accessable", "Ratable", …).
Sometimes this sounds awkward: Plugin handling Points => "Pointable"? No – we can simply chose a noun then: "Points". Note the plural form. Models are singular, so use a plural name your behavior. You never know when a new model "Point" is introduced. And we want no conflicts whatsoever.

Issues to be aware of

The ordering can be very important. With Cake2 there is now a "priority" setting you can leverage to create the ordering of callbacks you need.

As of Cake2 there is still no support for callbacks on associated behaviors. There is quite an old ticket on this which is still open.
I personally see no reason not to support the after{Find/Save} callbacks.
But until that happens you might need to find yourself some workaround. For example by manually triggering the necessary callbacks yourself from the AppModel.

 
No Comments

Posted in CakePHP