Models

Models represent data and are used in CakePHP applications for data access. A model usually represents a database table but can be used to access anything that stores data such as files, LDAP records, iCal events, or rows in a CSV file.

A model can be associated with other models. For example, a Recipe may be associated with the Author of the recipe as well as the Ingredient in the recipe.

This section will explain what features of the model can be automated, how to override those features, and what methods and properties a model can have. It’ll explain the different ways to associate your data. It’ll describe how to find, save, and delete data. Finally, it’ll look at Datasources.

Understanding Models

A Model represents your data model. In object-oriented programming a data model is an object that represents a “thing”, like a car, a person, or a house. A blog, for example, may have many blog posts and each blog post may have many comments. The Blog, Post, and Comment are all examples of models, each associated with another.

Here is a simple example of a model definition in CakePHP:

<?php

class Ingredient extends AppModel {
    var $name = 'Ingredient';
}

?>

With just this simple declaration, the Ingredient model is bestowed with all the functionality you need to create queries along with saving and deleting data. These magic methods come from CakePHP’s Model class by the magic of inheritance. The Ingredient model extends the application model, AppModel, which extends CakePHP’s internal Model class. It is this core Model class that bestows the functionality onto your Ingredient model.

This intermediate class, AppModel, is empty and if you haven’t created your own is taken from within the /cake/ folder. Overriding the AppModel allows you to define functionality that should be made available to all models within your application. To do so, you need to create your own app_model.php file that resides in the root of the /app/ folder. Creating a project using Code Generation with Bake will automatically generate this file for you.

Create your model PHP file in the /app/models/ directory or in a subdirectory of /app/models. CakePHP will find it anywhere in the directory. By convention it should have the same name as the class; for this example ingredient.php.

CakePHP will dynamically create a model object for you if it cannot find a corresponding file in /app/models. This also means that if your model file isn’t named correctly (i.e. Ingredient.php or ingredients.php) CakePHP will use a instance of AppModel rather than your missing (from CakePHP’s perspective) model file. If you’re trying to use a method you’ve defined in your model, or a behavior attached to your model and you’re getting SQL errors that are the name of the method you’re calling - it’s a sure sign CakePHP can’t find your model and you either need to check the file names, clear your tmp files, or both.

See also Behaviors for more information on how to apply similar logic to multiple models.

The $name property is necessary for PHP4 but optional for PHP5.

With your model defined, it can be accessed from within your Controllers. CakePHP will automatically make the model available for access when its name matches that of the controller. For example, a controller named IngredientsController will automatically initialize the Ingredient model and attach it to the controller at $this->Ingredient.

<?php
class IngredientsController extends AppController {
    function index() {
        //grab all ingredients and pass it to the view:
        $ingredients = $this->Ingredient->find('all');
        $this->set('ingredients', $ingredients);
    }
}

?>

Associated models are available through the main model. In the following example, Recipe has an association with the Ingredient model.

<?php
class RecipesController extends AppController {
    function index() {
        $ingredients = $this->Recipe->Ingredient->find('all');
        $this->set('ingredients', $ingredients);
    }
}
?>

If models have absolutely NO association between them, you can use Controller::loadModel() to get the model.

<?php
class RecipesController extends AppController {
    function index() {
       $recipes = $this->Recipe->find('all');

       $this->loadModel('Car');
       $cars = $this->Car->find('all');

       $this->set(compact('recipes', 'cars'));
    }
}
?>

Creating Database Tables

While CakePHP can have datasources that aren’t database driven, most of the time, they are. CakePHP is designed to be agnostic and will work with MySQL, MSSQL, Oracle, PostgreSQL and others. You can create your database tables as you normally would. When you create your Model classes, they’ll automatically map to the tables that you’ve created.

Table names are by convention lowercase and pluralized with multi-word table names separated by underscores. For example, a Model name of Ingredient expects the table name ingredients. A Model name of EventRegistration would expect a table name of event_registrations. CakePHP will inspect your tables to determine the data type of each field and uses this information to automate various features such as outputting form fields in the view.

Field names are by convention lowercase and separated by underscores.

Model to table name associations can be overridden with the useTable attribute of the model explained later in this chapter.

In the rest of this section, you’ll see how CakePHP maps database field types to PHP data types and how CakePHP can automate tasks based on how your fields are defined.

Data Type Associations by Database

Every RDBMS defines data types in slightly different ways. Within the datasource class for each database system, CakePHP maps those types to something it recognizes and creates a unified interface, no matter which database system you need to run on.

This breakdown describes how each one is mapped.

MySQL

CakePHP Type

Field Properties

primary_key

NOT NULL auto_increment

string

varchar(255)

text

text

integer

int(11)

float

float

datetime

datetime

timestamp

datetime

time

time

date

date

binary

blob

boolean

tinyint(1)

A tinyint(1) field is considered a boolean by CakePHP.

MySQLi

CakePHP Type

Field Properties

primary_key

DEFAULT NULL auto_increment

string

varchar(255)

text

text

integer

int(11)

float

float

datetime

datetime

timestamp

datetime

time

time

date

date

binary

blob

boolean

tinyint(1)

ADOdb

CakePHP Type

Field Properties

primary_key

R(11)

string

C(255)

text

X

integer

I(11)

float

N

datetime

T (Y-m-d H:i:s)

timestamp

T (Y-m-d H:i:s)

time

T (H:i:s)

date

T (Y-m-d)

binary

B

boolean

L(1)

DB2

CakePHP Type

Field Properties

primary_key

not null generated by default as identity (start with 1, increment by 1)

string

varchar(255)

text

clob

integer

integer(10)

float

double

datetime

timestamp (Y-m-d-H.i.s)

timestamp

timestamp (Y-m-d-H.i.s)

time

time (H.i.s)

date

date (Y-m-d)

binary

blob

boolean

smallint(1)

Firebird/Interbase

CakePHP Type

Field Properties

primary_key

IDENTITY (1, 1) NOT NULL

string

varchar(255)

text

BLOB SUB_TYPE 1 SEGMENT SIZE 100 CHARACTER SET NONE

integer

integer

float

float

datetime

timestamp (d.m.Y H:i:s)

timestamp

timestamp (d.m.Y H:i:s)

time

time (H:i:s)

date

date (d.m.Y)

binary

blob

boolean

smallint

MS SQL

CakePHP Type

Field Properties

primary_key

IDENTITY (1, 1) NOT NULL

string

varchar(255)

text

text

integer

int

float

numeric

datetime

datetime (Y-m-d H:i:s)

timestamp

timestamp (Y-m-d H:i:s)

time

datetime (H:i:s)

date

datetime (Y-m-d)

binary

image

boolean

bit

Oracle

CakePHP Type

Field Properties

primary_key

number NOT NULL

string

varchar2(255)

text

varchar2

integer

numeric

float

float

datetime

date (Y-m-d H:i:s)

timestamp

date (Y-m-d H:i:s)

time

date (H:i:s)

date

date (Y-m-d)

binary

bytea

boolean

boolean

number

numeric

inet

inet

PostgreSQL

CakePHP Type

Field Properties

primary_key

serial NOT NULL

string

varchar(255)

text

text

integer

integer

float

float

datetime

timestamp (Y-m-d H:i:s)

timestamp

timestamp (Y-m-d H:i:s)

time

time (H:i:s)

date

date (Y-m-d)

binary

bytea

boolean

boolean

number

numeric

inet

inet

SQLite

CakePHP Type

Field Properties

primary_key

integer primary key

string

varchar(255)

text

text

integer

integer

float

float

datetime

datetime (Y-m-d H:i:s)

timestamp

timestamp (Y-m-d H:i:s)

time

time (H:i:s)

date

date (Y-m-d)

binary

blob

boolean

boolean

Sybase

CakePHP Type

Field Properties

primary_key

numeric(9,0) IDENTITY PRIMARY KEY

string

varchar(255)

text

text

integer

int(11)

float

float

datetime

datetime (Y-m-d H:i:s)

timestamp

timestamp (Y-m-d H:i:s)

time

datetime (H:i:s)

date

datetime (Y-m-d)

binary

image

boolean

bit

Titles

An object, in the physical sense, often has a name or a title that refers to it. A person has a name like John or Mac or Buddy. A blog post has a title. A category has a name.

By specifying a title or name field, CakePHP will automatically use this label in various circumstances:

  • Scaffolding — page titles, fieldset labels

  • Lists — normally used for <select> drop-downs

  • TreeBehavior — reordering, tree views

If you have a title and name field in your table, the title will be used.

If you want to use something other than the convention set var $displayField = 'some_field';. Only one field can be set here.

created and modified

By defining a created or modified field in your database table as datetime fields, CakePHP will recognize those fields and populate them automatically whenever a record is created or saved to the database (unless the data being saved already contains a value for these fields).

The created and modified fields will be set to the current date and time when the record is initially added. The modified field will be updated with the current date and time whenever the existing record is saved.

Note: A field named updated will exhibit the same behavior as modified. These fields need to be datetime fields with the default value set to NULL to be recognized by CakePHP.

If you have updated, created or modified data in your $this->data (e.g. from a Model::read or Model::set) before a Model::save() then the values will be taken from $this->data and not automagically updated.

