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.
Saving Related Model Data (hasOne, hasMany, belongsTo)
When working with associated models, it is important to realize that
saving model data should always be done by the corresponding CakePHP
model. If you are saving a new Post and its associated Comments, then
you would use both Post and Comment models during the save operation.
If neither of the associated model records exists in the system yet (for
example, you want to save a new User and their related Profile records
at the same time), you’ll need to first save the primary, or parent
model.
To get an idea of how this works, let’s imagine that we have an action
in our UsersController that handles the saving of a new User and a
related Profile. The example action shown below will assume that you’ve
POSTed enough data (using the FormHelper) to create a single User and a
single Profile.
<?php
function add() {
if (!empty($this->data)) {
// We can save the User data:
// it should be in $this->data['User']
$user = $this->User->save($this->data);
// If the user was saved, Now we add this information to the data
// and save the Profile.
if (!empty($user)) {
// The ID of the newly created user has been set
// as $this->User->id.
$this->data['Profile']['user_id'] = $this->User->id;
// Because our User hasOne Profile, we can access
// the Profile model through the User model:
$this->User->Profile->save($this->data);
}
}
}
?>
As a rule, when working with hasOne, hasMany, and belongsTo
associations, its all about keying. The basic idea is to get the key
from one model and place it in the foreign key field on the other.
Sometimes this might involve using the $id
attribute of the model
class after a save()
, but other times it might just involve
gathering the ID from a hidden input on a form that’s just been POSTed
to a controller action.
To supplement the basic approach used above, CakePHP also offers a very
handy method saveAll()
, which allows you to validate and save
multiple models in one shot. In addtion, saveAll()
provides
transactional support to ensure data integrity in your database (i.e. if
one model fails to save, the other models will not be saved either).
For transactions to work correctly in MySQL your tables must use InnoDB
engine. Remember that MyISAM tables do not support transactions.
Let’s see how we can use saveAll()
to save Company and Account
models at the same time.
First, you need to build your form for both Company and Account models
(we’ll assume that Company hasMany Account).
echo $form->create('Company', array('action'=>'add'));
echo $form->input('Company.name', array('label'=>'Company name'));
echo $form->input('Company.description');
echo $form->input('Company.location');
echo $form->input('Account.0.name', array('label'=>'Account name'));
echo $form->input('Account.0.username');
echo $form->input('Account.0.email');
echo $form->end('Add');
Take a look at the way we named the form fields for the Account model.
If Company is our main model saveAll()
will expect the related
model’s (Account) data to arrive in a specific format. And having
Account.0.fieldName
is exactly what we need.
The above field naming is required for a hasMany association. If the
association between the models is hasOne, you have to use
ModelName.fieldName notation for the associated model.
Now, in our companies_controller we can create an add()
action:
function add() {
if(!empty($this->data)) {
$this->Company->saveAll($this->data, array('validate'=>'first'));
}
}
That’s all there is to it. Now our Company and Account models will be
validated and saved all at the same time. A quick thing to point out
here is the use of array('validate'=>'first')
; this option will
ensure that both of our models are validated.
counterCache - Cache your count()
This function helps you cache the count of related data. Instead of
counting the records manually via find('count')
, the model itself
tracks any addition/deleting towards the associated $hasMany
model
and increases/decreases a dedicated integer field within the parent
model table.
The name of the field consists of the singular model name followed by a
underscore and the word “count”.
Let’s say you have a model called ImageComment
and a model called
Image
, you would add a new INT-field to the image
table and name
it image_comment_count
.
Here are some more examples:
Model |
Associated Model |
Example |
User |
Image |
users.image_count |
Image |
ImageComment |
images.image_comment_count |
BlogEntry |
BlogEntryComment |
blog_entries.blog_entry_comment_count |
Once you have added the counter field you are good to go. Activate
counter-cache in your association by adding a counterCache
key and
set the value to true
.
class Image extends AppModel {
var $belongsTo = array(
'ImageAlbum' => array('counterCache' => true)
);
}
From now on, every time you add or remove a Image
associated to
ImageAlbum
, the number within image_count
is adjusted
automatically.
If you need to specify a custom counter field, set counterCache to the
name of that field:
class Image extends AppModel {
var $belongsTo = array(
'ImageAlbum' => array('counterCache' => 'number_of_images')
);
}
You can also specify counterScope
. It allows you to specify a simple
condition which tells the model when to update (or when not to,
depending on how you look at it) the counter value.
Using our Image model example, we can specify it like so:
class Image extends AppModel {
var $belongsTo = array(
'ImageAlbum' => array(
'counterCache' => true,
'counterScope' => array('Image.active' => 1) // only count if "Image" is active = 1
));
}
Saving Related Model Data (HABTM)
Saving models that are associated by hasOne, belongsTo, and hasMany is
pretty simple: you just populate the foreign key field with the ID of
the associated model. Once that’s done, you just call the save() method
on the model, and everything gets linked up correctly.
With HABTM, you need to set the ID of the associated model in your data
array. We’ll build a form that creates a new tag and associates it on
the fly with some recipe.
The simplest form might look something like this (we’ll assume that
$recipe_id is already set to something):
<?php echo $form->create('Tag');?>
<?php echo $form->input(
'Recipe.id',
array('type'=>'hidden', 'value' => $recipe_id)); ?>
<?php echo $form->input('Tag.name'); ?>
<?php echo $form->end('Add Tag'); ?>
In this example, you can see the Recipe.id
hidden field whose value
is set to the ID of the recipe we want to link the tag to.
When the save()
method is invoked within the controller, it’ll
automatically save the HABTM data to the database.
function add() {
//Save the association
if ($this->Tag->save($this->data)) {
//do something on success
}
}
With the preceding code, our new Tag is created and associated with a
Recipe, whose ID was set in $this->data[‘Recipe’][‘id’].
Other ways we might want to present our associated data can include a
select drop down list. The data can be pulled from the model using the
find('list')
method and assigned to a view variable of the model
name. An input with the same name will automatically pull in this data
into a <select>
.
// in the controller:
$this->set('tags', $this->Recipe->Tag->find('list'));
// in the view:
$form->input('tags');
A more likely scenario with a HABTM relationship would include a
<select>
set to allow multiple selections. For example, a Recipe can
have multiple Tags assigned to it. In this case, the data is pulled out
of the model the same way, but the form input is declared slightly
different. The tag name is defined using the ModelName
convention.
// in the controller:
$this->set('tags', $this->Recipe->Tag->find('list'));
// in the view:
$form->input('Tag');
Using the preceding code, a multiple select drop down is created,
allowing for multiple choices to automatically be saved to the existing
Recipe being added or saved to the database.
What to do when HABTM becomes complicated?
By default when saving a HasAndBelongsToMany relationship, Cake will
delete all rows on the join table before saving new ones. For example if
you have a Club that has 10 Children associated. You then update the
Club with 2 children. The Club will only have 2 Children, not 12.
Also note that if you want to add more fields to the join (when it was
created or meta information) this is possible with HABTM join tables,
but it is important to understand that you have an easy option.
HasAndBelongsToMany between two models is in reality shorthand for three
models associated through both a hasMany and a belongsTo association.
Consider this example:
Child hasAndBelongsToMany Club
Another way to look at this is adding a Membership model:
Child hasMany Membership
Membership belongsTo Child, Club
Club hasMany Membership.
These two examples are almost the exact same. They use the same amount
and named fields in the database and the same amount of models. The
important differences are that the “join” model is named differently and
it’s behavior is more predictable.
When your join table contains extra fields besides two foreign keys, in
most cases its easier to make a model for the join table and setup
hasMany, belongsTo associations as shown in example above instead of
using HABTM association.
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.