Either use unset($this->data['Model']['modified']), etc. Alternatively you can override the Model::save() to always do it for you:-

class AppModel extends Model {
//
//
    function save($data = null, $validate = true, $fieldList = array()) {

        //clear modified field value before each save
        if (isset($this->data) && isset($this->data[$this->name]))
            unset($this->data[$this->name]['modified']);
        if (isset($data) && isset($data[$this->name]))
            unset($data[$this->name]['modified']);

        return parent::save($data, $validate, $fieldList);
    }
//
//
}

Using UUIDs as Primary Keys

Primary keys are normally defined as INT fields. The database will automatically increment the field, starting at 1, for each new record that gets added. Alternatively, if you specify your primary key as a CHAR(36) or BINARY(36), CakePHP will automatically generate UUIDs when new records are created.

A UUID is a 32 byte string separated by four hyphens, for a total of 36 characters. For example:

550e8400-e29b-41d4-a716-446655440000

UUIDs are designed to be unique, not only within a single table, but also across tables and databases. If you require a field to remain unique across systems then UUIDs are a great approach.

Retrieving Your Data

find

find($type, $params)

Find is the multifunctional workhorse of all model data-retrieval functions. $type can be either 'all', 'first', 'count', 'list', 'neighbors' or 'threaded'. The default find type is 'first'. Keep in mind that $type is case sensitive. Using a upper case character (for example 'All') will not produce the expected results.

$params is used to pass all parameters to the various finds, and has the following possible keys by default—all of which are optional:

array(
    'conditions' => array('Model.field' => $thisValue), //array of conditions
    'recursive' => 1, //int
    'fields' => array('Model.field1', 'DISTINCT Model.field2'), //array of field names
    'order' => array('Model.created', 'Model.field3 DESC'), //string or array defining order
    'joins' => array(), // array of arrays defining join operations
    'group' => array('Model.field'), //fields to GROUP BY
    'limit' => n, //int
    'page' => n, //int
    'offset'=> n, //int
    'callbacks' => true //other possible values are false, 'before', 'after'
)

It’s also possible to add and use other parameters, as is made use of by some find types, behaviors and of course possible with your own model methods

More information about model callbacks is available Models. For details about join options, see Models.

find(‘first’)

find('first', $params)

‘first’ is the default find type, and will return one result, you’d use this for any use where you expect only one result. Below are a couple of simple (controller code) examples:

function some_function() {
   ...
   $this->Article->order = null; // resetting if it's set
   $semiRandomArticle = $this->Article->find();
   $this->Article->order = 'Article.created DESC'; // simulating the model having a default order
   $lastCreated = $this->Article->find();
   $alsoLastCreated = $this->Article->find('first', array('order' => array('Article.created DESC')));
   $specificallyThisOne = $this->Article->find('first', array('conditions' => array('Article.id' => 1)));
   ...
}

In the first example, no parameters at all are passed to find - therefore no conditions or sort order will be used. The format returned from find('first') call is of the form:

Array
(
    [ModelName] => Array
        (
            [id] => 83
            [field1] => value1
            [field2] => value2
            [field3] => value3
        )

    [AssociatedModelName] => Array
        (
            [id] => 1
            [field1] => value1
            [field2] => value2
            [field3] => value3
        )
)

There are no additional parameters used by find('first').

find(‘count’)

find('count', $params)

find('count', $params) returns an integer value. Below are a couple of simple (controller code) examples:

function some_function() {
   ...
   $total = $this->Article->find('count');
   $pending = $this->Article->find('count', array('conditions' => array('Article.status' => 'pending')));
   $authors = $this->Article->User->find('count');
   $publishedAuthors = $this->Article->find('count', array(
      'fields' => 'COUNT(DISTINCT Article.user_id) as count',
      'conditions' => array('Article.status !=' => 'pending')
   ));
   ...
}

Don’t pass fields as an array to find('count'). You would only need to specify fields for a DISTINCT count (since otherwise, the count is always the same - dictated by the conditions).

There are no additional parameters used by find('count').

find(‘all’)

find('all', $params)

find('all') returns an array of (potentially multiple) results. It is in fact the mechanism used by all find() variants, as well as paginate. Below are a couple of simple (controller code) examples:

function some_function() {
   ...
   $allArticles = $this->Article->find('all');
   $pending = $this->Article->find('all', array('conditions' => array('Article.status' => 'pending')));
   $allAuthors = $this->Article->User->find('all');
   $allPublishedAuthors = $this->Article->User->find('all', array('conditions' => array('Article.status !=' => 'pending')));
   ...
}

In the above example $allAuthors will contain every user in the users table, there will be no condition applied to the find as none were passed.

The results of a call to find('all') will be of the following form:

Array
(
    [0] => Array
        (
            [ModelName] => Array
                (
                    [id] => 83
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

            [AssociatedModelName] => Array
                (
                    [id] => 1
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

        )
)

There are no additional parameters used by find('all').

find(‘list’)

find('list', $params)

find('list', $params) returns an indexed array, useful for any use where you would want a list such as for populating input select boxes. Below are a couple of simple (controller code) examples:

function some_function() {
   ...
    $allArticles = $this->Article->find('list');
    $pending = $this->Article->find('list', array(
        'conditions' => array('Article.status' => 'pending')
    ));
    $allAuthors = $this->Article->User->find('list');
    $allPublishedAuthors = $this->Article->find('list', array(
        'fields' => array('User.id', 'User.name'),
        'conditions' => array('Article.status !=' => 'pending'),
        'recursive' => 0
    ));
   ...
}

In the above example $allAuthors will contain every user in the users table, there will be no condition applied to the find as none were passed.

The results of a call to find('list') will be in the following form:

Array
(
    //[id] => 'displayValue',
    [1] => 'displayValue1',
    [2] => 'displayValue2',
    [4] => 'displayValue4',
    [5] => 'displayValue5',
    [6] => 'displayValue6',
    [3] => 'displayValue3',
)

When calling find('list') the fields passed are used to determine what should be used as the array key, value and optionally what to group the results by. By default the primary key for the model is used for the key, and the display field (which can be configured using the model attribute Models) is used for the value. Some further examples to clarify:.

function some_function() {
   ...
   $justusernames = $this->Article->User->find('list', array('fields' => array('User.username')));
   $usernameMap = $this->Article->User->find('list', array('fields' => array('User.username', 'User.first_name')));
   $usernameGroups = $this->Article->User->find('list', array('fields' => array('User.username', 'User.first_name', 'User.group')));
   ...
}

With the above code example, the resultant vars would look something like this:

$justusernames = Array
(
    //[id] => 'username',
    [213] => 'AD7six',
    [25] => '_psychic_',
    [1] => 'PHPNut',
    [2] => 'gwoo',
    [400] => 'jperras',
)

$usernameMap = Array
(
    //[username] => 'firstname',
    ['AD7six'] => 'Andy',
    ['_psychic_'] => 'John',
    ['PHPNut'] => 'Larry',
    ['gwoo'] => 'Gwoo',
    ['jperras'] => 'Joël',
)

$usernameGroups = Array
(
    ['User'] => Array
        (
        ['PHPNut'] => 'Larry',
        ['gwoo'] => 'Gwoo',
        )

    ['Admin'] => Array
        (
        ['_psychic_'] => 'John',
        ['AD7six'] => 'Andy',
        ['jperras'] => 'Joël',
        )

)

find(‘threaded’)

find('threaded', $params)

find('threaded', $params) returns a nested array, and is appropriate if you want to use the parent_id field of your model data to build nested results. Below are a couple of simple (controller code) examples:

function some_function() {
   ...
   $allCategories = $this->Category->find('threaded');
   $aCategory = $this->Category->find('first', array('conditions' => array('parent_id' => 42))); // not the root
   $someCategories = $this->Category->find('threaded', array(
    'conditions' => array(
        'Article.lft >=' => $aCategory['Category']['lft'],
        'Article.rght <=' => $aCategory['Category']['rght']
    )
   ));
   ...
}

It is not necessary to use Tree to use this method - but all desired results must be possible to be found in a single query.

In the above code example, $allCategories will contain a nested array representing the whole category structure. The second example makes use of the data structure used by the Tree the return a partial, nested, result for $aCategory and everything below it. The results of a call to find('threaded') will be of the following form:

Array
(
    [0] => Array
        (
            [ModelName] => Array
                (
                    [id] => 83
                    [parent_id] => null
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

            [AssociatedModelName] => Array
                (
                    [id] => 1
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )
            [children] => Array
                (
            [0] => Array
            (
                [ModelName] => Array
                (
                    [id] => 42
                            [parent_id] => 83
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

                [AssociatedModelName] => Array
                (
                    [id] => 2
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )
                    [children] => Array
                (
                )
                    )
            ...
                )
        )
)

The order results appear can be changed as it is influence by the order of processing. For example, if 'order' => 'name ASC' is passed in the params to find('threaded'), the results will appear in name order. Likewise any order can be used, there is no inbuilt requirement of this method for the top result to be returned first.

There are no additional parameters used by find('threaded').

find(‘neighbors’)

find('neighbors', $params)

‘neighbors’ will perform a find similar to ‘first’, but will return the row before and after the one you request. Below is a simple (controller code) example:

function some_function() {
   $neighbors = $this->Article->find('neighbors', array('fields' => 'id', 'value' => 3));
}

You can see in this example the two required elements of the $params array: field and value. Other elements are still allowed as with any other find (e.g. if your model acts as containable, then you can specify ‘contain’ in $params). The format returned from a find('neighbors') call is in the form:

Array
(
    [prev] => Array
        (
            [ModelName] => Array
                (
                    [id] => 2
                    [field1] => value1
                    [field2] => value2
                    ...
                )
            [AssociatedModelName] => Array
                (
                    [id] => 151
                    [field1] => value1
                    [field2] => value2
                    ...
                )
        )
    [next] => Array
        (
            [ModelName] => Array
                (
                    [id] => 4
                    [field1] => value1
                    [field2] => value2
                    ...
                )
            [AssociatedModelName] => Array
                (
                    [id] => 122
                    [field1] => value1
                    [field2] => value2
                    ...
                )
        )
)

Note how the result always contains only two root elements: prev and next.

findAllBy

findAllBy<fieldName>(string $value)

These magic functions can be used as a shortcut to search your tables by a certain field. Just add the name of the field (in CamelCase format) to the end of these functions, and supply the criteria for that field as the first parameter.

PHP5 findAllBy<x> Example

Corresponding SQL Fragment

$this->Product->findAllByOrderStatus(‘3’);

Product.order_status = 3

$this->Recipe->findAllByType(‘Cookie’);

Recipe.type = ‘Cookie’

$this->User->findAllByLastName(‘Anderson’);

User.last_name = ‘Anderson’

$this->Cake->findAllById(7);

Cake.id = 7

$this->User->findAllByUserName(‘psychic’);

User.user_name = ‘psychic’

PHP4 users have to use this function a little differently due to some case-insensitivity in PHP4:

PHP4 findAllBy<x> Example

Corresponding SQL Fragment

$this->Product->findAllByOrder_status(‘3’);

Product.order_status = 3

$this->Recipe->findAllByType(‘Cookie’);

Recipe.type = ‘Cookie’

$this->User->findAllByLast_name(‘Anderson’);

User.last_name = ‘Anderson’

$this->Cake->findAllById(7);

Cake.id = 7

$this->User->findAllByUser_name(‘psychic’);

User.user_name = ‘psychic’

findBy() functions like find(‘first’,…), while findAllBy() functions like find(‘all’,…).

In either case, the returned result is an array formatted just as it would be from find() or findAll(), respectively.

findBy

findBy<fieldName>(string $value)

These magic functions can be used as a shortcut to search your tables by a certain field. Just add the name of the field (in CamelCase format) to the end of these functions, and supply the criteria for that field as the first parameter.

PHP5 findBy<x> Example

Corresponding SQL Fragment

$this->Product->findByOrderStatus(‘3’);

Product.order_status = 3

$this->Recipe->findByType(‘Cookie’);

Recipe.type = ‘Cookie’

$this->User->findByLastName(‘Anderson’);

User.last_name = ‘Anderson’

$this->Cake->findById(7);

Cake.id = 7

$this->User->findByUserName(‘psychic’);

User.user_name = ‘psychic’

PHP4 users have to use this function a little differently due to some case-insensitivity in PHP4:

PHP4 findBy<x> Example

Corresponding SQL Fragment

$this->Product->findByOrder_status(‘3’);

Product.order_status = 3

$this->Recipe->findByType(‘Cookie’);

Recipe.type = ‘Cookie’

$this->User->findByLast_name(‘Anderson’);

User.last_name = ‘Anderson’

$this->Cake->findById(7);

Cake.id = 7

$this->User->findByUser_name(‘psychic’);

User.user_name = ‘psychic’

findBy() functions like find(‘first’,…), while findAllBy() functions like find(‘all’,…).

In either case, the returned result is an array formatted just as it would be from find() or findAll(), respectively.

findBy returns false if an entry cannot be found.

query

query(string $query)

SQL calls that you can’t or don’t want to make via other model methods (careful - there are very few circumstances this is true) can be made using the model’s query() method.

If you’re ever using this method in your application, be sure to check out CakePHP’s Data Sanitization, which aids in cleaning up user-provided data from injection and cross-site scripting attacks.

query() does not honour $Model->cachequeries as its functionality is inherently disjoint from that of the calling model. To avoid caching calls to query, supply a second argument of false, ie: query($query, $cachequeries = false)

query() uses the table name in the query as the array key for the returned data, rather than the model name. For example,

$this->Picture->query("SELECT * FROM pictures LIMIT 2;");

might return

Array
(
    [0] => Array
        (
            [pictures] => Array
                (
                    [id] => 1304
                    [user_id] => 759
                )
        )

    [1] => Array
        (
            [pictures] => Array
                (
                    [id] => 1305
                    [user_id] => 759
                )
        )
)

To use the model name as the array key, and get a result consistent with that returned by the Find methods, the query can be rewritten:

$this->Picture->query("SELECT * FROM pictures AS Picture LIMIT 2;");

which returns

Array
(
    [0] => Array
        (
            [Picture] => Array
                (
                    [id] => 1304
                    [user_id] => 759
                )
        )

    [1] => Array
        (
            [Picture] => Array
                (
                    [id] => 1305
                    [user_id] => 759
                )
        )
)

This syntax and the corresponding array structure is valid for MySQL only. Cake does not provide any data abstraction when running queries manually, so exact results will vary between databases.

field

field(string $name, array $conditions = null, string $order = null)

Returns the value of a single field, specified as $name, from the first record matched by $conditions as ordered by $order. If no conditions are passed and the model id is set, will return the field value for the current model result. If no matching record is found returns false.

$this->Model->id = 22;
echo $this->Model->field('name'); // echo the name for row id 22

echo $this->Model->field('name', array('created <' => date('Y-m-d H:i:s')), 'created DESC'); // echo the name of the last created instance

read()

read($fields, $id)

read() is a method used to set the current model data (Model::$data)–such as during edits–but it can also be used in other circumstances to retrieve a single record from the database.

$fields is used to pass a single field name, as a string, or an array of field names; if left empty, all fields will be fetched.

$id specifies the ID of the record to be read. By default, the currently selected record, as specified by Model::$id, is used. Passing a different value to $id will cause that record to be selected.

read() always returns an array (even if only a single field name is requested). Use field to retrieve the value of a single field.

function beforeDelete($cascade) {
   ...
   $rating = $this->read('rating'); // gets the rating of the record being deleted.
   $name = $this->read('name', $id2); // gets the name of a second record.
   $rating = $this->read('rating'); // gets the rating of the second record.
   $this->id = $id3; //
   $this->read(); // reads a third record
   $record = $this->data // stores the third record in $record
   ...
}

Notice that the third call to read() fetches the rating of the same record read before. That is because read() changes Model::$id to any value passed as $id. Lines 6-8 demonstrate how read() changes the current model data.

Complex Find Conditions

Most of the model’s find calls involve passing sets of conditions in one way or another. The simplest approach to this is to use a WHERE clause snippet of SQL. If you find yourself needing more control, you can use arrays.

Using arrays is clearer and easier to read, and also makes it very easy to build queries. This syntax also breaks out the elements of your query (fields, values, operators, etc.) into discrete, manipulatable parts. This allows CakePHP to generate the most efficient query possible, ensure proper SQL syntax, and properly escape each individual part of the query.

At it’s most basic, an array-based query looks like this:

$conditions = array("Post.title" => "This is a post");
//Example usage with a model:
$this->Post->find('first', array('conditions' => $conditions));

The structure here is fairly self-explanatory: it will find any post where the title equals “This is a post”. Note that we could have used just “title” as the field name, but when building queries, it is good practice to always specify the model name, as it improves the clarity of the code, and helps prevent collisions in the future, should you choose to change your schema.

What about other types of matches? These are equally simple. Let’s say we wanted to find all the posts where the title is not “This is a post”:

array("Post.title <>" => "This is a post")

Notice the ‘<>’ that follows the field name. CakePHP can parse out any valid SQL comparison operator, including match expressions using LIKE, BETWEEN, or REGEX, as long as you leave a space between field name and the operator. The one exception here is IN (…)-style matches. Let’s say you wanted to find posts where the title was in a given set of values:

array(
    "Post.title" => array("First post", "Second post", "Third post")
)

To do a NOT IN(…) match to find posts where the title is not in the given set of values:

array(
    "NOT" => array("Post.title" => array("First post", "Second post", "Third post"))
)

Adding additional filters to the conditions is as simple as adding additional key/value pairs to the array:

array (
    "Post.title" => array("First post", "Second post", "Third post"),
    "Post.created >" => date('Y-m-d', strtotime("-2 weeks"))
)

You can also create finds that compare two fields in the database

array("Post.created = Post.modified")

This above example will return posts where the created date is equal to the modified date (ie it will return posts that have never been modified).

Remember that if you find yourself unable to form a WHERE clause in this method (ex. boolean operations), you can always specify it as a string like:

array(
    'Model.field & 8 = 1',
    //other conditions as usual
)

By default, CakePHP joins multiple conditions with boolean AND; which means, the snippet above would only match posts that have been created in the past two weeks, and have a title that matches one in the given set. However, we could just as easily find posts that match either condition:

array( "OR" => array (
    "Post.title" => array("First post", "Second post", "Third post"),
    "Post.created >" => date('Y-m-d', strtotime("-2 weeks"))
    )
)

Cake accepts all valid SQL boolean operations, including AND, OR, NOT, XOR, etc., and they can be upper or lower case, whichever you prefer. These conditions are also infinitely nest-able. Let’s say you had a belongsTo relationship between Posts and Authors. Let’s say you wanted to find all the posts that contained a certain keyword (“magic”) or were created in the past two weeks, but you want to restrict your search to posts written by Bob:

array (
    "Author.name" => "Bob",
    "OR" => array (
        "Post.title LIKE" => "%magic%",
        "Post.created >" => date('Y-m-d', strtotime("-2 weeks"))
    )
)

Cake can also check for null fields. In this example, the query will return records where the post title is not null:

array ("NOT" => array (
        "Post.title" => null
    )
)

To handle BETWEEN queries, you can use the following:

array('Post.id BETWEEN ? AND ?' => array(1,10))

Note: CakePHP will quote the numeric values depending on the field type in your DB.

How about GROUP BY?

array('fields'=>array('Product.type','MIN(Product.price) as price'), 'group' => 'Product.type');

The data returned for this would be in the following format:

Array
(
    [0] => Array
        (
            [Product] => Array
                (
                    [type] => Clothing
                )
            [0] => Array
                (
                    [price] => 32
                )
        )
    [1] => Array....

A quick example of doing a DISTINCT query. You can use other operators, such as MIN(), MAX(), etc., in a similar fashion

array('fields'=>array('DISTINCT (User.name) AS my_column_name'), 'order'=>array('User.id DESC'));

You can create very complex conditions, by nesting multiple condition arrays:

array(
   'OR' => array(
      array('Company.name' => 'Future Holdings'),
      array('Company.name' => 'Steel Mega Works')
   ),
   'AND' => array(
      array(
         'OR'=>array(
            array('Company.status' => 'active'),
            'NOT'=>array(
               array('Company.status'=> array('inactive', 'suspended'))
            )
         )
     )
   )
);

Which produces the following SQL:

SELECT `Company`.`id`, `Company`.`name`,
`Company`.`description`, `Company`.`location`,
`Company`.`created`, `Company`.`status`, `Company`.`size`

FROM
   `companies` AS `Company`
WHERE
   ((`Company`.`name` = 'Future Holdings')
   OR
   (`Company`.`name` = 'Steel Mega Works'))
AND
   ((`Company`.`status` = 'active')
   OR (NOT (`Company`.`status` IN ('inactive', 'suspended'))))

Sub-queries

For the example, imagine we have a “users” table with “id”, “name” and “status”. The status can be “A”, “B” or “C”. And we want to get all the users that have status different than “B” using sub-query.

In order to achieve that we are going to get the model data source and ask it to build the query as if we were calling a find method, but it will just return the SQL statement. After that we make an expression and add it to the conditions array.

$conditionsSubQuery['"User2"."status"'] = 'B';

$dbo = $this->User->getDataSource();
$subQuery = $dbo->buildStatement(
    array(
        'fields' => array('"User2"."id"'),
        'table' => $dbo->fullTableName($this->User),
        'alias' => 'User2',
        'limit' => null,
        'offset' => null,
        'joins' => array(),
        'conditions' => $conditionsSubQuery,
        'order' => null,
        'group' => null
    ),
    $this->User
);
$subQuery = ' "User"."id" NOT IN (' . $subQuery . ') ';
$subQueryExpression = $dbo->expression($subQuery);

$conditions[] = $subQueryExpression;

$this->User->find('all', compact('conditions'));

This should generate the following SQL:

SELECT
    "User"."id" AS "User__id",
    "User"."name" AS "User__name",
    "User"."status" AS "User__status"
FROM
    "users" AS "User"
WHERE
    "User"."id" NOT IN (
        SELECT
            "User2"."id"
        FROM
            "users" AS "User2"
        WHERE
            "User2"."status" = 'B'
    )

Saving Your Data

CakePHP makes saving model data a snap. Data ready to be saved should be passed to the model’s save() method using the following basic format:

Array
(
    [ModelName] => Array
        (
            [fieldname1] => 'value'
            [fieldname2] => 'value'
        )
)

Most of the time you won’t even need to worry about this format: CakePHP’s HtmlHelper, FormHelper, and find methods all package data in this format. If you’re using either of the helpers, the data is also conveniently available in $this->data for quick usage.

Here’s a quick example of a controller action that uses a CakePHP model to save data to a database table:

function edit($id) {
    //Has any form data been POSTed?
    if(!empty($this->data)) {
        //If the form data can be validated and saved...
        if($this->Recipe->save($this->data)) {
            //Set a session flash message and redirect.
            $this->Session->setFlash("Recipe Saved!");
            $this->redirect('/recipes');
        }
    }

    //If no form data, find the recipe to be edited
    //and hand it to the view.
    $this->set('recipe', $this->Recipe->findById($id));
}

One additional note: when save is called, the data passed to it in the first parameter is validated using CakePHP validation mechanism (see the Data Validation chapter for more information). If for some reason your data isn’t saving, be sure to check to see if some validation rules aren’t being broken.

There are a few other save-related methods in the model that you’ll find useful:

set($one, $two = null)

Model::set() can be used to set one or many fields of data to the data array inside a model. This is useful when using models with the ActiveRecord features offered by Model.

$this->Post->read(null, 1);
$this->Post->set('title', 'New title for the article');
$this->Post->save();

Is an example of how you can use set() to update and save single fields, in an ActiveRecord approach. You can also use set() to assign new values to multiple fields.

$this->Post->read(null, 1);
$this->Post->set(array(
    'title' => 'New title',
    'published' => false
));
$this->Post->save();

The above would update the title and published fields and save them to the database.

save(array $data = null, boolean $validate = true, array $fieldList = array())

Featured above, this method saves array-formatted data. The second parameter allows you to sidestep validation, and the third allows you to supply a list of model fields to be saved. For added security, you can limit the saved fields to those listed in $fieldList.

If $fieldList is not supplied, a malicious user can add additional fields to the form data (if you are not using Security component), and by this change fields that were not originally intended to be changed.

The save method also has an alternate syntax:

save(array $data = null, array $params = array())

$params array can have any of the following available options as keys:

array(
    'validate' => true,
    'fieldList' => array(),
    'callbacks' => true //other possible values are false, 'before', 'after'
)

More information about model callbacks is available Models

If you dont want the updated field to be updated when saving some data add 'updated' => false to your $data array

Once a save has been completed, the ID for the object can be found in the $id attribute of the model object - something especially handy when creating new objects.

$this->Ingredient->save($newData);

$newIngredientId = $this->Ingredient->id;

Creating or updating is controlled by the model’s id field. If $Model->id is set, the record with this primary key is updated. Otherwise a new record is created.

//Create: id isn't set or is null
$this->Recipe->create();
$this->Recipe->save($this->data);

//Update: id is set to a numerical value
$this->Recipe->id = 2;
$this->Recipe->save($this->data);

When calling save in a loop, don’t forget to call create().

create(array $data = array())

This method resets the model state for saving new information.

If the $data parameter (using the array format outlined above) is passed, the model instance will be ready to save with that data (accessible at $this->data).

If false is passed instead of an array, the model instance will not initialize fields from the model schema that are not already set, it will only reset fields that have already been set, and leave the rest unset. Use this to avoid updating fields in the database that were already set and are intended to be updated.

saveField(string $fieldName, string $fieldValue, $validate = false)

Used to save a single field value. Set the ID of the model ($this->ModelName->id = $id) just before calling saveField(). When using this method, $fieldName should only contain the name of the field, not the name of the model and field.

For example, to update the title of a blog post, the call to saveField from a controller might look something like this:

$this->Post->saveField('title', 'A New Title for a New Day');

You cant stop the updated field being updated with this method, you need to use the save() method.

updateAll(array $fields, array $conditions)

Updates many records in a single call. Records to be updated are identified by the $conditions array, and fields to be updated, along with their values, are identified by the $fields array.

For example, to approve all bakers who have been members for over a year, the update call might look something like:

$this_year = date('Y-m-d H:i:s', strtotime('-1 year'));

$this->Baker->updateAll(
    array('Baker.approved' => true),
    array('Baker.created <=' => $this_year)
);

The $fields array accepts SQL expressions. Literal values should be quoted manually.

For example, to close all tickets that belong to a certain customer:

$this->Ticket->updateAll(
    array('Ticket.status' => "'closed'"),
    array('Ticket.customer_id' => 453)
);

saveAll(array $data = null, array $options = array())

Used to save (a) multiple individual records for a single model or (b) this record, as well as all associated records

The following options may be used:

validate: Set to false to disable validation, true to validate each record before saving, ‘first’ to validate *all* records before any are saved, or ‘only’ to only validate the records, but not save them.

atomic: If true (default), will attempt to save all records in a single transaction. Should be set to false if database/table does not support transactions. If false, we return an array similar to the $data array passed, but values are set to true/false depending on whether each record saved successfully.

fieldList: Equivalent to the $fieldList parameter in Model::save()

For saving multiple records of a single model, $data needs to be a numerically indexed array of records like this:

Array
(
    [Article] => Array(
            [0] => Array
                (
                            [title] => title 1
                        )
            [1] => Array
                (
                            [title] => title 2
                        )
                )
)

The command for saving the above $data array would look like this:

$this->Article->saveAll($data['Article']);

For saving a record along with its related record having a hasOne or belongsTo association, the data array should be like this:

Array
(
    [User] => Array
        (
            [username] => billy
        )
    [Profile] => Array
        (
            [sex] => Male
        [occupation] => Programmer
        )
)

The command for saving the above $data array would look like this:

$this->Article->saveAll($data);

For saving a record along with its related records having hasMany association, the data array should be like this:

Array
(
    [Article] => Array
        (
            [title] => My first article
        )
    [Comment] => Array
        (
            [0] => Array
                (
                    [comment] => Comment 1
            [user_id] => 1
                )
        [1] => Array
                (
                    [comment] => Comment 2
            [user_id] => 2
                )
        )
)

The command for saving the above $data array would look like this:

$this->Article->saveAll($data);

Saving related data with saveAll() will only work for directly associated models.

Deleting Data

These methods can be used to remove data.

delete

delete(int $id = null, boolean $cascade = true);

Deletes the record identified by $id. By default, also deletes records dependent on the record specified to be deleted.

For example, when deleting a User record that is tied to many Recipe records:

  • if $cascade is set to true, the related Recipe records are also deleted if the models dependent-value is set to true.

  • if $cascade is set to false, the Recipe records will remain after the User has been deleted.

remove

remove(int $id = null, boolean $cascade = true);

A synonym for delete().

deleteAll

deleteAll(mixed $conditions, $cascade = true, $callbacks = false)

Same as with delete() and remove(), except that deleteAll() deletes all records that match the supplied conditions. The $conditions array should be supplied as an SQL fragment or array.

Associations: Linking Models Together

One of the most powerful features of CakePHP is the ability to link relational mapping provided by the model. In CakePHP, the links between models are handled through associations.

Defining relations between different objects in your application should be a natural process. For example: in a recipe database, a recipe may have many reviews, reviews have a single author, and authors may have many recipes. Defining the way these relations work allows you to access your data in an intuitive and powerful way.

The purpose of this section is to show you how to plan for, define, and utilize associations between models in CakePHP.

While data can come from a variety of sources, the most common form of storage in web applications is a relational database. Most of what this section covers will be in that context.

For information on associations with Plugin models, see Plugins.

Relationship Types

The four association types in CakePHP are: hasOne, hasMany, belongsTo, and hasAndBelongsToMany (HABTM).

Relationship

Association Type

Example

one to one

hasOne

A user has one profile.

one to many

hasMany

A user can have multiple recipes.

many to one

belongsTo

Many recipes belong to a user.

many to many

hasAndBelongsToMany

Recipes have, and belong to many tags.

Associations are defined by creating a class variable named after the association you are defining. The class variable can sometimes be as simple as a string, but can be as complete as a multidimensional array used to define association specifics.

<?php

class User extends AppModel {
    var $name = 'User';
    var $hasOne = 'Profile';
    var $hasMany = array(
        'Recipe' => array(
            'className'  => 'Recipe',
            'conditions' => array('Recipe.approved' => '1'),
            'order'      => 'Recipe.created DESC'
        )
    );
}

?>

In the above example, the first instance of the word ‘Recipe’ is what is termed an ‘Alias’. This is an identifier for the relationship and can be anything you choose. Usually, you will choose the same name as the class that it references. However, aliases must be unique both within a single model and on both sides of a belongsTo/hasMany or a belongsTo/hasOne relationship. Choosing non-unique names for model aliases can cause unexpected behavior.

Cake will automatically create links between associated model objects. So for example in your User model you can access the Recipe model as

$this->Recipe->someFunction();

Similarly in your controller you can access an associated model simply by following your model associations and without adding it to the $uses array:

$this->User->Recipe->someFunction();

Remember that associations are defined ‘one way’. If you define User hasMany Recipe that has no effect on the Recipe Model. You need to define Recipe belongsTo User to be able to access the User model from your Recipe model

hasOne

Let’s set up a User model with a hasOne relationship to a Profile model.

First, your database tables need to be keyed correctly. For a hasOne relationship to work, one table has to contain a foreign key that points to a record in the other. In this case the profiles table will contain a field called user_id. The basic pattern is:

Relation

Schema

Apple hasOne Banana

bananas.apple_id

User hasOne Profile

profiles.user_id

Doctor hasOne Mentor

mentors.doctor_id

Table: hasOne: the other model contains the foreign key.

The User model file will be saved in /app/models/user.php. To define the ‘User hasOne Profile’ association, add the $hasOne property to the model class. Remember to have a Profile model in /app/models/profile.php, or the association won’t work.

<?php

class User extends AppModel {
    var $name = 'User';
    var $hasOne = 'Profile';
}
?>

There are two ways to describe this relationship in your model files. The simplest method is to set the $hasOne attribute to a string containing the classname of the associated model, as we’ve done above.

If you need more control, you can define your associations using array syntax. For example, you might want to limit the association to include only certain records.

<?php

class User extends AppModel {
    var $name = 'User';
    var $hasOne = array(
        'Profile' => array(
            'className'    => 'Profile',
            'conditions'   => array('Profile.published' => '1'),
            'dependent'    => true
        )
    );
}
?>

Possible keys for hasOne association arrays include:

  • className: the classname of the model being associated to the current model. If you’re defining a ‘User hasOne Profile’ relationship, the className key should equal ‘Profile.’

  • foreignKey: the name of the foreign key found in the other model. This is especially handy if you need to define multiple hasOne relationships. The default value for this key is the underscored, singular name of the current model, suffixed with ‘_id’. In the example above it would default to ‘user_id’.

  • conditions: An SQL fragment used to filter related model records. It’s good practice to use model names in SQL fragments: “Profile.approved = 1” is always better than just “approved = 1.”

  • fields: A list of fields to be retrieved when the associated model data is fetched. Returns all fields by default.

  • order: An SQL fragment that defines the sorting order for the returned associated rows.

  • dependent: When the dependent key is set to true, and the model’s delete() method is called with the cascade parameter set to true, associated model records are also deleted. In this case we set it true so that deleting a User will also delete her associated Profile.

Once this association has been defined, find operations on the User model will also fetch a related Profile record if it exists:

//Sample results from a $this->User->find() call.

Array
(
    [User] => Array
        (
            [id] => 121
            [name] => Gwoo the Kungwoo
            [created] => 2007-05-01 10:31:01
        )
    [Profile] => Array
        (
            [id] => 12
            [user_id] => 121
            [skill] => Baking Cakes
            [created] => 2007-05-01 10:31:01
        )
)

belongsTo

Now that we have Profile data access from the User model, let’s define a belongsTo association in the Profile model in order to get access to related User data. The belongsTo association is a natural complement to the hasOne and hasMany associations: it allows us to see the data from the other direction.

When keying your database tables for a belongsTo relationship, follow this convention:

Relation

Schema

Banana belongsTo Apple

bananas.apple_id

Profile belongsTo User

profiles.user_id

Mentor belongsTo Doctor

mentors.doctor_id

Table: belongsTo: the current model contains the foreign key.

If a model(table) contains a foreign key, it belongsTo the other model(table).

We can define the belongsTo association in our Profile model at /app/models/profile.php using the string syntax as follows:

<?php

class Profile extends AppModel {
    var $name = 'Profile';
    var $belongsTo = 'User';
}
?>

We can also define a more specific relationship using array syntax:

<?php

class Profile extends AppModel {
    var $name = 'Profile';
    var $belongsTo = array(
        'User' => array(
            'className'    => 'User',
            'foreignKey'    => 'user_id'
        )
    );
}
?>

Possible keys for belongsTo association arrays include:

  • className: the classname of the model being associated to the current model. If you’re defining a ‘Profile belongsTo User’ relationship, the className key should equal ‘User.’

  • foreignKey: the name of the foreign key found in the current model. This is especially handy if you need to define multiple belongsTo relationships. The default value for this key is the underscored, singular name of the other model, suffixed with ‘_id’.

  • conditions: An SQL fragment used to filter related model records. It’s good practice to use model names in SQL fragments: “User.active = 1” is always better than just “active = 1.”

  • fields: A list of fields to be retrieved when the associated model data is fetched. Returns all fields by default.

  • order: An SQL fragment that defines the sorting order for the returned associated rows.

  • counterCache: If set to true the associated Model will automatically increase or decrease the “[singular_model_name]_count” field in the foreign table whenever you do a save() or delete(). If its a string then its the field name to use. The value in the counter field represents the number of related rows.

  • counterScope: Optional conditions array to use for updating counter cache field.

Once this association has been defined, find operations on the Profile model will also fetch a related User record if it exists:

//Sample results from a $this->Profile->find() call.

Array
(
   [Profile] => Array
        (
            [id] => 12
            [user_id] => 121
            [skill] => Baking Cakes
            [created] => 2007-05-01 10:31:01
        )
    [User] => Array
        (
            [id] => 121
            [name] => Gwoo the Kungwoo
            [created] => 2007-05-01 10:31:01
        )
)

hasMany

Next step: defining a “User hasMany Comment” association. A hasMany association will allow us to fetch a user’s comments when we fetch a User record.

When keying your database tables for a hasMany relationship, follow this convention:

hasMany: the other model contains the foreign key.

Relation

Schema

User hasMany Comment

Comment.user_id

Cake hasMany Virtue

Virtue.cake_id

Product hasMany Option

Option.product_id

We can define the hasMany association in our User model at /app/models/user.php using the string syntax as follows:

<?php

class User extends AppModel {
    var $name = 'User';
    var $hasMany = 'Comment';
}
?>

We can also define a more specific relationship using array syntax:

<?php

class User extends AppModel {
    var $name = 'User';
    var $hasMany = array(
        'Comment' => array(
            'className'     => 'Comment',
            'foreignKey'    => 'user_id',
            'conditions'    => array('Comment.status' => '1'),
            'order'    => 'Comment.created DESC',
            'limit'        => '5',
            'dependent'=> true
        )
    );
}
?>

Possible keys for hasMany association arrays include:

  • className: the classname of the model being associated to the current model. If you’re defining a ‘User hasMany Comment’ relationship, the className key should equal ‘Comment.’

  • foreignKey: the name of the foreign key found in the other model. This is especially handy if you need to define multiple hasMany relationships. The default value for this key is the underscored, singular name of the actual model, suffixed with ‘_id’.

  • conditions: An SQL fragment used to filter related model records. It’s good practice to use model names in SQL fragments: “Comment.status = 1” is always better than just “status = 1.”

  • fields: A list of fields to be retrieved when the associated model data is fetched. Returns all fields by default.

  • order: An SQL fragment that defines the sorting order for the returned associated rows.

  • limit: The maximum number of associated rows you want returned.

  • offset: The number of associated rows to skip over (given the current conditions and order) before fetching and associating.

  • dependent: When dependent is set to true, recursive model deletion is possible. In this example, Comment records will be deleted when their associated User record has been deleted.

  • exclusive: When exclusive is set to true, recursive model deletion does the delete with a deleteAll() call, instead of deleting each entity separately. This greatly improves performance, but may not be ideal for all circumstances.

  • finderQuery: A complete SQL query CakePHP can use to fetch associated model records. This should be used in situations that require very custom results. If a query you’re building requires a reference to the associated model ID, use the special {$__cakeID__$} marker in the query. For example, if your Apple model hasMany Orange, the query should look something like this:

    SELECT Orange.* from oranges as Orange WHERE Orange.apple_id IN ({$__cakeID__$});
    

    Remember to use IN ({$__cakeID__$}) for hasMany and hasAndBelongsToMany associations as Cake will replace {$__cakeID__$} with a list of ids for these types of associations.

Once this association has been defined, find operations on the User model will also fetch related Comment records if they exist:

//Sample results from a $this->User->find() call.

Array
(
    [User] => Array
        (
            [id] => 121
            [name] => Gwoo the Kungwoo
            [created] => 2007-05-01 10:31:01
        )
    [Comment] => Array
        (
            [0] => Array
                (
                    [id] => 123
                    [user_id] => 121
                    [title] => On Gwoo the Kungwoo
                    [body] => The Kungwooness is not so Gwooish
                    [created] => 2006-05-01 10:31:01
                )
            [1] => Array
                (
                    [id] => 124
                    [user_id] => 121
                    [title] => More on Gwoo
                    [body] => But what of the ‘Nut?
                    [created] => 2006-05-01 10:41:01
                )
        )
)

One thing to remember is that you’ll need a complimentary Comment belongsTo User association in order to get the data from both directions. What we’ve outlined in this section empowers you to get Comment data from the User. Adding the Comment belongsTo User association in the Comment model empowers you to get User data from the Comment model - completing the connection and allowing the flow of information from either model’s perspective.

hasAndBelongsToMany (HABTM)

Alright. At this point, you can already call yourself a CakePHP model associations professional. You’re already well versed in the three associations that take up the bulk of object relations.

Let’s tackle the final relationship type: hasAndBelongsToMany, or HABTM. This association is used when you have two models that need to be joined up, repeatedly, many times, in many different ways.

The main difference between hasMany and HABTM is that a link between models in HABTM is not exclusive. For example, we’re about to join up our Recipe model with a Tag model using HABTM. Attaching the “Italian” tag to my grandma’s Gnocci recipe doesn’t “use up” the tag. I can also tag my Honey Glazed BBQ Spaghettio’s with “Italian” if I want to.

Links between hasMany associated objects are exclusive. If my User hasMany Comments, a comment is only linked to a specific user. It’s no longer up for grabs.

Moving on. We’ll need to set up an extra table in the database to handle HABTM associations. This new join table’s name needs to include the names of both models involved, in alphabetical order, and separated with an underscore ( _ ). The contents of the table should be two fields, each foreign keys (which should be integers) pointing to both of the primary keys of the involved models. To avoid any issues - don’t define a combined primary key for these two fields, if your application requires it you can define a unique index. If you plan to add any extra information to this table, it’s a good idea to add an additional primary key field (by convention ‘id’) to make acting on the table as easy as any other model.

HABTM requires a separate join table that includes both model names.

Relation

Schema (HABTM table in bold)

Recipe HABTM Tag

recipes_tags.id, recipes_tags.recipe_id, recipes_tags.tag_id

Cake HABTM Fan

cakes_fans.id, cakes_fans.cake_id, cakes_fans.fan_id

Foo HABTM Bar

bars_foos.id, bars_foos.foo_id, bars_foos.bar_id

Table names are by convention in alphabetical order.

Make sure primary keys in tables cakes and recipes have “id” fields as assumed by convention. If they’re different than assumed, it Models

Once this new table has been created, we can define the HABTM association in the model files. We’re gonna skip straight to the array syntax this time:

<?php

class Recipe extends AppModel {
    var $name = 'Recipe';
    var $hasAndBelongsToMany = array(
        'Tag' =>
            array(
                'className'              => 'Tag',
                'joinTable'              => 'recipes_tags',
                'foreignKey'             => 'recipe_id',
                'associationForeignKey'  => 'tag_id',
                'unique'                 => true,
                'conditions'             => '',
                'fields'                 => '',
                'order'                  => '',
                'limit'                  => '',
                'offset'                 => '',
                'finderQuery'            => '',
                'deleteQuery'            => '',
                'insertQuery'            => ''
            )
    );
}
?>

Possible keys for HABTM association arrays include:

  • className: the classname of the model being associated to the current model. If you’re defining a ‘Recipe HABTM Tag’ relationship, the className key should equal ‘Tag.’

  • joinTable: The name of the join table used in this association (if the current table doesn’t adhere to the naming convention for HABTM join tables).

  • with: Defines the name of the model for the join table. By default CakePHP will auto-create a model for you. Using the example above it would be called RecipesTag. By using this key you can override this default name. The join table model can be used just like any “regular” model to access the join table directly.

  • foreignKey: the name of the foreign key found in the current model. This is especially handy if you need to define multiple HABTM relationships. The default value for this key is the underscored, singular name of the current model, suffixed with ‘_id’.

  • associationForeignKey: the name of the foreign key found in the other model. This is especially handy if you need to define multiple HABTM relationships. The default value for this key is the underscored, singular name of the other model, suffixed with ‘_id’.

  • unique: If true (default value) cake will first delete existing relationship records in the foreign keys table before inserting new ones, when updating a record. So existing associations need to be passed again when updating.

  • conditions: An SQL fragment used to filter related model records. It’s good practice to use model names in SQL fragments: “Comment.status = 1” is always better than just “status = 1.”

  • fields: A list of fields to be retrieved when the associated model data is fetched. Returns all fields by default.

  • order: An SQL fragment that defines the sorting order for the returned associated rows.

  • limit: The maximum number of associated rows you want returned.

  • offset: The number of associated rows to skip over (given the current conditions and order) before fetching and associating.

  • finderQuery, deleteQuery, insertQuery: A complete SQL query CakePHP can use to fetch, delete, or create new associated model records. This should be used in situations that require very custom results.

Once this association has been defined, find operations on the Recipe model will also fetch related Tag records if they exist:

//Sample results from a $this->Recipe->find() call.

Array
(
    [Recipe] => Array
        (
            [id] => 2745
            [name] => Chocolate Frosted Sugar Bombs
            [created] => 2007-05-01 10:31:01
            [user_id] => 2346
        )
    [Tag] => Array
        (
            [0] => Array
                (
                    [id] => 123
                    [name] => Breakfast
                )
           [1] => Array
                (
                    [id] => 124
                    [name] => Dessert
                )
           [2] => Array
                (
                    [id] => 125
                    [name] => Heart Disease
                )
        )
)

Remember to define a HABTM association in the Tag model if you’d like to fetch Recipe data when using the Tag model.

It is also possible to execute custom find queries based on HABTM relationships. Consider the following examples:

Assuming the same structure in the above example (Recipe HABTM Tag), let’s say we want to fetch all Recipes with the tag ‘Dessert’, one potential (wrong) way to achieve this would be to apply a condition to the association itself:

$this->Recipe->bindModel(array(
    'hasAndBelongsToMany' => array(
        'Tag' => array('conditions'=>array('Tag.name'=>'Dessert'))
)));
$this->Recipe->find('all');
//Data Returned
Array
(
    0 => Array
        {
        [Recipe] => Array
            (
                [id] => 2745
                [name] => Chocolate Frosted Sugar Bombs
                [created] => 2007-05-01 10:31:01
                [user_id] => 2346
            )
        [Tag] => Array
            (
               [0] => Array
                    (
                        [id] => 124
                        [name] => Dessert
                    )
            )
    )
    1 => Array
        {
        [Recipe] => Array
            (
                [id] => 2745
                [name] => Crab Cakes
                [created] => 2008-05-01 10:31:01
                [user_id] => 2349
            )
        [Tag] => Array
            (
            }
        }
}

Notice that this example returns ALL recipes but only the “Dessert” tags. To properly achieve our goal, there are a number of ways to do it. One option is to search the Tag model (instead of Recipe), which will also give us all of the associated Recipes.

$this->Recipe->Tag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));

We could also use the join table model (which CakePHP provides for us), to search for a given ID.

$this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));
$this->Recipe->find('all', array(
        'fields' => array('Recipe.*'),
        'conditions'=>array('RecipesTag.tag_id'=>124) // id of Dessert
));

It’s also possible to create an exotic association for the purpose of creating as many joins as necessary to allow filtering, for example:

$this->Recipe->bindModel(array(
    'hasOne' => array(
        'RecipesTag',
        'FilterTag' => array(
            'className' => 'Tag',
            'foreignKey' => false,
            'conditions' => array('FilterTag.id = RecipesTag.tag_id')
))));
$this->Recipe->find('all', array(
        'fields' => array('Recipe.*'),
        'conditions'=>array('FilterTag.name'=>'Dessert')
));

Both of which will return the following data:

//Data Returned
Array
(
    0 => Array
        {
        [Recipe] => Array
            (
                [id] => 2745
                [name] => Chocolate Frosted Sugar Bombs
                [created] => 2007-05-01 10:31:01
                [user_id] => 2346
            )
    [Tag] => Array
        (
            [0] => Array
                (
                    [id] => 123
                    [name] => Breakfast
                )
           [1] => Array
                (
                    [id] => 124
                    [name] => Dessert
                )
           [2] => Array
                (
                    [id] => 125
                    [name] => Heart Disease
                )
        )
}

The same binding trick can be used to easily paginate your HABTM models. Just one word of caution: since paginate requires two queries (one to count the records and one to get the actual data), be sure to supply the false parameter to your bindModel(); which essentially tells CakePHP to keep the binding persistent over multiple queries, rather than just one as in the default behavior. Please refer to the API for more details.

For more information on saving HABTM objects see Models

For more information on binding model associations on the fly see Models

Mix and match techniques to achieve your specific objective.

Creating and Destroying Associations on the Fly

Sometimes it becomes necessary to create and destroy model associations on the fly. This may be for any number of reasons:

  • You want to reduce the amount of associated data fetched, but all your associations are on the first level of recursion.

  • You want to change the way an association is defined in order to sort or filter associated data.

This association creation and destruction is done using the CakePHP model bindModel() and unbindModel() methods. (There is also a very helpful behavior called “Containable”, please refer to manual section about Built-in behaviors for more information). Let’s set up a few models so we can see how bindModel() and unbindModel() work. We’ll start with two models:

<?php

class Leader extends AppModel {
    var $name = 'Leader';

    var $hasMany = array(
        'Follower' => array(
            'className' => 'Follower',
            'order'     => 'Follower.rank'
        )
    );
}

?>

<?php

class Follower extends AppModel {
    var $name = 'Follower';
}

?>

Now, in the LeadersController, we can use the find() method in the Leader model to fetch a Leader and its associated followers. As you can see above, the association array in the Leader model defines a “Leader hasMany Followers” relationship. For demonstration purposes, let’s use unbindModel() to remove that association in a controller action.

function someAction() {
    // This fetches Leaders, and their associated Followers
    $this->Leader->find('all');

    // Let's remove the hasMany...
    $this->Leader->unbindModel(
        array('hasMany' => array('Follower'))
    );

    // Now using a find function will return
    // Leaders, with no Followers
    $this->Leader->find('all');

    // NOTE: unbindModel only affects the very next
    // find function. An additional find call will use
    // the configured association information.

    // We've already used find('all') after unbindModel(),
    // so this will fetch Leaders with associated
    // Followers once again...
    $this->Leader->find('all');
}

Removing or adding associations using bind- and unbindModel() only works for the next model operation only unless the second parameter has been set to false. If the second parameter has been set to false, the bind remains in place for the remainder of the request.

Here’s the basic usage pattern for unbindModel():

$this->Model->unbindModel(
    array('associationType' => array('associatedModelClassName'))
);

Now that we’ve successfully removed an association on the fly, let’s add one. Our as-of-yet unprincipled Leader needs some associated Principles. The model file for our Principle model is bare, except for the var $name statement. Let’s associate some Principles to our Leader on the fly (but remember–only for just the following find operation). This function appears in the LeadersController:

function anotherAction() {
    // There is no Leader hasMany Principles in
    // the leader.php model file, so a find here,
    // only fetches Leaders.
    $this->Leader->find('all');

    // Let's use bindModel() to add a new association
    // to the Leader model:
    $this->Leader->bindModel(
        array('hasMany' => array(
                'Principle' => array(
                    'className' => 'Principle'
                )
            )
        )
    );

    // Now that we're associated correctly,
    // we can use a single find function to fetch
    // Leaders with their associated principles:
    $this->Leader->find('all');
}

There you have it. The basic usage for bindModel() is the encapsulation of a normal association array inside an array whose key is named after the type of association you are trying to create:

$this->Model->bindModel(
        array('associationName' => array(
                'associatedModelClassName' => array(
                    // normal association keys go here...
                )
            )
        )
    );

Even though the newly bound model doesn’t need any sort of association definition in its model file, it will still need to be correctly keyed in order for the new association to work properly.

Multiple relations to the same model

There are cases where a Model has more than one relation to another Model. For example you might have a Message model that has two relations to the User model. One relation to the user that sends a message, and a second to the user that receives the message. The messages table will have a field user_id, but also a field recipient_id. Now your Message model can look something like:

<?php
class Message extends AppModel {
    var $name = 'Message';
    var $belongsTo = array(
        'Sender' => array(
            'className' => 'User',
            'foreignKey' => 'user_id'
        ),
        'Recipient' => array(
            'className' => 'User',
            'foreignKey' => 'recipient_id'
        )
    );
}
?>

Recipient is an alias for the User model. Now let’s see what the User model would look like.

<?php
class User extends AppModel {
    var $name = 'User';
    var $hasMany = array(
        'MessageSent' => array(
            'className' => 'Message',
            'foreignKey' => 'user_id'
        ),
        'MessageReceived' => array(
            'className' => 'Message',
            'foreignKey' => 'recipient_id'
        )
    );
}
?>

Joining tables

In SQL you can combine related tables using the JOIN statement. This allows you to perform complex searches across multiples tables (i.e: search posts given several tags).

In CakePHP some associations (belongsTo and hasOne) performs automatic joins to retrieve data, so you can issue queries to retrieve models based on data in the related one.

But this is not the case with hasMany and hasAndBelongsToMany associations. Here is where forcing joins comes to the rescue. You only have to define the necessary joins to combine tables and get the desired results for your query.

To force a join between tables you need to use the “modern” syntax for Model::find(), adding a ‘joins’ key to the $options array. For example:

$options['joins'] = array(
    array(
        'table' => 'channels',
        'alias' => 'Channel',
        'type' => 'LEFT',
        'conditions' => array(
            'Channel.id = Item.channel_id',
        )
    )
);

$Item->find('all', $options);

Note that the ‘join’ arrays are not keyed.

In the above example, a model called Item is left joined to the channels table. You can alias the table with the model name, so the retrieved data complies with the CakePHP data structure.

The keys that define the join are the following:

  • table: The table for the join.

  • alias: An alias to the table. The name of the model associated with the table is the best bet.

  • type: The type of join: inner, left or right.

  • conditions: The conditions to perform the join.

With joins, you could add conditions based on related model fields:

$options['joins'] = array(
    array('table' => 'channels',
        'alias' => 'Channel',
        'type' => 'LEFT',
        'conditions' => array(
            'Channel.id = Item.channel_id',
        )
    )
);

$options['conditions'] = array(
    'Channel.private' => 1
);

$pirvateItems = $Item->find('all', $options);

You could perform several joins as needed in hasBelongsToMany:

Suppose a Book hasAndBelongsToMany Tag association. This relation uses a books_tags table as join table, so you need to join the books table to the books_tags table, and this with the tags table:

$options['joins'] = array(
    array('table' => 'books_tags',
        'alias' => 'BooksTag',
        'type' => 'inner',
        'conditions' => array(
            'Books.id = BooksTag.book_id'
        )
    ),
    array('table' => 'tags',
        'alias' => 'Tag',
        'type' => 'inner',
        'conditions' => array(
            'BooksTag.tag_id = Tag.id'
        )
    )
);

$options['conditions'] = array(
    'Tag.tag' => 'Novel'
);

$books = $Book->find('all', $options);

Using joins with Containable behavior could lead to some SQL errors (duplicate tables), so you need to use the joins method as an alternative for Containable if your main goal is to perform searches based on related data. Containable is best suited to restricting the amount of related data brought by a find statement.

Callback Methods

If you want to sneak in some logic just before or after a CakePHP model operation, use model callbacks. These functions can be defined in model classes (including your AppModel) class. Be sure to note the expected return values for each of these special functions.

Please note that these callbacks are not called when dealing with associated models; callbacks are only executed for the main model of a query.

beforeFind

beforeFind(mixed $queryData)

Called before any find-related operation. The $queryData passed to this callback contains information about the current query: conditions, fields, etc.

If you do not wish the find operation to begin (possibly based on a decision relating to the $queryData options), return false. Otherwise, return the possibly modified $queryData, or anything you want to get passed to find and its counterparts.

You might use this callback to restrict find operations based on a user’s role, or make caching decisions based on the current load.

afterFind

afterFind(array $results, bool $primary)

Use this callback to modify results that have been returned from a find operation, or to perform any other post-find logic. The $results parameter passed to this callback contains the returned results from the model’s find operation, i.e. something like:

$results = array(
  0 => array(
    'ModelName' => array(
      'field1' => 'value1',
      'field2' => 'value2',
    ),
  ),
);

The return value for this callback should be the (possibly modified) results for the find operation that triggered this callback.

The $primary parameter indicates whether or not the current model was the model that the query originated on or whether or not this model was queried as an association. If a model is queried as an assocation the format of $results can differ; instead of the result you would normally get from a find operation, you may get this:

$results = array(
  'field_1' => 'value1',
  'field_2' => 'value2'
);

Code expecting $primary to be true will probably get a “Cannot use string offset as an array” fatal error from PHP if a recursive find is used.

Below is an example of how afterfind can be used for date formating.

function afterFind($results) {
    foreach ($results as $key => $val) {
        if (isset($val['Event']['begindate'])) {
            $results[$key]['Event']['begindate'] = $this->dateFormatAfterFind($val['Event']['begindate']);
        }
    }
    return $results;
}

function dateFormatAfterFind($dateString) {
    return date('d-m-Y', strtotime($dateString));
}

beforeValidate

beforeValidate()

Use this callback to modify model data before it is validated, or to modify validation rules if required. This function must also return true, otherwise the current save() execution will abort.

beforeSave

beforeSave()

Place any pre-save logic in this function. This function executes immediately after model data has been successfully validated, but just before the data is saved. This function should also return true if you want the save operation to continue.

This callback is especially handy for any data-massaging logic that needs to happen before your data is stored. If your storage engine needs dates in a specific format, access it at $this->data and modify it.

Below is an example of how beforeSave can be used for date conversion. The code in the example is used for an application with a begindate formatted like YYYY-MM-DD in the database and is displayed like DD-MM-YYYY in the application. Of course this can be changed very easily. Use the code below in the appropriate model.

function beforeSave() {
    if (!empty($this->data['Event']['begindate']) && !empty($this->data['Event']['enddate'])) {
            $this->data['Event']['begindate'] = $this->dateFormatBeforeSave($this->data['Event']['begindate']);
            $this->data['Event']['enddate'] = $this->dateFormatBeforeSave($this->data['Event']['enddate']);
    }
    return true;
}

function dateFormatBeforeSave($dateString) {
    return date('Y-m-d', strtotime($dateString)); // Direction is from
}

Be sure that beforeSave() returns true, or your save is going to fail.

afterSave

afterSave(boolean $created)

If you have logic you need to be executed just after every save operation, place it in this callback method.

The value of $created will be true if a new record was created (rather than an update).

beforeDelete

beforeDelete(boolean $cascade)

Place any pre-deletion logic in this function. This function should return true if you want the deletion to continue, and false if you want to abort.

The value of $cascade will be true if records that depend on this record will also be deleted.

afterDelete

afterDelete()

Place any logic that you want to be executed after every deletion in this callback method.

onError

onError()

Called if any problems occur.

Model Attributes

Model attributes allow you to set properties that can override the default model behavior.

For a complete list of model attributes and their descriptions visit the CakePHP API. Check out https://api.cakephp.org/class/model.

useDbConfig

The useDbConfig property is a string that specifies the name of the database connection to use to bind your model class to the related database table. You can set it to any of the database connections defined within your database configuration file. The database configuration file is stored in /app/config/database.php.

The useDbConfig property is defaulted to the ‘default’ database connection.

Example usage:

class Example extends AppModel {
   var $useDbConfig = 'alternate';
}

useTable

The useTable property specifies the database table name. By default, the model uses the lowercase, plural form of the model’s class name. Set this attribute to the name of an alternate table, or set it to false if you wish the model to use no database table.

Example usage:

class Example extends AppModel {
   var $useTable = false; // This model does not use a database table
}

Alternatively:

class Example extends AppModel {
   var $useTable = 'exmp'; // This model uses a database table 'exmp'
}

tablePrefix

The name of the table prefix used for the model. The table prefix is initially set in the database connection file at /app/config/database.php. The default is no prefix. You can override the default by setting the tablePrefix attribute in the model.

Example usage:

class Example extends AppModel {
   var $tablePrefix = 'alternate_'; // will look for 'alternate_examples'
}

primaryKey

Each table normally has a primary key, id. You may change which field name the model uses as its primary key. This is common when setting CakePHP to use an existing database table.

Example usage:

class Example extends AppModel {
    var $primaryKey = 'example_id'; // example_id is the field name in the database
}

displayField

The displayField attribute specifies which database field should be used as a label for the record. The label is used in scaffolding and in find('list') calls. The model will use name or title, by default.

For example, to use the username field:

class User extends AppModel {
   var $displayField = 'username';
}

Multiple field names cannot be combined into a single display field. For example, you cannot specify, array('first_name', 'last_name') as the display field.

recursive

The recursive property defines how deep CakePHP should go to fetch associated model data via find(), findAll() and read() methods.

Imagine your application features Groups which belong to a domain and have many Users which in turn have many Articles. You can set $recursive to different values based on the amount of data you want back from a $this->Group->find() call:

Depth

Description

-1

Cake fetches Group data only, no joins.

0

Cake fetches Group data and its domain

1

Cake fetches a Group, its domain and its associated Users

2

Cake fetches a Group, its domain, its associated Users, and the Users’ associated Articles

Set it no higher than you need. Having CakePHP fetch data you aren’t going to use slows your app unnecessarily. Also note that the default recursive level is 1.

If you want to combine $recursive with the fields functionality, you will have to add the columns containing the required foreign keys to the fields array manually. In the example above, this could mean adding domain_id.

order

The default ordering of data for any find operation. Possible values include:

$order = "field"
$order = "Model.field";
$order = "Model.field asc";
$order = "Model.field ASC";
$order = "Model.field DESC";
$order = array("Model.field" => "asc", "Model.field2" => "DESC");

data

The container for the model’s fetched data. While data returned from a model class is normally used as returned from a find() call, you may need to access information stored in $data inside of model callbacks.

_schema

Contains metadata describing the model’s database table fields. Each field is described by:

  • name

  • type (integer, string, datetime, etc.)

  • null

  • default value

  • length

Example Usage:

var $_schema = array(
    'first_name' => array(
        'type' => 'string',
        'length' => 30
    ),
    'last_name' => array(
        'type' => 'string',
        'length' => 30
    ),
    'email' => array(
        'type' => 'string',
        'length' => 30
    ),
    'message' => array('type' => 'text')
);

validate

This attribute holds rules that allow the model to make data validation decisions before saving. Keys named after fields hold regex values allowing the model to try to make matches.

It is not necessary to call validate() before save() as save() will automatically validate your data before actually saving.

For more information on validation, see the Data Validation later on in this manual.

name

As you saw earlier in this chapter, the name attribute is a compatibility feature for PHP4 users and is set to the same value as the model name.

Example usage:

class Example extends AppModel {
   var $name = 'Example';
}

cacheQueries

If set to true, data fetched by the model during a single request is cached. This caching is in-memory only, and only lasts for the duration of the request. Any duplicate requests for the same data is handled by the cache.

Additional Methods and Properties

While CakePHP’s model functions should get you where you need to go, don’t forget that model classes are just that: classes that allow you to write your own methods or define your own properties.

Any operation that handles the saving and fetching of data is best housed in your model classes. This concept is often referred to as the fat model.

class Example extends AppModel {

   function getRecent() {
      $conditions = array(
         'created BETWEEN (curdate() - interval 7 day) and (curdate() - interval 0 day))'
      );
      return $this->find('all', compact('conditions'));
   }
}

This getRecent() method can now be used within the controller.

$recent = $this->Example->getRecent();