Database Access & ORM. CakePHP 3.5
CakePHP 3.5 is a powerful PHP framework that makes building web applications faster and easier. Its core features include a flexible ORM (Object Relational Mapper), a robust routing system, a powerful templating engine, and a comprehensive set of components for common tasks like authentication, caching, and email. You can use it to build everything from simple websites to complex web applications and APIs.
Advertisement
Advertisement
CHAPTER 12
Database Access & ORM
In CakePHP working with data through the database is done with two primary object types. The first are repositories or table objects. These objects provide access to collections of data. They allow you to save new records, modify/delete existing ones, define relations, and perform bulk operations. The second type of objects are entities. Entities represent individual records and allow you to define row/record level behavior & functionality.
These two classes are usually responsible for managing almost everything that happens regarding your data, its validity, interactions and evolution of the information workflow in your domain of work.
CakePHP’s built-in ORM specializes in relational databases, but can be extended to support alternative datasources.
The CakePHP ORM borrows ideas and concepts from both ActiveRecord and Datamapper patterns. It aims to create a hybrid implementation that combines aspects of both patterns to create a fast, simple to use ORM.
Before we get started exploring the ORM, make sure you
configure your database connections
Note: If you are familiar with previous versions of CakePHP, you should read the
New ORM Upgrade Guide
for important differences between CakePHP 3.0 and older versions of CakePHP.
Quick Example
To get started you don’t have to write any code. If you’ve followed the
CakePHP conventions for your database tables
you can just start using the ORM. For example if we wanted to load some data from our articles table we could do:
use
Cake\ORM\TableRegistry;
$articles = TableRegistry :: get ( 'Articles' );
$query = $articles -> find ();
foreach
( $query
as
$row ) {
echo
$row -> title ;
}
Note that we didn’t have to create any code or wire any configuration up. The conventions in CakePHP allow us to skip some boilerplate code and allow the framework to insert base classes when your application has not created a concrete
369
CakePHP Cookbook Documentation, Release 3.5
class. If we wanted to customize our ArticlesTable class adding some associations or defining some additional methods we would add the following to src/Model/Table/ArticlesTable.php after the <?php opening tag:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
}
Table classes use the CamelCased version of the table name with the Table suffix as the class name. Once your class has been created you get a reference to it using the
ORM\TableRegistry
as before:
use
Cake\ORM\TableRegistry;
// Now $articles is an instance of our ArticlesTable class.
$articles = TableRegistry :: get ( 'Articles' );
Now that we have a concrete table class, we’ll probably want to use a concrete entity class. Entity classes let you define accessor and mutator methods, define custom logic for individual records and much more. We’ll start off by adding the following to src/Model/Entity/Article.php after the <?php opening tag:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class Article extends
Entity
{
}
Entities use the singular CamelCase version of the table name as their class name by default. Now that we have created our entity class, when we load entities from the database we’ll get instances of our new Article class:
use
Cake\ORM\TableRegistry;
// Now an instance of ArticlesTable.
$articles = TableRegistry :: get ( 'Articles' );
$query = $articles -> find ();
foreach
( $query
as
$row ) {
// Each row is now an instance of our Article class.
echo
$row -> title ;
}
CakePHP uses naming conventions to link the Table and Entity class together. If you need to customize which entity a table uses you can use the entityClass() method to set a specific classname.
See the chapters on
Table Objects
and
Entities
for more information on how to use table objects and entities in your application.
370 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
More Information
Database Basics
The CakePHP database access layer abstracts and provides help with most aspects of dealing with relational databases such as, keeping connections to the server, building queries, preventing SQL injections, inspecting and altering schemas, and with debugging and profiling queries sent to the database.
Quick Tour
The functions described in this chapter illustrate what is possible to do with the lower-level database access API. If instead you want to learn more about the complete ORM, you can read the
Query Builder
and
Table Objects
sections.
The easiest way to create a database connection is using a DSN string:
use
Cake\Datasource\ConnectionManager;
$dsn = 'mysql://root:password@localhost/my_database' ;
ConnectionManager :: config ( 'default' , [ 'url' => $dsn ]);
Once created, you can access the connection object to start using it:
$connection = ConnectionManager :: get ( 'default' );
Supported Databases
CakePHP supports the following relational database servers:
• MySQL 5.1+
• SQLite 3
• PostgreSQL 8+
• SQLServer 2008+
• Oracle (through a community plugin)
You will need the correct PDO extension installed for each of the above database drivers. Procedural APIs are not supported.
The Oracle database is supported through the Driver for Oracle Database
127 community plugin.
Running Select Statements
Running raw SQL queries is a breeze:
use
Cake\Datasource\ConnectionManager;
$connection = ConnectionManager :: get ( 'default' );
$results = $connection -> execute ( 'SELECT * FROM articles' ) -> fetchAll ( 'assoc' );
You can use prepared statements to insert parameters:
127 https://github.com/CakeDC/cakephp-oracle-driver
More Information 371
CakePHP Cookbook Documentation, Release 3.5
$results = $connection
-> execute ( 'SELECT * FROM articles WHERE id = :id' , [ 'id' => 1 ])
-> fetchAll ( 'assoc' );
It is also possible to use complex data types as arguments:
$results = $connection
-> execute (
'SELECT * FROM articles WHERE created >= :created' ,
[ 'created' =>
new
DateTime( '1 day ago' )],
[ 'created' => 'datetime' ]
)
-> fetchAll ( 'assoc' );
Instead of writing the SQL manually, you can use the query builder:
$results = $connection
-> newQuery ()
-> select ( '*' )
-> from ( 'articles' )
-> where ([ 'created >' =>
new
DateTime( '1 day ago' ), [ 'created' => 'datetime' ]])
-> order ([ 'title' => 'DESC' ])
-> execute ()
-> fetchAll ( 'assoc' );
Running Insert Statements
Inserting rows in the database is usually a matter of a couple lines:
use
Cake\Datasource\ConnectionManager;
$connection = ConnectionManager :: get ( 'default' );
$connection -> insert ( 'articles' , [
'title' => 'A New Article' ,
'created' =>
new
DateTime( 'now' )
], [ 'created' => 'datetime' ]);
Running Update Statements
Updating rows in the database is equally intuitive, the following example will update the article with id 10:
use
Cake\Datasource\ConnectionManager;
$connection = ConnectionManager :: get ( 'default' );
$connection -> update ( 'articles' , [ 'title' => 'New title' ], [ 'id' => 10 ]);
Running Delete Statements
Similarly, the delete() method is used to delete rows from the database, the following example deletes the article with id 10:
use
Cake\Datasource\ConnectionManager;
$connection = ConnectionManager :: get ( 'default' );
$connection -> delete ( 'articles' , [ 'id' => 10 ]);
372 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Configuration
By convention database connections are configured in config/app.php. The connection information defined in this file is fed into
Cake\Datasource\ConnectionManager
creating the connection configuration your application will be using. Sample connection information can be found in config/app.default.php. A sample connection configuration would look like:
'Datasources' => [
'default' => [
'className' => 'Cake\Database\Connection' ,
'driver' => 'Cake\Database\Driver\Mysql' ,
'persistent' =>
false
,
'host' => 'localhost' ,
'username' => 'my_app' ,
'password' => 'sekret' ,
'database' => 'my_app' ,
'encoding' => 'utf8' ,
'timezone' => 'UTC' ,
'cacheMetadata' =>
true
,
]
],
The above will create a ‘default’ connection, with the provided parameters.
You can define as many connections as you want in your configuration file.
You can also define additional connections at runtime using
Cake\Datasource\ConnectionManager::config() . An example of that would be:
use
Cake\Datasource\ConnectionManager;
ConnectionManager :: config ( 'default' , [
'className' => 'Cake\Database\Connection' ,
'driver' => 'Cake\Database\Driver\Mysql' ,
'persistent' =>
false
,
'host' => 'localhost' ,
'username' => 'my_app' ,
'password' => 'sekret' ,
'database' => 'my_app' ,
'encoding' => 'utf8' ,
'timezone' => 'UTC' ,
'cacheMetadata' =>
true
,
]);
Configuration options can also be provided as a
DSN
string. This is useful when working with environment variables or
PaaS
providers:
ConnectionManager :: config ( 'default' , [
'url' => 'mysql://my_app:sekret@localhost/my_app?encoding=utf8&timezone=UTC&
˓→ cacheMetadata=true' ,
]);
When using a DSN string you can define any additional parameters/options as query string arguments.
By default, all Table objects will use the default connection. To use a non-default connection, see
Configuring
Connections
There are a number of keys supported in database configuration. A full list is as follows:
More Information 373
CakePHP Cookbook Documentation, Release 3.5
className The fully namespaced class name of the class that represents the connection to a database server. This class is responsible for loading the database driver, providing SQL transaction mechanisms and preparing SQL statements among other things.
driver The class name of the driver used to implements all specificities for a database engine. This can either be a short classname using
plugin syntax
, a fully namespaced name, or a constructed driver instance. Examples of
short classnames are Mysql, Sqlite, Postgres, and Sqlserver.
persistent Whether or not to use a persistent connection to the database. This option is not supported by SqlServer. As of CakePHP version 3.4.13 an exception is thrown if you attempt to set persistent to true with SqlServer.
host The database server’s hostname (or IP address).
username The username for the account.
password The password for the account.
database The name of the database for this connection to use. Avoid using . in your database name. Because of how it complicates identifier quoting CakePHP does not support . in database names. The path to your SQLite database should be an absolute path (e.g. ROOT . DS . 'my_app.db') to avoid incorrect paths caused by relative paths.
port (optional) The TCP port or Unix socket used to connect to the server.
encoding Indicates the character set to use when sending SQL statements to the server. This defaults to the database’s default encoding for all databases other than DB2. If you wish to use UTF-8 encoding with MySQL connections you must use ‘utf8’ without the hyphen.
timezone Server timezone to set.
schema Used in PostgreSQL database setups to specify which schema to use.
unix_socket Used by drivers that support it to connect via Unix socket files. If you are using PostgreSQL and want to use Unix sockets, leave the host key blank.
ssl_key The file path to the SSL key file. (Only supported by MySQL).
ssl_cert The file path to the SSL certificate file. (Only supported by MySQL).
ssl_ca The file path to the SSL certificate authority. (Only supported by MySQL).
init A list of queries that should be sent to the database server as when the connection is created.
log Set to true to enable query logging.
When enabled queries will be logged at a debug level with the queriesLog scope.
quoteIdentifiers Set to true if you are using reserved words or special characters in your table or column names.
Enabling this setting will result in queries built using the
Query Builder
having identifiers quoted when creating SQL. It should be noted that this decreases performance because each query needs to be traversed and manipulated before being executed.
flags An associative array of PDO constants that should be passed to the underlying PDO instance. See the PDO documentation for the flags supported by the driver you are using.
cacheMetadata Either boolean true, or a string containing the cache configuration to store meta data in. Having metadata caching disable is not advised and can result in very poor performance. See the
Metadata Caching
section for more information.
mask Set the permissions on the generated database file. (Only supported by SQLite)
At this point, you might want to take a look at the
CakePHP Conventions
. The correct naming for your tables (and the
addition of some columns) can score you some free functionality and help you avoid configuration. For example, if you name your database table big_boxes, your table BigBoxesTable, and your controller BigBoxesController, everything
374 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
will work together automatically. By convention, use underscores, lower case, and plural forms for your database table names - for example: bakers, pastry_stores, and savory_cakes.
Managing Connections
class Cake\Datasource\ConnectionManager
The ConnectionManager class acts as a registry to access database connections your application has. It provides a place that other objects can get references to existing connections.
Accessing Connections
static Cake\Datasource\ConnectionManager::get($name)
Once configured connections can be fetched using
Cake\Datasource\ConnectionManager::get()
method will construct and load a connection if it has not been built before, or return the existing known connection:
use
Cake\Datasource\ConnectionManager;
$conn = ConnectionManager :: get ( 'default' );
Attempting to load connections that do not exist will throw an exception.
Creating Connections at Runtime
Using config() and get() you can create new connections that are not defined in your configuration files at runtime:
ConnectionManager :: config ( 'my_connection' , $config );
$conn = ConnectionManager :: get ( 'my_connection' );
See the
Configuration
for more information on the configuration data used when creating connections.
Data Types
class Cake\Database\Type
Since not every database vendor includes the same set of data types, or the same names for similar data types, CakePHP provides a set of abstracted data types for use with the database layer. The types CakePHP supports are: string Generally backed by CHAR or VARCHAR columns. Using the fixed option will force a CHAR column. In
SQL Server, NCHAR and NVARCHAR types are used.
text Maps to TEXT types.
uuid Maps to the UUID type if a database provides one, otherwise this will generate a CHAR(36) field.
integer Maps to the INTEGER type provided by the database. BIT is not yet supported at this moment.
smallinteger Maps to the SMALLINT type provided by the database.
tinyinteger Maps to the TINYINT or SMALLINT type provided by the database. In MySQL TINYINT(1) is treated as a boolean.
biginteger Maps to the BIGINT type provided by the database.
More Information 375
CakePHP Cookbook Documentation, Release 3.5
float Maps to either DOUBLE or FLOAT depending on the database. The precision option can be used to define the precision used.
decimal Maps to the DECIMAL type. Supports the length and precision options.
boolean Maps to BOOLEAN except in MySQL, where TINYINT(1) is used to represent booleans. BIT(1) is not yet supported at this moment.
binary Maps to the BLOB or BYTEA type provided by the database.
date Maps to a timezone naive DATE column type. The return value of this column type is Cake\I18n\Date which extends the native DateTime class.
datetime Maps to a timezone naive DATETIME column type. In PostgreSQL, and SQL Server this turns into a
TIMESTAMP type. The default return value of this column type is
Cake\I18n\Time
which extends the built-in DateTime class and Chronos
128
timestamp Maps to the TIMESTAMP type.
time Maps to a TIME type in all databases.
json Maps to a JSON type if it’s available, otherwise it maps to TEXT. The ‘json’ type was added in 3.3.0
These types are used in both the schema reflection features that CakePHP provides, and schema generation features
CakePHP uses when using test fixtures.
Each type can also provide translation functions between PHP and SQL representations. These methods are invoked based on the type hints provided when doing queries. For example a column that is marked as ‘datetime’ will automatically convert input parameters from DateTime instances into a timestamp or formatted datestrings. Likewise,
‘binary’ columns will accept file handles, and generate file handles when reading data.
Changed in version 3.3.0: The json type was added.
Changed in version 3.5.0: The smallinteger and tinyinteger types were added.
Adding Custom Types
static Cake\Database\Type::map($name, $class)
If you need to use vendor specific types that are not built into CakePHP you can add additional new types to CakePHP’s type system. Type classes are expected to implement the following methods:
• toPHP: Casts given value from a database type to a PHP equivalent.
• toDatabase: Casts given value from a PHP type to one acceptable by a database.
• toStatement: Casts given value to its Statement equivalent.
• marshal: Marshals flat data into PHP objects.
An easy way to fulfill the basic interface is to extend
Cake\Database\Type
. For example if we wanted to add a
JSON type, we could make the following type class:
// in src/Database/Type/JsonType.php
namespace
App\Database\Type;
use
Cake\Database\Driver;
use
Cake\Database\Type;
use
PDO;
128 https://github.com/cakephp/chronos
376 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
class JsonType extends
Type
{
public function
toPHP ( $value , Driver $driver )
{
if
( $value ===
null
) {
return null
;
}
return
json_decode ( $value ,
true
);
}
public function
marshal ( $value )
{
if
( is_array ( $value ) || $value ===
null
) {
return
$value ;
}
return
json_decode ( $value ,
true
);
}
public function
toDatabase ( $value , Driver $driver )
{
return
json_encode ( $value );
}
public function
toStatement ( $value , Driver $driver )
{
if
( $value ===
null
) {
return
PDO :: PARAM_NULL ;
}
return
PDO :: PARAM_STR ;
}
}
By default the toStatement() method will treat values as strings which will work for our new type. Once we’ve created our new type, we need to add it into the type mapping. During our application bootstrap we should do the following:
use
Cake\Database\Type;
Type :: map ( 'json' , 'App\Database\Type\JsonType' );
New in version 3.3.0: The JsonType described in this example was added to the core.
We can then overload the reflected schema data to use our new type, and CakePHP’s database layer will automatically convert our JSON data when creating queries. You can use the custom types you’ve created by mapping the types in your Table’s
_initializeSchema() method
use
Cake\Database\Schema\TableSchema;
class WidgetsTable extends
Table
{
protected function
_initializeSchema (TableSchema $schema )
{
$schema -> columnType ( 'widget_prefs' , 'json' );
return
$schema ;
}
More Information 377
CakePHP Cookbook Documentation, Release 3.5
}
Mapping Custom Datatypes to SQL Expressions
New in version 3.3.0: Support for mapping custom data types to SQL expressions was added in 3.3.0.
The previous example maps a custom datatype for a ‘json’ column type which is easily represented as a string in a SQL statement.
Complex SQL data types cannot be represented as strings/integers in SQL queries.
When working with these datatypes your Type class needs to implement the
Cake\Database\Type\ExpressionTypeInterface interface. This interface lets your custom type represent a value as a SQL expression. As an example, we’ll build a simple Type class for handling POINT type data out of MySQL. First we’ll define a ‘value’ object that we can use to represent POINT data in PHP:
// in src/Database/Point.php
namespace
App\Database;
// Our value object is immutable.
class Point
{
protected
$_lat ;
protected
$_long ;
// Factory method.
public static function
parse ( $value )
{
// Parse the data from MySQL.
return new static
( $value [ 0 ], $value [ 1 ]);
}
public function
__construct ( $lat , $long )
{
$this -> _lat = $lat ;
$this -> _long = $long ;
}
public function
lat ()
{
return
$this -> _lat ;
}
public function
long ()
{
return
$this -> _long ;
}
}
With our value object created, we’ll need a Type class to map data into this value object and into SQL expressions:
namespace
App\Database\Type;
use
App\Database\Point;
use
Cake\Database\Expression\FunctionExpression;
use
Cake\Database\Type
as
BaseType;
use
Cake\Database\Type\ExpressionTypeInterface;
378 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
class PointType extends
BaseType
implements
ExpressionTypeInterface
{
public function
toPHP ( $value , Driver $d )
{
return
Point :: parse ( $value );
}
public function
marshal ( $value )
{
if
( is_string ( $value )) {
$value = explode ( ',' , $value );
}
if
( is_array ( $value )) {
return new
Point( $value [ 0 ], $value [ 1 ]);
}
return null
;
}
public function
toExpression ( $value )
{
if
( $value instanceof Point) {
return new
FunctionExpression(
'POINT' ,
[
$value -> lat (),
$value -> long ()
]
);
}
if
( is_array ( $value )) {
return new
FunctionExpression( 'POINT' , [ $value [ 0 ], $value [ 1 ]]);
}
// Handle other cases.
}
}
The above class does a few interesting things:
• The toPHP method handles parsing the SQL query results into a value object.
• The marshal method handles converting, data such as given request data, into our value object. We’re going to accept string values like '10.24,12.34 and arrays for now.
• The toExpression method handles converting our value object into the equivalent SQL expressions. In our example the resulting SQL would be something like POINT(10.24,12.34).
Once we’ve built our custom type, we’ll need to
connect our type to our table class
Enabling Immutable DateTime Objects
New in version 3.2: Immutable date/time objects were added in 3.2.
Because Date/Time objects are easily mutated in place, CakePHP allows you to enable immutable value objects. This is best done in your application’s config/bootstrap.php file:
Type :: build ( 'datetime' ) -> useImmutable ();
Type :: build ( 'date' ) -> useImmutable ();
More Information 379
CakePHP Cookbook Documentation, Release 3.5
Type :: build ( 'time' ) -> useImmutable ();
Type :: build ( 'timestamp' ) -> useImmutable ();
Note: New applications will have immutable objects enabled by default.
Connection Classes
class Cake\Database\Connection
Connection classes provide a simple interface to interact with database connections in a consistent way. They are intended as a more abstract interface to the driver layer and provide features for executing queries, logging queries, and doing transactional operations.
Executing Queries
Cake\Database\Connection::query( $sql)
Once you’ve gotten a connection object, you’ll probably want to issue some queries with it. CakePHP’s database abstraction layer provides wrapper features on top of PDO and native drivers. These wrappers provide a similar interface to PDO. There are a few different ways you can run queries depending on the type of query you need to run and what kind of results you need back. The most basic method is query() which allows you to run already completed SQL queries:
$stmt = $conn -> query ( 'UPDATE articles SET published = 1 WHERE id = 2' );
Cake\Database\Connection::execute(
$sql , $params, $types)
The query() method does not allow for additional parameters. If you need additional parameters you should use the execute() method, which allows for placeholders to be used:
$stmt = $conn -> execute (
'UPDATE articles SET published = ? WHERE id = ?' ,
[ 1 , 2 ]
);
Without any type hinting information, execute will assume all placeholders are string values. If you need to bind specific types of data, you can use their abstract type names when creating a query:
$stmt = $conn -> execute (
'UPDATE articles SET published_date = ? WHERE id = ?' ,
[
new
DateTime( 'now' ), 2 ],
[ 'date' , 'integer' ]
);
Cake\Database\Connection::newQuery()
This allows you to use rich data types in your applications and properly convert them into SQL statements. The last and most flexible way of creating queries is to use the
Query Builder
. This approach allows you to build complex and
expressive queries without having to use platform specific SQL:
$query = $conn -> newQuery ();
$query -> update ( 'articles' )
-> set ([ 'published' =>
true
])
380 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
-> where ([ 'id' => 2 ]);
$stmt = $query -> execute ();
When using the query builder, no SQL will be sent to the database server until the execute() method is called, or the query is iterated. Iterating a query will first execute it and then start iterating over the result set:
$query = $conn -> newQuery ();
$query -> select ( '*' )
-> from ( 'articles' )
-> where ([ 'published' =>
true
]);
foreach
( $query
as
$row ) {
// Do something with the row.
}
Note:
When you have an instance of
Cake\ORM\Query
you can use all() to get the result set for SELECT queries.
Using Transactions
The connection objects provide you a few simple ways you do database transactions. The most basic way of doing transactions is through the begin(), commit() and rollback() methods, which map to their SQL equivalents:
$conn -> begin ();
$conn -> execute ( 'UPDATE articles SET published = ? WHERE id = ?' , [
true
, 2 ]);
$conn -> execute ( 'UPDATE articles SET published = ? WHERE id = ?' , [
false
, 4 ]);
$conn -> commit ();
Cake\Database\Connection::transactional( callable $callback)
In addition to this interface connection instances also provide the transactional() method which makes handling the begin/commit/rollback calls much simpler:
$conn -> transactional (
function
( $conn ) {
$conn -> execute ( 'UPDATE articles SET published = ? WHERE id = ?' , [
true
, 2 ]);
$conn -> execute ( 'UPDATE articles SET published = ? WHERE id = ?' , [
false
, 4 ]);
});
In addition to basic queries, you can execute more complex queries using either the
Query Builder
or
Table Objects
The transactional method will do the following:
• Call begin.
• Call the provided closure.
• If the closure raises an exception, a rollback will be issued. The original exception will be re-thrown.
• If the closure returns false, a rollback will be issued.
• If the closure executes successfully, the transaction will be committed.
Interacting with Statements
When using the lower level database API, you will often encounter statement objects. These objects allow you to manipulate the underlying prepared statement from the driver. After creating and executing a query object, or using
More Information 381
CakePHP Cookbook Documentation, Release 3.5
execute() you will have a StatementDecorator instance. It wraps the underlying basic statement object and provides a few additional features.
Preparing a Statement
You can create a statement object using execute(), or prepare(). The execute() method returns a statement with the provided values bound to it. While prepare() returns an incomplete statement:
// Statements from execute will have values bound to them already.
$stmt = $conn -> execute (
'SELECT * FROM articles WHERE published = ?' ,
[
true
]
);
// Statements from prepare will be parameters for placeholders.
// You need to bind parameters before attempting to execute it.
$stmt = $conn -> prepare ( 'SELECT * FROM articles WHERE published = ?' );
Once you’ve prepared a statement you can bind additional data and execute it.
Binding Values
Once you’ve created a prepared statement, you may need to bind additional data. You can bind multiple values at once using the bind() method, or bind individual elements using bindValue:
$stmt = $conn -> prepare (
'SELECT * FROM articles WHERE published = ? AND created > ?'
);
// Bind multiple values
$stmt -> bind (
[
true
,
new
DateTime( '2013-01-01' )],
[ 'boolean' , 'date' ]
);
// Bind a single value
$stmt -> bindValue ( 1 ,
true
, 'boolean' );
$stmt -> bindValue ( 2 ,
new
DateTime( '2013-01-01' ), 'date' );
When creating statements you can also use named array keys instead of positional ones:
$stmt = $conn -> prepare (
'SELECT * FROM articles WHERE published = :published AND created > :created'
);
// Bind multiple values
$stmt -> bind (
[ 'published' =>
true
, 'created' =>
new
DateTime( '2013-01-01' )],
[ 'published' => 'boolean' , 'created' => 'date' ]
);
// Bind a single value
$stmt -> bindValue ( 'published' ,
true
, 'boolean' );
$stmt -> bindValue ( 'created' ,
new
DateTime( '2013-01-01' ), 'date' );
382 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Warning: You cannot mix positional and named array keys in the same statement.
Executing & Fetching Rows
After preparing a statement and binding data to it, you can execute it and fetch rows. Statements should be executed using the execute() method. Once executed, results can be fetched using fetch(), fetchAll() or iterating the statement:
$stmt -> execute ();
// Read one row.
$row = $stmt -> fetch ( 'assoc' );
// Read all rows.
$rows = $stmt -> fetchAll ( 'assoc' );
// Read rows through iteration.
foreach
( $rows
as
$row ) {
// Do work
}
Note: Reading rows through iteration will fetch rows in ‘both’ mode. This means you will get both the numerically indexed and associatively indexed results.
Getting Row Counts
After executing a statement, you can fetch the number of affected rows:
$rowCount = count ( $stmt );
$rowCount = $stmt -> rowCount ();
Checking Error Codes
If your query was not successful, you can get related error information using the errorCode() and errorInfo() methods. These methods work the same way as the ones provided by PDO:
$code = $stmt -> errorCode ();
$info = $stmt -> errorInfo ();
Query Logging
Query logging can be enabled when configuring your connection by setting the log option to true. You can also toggle query logging at runtime, using logQueries:
// Turn query logging on.
$conn -> logQueries (
true
);
More Information 383
CakePHP Cookbook Documentation, Release 3.5
// Turn query logging off
$conn -> logQueries (
false
);
When query logging is enabled, queries will be logged to
Cake\Log\Log
using the ‘debug’ level, and the ‘queries-
Log’ scope. You will need to have a logger configured to capture this level & scope. Logging to stderr can be useful when working on unit tests, and logging to files/syslog can be useful when working with web requests:
use
Cake\Log\Log;
// Console logging
Log :: config ( 'queries' , [
'className' => 'Console' ,
'stream' => 'php://stderr' ,
'scopes' => [ 'queriesLog' ]
]);
// File logging
Log :: config ( 'queries' , [
'className' => 'File' ,
'path' => LOGS,
'file' => 'queries.log' ,
'scopes' => [ 'queriesLog' ]
]);
Note: Query logging is only intended for debugging/development uses. You should never leave query logging on in production as it will negatively impact the performance of your application.
Identifier Quoting
By default CakePHP does not quote identifiers in generated SQL queries. The reason for this is identifier quoting has a few drawbacks:
• Performance overhead - Quoting identifiers is much slower and complex than not doing it.
• Not necessary in most cases - In non-legacy databases that follow CakePHP’s conventions there is no reason to quote identifiers.
If you are using a legacy schema that requires identifier quoting you can enable it using the quoteIdentifiers setting in your
Configuration
. You can also enable this feature at runtime:
$conn -> driver () -> autoQuoting (
true
);
When enabled, identifier quoting will cause additional query traversal that converts all identifiers into
IdentifierExpression objects.
Note: SQL snippets contained in QueryExpression objects will not be modified.
Metadata Caching
CakePHP’s ORM uses database reflection to determine the schema, indexes and foreign keys your application contains. Because this metadata changes infrequently and can be expensive to access, it is typically cached. By default,
384 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
metadata is stored in the _cake_model_ cache configuration. You can define a custom cache configuration using the cacheMetatdata option in your datasource configuration:
'Datasources' => [
'default' => [
// Other keys go here.
// Use the 'orm_metadata' cache config for metadata.
'cacheMetadata' => 'orm_metadata' ,
],
]
You can also configure the metadata caching at runtime with the cacheMetadata() method:
// Disable the cache
$connection -> cacheMetadata (
false
);
// Enable the cache
$connection -> cacheMetadata (
true
);
// Use a custom cache config
$connection -> cacheMetadata ( 'orm_metadata' );
CakePHP also includes a CLI tool for managing metadata caches. See the
ORM Cache Shell
chapter for more information.
Creating Databases
If you want to create a connection without selecting a database you can omit the database name:
$dsn = 'mysql://root:password@localhost/' ;
You can now use your connection object to execute queries that create/modify databases. For example to create a database:
$connection -> query ( "CREATE DATABASE IF NOT EXISTS my_database" );
Note: When creating a database it is a good idea to set the character set and collation parameters. If these values are missing, the database will set whatever system default values it uses.
Query Builder
class Cake\ORM\Query
The ORM’s query builder provides a simple to use fluent interface for creating and running queries. By composing queries together, you can create advanced queries using unions and subqueries with ease.
Underneath the covers, the query builder uses PDO prepared statements which protect against SQL injection attacks.
The Query Object
The easiest way to create a Query object is to use find() from a Table object. This method will return an incomplete query ready to be modified. You can also use a table’s connection object to access the lower level Query
More Information 385
CakePHP Cookbook Documentation, Release 3.5
builder that does not include ORM features, if necessary. See the
Executing Queries
section for more information:
use
Cake\ORM\TableRegistry;
$articles = TableRegistry :: get ( 'Articles' );
// Start a new query.
$query = $articles -> find ();
When inside a controller, you can use the automatic table variable that is created using the conventions system:
// Inside ArticlesController.php
$query = $this -> Articles -> find ();
Selecting Rows From A Table use
Cake\ORM\TableRegistry;
$query = TableRegistry :: get ( 'Articles' ) -> find ();
foreach
( $query
as
$article ) { debug( $article -> title );
}
For the remaining examples, assume that $articles is a
ORM\Table
. When inside controllers, you can use
$this->Articles instead of $articles.
Almost every method in a Query object will return the same query, this means that Query objects are lazy, and will not be executed unless you tell them to:
$query -> where ([ 'id' => 1 ]); // Return the same query object
$query -> order ([ 'title' => 'DESC' ]); // Still same object, no SQL executed
You can of course chain the methods you call on Query objects:
$query = $articles
-> find ()
-> select ([ 'id' , 'name' ])
-> where ([ 'id !=' => 1 ])
-> order ([ 'created' => 'DESC' ]);
foreach
( $query
as
$article ) { debug( $article -> created );
}
If you try to call debug() on a Query object, you will see its internal state and the SQL that will be executed in the database: debug( $articles -> find () -> where ([ 'id' => 1 ]));
// Outputs
// ...
// 'sql' => 'SELECT * FROM articles where id = ?'
// ...
You can execute a query directly without having to use foreach on it. The easiest way is to either call the all() or toArray() methods:
386 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$resultsIteratorObject = $articles
-> find ()
-> where ([ 'id >' => 1 ])
-> all ();
foreach
( $resultsIteratorObject
as
$article ) { debug( $article -> id );
}
$resultsArray = $articles
-> find ()
-> where ([ 'id >' => 1 ])
-> toArray ();
foreach
( $resultsArray
as
$article ) { debug( $article -> id );
} debug( $resultsArray [ 0 ] -> title );
In the above example, $resultsIteratorObject will be an instance of Cake\ORM\ResultSet, an object you can iterate and apply several extracting and traversing methods on.
Often, there is no need to call all(), you can simply iterate the Query object to get its results. Query objects can also be used directly as the result object; trying to iterate the query, calling toArray() or some of the methods inherited from
Collection
, will result in the query being executed and results returned to you.
Selecting A Single Row From A Table
You can use the first() method to get the first result in the query:
$article = $articles
-> find ()
-> where ([ 'id' => 1 ])
-> first (); debug( $article -> title );
Getting A List Of Values From A Column
// Use the extract() method from the collections library
// This executes the query as well
$allTitles = $articles -> find () -> extract ( 'title' );
foreach
( $allTitles
as
$title ) {
echo
$title ;
}
You can also get a key-value list out of a query result:
$list = $articles -> find ( 'list' );
foreach
( $list
as
$id => $title ) {
echo
" $id : $title "
}
More Information 387
CakePHP Cookbook Documentation, Release 3.5
For more information on how to customize the fields used for populating the list refer to
Finding Key/Value Pairs
section.
Queries Are Collection Objects
Once you get familiar with the Query object methods, it is strongly encouraged that you visit the
Collection
section to improve your skills in efficiently traversing the data. In short, it is important to remember that anything you can call on a Collection object, you can also do in a Query object:
// Use the combine() method from the collections library
// This is equivalent to find('list')
$keyValueList = $articles -> find () -> combine ( 'id' , 'title' );
// An advanced example
$results = $articles -> find ()
-> where ([ 'id >' => 1 ])
-> order ([ 'title' => 'DESC' ])
-> map (
function
( $row ) { // map() is a collection method, it executes the query
$row -> trimmedTitle = trim ( $row -> title );
return
$row ;
})
-> combine ( 'id' , 'trimmedTitle' ) // combine() is another collection method
-> toArray (); // Also a collections library method
foreach
( $results
as
$id => $trimmedTitle ) {
echo
" $id : $trimmedTitle " ;
}
How Are Queries Lazily Evaluated
Query objects are lazily evaluated. This means a query is not executed until one of the following things occur:
• The query is iterated with foreach().
• The query’s execute() method is called. This will return the underlying statement object, and is to be used with insert/update/delete queries.
• The query’s first() method is called. This will return the first result in the set built by SELECT (it adds
LIMIT 1 to the query).
• The query’s all() method is called. This will return the result set and can only be used with SELECT statements.
• The query’s toArray() method is called.
Until one of these conditions are met, the query can be modified without additional SQL being sent to the database. It also means that if a Query hasn’t been evaluated, no SQL is ever sent to the database. Once executed, modifying and re-evaluating a query will result in additional SQL being run.
If you want to take a look at what SQL CakePHP is generating, you can turn database
query logging
on.
The following sections will show you everything there is to know about using and combining the Query object methods to construct SQL statements and extract data.
388 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Selecting Data
Most web applications make heavy use of SELECT queries. CakePHP makes building them a snap. To limit the fields fetched, you can use the select() method:
$query = $articles -> find ();
$query -> select ([ 'id' , 'title' , 'body' ]);
foreach
( $query
as
$row ) { debug( $row -> title );
}
You can set aliases for fields by providing fields as an associative array:
// Results in SELECT id AS pk, title AS aliased_title, body ...
$query = $articles -> find ();
$query -> select ([ 'pk' => 'id' , 'aliased_title' => 'title' , 'body' ]);
To select distinct fields, you can use the distinct() method:
// Results in SELECT DISTINCT country FROM ...
$query = $articles -> find ();
$query -> select ([ 'country' ])
-> distinct ([ 'country' ]);
To set some basic conditions you can use the where() method:
// Conditions are combined with AND
$query = $articles -> find ();
$query -> where ([ 'title' => 'First Post' , 'published' =>
true
]);
// You can call where() multiple times
$query = $articles -> find ();
$query -> where ([ 'title' => 'First Post' ])
-> where ([ 'published' =>
true
]);
See the
Advanced Conditions
section to find out how to construct more complex WHERE conditions. To apply ordering, you can use the order method:
$query = $articles -> find ()
-> order ([ 'title' => 'ASC' , 'id' => 'ASC' ]);
When calling order() multiple times on a query, multiple clauses will be appended.
However, when using finders you may sometimes need to overwrite the ORDER BY. Set the second parameter of order() (as well as orderAsc() or orderDesc()) to Query::OVERWRITE or to true:
$query = $articles -> find ()
-> order ([ 'title' => 'ASC' ]);
// Later, overwrite the ORDER BY clause instead of appending to it.
$query = $articles -> find ()
-> order ([ 'created' => 'DESC' ], Query :: OVERWRITE );
New in version 3.0.12: In addition to order, the orderAsc and orderDesc methods can be used when you need to sort on complex expressions:
$query = $articles -> find ();
$concat = $query -> func () -> concat ([
'title' => 'identifier' ,
'synopsis' => 'identifier'
More Information 389
CakePHP Cookbook Documentation, Release 3.5
]);
$query -> orderAsc ( $concat );
To limit the number of rows or set the row offset you can use the limit() and page() methods:
// Fetch rows 50 to 100
$query = $articles -> find ()
-> limit ( 50 )
-> page ( 2 );
As you can see from the examples above, all the methods that modify the query provide a fluent interface, allowing you to build a query through chained method calls.
Selecting All Fields From a Table
By default a query will select all fields from a table, the exception is when you call the select() function yourself and pass certain fields:
// Only select id and title from the articles table
$articles -> find () -> select ([ 'id' , 'title' ]);
If you wish to still select all fields from a table after having called select($fields), you can pass the table instance to select() for this purpose:
// Only all fields from the articles table including
// a calculated slug field.
$query = $articlesTable -> find ();
$query
-> select ([ 'slug' => $query -> func () -> concat ([ 'title' => 'identifier' , '-' , 'id' =>
˓→
'identifier' ])])
-> select ( $articlesTable ); // Select all fields from articles
New in version 3.1: Passing a table object to select() was added in 3.1.
Using SQL Functions
CakePHP’s ORM offers abstraction for some commonly used SQL functions. Using the abstraction allows the ORM to select the platform specific implementation of the function you want. For example, concat is implemented differently in MySQL, PostgreSQL and SQL Server. Using the abstraction allows your code to be portable:
// Results in SELECT COUNT(*) count FROM ...
$query = $articles -> find ();
$query -> select ([ 'count' => $query -> func () -> count ( '*' )]);
A number of commonly used functions can be created with the func() method:
• sum() Calculate a sum. The arguments will be treated as literal values.
• avg() Calculate an average. The arguments will be treated as literal values.
• min() Calculate the min of a column. The arguments will be treated as literal values.
• max() Calculate the max of a column. The arguments will be treated as literal values.
• count() Calculate the count. The arguments will be treated as literal values.
390 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• concat() Concatenate two values together. The arguments are treated as bound parameters unless marked as literal.
• coalesce() Coalesce values. The arguments are treated as bound parameters unless marked as literal.
• dateDiff() Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal.
• now() Take either ‘time’ or ‘date’ as an argument allowing you to get either the current time, or current date.
• extract() Returns the specified date part from the SQL expression.
• dateAdd() Add the time unit to the date expression.
• dayOfWeek() Returns a FunctionExpression representing a call to SQL WEEKDAY function.
New in version 3.1: extract(), dateAdd() and dayOfWeek() methods have been added.
When providing arguments for SQL functions, there are two kinds of parameters you can use, literal arguments and bound parameters. Identifier/Literal parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example:
$query = $articles -> find () -> innerJoinWith ( 'Categories' );
$concat = $query -> func () -> concat ([
'Articles.title' => 'identifier' ,
' - CAT: ' ,
'Categories.name' => 'identifier' ,
' - Age: ' ,
'(DATEDIFF(NOW(), Articles.created))' => 'literal' ,
]);
$query -> select ([ 'link_title' => $concat ]);
By making arguments with a value of literal, the ORM will know that the key should be treated as a literal SQL value. By making arguments with a value of identifier, the ORM will know that the key should be treated as a field identifier. The above would generate the following SQL on MySQL:
SELECT CONCAT(Articles .
title, : c0, Categories .
name, : c1, (DATEDIFF(NOW(), Articles .
˓→ created))) FROM articles;
The :c0 value will have the ' -CAT:' text bound when the query is executed.
In addition to the above functions, the func() method can be used to create any generic SQL function such as year, date_format
, convert, etc. For example:
$query = $articles -> find ();
$year = $query -> func () -> year ([
'created' => 'identifier'
]);
$time = $query -> func () -> date_format ([
'created' => 'identifier' ,
"'%H:%i'" => 'literal'
]);
$query -> select ([
'yearCreated' => $year ,
'timeCreated' => $time
]);
Would result in:
SELECT YEAR(created)
as
yearCreated, DATE_FORMAT(created, '%H:%i' )
as
timeCreated
˓→
FROM articles;
More Information 391
CakePHP Cookbook Documentation, Release 3.5
You should remember to use the function builder whenever you need to put untrusted data into SQL functions or stored procedures:
// Use a stored procedure
$query = $articles -> find ();
$lev = $query -> func () -> levenshtein ([ $search , 'LOWER(title)' => 'literal' ]);
$query -> where (
function
( $exp )
use
( $lev ) {
return
$exp -> between ( $lev , 0 , $tolerance );
});
// Generated SQL would be
WHERE levenshtein ( : c0, lower(street)) BETWEEN : c1
AND
: c2
Aggregates - Group and Having
When using aggregate functions like count and sum you may want to use group by and having clauses:
$query = $articles -> find ();
$query -> select ([
'count' => $query -> func () -> count ( 'view_count' ),
'published_date' => 'DATE(created)'
])
-> group ( 'published_date' )
-> having ([ 'count >' => 3 ]);
Case statements
The ORM also offers the SQL case expression. The case expression allows for implementing if ...
then
...
else logic inside your SQL. This can be useful for reporting on data where you need to conditionally sum or count data, or where you need to specific data based on a condition.
If we wished to know how many published articles are in our database, we could use the following SQL:
SELECT
COUNT(
CASE
WHEN published = 'Y' THEN 1 END)
AS
number_published,
COUNT(
CASE
WHEN published = 'N' THEN 1 END)
AS
number_unpublished
FROM articles
To do this with the query builder, we’d use the following code:
$query = $articles -> find ();
$publishedCase = $query -> newExpr ()
-> addCase (
$query -> newExpr () -> add ([ 'published' => 'Y' ]),
1 ,
'integer'
);
$unpublishedCase = $query -> newExpr ()
-> addCase (
$query -> newExpr () -> add ([ 'published' => 'N' ]),
1 ,
'integer'
);
$query -> select ([
392 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
]);
'number_published' => $query -> func () -> count ( $publishedCase ),
'number_unpublished' => $query -> func () -> count ( $unpublishedCase )
The addCase function can also chain together multiple statements to create if ..
then ..
[elseif ..
then ..
] [ ..
else ] logic inside your SQL.
If we wanted to classify cities into SMALL, MEDIUM, or LARGE based on population size, we could do the following:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> addCase (
[
$q -> newExpr () -> lt ( 'population' , 100000 ),
$q -> newExpr () -> between ( 'population' , 100000 , 999000 ),
$q -> newExpr () -> gte ( 'population' , 999001 ),
],
[ 'SMALL' , 'MEDIUM' , 'LARGE' ], # values matching conditions
[ 'string' , 'string' , 'string' ] # type of each value
);
});
# WHERE CASE
# WHEN population < 100000 THEN 'SMALL'
# WHEN population BETWEEN 100000 AND 999000 THEN 'MEDIUM'
# WHEN population >= 999001 THEN 'LARGE'
# END
Any time there are fewer case conditions than values, addCase will automatically produce an if ..
then ..
else statement:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> addCase (
[
$q -> newExpr () -> eq ( 'population' , 0 ),
],
[ 'DESERTED' , 'INHABITED' ], # values matching conditions
[ 'string' , 'string' ] # type of each value
);
});
# WHERE CASE
# WHEN population = 0 THEN 'DESERTED' ELSE 'INHABITED' END
Getting Arrays Instead of Entities
While ORMs and object result sets are powerful, creating entities is sometimes unnecessary. For example, when accessing aggregated data, building an Entity may not make sense. The process of converting the database results to entities is called hydration. If you wish to disable this process you can do this:
$query = $articles -> find ();
$query -> hydrate (
false
); // Results as arrays intead of entities
$result = $query -> toList (); // Execute the query and return the array
After executing those lines, your result should look similar to this:
More Information 393
CakePHP Cookbook Documentation, Release 3.5
[
]
[ 'id' => 1 , 'title' => 'First Article' , 'body' => 'Article 1 body' ...
],
[ 'id' => 2 , 'title' => 'Second Article' , 'body' => 'Article 2 body' ...
],
...
Adding Calculated Fields
After your queries, you may need to do some post-processing. If you need to add a few calculated fields or derived data, you can use the formatResults() method. This is a lightweight way to map over the result sets. If you need more control over the process, or want to reduce results you should use the
Map/Reduce
feature instead. If you were querying a list of people, you could calculate their age with a result formatter:
// Assuming we have built the fields, conditions and containments.
$query -> formatResults (
function
(\Cake\Collection\CollectionInterface $results ) {
return
$results -> map (
function
( $row ) {
$row [ 'age' ] = $row [ 'birth_date' ] -> diff (
new
\DateTime) -> y ;
return
$row ;
});
});
As you can see in the example above, formatting callbacks will get a ResultSetDecorator as their first argument.
The second argument will be the Query instance the formatter was attached to. The $results argument can be traversed and modified as necessary.
Result formatters are required to return an iterator object, which will be used as the return value for the query. Formatter functions are applied after all the Map/Reduce routines have been executed. Result formatters can be applied from within contained associations as well. CakePHP will ensure that your formatters are properly scoped. For example, doing the following would work as you may expect:
// In a method in the Articles table
$query -> contain ([ 'Authors' =>
function
( $q ) {
return
$q -> formatResults (
function
(\Cake\Collection\CollectionInterface $authors )
˓→
{
return
$authors -> map (
function
( $author ) {
$author [ 'age' ] = $author [ 'birth_date' ] -> diff (
new
\DateTime) -> y ;
return
$author ;
});
}]);
});
// Get results
$results = $query -> all ();
// Outputs 29
echo
$results -> first () -> author -> age ;
As seen above, the formatters attached to associated query builders are scoped to operate only on the data in the association. CakePHP will ensure that computed values are inserted into the correct entity.
Advanced Conditions
The query builder makes it simple to build complex where clauses. Grouped conditions can be expressed by providing combining where(), andWhere() and orWhere(). The where() method works similar to the conditions arrays in previous versions of CakePHP:
394 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$query = $articles -> find ()
-> where ([
'author_id' => 3 ,
'OR' => [[ 'view_count' => 2 ], [ 'view_count' => 3 ]],
]);
The above would generate SQL like:
SELECT
*
FROM articles WHERE author_id = 3
AND
(view_count = 2
OR
view_count = 3 )
If you’d prefer to avoid deeply nested arrays, you can use the orWhere() and andWhere() methods to build your queries. Each method sets the combining operator used between the current and previous condition. For example:
$query = $articles -> find ()
-> where ([ 'author_id' => 2 ])
-> orWhere ([ 'author_id' => 3 ]);
The above will output SQL similar to:
SELECT
*
FROM articles WHERE (author_id = 2
OR
author_id = 3 )
By combining orWhere() and andWhere(), you can express complex conditions that use a mixture of operators:
$query = $articles -> find ()
-> where ([ 'author_id' => 2 ])
-> orWhere ([ 'author_id' => 3 ])
-> andWhere ([
'published' =>
true
,
'view_count >' => 10
])
-> orWhere ([ 'promoted' =>
true
]);
The above generates SQL similar to:
SELECT
*
FROM articles
WHERE (
(
(author_id = 2
OR
author_id = 3 )
AND
(published = 1
AND
view_count > 10 )
)
OR
promoted = 1
)
By using functions as the parameters to orWhere() and andWhere(), you can compose conditions together with the expression objects:
$query = $articles -> find ()
-> where ([ 'title LIKE' => '%First%' ])
-> andWhere (
function
( $exp ) {
return
$exp -> or_ ([
'author_id' => 2 ,
'is_highlighted' =>
true
]);
});
The above would create SQL like:
More Information 395
CakePHP Cookbook Documentation, Release 3.5
SELECT
*
FROM articles
WHERE ( title LIKE '%First%'
AND
(author_id = 2
OR
is_highlighted = 1 )
)
The expression object that is passed into where() functions has two kinds of methods. The first type of methods are combinators. The and_() and or_() methods create new expression objects that change how conditions are combined. The second type of methods are conditions. Conditions are added into an expression where they are combined with the current combinator.
For example, calling $exp->and_(...) will create a new Expression object that combines all conditions it contains with AND. While $exp->or_() will create a new Expression object that combines all conditions added to it with OR. An example of adding conditions with an Expression object would be:
$query = $articles -> find ()
-> where (
function
( $exp ) {
return
$exp
-> eq ( 'author_id' , 2 )
-> eq ( 'published' ,
true
)
-> notEq ( 'spam' ,
true
)
-> gt ( 'view_count' , 10 );
});
Since we started off using where(), we don’t need to call and_(), as that happens implicitly. The above shows a few new condition methods being combined with AND. The resulting SQL would look like:
SELECT
*
FROM articles
WHERE ( author_id = 2
AND
published = 1
AND
spam != 1
AND
view_count > 10 )
Deprecated since version 3.5.0: As of 3.5.0 the orWhere() method is deprecated. This method creates hard to predict
SQL based on the current query state. Use where() instead as it has more predicatable and easier to understand behavior.
However, if we wanted to use both AND & OR conditions we could do the following:
$query = $articles -> find ()
-> where (
function
( $exp ) {
$orConditions = $exp -> or_ ([ 'author_id' => 2 ])
-> eq ( 'author_id' , 5 );
return
$exp
-> add ( $orConditions )
-> eq ( 'published' ,
true
)
-> gte ( 'view_count' , 10 );
});
Which would generate the SQL similar to:
SELECT
*
FROM articles
WHERE (
396 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
(author_id = 2
OR
author_id = 5 )
AND
published = 1
AND
view_count >= 10 )
The or_() and and_() methods also allow you to use functions as their parameters. This is often easier to read than method chaining:
$query = $articles -> find ()
-> where (
function
( $exp ) {
$orConditions = $exp -> or_ (
function
( $or ) {
return
$or -> eq ( 'author_id' , 2 )
-> eq ( 'author_id' , 5 );
});
return
$exp
-> not ( $orConditions )
-> lte ( 'view_count' , 10 );
});
You can negate sub-expressions using not():
$query = $articles -> find ()
-> where (
function
( $exp ) {
$orConditions = $exp -> or_ ([ 'author_id' => 2 ])
-> eq ( 'author_id' , 5 );
return
$exp
-> not ( $orConditions )
-> lte ( 'view_count' , 10 );
});
Which will generate the following SQL looking like:
SELECT
*
FROM articles
WHERE (
NOT
(author_id = 2
OR
author_id = 5 )
AND
view_count <= 10 )
It is also possible to build expressions using SQL functions:
$query = $articles -> find ()
-> where (
function
( $exp , $q ) {
$year = $q -> func () -> year ([
'created' => 'identifier'
]);
return
$exp
-> gte ( $year , 2014 )
-> eq ( 'published' ,
true
);
});
Which will generate the following SQL looking like:
SELECT
*
FROM articles
WHERE (
YEAR(created) >= 2014
AND
published = 1
)
More Information 397
CakePHP Cookbook Documentation, Release 3.5
When using the expression objects you can use the following methods to create conditions:
• eq() Creates an equality condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> eq ( 'population' , '10000' );
});
# WHERE population = 10000
• notEq() Creates an inequality condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> notEq ( 'population' , '10000' );
});
# WHERE population != 10000
• like() Creates a condition using the LIKE operator:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> like ( 'name' , '%A%' );
});
# WHERE name LIKE "%A%"
• notLike() Creates a negated LIKE condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> notLike ( 'name' , '%A%' );
});
# WHERE name NOT LIKE "%A%"
• in() Create a condition using IN:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> in ( 'country_id' , [ 'AFG' , 'USA' , 'EST' ]);
});
# WHERE country_id IN ('AFG', 'USA', 'EST')
• notIn() Create a negated condition using IN:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> notIn ( 'country_id' , [ 'AFG' , 'USA' , 'EST' ]);
});
# WHERE country_id NOT IN ('AFG', 'USA', 'EST')
• gt() Create a > condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> gt ( 'population' , '10000' );
});
# WHERE population > 10000
• gte() Create a >= condition:
398 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> gte ( 'population' , '10000' );
});
# WHERE population >= 10000
• lt() Create a < condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> lt ( 'population' , '10000' );
});
# WHERE population < 10000
• lte() Create a <= condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> lte ( 'population' , '10000' );
});
# WHERE population <= 10000
• isNull() Create an IS NULL condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> isNull ( 'population' );
});
# WHERE (population) IS NULL
• isNotNull() Create a negated IS NULL condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> isNotNull ( 'population' );
});
# WHERE (population) IS NOT NULL
• between() Create a BETWEEN condition:
$query = $cities -> find ()
-> where (
function
( $exp , $q ) {
return
$exp -> between ( 'population' , 999 , 5000000 );
});
# WHERE population BETWEEN 999 AND 5000000,
• exists() Create a condition using EXISTS:
$subquery = $cities -> find ()
-> select ([ 'id' ])
-> where (
function
( $exp , $q ) {
return
$exp -> equalFields ( 'countries.id' , 'cities.country_id' );
})
-> andWhere ([ 'population >' , 5000000 ]);
$query = $countries -> find ()
-> where (
function
( $exp , $q )
use
( $subquery ) {
return
$exp -> exists ( $subquery );
More Information 399
CakePHP Cookbook Documentation, Release 3.5
});
# WHERE EXISTS (SELECT id FROM cities WHERE countries.id = cities.country_id AND
˓→ population > 5000000)
• notExists() Create a negated condition using EXISTS:
$subquery = $cities -> find ()
-> select ([ 'id' ])
-> where (
function
( $exp , $q ) {
return
$exp -> equalFields ( 'countries.id' , 'cities.country_id' );
})
-> andWhere ([ 'population >' , 5000000 ]);
$query = $countries -> find ()
-> where (
function
( $exp , $q )
use
( $subquery ) {
return
$exp -> notExists ( $subquery );
});
# WHERE NOT EXISTS (SELECT id FROM cities WHERE countries.id = cities.country_id
˓→
AND population > 5000000)
In situations when you can’t get, or don’t want to use the builder methods to create the conditions you want you can also use snippets of SQL in where clauses:
// Compare two fields to each other
$query -> where ([ 'Categories.parent_id != Parents.id' ]);
Warning: The field names used in expressions, and SQL snippets should never contain untrusted content. See the
Using SQL Functions
section for how to safely include unsafe data into function calls.
Automatically Creating IN Clauses
When building queries using the ORM, you will generally not have to indicate the data types of the columns you are interacting with, as CakePHP can infer the types based on the schema data. If in your queries you’d like CakePHP to automatically convert equality to IN comparisons, you’ll need to indicate the column data type:
$query = $articles -> find ()
-> where ([ 'id' => $ids ], [ 'id' => 'integer[]' ]);
// Or include IN to automatically cast to an array.
$query = $articles -> find ()
-> where ([ 'id IN' => $ids ]);
The above will automatically create id IN (...) instead of id = ?. This can be useful when you do not know whether you will get a scalar or array of parameters. The [] suffix on any data type name indicates to the query builder that you want the data handled as an array. If the data is not an array, it will first be cast to an array. After that, each value in the array will be cast using the
type system
. This works with complex types as well. For example, you
could take a list of DateTime objects using:
$query = $articles -> find ()
-> where ([ 'post_date' => $dates ], [ 'post_date' => 'date[]' ]);
400 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Automatic IS NULL Creation
When a condition value is expected to be null or any other value, you can use the IS operator to automatically create the correct expression:
$query = $categories -> find ()
-> where ([ 'parent_id IS' => $parentId ]);
The above will create parent_id` = :c1 or parent_id IS NULL depending on the type of $parentId
Automatic IS NOT NULL Creation
When a condition value is expected not to be null or any other value, you can use the IS NOT operator to automatically create the correct expression:
$query = $categories -> find ()
-> where ([ 'parent_id IS NOT' => $parentId ]);
The above will create parent_id` != :c1 or parent_id IS NOT NULL depending on the type of
$parentId
Raw Expressions
When you cannot construct the SQL you need using the query builder, you can use expression objects to add snippets of SQL to your queries:
$query = $articles -> find ();
$expr = $query -> newExpr () -> add ( '1 + 1' );
$query -> select ([ 'two' => $expr ]);
Expression objects can be used with any query builder methods like where(), limit(), group(), select() and many other methods.
Warning: Using expression objects leaves you vulnerable to SQL injection. You should avoid interpolating user data into expressions.
Getting Results
Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this:
// Iterate the query
foreach
( $query
as
$row ) {
// Do stuff.
}
// Get the results
$results = $query -> all ();
You can use
any of the collection
methods on your query objects to pre-process or transform the results:
More Information 401
CakePHP Cookbook Documentation, Release 3.5
// Use one of the collection methods.
$ids = $query -> map (
function
( $row ) {
return
$row -> id ;
});
$maxAge = $query -> max (
function
( $max ) {
return
$max -> age ;
});
You can use first or firstOrFail to retrieve a single record. These methods will alter the query adding a LIMIT
1 clause:
// Get just the first row
$row = $query -> first ();
// Get the first row or an exception.
$row = $query -> firstOrFail ();
Returning the Total Count of Records
Using a single query object, it is possible to obtain the total number of rows found for a set of conditions:
$total = $articles -> find () -> where ([ 'is_active' =>
true
]) -> count ();
The count() method will ignore the limit, offset and page clauses, thus the following will return the same result:
$total = $articles -> find () -> where ([ 'is_active' =>
true
]) -> limit ( 10 ) -> count ();
This is useful when you need to know the total result set size in advance, without having to construct another Query object. Likewise, all result formatting and map-reduce routines are ignored when using the count() method.
Moreover, it is possible to return the total count for a query containing group by clauses without having to rewrite the query in any way. For example, consider this query for retrieving article ids and their comments count:
$query = $articles -> find ();
$query -> select ([ 'Articles.id' , $query -> func () -> count ( 'Comments.id' )])
-> matching ( 'Comments' )
-> group ([ 'Articles.id' ]);
$total = $query -> count ();
After counting, the query can still be used for fetching the associated records:
$list = $query -> all ();
Sometimes, you may want to provide an alternate method for counting the total records of a query. One common use case for this is providing a cached value or an estimate of the total rows, or to alter the query to remove expensive unneeded parts such as left joins. This becomes particularly handy when using the CakePHP built-in pagination system which calls the count() method:
$query = $query -> where ([ 'is_active' =>
true
]) -> counter (
function
( $query ) {
return
100000 ;
});
$query -> count (); // Returns 100000
402 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
In the example above, when the pagination component calls the count method, it will receive the estimated hard-coded number of rows.
Caching Loaded Results
When fetching entities that don’t change often you may want to cache the results. The Query class makes this simple:
$query -> cache ( 'recent_articles' );
Will enable caching on the query’s result set. If only one argument is provided to cache() then the ‘default’ cache configuration will be used. You can control which caching configuration is used with the second parameter:
// String config name.
$query -> cache ( 'recent_articles' , 'dbResults' );
// Instance of CacheEngine
$query -> cache ( 'recent_articles' , $memcache );
In addition to supporting static keys, the cache() method accepts a function to generate the key. The function you give it will receive the query as an argument. You can then read aspects of the query to dynamically generate the cache key:
// Generate a key based on a simple checksum
// of the query's where clause
$query -> cache (
function
( $q ) {
return
'articles-' .
md5 ( serialize ( $q -> clause ( 'where' )));
});
The cache method makes it simple to add cached results to your custom finders or through event listeners.
When the results for a cached query are fetched the following happens:
1. The Model.beforeFind event is triggered.
2. If the query has results set, those will be returned.
3. The cache key will be resolved and cache data will be read. If the cache data is not empty, those results will be returned.
4. If the cache misses, the query will be executed and a new ResultSet will be created. This ResultSet will be written to the cache and returned.
Note: You cannot cache a streaming query result.
Loading Associations
The builder can help you retrieve data from multiple tables at the same time with the minimum amount of queries possible. To be able to fetch associated data, you first need to setup associations between the tables as described in the
Associations - Linking Tables Together
section. This technique of combining queries to fetch associated data from other tables is called eager loading.
Eager loading helps avoid many of the potential performance problems surrounding lazy-loading in an ORM. The queries generated by eager loading can better leverage joins, allowing more efficient queries to be made. In CakePHP you define eager loaded associations using the ‘contain’ method:
More Information 403
CakePHP Cookbook Documentation, Release 3.5
// In a controller or table method.
// As an option to find()
$query = $articles -> find ( 'all' , [ 'contain' => [ 'Authors' , 'Comments' ]]);
// As a method on the query object
$query = $articles -> find ( 'all' );
$query -> contain ([ 'Authors' , 'Comments' ]);
The above will load the related author and comments for each article in the result set. You can load nested associations using nested arrays to define the associations to be loaded:
$query = $articles -> find () -> contain ([
'Authors' => [ 'Addresses' ], 'Comments' => [ 'Authors' ]
]);
Alternatively, you can express nested associations using the dot notation:
$query = $articles -> find () -> contain ([
'Authors.Addresses' ,
'Comments.Authors'
]);
You can eager load associations as deep as you like:
$query = $products -> find () -> contain ([
'Shops.Cities.Countries' ,
'Shops.Managers'
]);
You can select fields from all associations with multiple easy contain() statements:
$query = $this -> find () -> select ([
'Realestates.id' ,
'Realestates.title' ,
'Realestates.description'
])
-> contain ([
'RealestateAttributes' => [
'Attributes' => [
'fields' => [
// Aliased fields in contain() must include
// the model prefix to be mapped correctly.
'Attributes__name' => 'attr_name'
]
]
]
])
-> contain ([
'RealestateAttributes' => [
'fields' => [
'RealestateAttributes.realestate_id' ,
'RealestateAttributes.value'
]
]
])
-> where ( $condition );
404 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
If you need to reset the containments on a query you can set the second argument to true:
$query = $articles -> find ();
$query -> contain ([ 'Authors' , 'Comments' ],
true
);
Passing Conditions to Contain
When using contain() you are able to restrict the data returned by the associations and filter them by conditions:
// In a controller or table method.
$query = $articles -> find () -> contain ( 'Comments' ,
function
( $q ) {
});
return
$q
-> select ([ 'body' , 'author_id' ])
-> where ([ 'Comments.approved' =>
true
]);
This also works for pagination at the Controller level:
$this -> paginate [ 'contain' ] = [
'Comments' =>
function
(\Cake\ORM\Query $query ) {
return
$query -> select ([ 'body' , 'author_id' ])
-> where ([ 'Comments.approved' =>
true
]);
}
];
Note: When you limit the fields that are fetched from an association, you must ensure that the foreign key columns are selected. Failing to select foreign key fields will cause associated data to not be present in the final result.
It is also possible to restrict deeply-nested associations using the dot notation:
$query = $articles -> find () -> contain ([
'Comments' ,
'Authors.Profiles' =>
function
( $q ) {
return
$q -> where ([ 'Profiles.is_published' =>
true
]);
}
]);
In the above example, you’ll still get authors even if they don’t have a published profile. To only get authors with a published profile use
matching()
. If you have defined custom finders in your associations, you can use them inside
contain() :
// Bring all articles, but only bring the comments that are approved and
// popular.
$query = $articles -> find () -> contain ( 'Comments' ,
function
( $q ) {
return
$q -> find ( 'approved' ) -> find ( 'popular' );
});
Note: For BelongsTo and HasOne associations only the where and select clauses are used when loading the associated records. For the rest of the association types you can use every clause that the query object provides.
If you need full control over the query that is generated, you can tell contain() to not append the foreignKey constraints to the generated query. In that case you should use an array passing foreignKey and queryBuilder:
More Information 405
CakePHP Cookbook Documentation, Release 3.5
$query = $articles -> find () -> contain ([
'Authors' => [
'foreignKey' =>
false
,
'queryBuilder' =>
function
( $q ) {
return
$q -> where ( ...
); // Full conditions for filtering
}
]
]);
If you have limited the fields you are loading with select() but also want to load fields off of contained associations, you can pass the association object to select():
// Select id & title from articles, but all fields off of Users.
$query = $articles -> find ()
-> select ([ 'id' , 'title' ])
-> select ( $articles -> Users )
-> contain ([ 'Users' ]);
Alternatively, if you have multiple associations, you can use enableAutoFields():
// Select id & title from articles, but all fields off of Users, Comments
// and Tags.
$query -> select ([ 'id' , 'title' ])
-> contain ([ 'Comments' , 'Tags' ])
-> enableAutoFields (
true
) // Prior to 3.4.0 use autoFields(true)
-> contain ([ 'Users' =>
function
( $q ) {
return
$q -> autoFields (
true
);
}]);
New in version 3.1: Selecting columns via an association object was added in 3.1
Sorting Contained Associations
When loading HasMany and BelongsToMany associations, you can use the sort option to sort the data in those associations:
$query -> contain ([
'Comments' => [
'sort' => [ 'Comments.created' => 'DESC' ]
]
]);
Filtering by Associated Data
A fairly common query case with associations is finding records ‘matching’ specific associated data. For example if you have ‘Articles belongsToMany Tags’ you will probably want to find Articles that have the CakePHP tag. This is extremely simple to do with the ORM in CakePHP:
// In a controller or table method.
$query = $articles -> find ();
$query -> matching ( 'Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'CakePHP' ]);
});
406 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
You can apply this strategy to HasMany associations as well. For example if ‘Authors HasMany Articles’, you could find all the authors with recently published articles using the following:
$query = $authors -> find ();
$query -> matching ( 'Articles' ,
function
( $q ) {
return
$q -> where ([ 'Articles.created >=' =>
new
DateTime( '-10 days' )]);
});
Filtering by deep associations is surprisingly easy, and the syntax should be already familiar to you:
// In a controller or table method.
$query = $products -> find () -> matching (
'Shops.Cities.Countries' ,
function
( $q ) {
return
$q -> where ([ 'Countries.name' => 'Japan' ]);
}
);
// Bring unique articles that were commented by 'markstory' using passed variable
// Dotted matching paths should be used over nested matching() calls
$username = 'markstory' ;
$query = $articles -> find () -> matching ( 'Comments.Users' ,
function
( $q )
use
( $username ) {
return
$q -> where ([ 'username' => $username ]);
});
Note: As this function will create an INNER JOIN, you might want to consider calling distinct on the find query as you might get duplicate rows if your conditions don’t exclude them already. This might be the case, for example, when the same users comments more than once on a single article.
The data from the association that is ‘matched’ will be available on the _matchingData property of entities. If you both match and contain the same association, you can expect to get both the _matchingData and standard association properties in your results.
Using innerJoinWith
Using the matching() function, as we saw already, will create an INNER JOIN with the specified association and will also load the fields into the result set.
There may be cases where you want to use matching() but are not interested in loading the fields into the result set. For this purpose, you can use innerJoinWith():
$query = $articles -> find ();
$query -> innerJoinWith ( 'Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'CakePHP' ]);
});
The innerJoinWith() method works the same as matching(), that means that you can use dot notation to join deeply nested associations:
$query = $products -> find () -> innerJoinWith (
'Shops.Cities.Countries' ,
function
( $q ) {
return
$q -> where ([ 'Countries.name' => 'Japan' ]);
}
);
More Information 407
CakePHP Cookbook Documentation, Release 3.5
Again, the only difference is that no additional columns will be added to the result set, and no _matchingData property will be set.
New in version 3.1: Query::innerJoinWith() was added in 3.1
Using notMatching
The opposite of matching() is notMatching(). This function will change the query so that it filters results that have no relation to the specified association:
// In a controller or table method.
$query = $articlesTable
-> find ()
-> notMatching ( 'Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'boring' ]);
});
The above example will find all articles that were not tagged with the word boring. You can apply this method to
HasMany associations as well. You could, for example, find all the authors with no published articles in the last 10 days:
$query = $authorsTable
-> find ()
-> notMatching ( 'Articles' ,
function
( $q ) {
return
$q -> where ([ 'Articles.created >=' =>
new
\DateTime( '-10 days' )]);
});
It is also possible to use this method for filtering out records not matching deep associations. For example, you could find articles that have not been commented on by a certain user:
$query = $articlesTable
-> find ()
-> notMatching ( 'Comments.Users' ,
function
( $q ) {
return
$q -> where ([ 'username' => 'jose' ]);
});
Since articles with no comments at all also satisfy the condition above, you may want to combine matching() and notMatching() in the same query. The following example will find articles having at least one comment, but not commented by a certain user:
$query = $articlesTable
-> find ()
-> notMatching ( 'Comments.Users' ,
function
( $q ) {
return
$q -> where ([ 'username' => 'jose' ]);
})
-> matching ( 'Comments' );
Note: As notMatching() will create a LEFT JOIN, you might want to consider calling distinct on the find query as you can get duplicate rows otherwise.
Keep in mind that contrary to the matching() function, notMatching() will not add any data to the
_matchingData property in the results.
New in version 3.1: Query::notMatching() was added in 3.1
408 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Using leftJoinWith
On certain occasions you may want to calculate a result based on an association, without having to load all the records for it. For example, if you wanted to load the total number of comments an article has along with all the article data, you can use the leftJoinWith() function:
$query = $articlesTable -> find ();
$query -> select ([ 'total_comments' => $query -> func () -> count ( 'Comments.id' )])
-> leftJoinWith ( 'Comments' )
-> group ([ 'Articles.id' ])
-> enableAutoFields (
true
); // Prior to 3.4.0 use autoFields(true);
The results for the above query will contain the article data and the total_comments property for each of them.
leftJoinWith() can also be used with deeply nested associations. This is useful, for example, for bringing the count of articles tagged with a certain word, per author:
$query = $authorsTable
-> find ()
-> select ([ 'total_articles' => $query -> func () -> count ( 'Articles.id' )])
-> leftJoinWith ( 'Articles.Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'awesome' ]);
})
-> group ([ 'Authors.id' ])
-> enableAutoFields (
true
); // Prior to 3.4.0 use autoFields(true);
This function will not load any columns from the specified associations into the result set.
New in version 3.1: Query::leftJoinWith() was added in 3.1
Adding Joins
In addition to loading related data with contain(), you can also add additional joins with the query builder:
$query = $articles -> find ()
-> hydrate (
false
)
-> join ([
'table' => 'comments' ,
'alias' => 'c' ,
'type' => 'LEFT' ,
'conditions' => 'c.article_id = articles.id' ,
]);
You can append multiple joins at the same time by passing an associative array with multiple joins:
$query = $articles -> find ()
-> hydrate (
false
)
-> join ([
'c' => [
'table' => 'comments' ,
'type' => 'LEFT' ,
'conditions' => 'c.article_id = articles.id' ,
],
'u' => [
'table' => 'users' ,
'type' => 'INNER' ,
'conditions' => 'u.id = articles.user_id' ,
More Information 409
CakePHP Cookbook Documentation, Release 3.5
]);
]
As seen above, when adding joins the alias can be the outer array key. Join conditions can also be expressed as an array of conditions:
$query = $articles -> find ()
-> hydrate (
false
)
-> join ([
'c' => [
'table' => 'comments' ,
'type' => 'LEFT' ,
'conditions' => [
'c.created >' =>
new
DateTime( '-5 days' ),
'c.moderated' =>
true
,
'c.article_id = articles.id'
]
],
], [ 'c.created' => 'datetime' , 'c.moderated' => 'boolean' ]);
When creating joins by hand and using array based conditions, you need to provide the datatypes for each column in the join conditions. By providing datatypes for the join conditions, the ORM can correctly convert data types into
SQL. In addition to join() you can use rightJoin(), leftJoin() and innerJoin() to create joins:
// Join with an alias and string conditions
$query = $articles -> find ();
$query -> leftJoin (
[ 'Authors' => 'authors' ],
[ 'Authors.id = Articles.author_id' ]);
// Join with an alias, array conditions, and types
$query = $articles -> find ();
$query -> innerJoin (
[ 'Authors' => 'authors' ],
[
'Authors.promoted' =>
true
,
'Authors.created' =>
new
DateTime( '-5 days' ),
'Authors.id = Articles.author_id'
],
[ 'Authors.promoted' => 'boolean' , 'Authors.created' => 'datetime' ]);
It should be noted that if you set the quoteIdentifiers option to true when defining your Connection, join conditions between table fields should be set as follow:
$query = $articles -> find ()
-> join ([
'c' => [
˓→
'articles.id' )
]
],
'conditions' => [
'c.article_id' =>
new
\Cake\Database\Expression\IdentifierExpression(
]);
'table' => 'comments' ,
'type' => 'LEFT' ,
This ensures that all of your identifiers will be quoted across the Query, avoiding errors with some database Drivers
(PostgreSQL notably)
410 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Inserting Data
Unlike earlier examples, you should not use find() to create insert queries. Instead, create a new Query object using query():
$query = $articles -> query ();
$query -> insert ([ 'title' , 'body' ])
-> values ([
'title' => 'First post' ,
'body' => 'Some body text'
])
-> execute ();
To insert multiple rows with only one query, you can chain the values() method as many times as you need:
$query = $articles -> query ();
$query -> insert ([ 'title' , 'body' ])
-> values ([
'title' => 'First post' ,
'body' => 'Some body text'
])
-> values ([
'title' => 'Second post' ,
'body' => 'Another body text'
])
-> execute ();
Generally, it is easier to insert data using entities and
ORM\Table::save()
INSERT query together, you can create INSERT INTO ...
SELECT style queries:
$select = $articles -> find ()
-> select ([ 'title' , 'body' , 'published' ])
-> where ([ 'id' => 3 ]);
$query = $articles -> query ()
-> insert ([ 'title' , 'body' , 'published' ])
-> values ( $select )
-> execute ();
Note: Inserting records with the query builder will not trigger events such as Model.afterSave. Instead you should use the
ORM to save data
Updating Data
As with insert queries, you should not use find() to create update queries. Instead, create new a Query object using query():
$query = $articles -> query ();
$query -> update ()
-> set ([ 'published' =>
true
])
-> where ([ 'id' => $id ])
-> execute ();
Generally, it is easier to update data using entities and ORM\Table::patchEntity().
More Information 411
CakePHP Cookbook Documentation, Release 3.5
Note: Updating records with the query builder will not trigger events such as Model.afterSave. Instead you should use the
ORM to save data
Deleting Data
As with insert queries, you should not use find() to create delete queries. Instead, create new a query object using query()
:
$query = $articles -> query ();
$query -> delete ()
-> where ([ 'id' => $id ])
-> execute ();
Generally, it is easier to delete data using entities and
ORM\Table::delete()
SQL Injection Prevention
While the ORM and database abstraction layers prevent most SQL injections issues, it is still possible to leave yourself vulnerable through improper use.
When using condition arrays, the key/left-hand side as well as single value entries must not contain user data:
$query -> where ([
// Data on the key/left-hand side is unsafe, as it will be
// inserted into the generated query as-is
$userData => $value ,
]);
// The same applies to single value entries, they are not
// safe to use with user data in any form
$userData ,
"MATCH (comment) AGAINST ( $userData )" ,
'created < NOW() - ' .
$userData
When using the expression builder, column names must not contain user data:
$query -> where (
function
( $exp )
use
( $userData , $values ) {
// Column names in all expressions are not safe.
return
$exp -> in ( $userData , $values );
});
When building function expressions, function names should never contain user data:
// Not safe.
$query -> func () -> { $userData }( $arg1 );
// Also not safe to use an array of
// user data in a function expression
$query -> func () -> coalesce ( $userData );
Raw expressions are never safe:
$expr = $query -> newExpr () -> add ( $userData );
$query -> select ([ 'two' => $expr ]);
412 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Binding values
It is possible to protect against many unsafe situations by using bindings. Similar to
binding values to prepared statements
, values can be bound to queries using the Cake\Database\Query::bind() method.
The following example would be a safe variant of the unsafe, SQL injection prone example given above:
$query
-> where ([
'MATCH (comment) AGAINST (:userData)' ,
'created < NOW() - :moreUserData'
])
-> bind ( ':userData' , $userData , 'string' )
-> bind ( ':moreUserData' , $moreUserData , 'datetime' );
Note: Unlike Cake\Database\StatementInterface::bindValue(), Query::bind() requires to pass the named placeholders including the colon!
More Complex Queries
The query builder is capable of building complex queries like UNION queries and sub-queries.
Unions
Unions are created by composing one or more select queries together:
$inReview = $articles -> find ()
-> where ([ 'need_review' =>
true
]);
$unpublished = $articles -> find ()
-> where ([ 'published' =>
false
]);
$unpublished -> union ( $inReview );
You can create UNION ALL queries using the unionAll() method:
$inReview = $articles -> find ()
-> where ([ 'need_review' =>
true
]);
$unpublished = $articles -> find ()
-> where ([ 'published' =>
false
]);
$unpublished -> unionAll ( $inReview );
Subqueries
Subqueries are a powerful feature in relational databases and building them in CakePHP is fairly intuitive. By composing queries together, you can make subqueries:
$matchingComment = $articles -> association ( 'Comments' ) -> find ()
-> select ([ 'article_id' ])
More Information 413
CakePHP Cookbook Documentation, Release 3.5
-> distinct ()
-> where ([ 'comment LIKE' => '%CakePHP%' ]);
$query = $articles -> find ()
-> where ([ 'id' => $matchingComment ]);
Subqueries are accepted anywhere a query expression can be used. For example, in the select() and join() methods.
Adding Locking Statements
Most relational database vendors support taking out locks when doing select operations. You can use the epilog() method for this:
// In MySQL
$query -> epilog ( 'FOR UPDATE' );
The epilog() method allows you to append raw SQL to the end of queries. You should never put raw user data into epilog()
.
Executing Complex Queries
While the query builder makes it easy to build most queries, very complex queries can be tedious and complicated to build. You may want to
execute the desired SQL directly
Executing SQL directly allows you to fine tune the query that will be run. However, doing so doesn’t let you use contain or other higher level ORM features.
Table Objects
class Cake\ORM\Table
Table objects provide access to the collection of entities stored in a specific table. Each table in your application should have an associated Table class which is used to interact with a given table. If you do not need to customize the behavior of a given table CakePHP will generate a Table instance for you to use.
Before trying to use Table objects and the ORM, you should ensure that you have configured your
database connection
Basic Usage
To get started, create a Table class. These classes live in src/Model/Table. Tables are a type model collection specific to relational databases, and the main interface to your database in CakePHP’s ORM. The most basic table class would look like:
// src/Model/Table/ArticlesTable.php
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
}
414 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Note that we did not tell the ORM which table to use for our class. By convention table objects will use a table that matches the lower cased and underscored version of the class name. In the above example the articles table will be used. If our table class was named BlogPosts your table should be named blog_posts. You can specify the table to using the setTable() method:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> setTable ( 'my_table' );
// Prior to 3.4.0
$this -> table ( 'my_table' );
}
}
No inflection conventions will be applied when specifying a table. By convention the ORM also expects each table to have a primary key with the name of id. If you need to modify this you can use the setPrimaryKey() method:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> setPrimaryKey ( 'my_id' );
// Prior to 3.4.0
$this -> primaryKey ( 'my_id' );
}
}
Customizing the Entity Class a Table Uses
By default table objects use an entity class based on naming conventions. For example if your table class is called
ArticlesTable the entity would be Article. If the table class was PurchaseOrdersTable the entity would be PurchaseOrder. If however, you want to use an entity that doesn’t follow the conventions you can use the setEntityClass() method to change things up:
class PurchaseOrdersTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> setEntityClass ( 'App\Model\Entity\PO' );
// Prior to 3.4.0
$this -> entityClass ( 'App\Model\Entity\PO' );
}
More Information 415
CakePHP Cookbook Documentation, Release 3.5
}
As seen in the examples above Table objects have an initialize() method which is called at the end of the constructor. It is recommended that you use this method to do initialization logic instead of overriding the constructor.
Getting Instances of a Table Class
Before you can query a table, you’ll need to get an instance of the table.
You can do this by using the
TableRegistry class:
// In a controller or table method.
use
Cake\ORM\TableRegistry;
$articles = TableRegistry :: get ( 'Articles' );
The TableRegistry class provides the various dependencies for constructing a table, and maintains a registry of all the constructed table instances making it easier to build relations and configure the ORM. See
Using the TableRegistry
for more information.
If your table class is in a plugin, be sure to use the correct name for your table class. Failing to do so can result in validation rules, or callbacks not being triggered as a default class is used instead of your actual class. To correctly load plugin table classes use the following:
// Plugin table
$articlesTable = TableRegistry :: get ( 'PluginName.Articles' );
// Vendor prefixed plugin table
$articlesTable = TableRegistry :: get ( 'VendorName/PluginName.Articles' );
Lifecycle Callbacks
As you have seen above table objects trigger a number of events. Events are useful if you want to hook into the ORM and add logic in without subclassing or overriding methods. Event listeners can be defined in table or behavior classes.
You can also use a table’s event manager to bind listeners in.
When using callback methods behaviors attached in the initialize() method will have their listeners fired before the table callback methods are triggered. This follows the same sequencing as controllers & components.
To add an event listener to a Table class or Behavior simply implement the method signatures as described below. See the
Events System
for more detail on how to use the events subsystem.
Event List
• Model.initialize
• Model.beforeMarshal
• Model.beforeFind
• Model.buildValidator
• Model.buildRules
• Model.beforeRules
• Model.afterRules
416 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• Model.beforeSave
• Model.afterSave
• Model.afterSaveCommit
• Model.beforeDelete
• Model.afterDelete
• Model.afterDeleteCommit
initialize
Cake\ORM\Table::initialize(
Event $event , ArrayObject $data, ArrayObject $options)
The Model.initialize event is fired after the constructor and initialize methods are called. The Table classes do not listen to this event by default, and instead use the initialize hook method.
To respond to the Model.initialize event you can create a listener class which implements
EventListenerInterface :
use
Cake\Event\EventListenerInterface;
class ModelInitializeListener implements
EventListenerInterface
{
public function
implementedEvents ()
{
return array
(
'Model.initialize' => 'initializeEvent' ,
);
}
public function
initializeEvent ( $event )
{
$table = $event -> getSubject ();
// do something here
}
} and attach the listener to the EventManager as below:
use
Cake\Event\EventManager;
$listener =
new
ModelInitializeListener();
EventManager :: instance () -> attach ( $listener );
This will call the initializeEvent when any Table class is constructed.
beforeMarshal
Cake\ORM\Table::beforeMarshal(
Event $event , ArrayObject $data, ArrayObject $options)
The Model.beforeMarshal event is fired before request data is converted into entities. See the
Modifying Request
Data Before Building Entities
documentation for more information.
beforeFind
Cake\ORM\Table::beforeFind( Event $event , Query $query, ArrayObject $options, $primary)
More Information 417
CakePHP Cookbook Documentation, Release 3.5
The Model.beforeFind event is fired before each find operation. By stopping the event and supplying a return value you can bypass the find operation entirely. Any changes done to the $query instance will be retained for the rest of the find. The $primary parameter indicates whether or not this is the root query, or an associated query.
All associations participating in a query will have a Model.beforeFind event triggered. For associations that use joins, a dummy query will be provided. In your event listener you can set additional fields, conditions, joins or result formatters. These options/features will be copied onto the root query.
You might use this callback to restrict find operations based on a user’s role, or make caching decisions based on the current load.
In previous versions of CakePHP there was an afterFind callback, this has been replaced with the
Modifying
Results with Map/Reduce
features and entity constructors.
buildValidator
Cake\ORM\Table::buildValidator(
Event $event , Validator $validator, $name)
The Model.buildValidator event is fired when $name validator is created. Behaviors, can use this hook to add in validation methods.
buildRules
Cake\ORM\Table::buildRules(
Event $event , RulesChecker $rules)
The Model.buildRules event is fired after a rules instance has been created and after the table’s buildRules() method has been called.
beforeRules
Cake\ORM\Table::beforeRules(
Event $event , EntityInterface $entity, ArrayObject $options, $operation)
The Model.beforeRules event is fired before an entity has had rules applied. By stopping this event, you can halt the rules checking and set the result of applying rules.
afterRules
Cake\ORM\Table::afterRules( Event $event
, EntityInterface $entity, ArrayObject $options, $result,
$operation)
The Model.afterRules event is fired after an entity has rules applied. By stopping this event, you can return the final value of the rules checking operation.
beforeSave
Cake\ORM\Table::beforeSave( Event $event , EntityInterface $entity, ArrayObject $options)
The Model.beforeSave event is fired before each entity is saved. Stopping this event will abort the save operation.
When the event is stopped the result of the event will be returned. How to stop an event is documented
here
418 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
afterSave
Cake\ORM\Table::afterSave( Event $event , EntityInterface $entity, ArrayObject $options)
The Model.afterSave event is fired after an entity is saved.
afterSaveCommit
Cake\ORM\Table::afterSaveCommit(
Event $event , EntityInterface $entity, ArrayObject $options)
The Model.afterSaveCommit event is fired after the transaction in which the save operation is wrapped has been committed. It’s also triggered for non atomic saves where database operations are implicitly committed. The event is triggered only for the primary table on which save() is directly called. The event is not triggered if a transaction is started before calling save.
beforeDelete
Cake\ORM\Table::beforeDelete( Event $event , EntityInterface $entity, ArrayObject $options)
The Model.beforeDelete event is fired before an entity is deleted. By stopping this event you will abort the delete operation. When the event is stopped the result of the event will be returned. How to stop an event is documented
here
afterDelete
Cake\ORM\Table::afterDelete(
Event $event , EntityInterface $entity, ArrayObject $options)
The Model.afterDelete event is fired after an entity has been deleted.
afterDeleteCommit
Cake\ORM\Table::afterDeleteCommit( Event $event
, EntityInterface $entity, ArrayObject $options)
The Model.afterDeleteCommit event is fired after the transaction in which the delete operation is wrapped has been is committed. It’s also triggered for non atomic deletes where database operations are implicitly committed. The event is triggered only for the primary table on which delete() is directly called. The event is not triggered if a transaction is started before calling delete.
Behaviors
Cake\ORM\Table::addBehavior(
$name , array $options = [])
Behaviors provide an easy way to create horizontally re-usable pieces of logic related to table classes. You may be wondering why behaviors are regular classes and not traits. The primary reason for this is event listeners. While traits would allow for re-usable pieces of logic, they would complicate binding events.
To add a behavior to your table you can call the addBehavior() method. Generally the best place to do this is in the initialize() method:
More Information 419
CakePHP Cookbook Documentation, Release 3.5
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Timestamp' );
}
}
As with associations, you can use
plugin syntax
and provide additional configuration options:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Timestamp' , [
'events' => [
'Model.beforeSave' => [
'created_at' => 'new' ,
'modified_at' => 'always'
]
]
]);
}
}
You can find out more about behaviors, including the behaviors provided by CakePHP in the chapter on
Behaviors
Configuring Connections
By default all table instances use the default database connection. If your application uses multiple database connections you will want to configure which tables use which connections. This is the defaultConnectionName() method:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public static function
defaultConnectionName () {
return
'replica_db' ;
}
}
Note: The defaultConnectionName() method must be static.
420 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Using the TableRegistry
class Cake\ORM\TableRegistry
As we’ve seen earlier, the TableRegistry class provides an easy to use factory/registry for accessing your applications table instances. It provides a few other useful features as well.
Configuring Table Objects
static Cake\ORM\TableRegistry::get($alias, $config)
When loading tables from the registry you can customize their dependencies, or use mock objects by providing an
$options array:
$articles = TableRegistry :: get ( 'Articles' , [
'className' => 'App\Custom\ArticlesTable' ,
'table' => 'my_articles' ,
'connection' => $connectionObject ,
'schema' => $schemaObject ,
'entityClass' => 'Custom\EntityClass' ,
'eventManager' => $eventManager ,
'behaviors' => $behaviorRegistry
]);
Pay attention to the connection and schema configuration settings, they aren’t string values but objects. The connection will take an object of Cake\Database\Connection and schema Cake\Database\Schema\Collection.
Note: If your table also does additional configuration in its initialize() method, those values will overwrite the ones provided to the registry.
You can also pre-configure the registry using the config() method. Configuration data is stored per alias, and can be overridden by an object’s initialize() method:
TableRegistry :: config ( 'Users' , [ 'table' => 'my_users' ]);
Note: You can only configure a table before or during the first time you access that alias. Doing it after the registry is populated will have no effect.
Flushing the Registry
static Cake\ORM\TableRegistry::clear
During test cases you may want to flush the registry. Doing so is often useful when you are using mock objects, or modifying a table’s dependencies:
TableRegistry :: clear ();
Configuring the Namespace to Locate ORM classes
If you have not followed the conventions it is likely that your Table or Entity classes will not be detected by CakePHP.
In order to fix this, you can set a namespace with the Cake\Core\Configure::write method. As an example:
More Information 421
CakePHP Cookbook Documentation, Release 3.5
/ src
/ App
/ My
/
Namespace
/ Model
/ Entity
/ Table
Would be configured with:
Cake\Core\Configure :: write ( 'App.namespace' , 'App\My\Namespace' );
Entities
class Cake\ORM\Entity
While
Table Objects
represent and provide access to a collection of objects, entities represent individual rows or domain objects in your application. Entities contain persistent properties and methods to manipulate and access the data they contain.
Entities are created for you by CakePHP each time you use find() on a table object.
Creating Entity Classes
You don’t need to create entity classes to get started with the ORM in CakePHP. However, if you want to have custom logic in your entities you will need to create classes. By convention entity classes live in src/Model/Entity/. If our application had an articles table we could create the following entity:
// src/Model/Entity/Article.php
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class Article extends
Entity
{
}
Right now this entity doesn’t do very much. However, when we load data from our articles table, we’ll get instances of this class.
Note: If you don’t define an entity class CakePHP will use the basic Entity class.
Creating Entities
Entities can be directly instantiated:
use
App\Model\Entity\Article;
$article =
new
Article();
When instantiating an entity you can pass the properties with the data you want to store in them:
422 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
use
App\Model\Entity\Article;
$article =
new
Article([
'id' => 1 ,
'title' => 'New Article' ,
'created' =>
new
DateTime( 'now' )
]);
Another way of getting new entities is using the newEntity() method from the Table objects:
use
Cake\ORM\TableRegistry;
$article = TableRegistry :: get ( 'Articles' ) -> newEntity ();
$article = TableRegistry :: get ( 'Articles' ) -> newEntity ([
'id' => 1 ,
'title' => 'New Article' ,
'created' =>
new
DateTime( 'now' )
]);
Accessing Entity Data
Entities provide a few ways to access the data they contain. Most commonly you will access the data in an entity using object notation:
use
App\Model\Entity\Article;
$article =
new
Article;
$article -> title = 'This is my first post' ;
echo
$article -> title ;
You can also use the get() and set() methods:
$article -> set ( 'title' , 'This is my first post' );
echo
$article -> get ( 'title' );
When using set() you can update multiple properties at once using an array:
$article -> set ([
'title' => 'My first post' ,
'body' => 'It is the best ever!'
]);
Warning: When updating entities with request data you should whitelist which fields can be set with mass assignment.
Accessors & Mutators
In addition to the simple get/set interface, entities allow you to provide accessors and mutator methods. These methods let you customize how properties are read or set.
Accessors use the convention of _get followed by the CamelCased version of the field name.
Cake\ORM\Entity::get(
$field)
More Information 423
CakePHP Cookbook Documentation, Release 3.5
They receive the basic value stored in the _properties array as their only argument. Accessors will be used when saving entities, so be careful when defining methods that format data, as the formatted data will be persisted. For example:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class Article extends
Entity
{
protected function
_getTitle ( $title )
{
return
ucwords ( $title );
}
}
The accessor would be run when getting the property through any of these two ways:
echo
$user -> title ;
echo
$user -> get ( 'title' );
You can customize how properties get set by defining a mutator:
Cake\ORM\Entity::set(
$field = null , $value = null)
Mutator methods should always return the value that should be stored in the property. As you can see above, you can also use mutators to set other calculated properties. When doing this, be careful to not introduce any loops, as
CakePHP will not prevent infinitely looping mutator methods.
Mutators allow you to convert properties as they are set, or create calculated data. Mutators and accessors are applied when properties are read using object notation, or using get() and set(). For example:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
use
Cake\Utility\Text;
class Article extends
Entity
{
protected function
_setTitle ( $title )
{
return
Text :: slug ( $title );
}
}
The mutator would be run when setting the property through any of these two ways:
$user -> title = 'foo' ; // slug is set as well
$user -> set ( 'title' , 'foo' ); // slug is set as well
Warning: Accessors are also run before entities are persisted to the database. If you want to transform fields but not persist that transformation, we recommend using virtual properties as those are not persisted.
424 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Creating Virtual Properties
By defining accessors you can provide access to fields/properties that do not actually exist. For example if your users table has first_name and last_name you could create a method for the full name:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class User extends
Entity
{
protected function
_getFullName ()
{
return
$this -> _properties [ 'first_name' ] .
' ' .
$this -> _properties [ 'last_name' ];
}
}
You can access virtual properties as if they existed on the entity. The property name will be the lower case and underscored version of the method:
echo
$user -> full_name ;
Do bear in mind that virtual properties cannot be used in finds. If you want virtual properties to be part of JSON or array representations of your entities, see
Exposing Virtual Properties
Checking if an Entity Has Been Modified
Cake\ORM\Entity::dirty(
$field = null , $dirty = null)
You may want to make code conditional based on whether or not properties have changed in an entity. For example, you may only want to validate fields when they change:
// See if the title has been modified.
$article -> dirty ( 'title' );
You can also flag fields as being modified. This is handy when appending into array properties:
// Add a comment and mark the field as changed.
$article -> comments [] = $newComment ;
$article -> dirty ( 'comments' ,
true
);
In addition you can also base your conditional code on the original property values by using the getOriginal() method. This method will either return the original value of the property if it has been modified or its actual value.
You can also check for changes to any property in the entity:
// See if the entity has changed
$article -> dirty ();
To remove the dirty mark from fields in an entity, you can use the clean() method:
$article -> clean ();
When creating a new entity, you can avoid the fields from being marked as dirty by passing an extra option:
More Information 425
CakePHP Cookbook Documentation, Release 3.5
$article =
new
Article([ 'title' => 'New Article' ], [ 'markClean' =>
true
]);
To get a list of all dirty properties of an Entity you may call:
$dirtyFields = $entity -> getDirty ();
New in version 3.4.3: getDirty() has been added.
Validation Errors
Cake\ORM\Entity::errors( $field = null , $errors = null)
After you
save an entity
any validation errors will be stored on the entity itself. You can access any validation errors using the getErrors() or getError() method:
// Get all the errors
$errors = $user -> getErrors ();
// Prior to 3.4.0
$errors = $user -> errors ();
// Get the errors for a single field.
$errors = $user -> getError ( 'password' );
// Prior to 3.4.0
$errors = $user -> errors ( 'password' );
The setErrors() or setError() method can also be used to set the errors on an entity, making it easier to test code that works with error messages:
$user -> setError ( 'password' , [ 'Password is required' ]);
$user -> setErrors ([ 'password' => [ 'Password is required' ], 'username' => [ 'Username is
˓→ required' ]]);
// Prior to 3.4.0
$user -> errors ( 'password' , [ 'Password is required.' ]);
Mass Assignment
While setting properties to entities in bulk is simple and convenient, it can create significant security issues. Bulk assigning user data from the request into an entity allows the user to modify any and all columns. When using anonymous entity classes or creating the entity class with the
Bake Console
CakePHP does not protect against massassignment.
The _accessible property allows you to provide a map of properties and whether or not they can be mass-assigned.
The values true and false indicate whether a field can or cannot be mass-assigned:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class Article extends
Entity
{
protected
$_accessible = [
'title' =>
true
,
'body' =>
true
];
}
426 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
In addition to concrete fields there is a special * field which defines the fallback behavior if a field is not specifically named:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class Article extends
Entity
{
protected
$_accessible = [
'title' =>
true
,
'body' =>
true
,
'*' =>
false
,
];
}
Note: If the * property is not defined it will default to false.
Avoiding Mass Assignment Protection
When creating a new entity using the new keyword you can tell it to not protect itself against mass assignment:
use
App\Model\Entity\Article;
$article =
new
Article([ 'id' => 1 , 'title' => 'Foo' ], [ 'guard' =>
false
]);
Modifying the Guarded Fields at Runtime
You can modify the list of guarded fields at runtime using the accessible method:
// Make user_id accessible.
$article -> accessible ( 'user_id' ,
true
);
// Make title guarded.
$article -> accessible ( 'title' ,
false
);
Note: Modifying accessible fields effects only the instance the method is called on.
When using the newEntity() and patchEntity() methods in the Table objects you can customize mass assignment protection with options. Please refer to the
Changing Accessible Fields
section for more information.
Bypassing Field Guarding
There are some situations when you want to allow mass-assignment to guarded fields:
$article -> set ( $properties , [ 'guard' =>
false
]);
By setting the guard option to false, you can ignore the accessible field list for a single call to set().
More Information 427
CakePHP Cookbook Documentation, Release 3.5
Checking if an Entity was Persisted
It is often necessary to know if an entity represents a row that is already in the database. In those situations use the isNew() method:
if
( !
$article -> isNew ()) {
echo
'This article was saved already!' ;
}
If you are certain that an entity has already been persisted, you can use isNew() as a setter:
$article -> isNew (
false
);
$article -> isNew (
true
);
Lazy Loading Associations
While eager loading associations is generally the most efficient way to access your associations, there may be times when you need to lazily load associated data. Before we get into how to lazy load associations, we should discuss the differences between eager loading and lazy loading associations:
Eager loading Eager loading uses joins (where possible) to fetch data from the database in as few queries as possible.
When a separate query is required, like in the case of a HasMany association, a single query is emitted to fetch all the associated data for the current set of objects.
Lazy loading Lazy loading defers loading association data until it is absolutely required. While this can save CPU time because possibly unused data is not hydrated into objects, it can result in many more queries being emitted to the database. For example looping over a set of articles & their comments will frequently emit N queries where N is the number of articles being iterated.
While lazy loading is not included by CakePHP’s ORM, you can just use one of the community plugins to do so. We recommend the LazyLoad Plugin
129
After adding the plugin to your entity, you will be able to do the following:
$article = $this -> Articles -> findById ( $id );
// The comments property was lazy loaded
foreach
( $article -> comments
as
$comment ) {
echo
$comment -> body ;
}
Creating Re-usable Code with Traits
You may find yourself needing the same logic in multiple entity classes. PHP’s traits are a great fit for this. You can put your application’s traits in src/Model/Entity. By convention traits in CakePHP are suffixed with Trait so they can be discernible from classes or interfaces. Traits are often a good complement to behaviors, allowing you to provide functionality for the table and entity objects.
For example if we had SoftDeletable plugin, it could provide a trait. This trait could give methods for marking entities as ‘deleted’, the method softDelete could be provided by a trait:
129 https://github.com/jeremyharris/cakephp-lazyload
428 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
// SoftDelete/Model/Entity/SoftDeleteTrait.php
namespace
SoftDelete\Model\Entity;
trait
SoftDeleteTrait
{
public function
softDelete ()
{
$this -> set ( 'deleted' ,
true
);
}
}
You could then use this trait in your entity class by importing it and including it:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
use
SoftDelete\Model\Entity\SoftDeleteTrait;
class Article extends
Entity
{
use
SoftDeleteTrait;
}
Converting to Arrays/JSON
When building APIs, you may often need to convert entities into arrays or JSON data. CakePHP makes this simple:
// Get an array.
// Associations will be converted with toArray() as well.
$array = $user -> toArray ();
// Convert to JSON
// Associations will be converted with jsonSerialize hook as well.
$json = json_encode ( $user );
When converting an entity to an JSON the virtual & hidden field lists are applied. Entities are recursively converted to JSON as well. This means that if you eager loaded entities and their associations CakePHP will correctly handle converting the associated data into the correct format.
Exposing Virtual Properties
By default virtual fields are not exported when converting entities to arrays or JSON. In order to expose virtual properties you need to make them visible. When defining your entity class you can provide a list of virtual properties that should be exposed:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class User extends
Entity
{
More Information 429
CakePHP Cookbook Documentation, Release 3.5
protected
$_virtual = [ 'full_name' ];
}
This list can be modified at runtime using virtualProperties:
$user -> virtualProperties ([ 'full_name' , 'is_admin' ]);
Hiding Properties
There are often fields you do not want exported in JSON or array formats. For example it is often unwise to expose password hashes or account recovery questions. When defining an entity class, define which properties should be hidden:
namespace
App\Model\Entity;
use
Cake\ORM\Entity;
class User extends
Entity
{
protected
$_hidden = [ 'password' ];
}
This list can be modified at runtime using hiddenProperties:
$user -> hiddenProperties ([ 'password' , 'recovery_question' ]);
Storing Complex Types
Accessor & Mutator methods on entities are not intended to contain the logic for serializing and unserializing complex data coming from the database. Refer to the
Saving Complex Types
section to understand how your application can store more complex data types like arrays and objects.
Retrieving Data & Results Sets
class Cake\ORM\Table
While table objects provide an abstraction around a ‘repository’ or collection of objects, when you query for individual records you get ‘entity’ objects. While this section discusses the different ways you can find and load entities, you should read the
Entities
section for more information on entities.
Debugging Queries and ResultSets
Since the ORM now returns Collections and Entities, debugging these objects can be more complicated than in previous CakePHP versions. There are now various ways to inspect the data returned by the ORM.
• debug($query) Shows the SQL and bound params, does not show results.
• debug($query->all()) Shows the ResultSet properties (not the results).
• debug($query->toArray()) An easy way to show each of the results.
430 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• debug(json_encode($query,JSON_PRETTY_PRINT)) More human readable results.
• debug($query->first()) Show the properties of a single entity.
• debug((string)$query->first()) Show the properties of a single entity as JSON.
Getting a Single Entity by Primary Key
Cake\ORM\Table::get(
$id , $options = [])
It is often convenient to load a single entity from the database when editing or view entities and their related data. You can do this by using get():
// In a controller or table method.
// Get a single article
$article = $articles -> get ( $id );
// Get a single article, and related comments
$article = $articles -> get ( $id , [
'contain' => [ 'Comments' ]
]);
If the get operation does not find any results a Cake\Datasource\Exception\RecordNotFoundException will be raised. You can either catch this exception yourself, or allow CakePHP to convert it into a 404 error.
Like find(), get() also has caching integrated. You can use the cache option when calling get() to perform read-through caching:
// In a controller or table method.
// Use any cache config or CacheEngine instance & a generated key
$article = $articles -> get ( $id , [
'cache' => 'custom' ,
]);
// Use any cache config or CacheEngine instance & specific key
$article = $articles -> get ( $id , [
'cache' => 'custom' , 'key' => 'mykey'
]);
// Explicitly disable caching
$article = $articles -> get ( $id , [
'cache' =>
false
]);
Optionally you can get() an entity using
Custom Finder Methods
. For example you may want to get all translations
for an entity. You can achieve that by using the finder option:
$article = $articles -> get ( $id , [
'finder' => 'translations' ,
]);
Using Finders to Load Data
Cake\ORM\Table::find(
$type , $options = [])
More Information 431
CakePHP Cookbook Documentation, Release 3.5
Before you can work with entities, you’ll need to load them. The easiest way to do this is using the find() method.
The find method provides an easy and extensible way to find the data you are interested in:
// In a controller or table method.
// Find all the articles
$query = $articles -> find ( 'all' );
The return value of any find() method is always a
Cake\ORM\Query
object. The Query class allows you to further refine a query after creating it. Query objects are evaluated lazily, and do not execute until you start fetching rows, convert it to an array, or when the all() method is called:
// In a controller or table method.
// Find all the articles.
// At this point the query has not run.
$query = $articles -> find ( 'all' );
// Iteration will execute the query.
foreach
( $query
as
$row ) {
}
// Calling all() will execute the query
// and return the result set.
$results = $query -> all ();
// Once we have a result set we can get all the rows
$data = $results -> toArray ();
// Converting the query to an array will execute it.
$data = $query -> toArray ();
Note: Once you’ve started a query you can use the
Query Builder
interface to build more complex queries, adding additional conditions, limits, or include associations using the fluent interface.
// In a controller or table method.
$query = $articles -> find ( 'all' )
-> where ([ 'Articles.created >' =>
new
DateTime( '-10 days' )])
-> contain ([ 'Comments' , 'Authors' ])
-> limit ( 10 );
You can also provide many commonly used options to find(). This can help with testing as there are fewer methods to mock:
// In a controller or table method.
$query = $articles -> find ( 'all' , [
'conditions' => [ 'Articles.created >' =>
new
DateTime( '-10 days' )],
'contain' => [ 'Authors' , 'Comments' ],
'limit' => 10
]);
The list of options supported by find() are:
• conditions provide conditions for the WHERE clause of your query.
• limit Set the number of rows you want.
432 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• offset Set the page offset you want. You can also use page to make the calculation simpler.
• contain define the associations to eager load.
• fields limit the fields loaded into the entity. Only loading some fields can cause entities to behave incorrectly.
• group add a GROUP BY clause to your query. This is useful when using aggregating functions.
• having add a HAVING clause to your query.
• join define additional custom joins.
• order order the result set.
Any options that are not in this list will be passed to beforeFind listeners where they can be used to modify the query object. You can use the getOptions() method on a query object to retrieve the options used. While you can pass query objects to your controllers, we recommend that you package your queries up as
Custom Finder Methods
instead.
Using custom finder methods will let you re-use your queries and make testing easier.
By default queries and result sets will return
Entities
objects. You can retrieve basic arrays by disabling hydration:
$query -> enableHydration (
false
);
// Prior to 3.4.0
$query -> hydrate (
false
);
// $data is ResultSet that contains array data.
$data = $query -> all ();
Getting the First Result
The first() method allows you to fetch only the first row from a query. If the query has not been executed, a
LIMIT 1 clause will be applied:
// In a controller or table method.
$query = $articles -> find ( 'all' , [
'order' => [ 'Articles.created' => 'DESC' ]
]);
$row = $query -> first ();
This approach replaces find('first') in previous versions of CakePHP. You may also want to use the get() method if you are loading entities by primary key.
Note: The first() method will return null if no results are found.
Getting a Count of Results
Once you have created a query object, you can use the count() method to get a result count of that query:
// In a controller or table method.
$query = $articles -> find ( 'all' , [
'conditions' => [ 'Articles.title LIKE' => '%Ovens%' ]
]);
$number = $query -> count ();
See
Returning the Total Count of Records
for additional usage of the count() method.
More Information 433
CakePHP Cookbook Documentation, Release 3.5
Finding Key/Value Pairs
It is often useful to generate an associative array of data from your application’s data. For example, this is very useful when creating <select> elements. CakePHP provides a simple to use method for generating ‘lists’ of data:
// In a controller or table method.
$query = $articles -> find ( 'list' );
$data = $query -> toArray ();
// Data now looks like
$data = [
1 => 'First post' ,
2 => 'Second article I wrote' ,
];
With no additional options the keys of $data will be the primary key of your table, while the values will be the
‘displayField’ of the table. You can use the setDisplayField() method on a table object to configure the display field of a table:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> setDisplayField ( 'title' );
// Prior to 3.4.0
$this -> displayField ( 'title' );
}
}
When calling list you can configure the fields used for the key and value with the keyField and valueField options respectively:
// In a controller or table method.
$query = $articles -> find ( 'list' , [
'keyField' => 'slug' ,
'valueField' => 'title'
]);
$data = $query -> toArray ();
// Data now looks like
$data = [
'first-post' => 'First post' ,
'second-article-i-wrote' => 'Second article I wrote' ,
];
Results can be grouped into nested sets. This is useful when you want bucketed sets, or want to build <optgroup> elements with FormHelper:
// In a controller or table method.
$query = $articles -> find ( 'list' , [
'keyField' => 'slug' ,
'valueField' => 'title' ,
'groupField' => 'author_id'
]);
$data = $query -> toArray ();
// Data now looks like
434 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$data = [
1 => [
'first-post' => 'First post' ,
'second-article-i-wrote' => 'Second article I wrote' ,
],
2 => [
// More data.
]
];
You can also create list data from associations that can be reached with joins:
$query = $articles -> find ( 'list' , [
'keyField' => 'id' ,
'valueField' => 'author.name'
]) -> contain ([ 'Authors' ]);
Customize Key-Value Output
Lastly it is possible to use closures to access entity accessor methods in your list finds.
// In your Authors Entity create a virtual field to be used as the displayField:
protected function
_getLabel ()
{
return
$this -> _properties [ 'first_name' ] .
' ' .
$this -> _properties [ 'last_name' ]
.
' / ' .
__( 'User ID %s' , $this -> _properties [ 'user_id' ]);
}
This example shows using the _getLabel() accessor method from the Author entity.
// In your finders/controller:
$query = $articles -> find ( 'list' , [
'keyField' => 'id' ,
'valueField' =>
function
( $article ) {
return
$article -> author -> get ( 'label' );
}
]);
You can also fetch the label in the list directly using.
// In AuthorsTable::initialize():
$this -> setDisplayField ( 'label' ); // Will utilize Author::_getLabel()
// In your finders/controller:
$query = $authors -> find ( 'list' ); // Will utilize AuthorsTable::getDisplayField()
Finding Threaded Data
The find('threaded') finder returns nested entities that are threaded together through a key field. By default this field is parent_id. This finder allows you to access data stored in an ‘adjacency list’ style table. All entities matching a given parent_id are placed under the children attribute:
// In a controller or table method.
$query = $comments -> find ( 'threaded' );
More Information 435
CakePHP Cookbook Documentation, Release 3.5
// Expanded default values
$query = $comments -> find ( 'threaded' , [
'keyField' => $comments -> primaryKey (),
'parentField' => 'parent_id'
]);
$results = $query -> toArray ();
echo
count ( $results [ 0 ] -> children );
echo
$results [ 0 ] -> children [ 0 ] -> comment ;
The parentField and keyField keys can be used to define the fields that threading will occur on.
Tip: If you need to manage more advanced trees of data, consider using
Tree
instead.
Custom Finder Methods
The examples above show how to use the built-in all and list finders. However, it is possible and recommended that you implement your own finder methods. Finder methods are the ideal way to package up commonly used queries, allowing you to abstract query details into a simple to use method. Finder methods are defined by creating methods following the convention of findFoo where Foo is the name of the finder you want to create. For example if we wanted to add a finder to our articles table for finding published articles we would do the following:
use
Cake\ORM\Query;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
findOwnedBy (Query $query ,
array
$options )
{
$user = $options [ 'user' ];
return
$query -> where ([ 'author_id' => $user -> id ]);
}
}
// In a controller or table method.
$articles = TableRegistry :: get ( 'Articles' );
$query = $articles -> find ( 'ownedBy' , [ 'user' => $userEntity ]);
Finder methods can modify the query as required, or use the $options to customize the finder operation with relevant application logic. You can also ‘stack’ finders, allowing you to express complex queries effortlessly. Assuming you have both the ‘published’ and ‘recent’ finders, you could do the following:
// In a controller or table method.
$articles = TableRegistry :: get ( 'Articles' );
$query = $articles -> find ( 'published' ) -> find ( 'recent' );
While all the examples so far have show finder methods on table classes, finder methods can also be defined on
Behaviors
If you need to modify the results after they have been fetched you should use a
Modifying Results with Map/Reduce
function to modify the results. The map reduce features replace the ‘afterFind’ callback found in previous versions of
CakePHP.
436 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Dynamic Finders
CakePHP’s ORM provides dynamically constructed finder methods which allow you to express simple queries with no additional code. For example if you wanted to find a user by username you could do:
// In a controller
// The following two calls are equal.
$query = $this -> Users -> findByUsername ( 'joebob' );
$query = $this -> Users -> findAllByUsername ( 'joebob' );
// In a table method
$users = TableRegistry :: get ( 'Users' );
// The following two calls are equal.
$query = $users -> findByUsername ( 'joebob' );
$query = $users -> findAllByUsername ( 'joebob' );
When using dynamic finders you can constrain on multiple fields:
$query = $users -> findAllByUsernameAndApproved ( 'joebob' , 1 );
You can also create OR conditions:
$query = $users -> findAllByUsernameOrEmail ( 'joebob' , '[email protected]' );
While you can use either OR or AND conditions, you cannot combine the two in a single dynamic finder. Other query options like contain are also not supported with dynamic finders. You should use
Custom Finder Methods
to encapsulate more complex queries. Lastly, you can also combine dynamic finders with custom finders:
$query = $users -> findTrollsByUsername ( 'bro' );
The above would translate into the following:
$users -> find ( 'trolls' , [
'conditions' => [ 'username' => 'bro' ]
]);
Once you have a query object from a dynamic finder, you’ll need to call first() if you want the first result.
Note: While dynamic finders make it simple to express queries, they come with some additional performance overhead.
Retrieving Associated Data
When you want to grab associated data, or filter based on associated data, there are two ways:
• use CakePHP ORM query functions like contain() and matching()
• use join functions like innerJoin(), leftJoin(), and rightJoin()
You should use contain() when you want to load the primary model, and its associated data. While contain() will let you apply additional conditions to the loaded associations, you cannot constrain the primary model based on the associations. For more details on the contain(), look at
Eager Loading Associations Via Contain
You should use matching() when you want to restrict the primary model based on associations. For example, you want to load all the articles that have a specific tag on them. For more details on the matching(), look at
Filtering by Associated Data Via Matching And Joins
More Information 437
CakePHP Cookbook Documentation, Release 3.5
If you prefer to use join functions, you can look at
Adding Joins
for more information.
Eager Loading Associations Via Contain
By default CakePHP does not load any associated data when using find(). You need to ‘contain’ or eager-load each association you want loaded in your results.
Eager loading helps avoid many of the potential performance problems surrounding lazy-loading in an ORM. The queries generated by eager loading can better leverage joins, allowing more efficient queries to be made. In CakePHP you define eager loaded associations using the ‘contain’ method:
// In a controller or table method.
// As an option to find()
$query = $articles -> find ( 'all' , [ 'contain' => [ 'Authors' , 'Comments' ]]);
// As a method on the query object
$query = $articles -> find ( 'all' );
$query -> contain ([ 'Authors' , 'Comments' ]);
The above will load the related author and comments for each article in the result set. You can load nested associations using nested arrays to define the associations to be loaded:
$query = $articles -> find () -> contain ([
'Authors' => [ 'Addresses' ], 'Comments' => [ 'Authors' ]
]);
Alternatively, you can express nested associations using the dot notation:
$query = $articles -> find () -> contain ([
'Authors.Addresses' ,
'Comments.Authors'
]);
You can eager load associations as deep as you like:
$query = $products -> find () -> contain ([
'Shops.Cities.Countries' ,
'Shops.Managers'
]);
You can select fields from all associations with multiple easy contain() statements:
$query = $this -> find () -> select ([
'Realestates.id' ,
'Realestates.title' ,
'Realestates.description'
])
-> contain ([
'RealestateAttributes' => [
'Attributes' => [
'fields' => [
// Aliased fields in contain() must include
// the model prefix to be mapped correctly.
'Attributes__name' => 'attr_name'
]
]
438 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
]
])
-> contain ([
'RealestateAttributes' => [
'fields' => [
'RealestateAttributes.realestate_id' ,
'RealestateAttributes.value'
]
]
])
-> where ( $condition );
If you need to reset the containments on a query you can set the second argument to true:
$query = $articles -> find ();
$query -> contain ([ 'Authors' , 'Comments' ],
true
);
Passing Conditions to Contain
When using contain() you are able to restrict the data returned by the associations and filter them by conditions:
// In a controller or table method.
$query = $articles -> find () -> contain ( 'Comments' ,
function
( $q ) {
return
$q
-> select ([ 'body' , 'author_id' ])
-> where ([ 'Comments.approved' =>
true
]);
});
This also works for pagination at the Controller level:
$this -> paginate [ 'contain' ] = [
'Comments' =>
function
(\Cake\ORM\Query $query ) {
return
$query -> select ([ 'body' , 'author_id' ])
-> where ([ 'Comments.approved' =>
true
]);
}
];
Note: When you limit the fields that are fetched from an association, you must ensure that the foreign key columns are selected. Failing to select foreign key fields will cause associated data to not be present in the final result.
It is also possible to restrict deeply-nested associations using the dot notation:
$query = $articles -> find () -> contain ([
'Comments' ,
'Authors.Profiles' =>
function
( $q ) {
return
$q -> where ([ 'Profiles.is_published' =>
true
]);
}
]);
In the above example, you’ll still get authors even if they don’t have a published profile. To only get authors with a published profile use
matching()
. If you have defined custom finders in your associations, you can use them inside
contain()
:
More Information 439
CakePHP Cookbook Documentation, Release 3.5
// Bring all articles, but only bring the comments that are approved and
// popular.
$query = $articles -> find () -> contain ( 'Comments' ,
function
( $q ) {
return
$q -> find ( 'approved' ) -> find ( 'popular' );
});
Note: For BelongsTo and HasOne associations only the where and select clauses are used when loading the associated records. For the rest of the association types you can use every clause that the query object provides.
If you need full control over the query that is generated, you can tell contain() to not append the foreignKey constraints to the generated query. In that case you should use an array passing foreignKey and queryBuilder:
$query = $articles -> find () -> contain ([
'Authors' => [
'foreignKey' =>
false
,
'queryBuilder' =>
function
( $q ) {
return
$q -> where ( ...
); // Full conditions for filtering
}
]
]);
If you have limited the fields you are loading with select() but also want to load fields off of contained associations, you can pass the association object to select():
// Select id & title from articles, but all fields off of Users.
$query = $articles -> find ()
-> select ([ 'id' , 'title' ])
-> select ( $articles -> Users )
-> contain ([ 'Users' ]);
Alternatively, if you have multiple associations, you can use enableAutoFields():
// Select id & title from articles, but all fields off of Users, Comments
// and Tags.
$query -> select ([ 'id' , 'title' ])
-> contain ([ 'Comments' , 'Tags' ])
-> enableAutoFields (
true
) // Prior to 3.4.0 use autoFields(true)
-> contain ([ 'Users' =>
function
( $q ) {
}]);
return
$q -> autoFields (
true
);
New in version 3.1: Selecting columns via an association object was added in 3.1
Sorting Contained Associations
When loading HasMany and BelongsToMany associations, you can use the sort option to sort the data in those associations:
$query -> contain ([
'Comments' => [
'sort' => [ 'Comments.created' => 'DESC' ]
]
]);
440 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Filtering by Associated Data Via Matching And Joins
A fairly common query case with associations is finding records ‘matching’ specific associated data. For example if you have ‘Articles belongsToMany Tags’ you will probably want to find Articles that have the CakePHP tag. This is extremely simple to do with the ORM in CakePHP:
// In a controller or table method.
$query = $articles -> find ();
$query -> matching ( 'Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'CakePHP' ]);
});
You can apply this strategy to HasMany associations as well. For example if ‘Authors HasMany Articles’, you could find all the authors with recently published articles using the following:
$query = $authors -> find ();
$query -> matching ( 'Articles' ,
function
( $q ) {
return
$q -> where ([ 'Articles.created >=' =>
new
DateTime( '-10 days' )]);
});
Filtering by deep associations is surprisingly easy, and the syntax should be already familiar to you:
// In a controller or table method.
$query = $products -> find () -> matching (
'Shops.Cities.Countries' ,
function
( $q ) {
return
$q -> where ([ 'Countries.name' => 'Japan' ]);
}
);
// Bring unique articles that were commented by 'markstory' using passed variable
// Dotted matching paths should be used over nested matching() calls
$username = 'markstory' ;
$query = $articles -> find () -> matching ( 'Comments.Users' ,
function
( $q )
use
( $username ) {
return
$q -> where ([ 'username' => $username ]);
});
Note: As this function will create an INNER JOIN, you might want to consider calling distinct on the find query as you might get duplicate rows if your conditions don’t exclude them already. This might be the case, for example, when the same users comments more than once on a single article.
The data from the association that is ‘matched’ will be available on the _matchingData property of entities. If you both match and contain the same association, you can expect to get both the _matchingData and standard association properties in your results.
Using innerJoinWith
Using the matching() function, as we saw already, will create an INNER JOIN with the specified association and will also load the fields into the result set.
There may be cases where you want to use matching() but are not interested in loading the fields into the result set. For this purpose, you can use innerJoinWith():
$query = $articles -> find ();
$query -> innerJoinWith ( 'Tags' ,
function
( $q ) {
More Information 441
CakePHP Cookbook Documentation, Release 3.5
});
return
$q -> where ([ 'Tags.name' => 'CakePHP' ]);
The innerJoinWith() method works the same as matching(), that means that you can use dot notation to join deeply nested associations:
$query = $products -> find () -> innerJoinWith (
'Shops.Cities.Countries' ,
function
( $q ) {
return
$q -> where ([ 'Countries.name' => 'Japan' ]);
}
);
Again, the only difference is that no additional columns will be added to the result set, and no _matchingData property will be set.
New in version 3.1: Query::innerJoinWith() was added in 3.1
Using notMatching
The opposite of matching() is notMatching(). This function will change the query so that it filters results that have no relation to the specified association:
// In a controller or table method.
$query = $articlesTable
-> find ()
-> notMatching ( 'Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'boring' ]);
});
The above example will find all articles that were not tagged with the word boring. You can apply this method to
HasMany associations as well. You could, for example, find all the authors with no published articles in the last 10 days:
$query = $authorsTable
-> find ()
-> notMatching ( 'Articles' ,
function
( $q ) {
return
$q -> where ([ 'Articles.created >=' =>
new
\DateTime( '-10 days' )]);
});
It is also possible to use this method for filtering out records not matching deep associations. For example, you could find articles that have not been commented on by a certain user:
$query = $articlesTable
-> find ()
-> notMatching ( 'Comments.Users' ,
function
( $q ) {
return
$q -> where ([ 'username' => 'jose' ]);
});
Since articles with no comments at all also satisfy the condition above, you may want to combine matching() and notMatching() in the same query. The following example will find articles having at least one comment, but not commented by a certain user:
$query = $articlesTable
-> find ()
-> notMatching ( 'Comments.Users' ,
function
( $q ) {
442 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
return
$q -> where ([ 'username' => 'jose' ]);
})
-> matching ( 'Comments' );
Note: As notMatching() will create a LEFT JOIN, you might want to consider calling distinct on the find query as you can get duplicate rows otherwise.
Keep in mind that contrary to the matching() function, notMatching() will not add any data to the
_matchingData property in the results.
New in version 3.1: Query::notMatching() was added in 3.1
Using leftJoinWith
On certain occasions you may want to calculate a result based on an association, without having to load all the records for it. For example, if you wanted to load the total number of comments an article has along with all the article data, you can use the leftJoinWith() function:
$query = $articlesTable -> find ();
$query -> select ([ 'total_comments' => $query -> func () -> count ( 'Comments.id' )])
-> leftJoinWith ( 'Comments' )
-> group ([ 'Articles.id' ])
-> enableAutoFields (
true
); // Prior to 3.4.0 use autoFields(true);
The results for the above query will contain the article data and the total_comments property for each of them.
leftJoinWith() can also be used with deeply nested associations. This is useful, for example, for bringing the count of articles tagged with a certain word, per author:
$query = $authorsTable
-> find ()
-> select ([ 'total_articles' => $query -> func () -> count ( 'Articles.id' )])
-> leftJoinWith ( 'Articles.Tags' ,
function
( $q ) {
return
$q -> where ([ 'Tags.name' => 'awesome' ]);
})
-> group ([ 'Authors.id' ])
-> enableAutoFields (
true
); // Prior to 3.4.0 use autoFields(true);
This function will not load any columns from the specified associations into the result set.
New in version 3.1: Query::leftJoinWith() was added in 3.1
Changing Fetching Strategies
As you may know already, belongsTo and hasOne associations are loaded using a JOIN in the main finder query.
While this improves query and fetching speed and allows for creating more expressive conditions when retrieving data, this may be a problem when you want to apply certain clauses to the finder query for the association, such as order() or limit().
For example, if you wanted to get the first comment of an article as an association:
$articles -> hasOne ( 'FirstComment' , [
'className' => 'Comments' ,
More Information 443
CakePHP Cookbook Documentation, Release 3.5
]);
'foreignKey' => 'article_id'
In order to correctly fetch the data from this association, we will need to tell the query to use the select strategy, since we want order by a particular column:
$query = $articles -> find () -> contain ([
'FirstComment' => [
'strategy' => 'select' ,
'queryBuilder' =>
function
( $q ) {
return
$q -> order ([ 'FirstComment.created' => 'ASC' ]) -> limit ( 1 );
}
]
]);
Dynamically changing the strategy in this way will only apply to a specific query. If you want to make the strategy change permanent you can do:
$articles -> FirstComment -> setStrategy ( 'select' );
// Prior to 3.4.0
$articles -> FirstComment -> strategy ( 'select' );
Using the select strategy is also a great way of making associations with tables in another database, since it would not be possible to fetch records using joins.
Fetching With The Subquery Strategy
As your tables grow in size, fetching associations from them can become slower, especially if you are querying big batches at once. A good way of optimizing association loading for hasMany and belongsToMany associations is by using the subquery strategy:
$query = $articles -> find () -> contain ([
'Comments' => [
'strategy' => 'subquery' ,
'queryBuilder' =>
function
( $q ) {
return
$q -> where ([ 'Comments.approved' =>
true
]);
}
]
]);
The result will remain the same as with using the default strategy, but this can greatly improve the query and fetching time in some databases, in particular it will allow to fetch big chunks of data at the same time in databases that limit the amount of bound parameters per query, such as Microsoft SQL Server.
You can also make the strategy permanent for the association by doing:
$articles -> Comments -> setStrategy ( 'subquery' );
// Prior to 3.4.0
$articles -> Comments -> strategy ( 'subquery' );
Lazy Loading Associations
While CakePHP makes it easy to eager load your associations, there may be cases where you need to lazy-load associations. You should refer to the
Lazy Loading Associations
and
Loading Additional Associations
sections for more information.
444 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Working with Result Sets
Once a query is executed with all(), you will get an instance of Cake\ORM\ResultSet. This object offers powerful ways to manipulate the resulting data from your queries. Like Query objects, ResultSets are a
Collection
and you can use any collection method on ResultSet objects.
Result set objects will lazily load rows from the underlying prepared statement. By default results will be buffered in memory allowing you to iterate a result set multiple times, or cache and iterate the results. If you need work with a data set that does not fit into memory you can disable buffering on the query to stream results:
$query -> enableBufferResults (
false
);
// Prior to 3.4.0
$query -> bufferResults (
false
);
Turning buffering off has a few caveats:
1. You will not be able to iterate a result set more than once.
2. You will also not be able to iterate & cache the results.
3. Buffering cannot be disabled for queries that eager load hasMany or belongsToMany associations, as these association types require eagerly loading all results so that dependent queries can be generated.
Warning: Streaming results will still allocate memory for the entire results when using PostgreSQL and SQL
Server. This is due to limitations in PDO.
Result sets allow you to cache/serialize or JSON encode results for API results:
// In a controller or table method.
$results = $query -> all ();
// Serialized
$serialized = serialize ( $results );
// Json
$json = json_encode ( $results );
Both serializing and JSON encoding result sets work as you would expect. The serialized data can be unserialized into a working result set. Converting to JSON respects hidden & virtual field settings on all entity objects within a result set.
In addition to making serialization easy, result sets are a ‘Collection’ object and support the same methods that
collection objects
do. For example, you can extract a list of unique tags on a collection of articles by running:
// In a controller or table method.
$articles = TableRegistry :: get ( 'Articles' );
$query = $articles -> find () -> contain ([ 'Tags' ]);
$reducer =
function
( $output , $value ) {
if
( !
in_array ( $value , $output )) {
$output [] = $value ;
}
return
$output ;
};
$uniqueTags = $query -> all ()
-> extract ( 'tags.name' )
-> reduce ( $reducer , []);
More Information 445
CakePHP Cookbook Documentation, Release 3.5
Some other examples of the collection methods being used with result sets are:
// Filter the rows by a calculated property
$filtered = $results -> filter (
function
( $row ) {
return
$row -> is_recent ;
});
// Create an associative array from result properties
$articles = TableRegistry :: get ( 'Articles' );
$results = $articles -> find () -> contain ([ 'Authors' ]) -> all ();
$authorList = $results -> combine ( 'id' , 'author.name' );
The
Collections
chapter has more detail on what can be done with result sets using the collections features. The
Adding
Calculated Fields
section show how you can add calculated fields, or replace the result set.
Getting the First & Last Record From a ResultSet
You can use the first() and last() methods to get the respective records from a result set:
$result = $articles -> find ( 'all' ) -> all ();
// Get the first and/or last result.
$row = $result -> first ();
$row = $result -> last ();
Getting an Arbitrary Index From a ResultSet
You can use skip() and first() to get an arbitrary record from a ResultSet:
$result = $articles -> find ( 'all' ) -> all ();
// Get the 5th record
$row = $result -> skip ( 4 ) -> first ();
Checking if a Query or ResultSet is Empty
You can use the isEmpty() method on a Query or ResultSet object to see if it has any rows in it. Calling isEmpty() on a Query object will evaluate the query:
// Check a query.
$query -> isEmpty ();
// Check results
$results = $query -> all ();
$results -> isEmpty ();
446 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Loading Additional Associations
Once you’ve created a result set, you may need to load additional associations. This is the perfect time to lazily eager load data. You can load additional associations using loadInto():
$articles = $this -> Articles -> find () -> all ();
$withMore = $this -> Articles -> loadInto ( $articles , [ 'Comments' , 'Users' ]);
You can eager load additional data into a single entity, or a collection of entities.
Modifying Results with Map/Reduce
More often than not, find operations require post-processing the data that is found in the database. While entities’ getter methods can take care of most of the virtual property generation or special data formatting, sometimes you need to change the data structure in a more fundamental way.
For those cases, the Query object offers the mapReduce() method, which is a way of processing results once they are fetched from the database.
A common example of changing the data structure is grouping results together based on certain conditions. For this task we can use the mapReduce() function. We need two callable functions the $mapper and the $reducer. The
$mapper callable receives the current result from the database as first argument, the iteration key as second argument and finally it receives an instance of the MapReduce routine it is running:
$mapper =
function
( $article , $key , $mapReduce ) {
$status = 'published' ;
if
( $article -> isDraft () || $article -> isInReview ()) {
$status = 'unpublished' ;
}
$mapReduce -> emitIntermediate ( $article , $status );
};
In the above example $mapper is calculating the status of an article, either published or unpublished, then it calls emitIntermediate() on the MapReduce instance. This method stores the article in the list of articles labelled as either published or unpublished.
The next step in the map-reduce process is to consolidate the final results. For each status created in the mapper, the
$reducer function will be called so you can do any extra processing. This function will receive the list of articles in a particular “bucket” as the first parameter, the name of the “bucket” it needs to process as the second parameter, and again, as in the mapper() function, the instance of the MapReduce routine as the third parameter. In our example, we did not have to do any extra processing, so we just emit() the final results:
$reducer =
function
( $articles , $status , $mapReduce ) {
$mapReduce -> emit ( $articles , $status );
};
Finally, we can put these two functions together to do the grouping:
$articlesByStatus = $articles -> find ()
-> where ([ 'author_id' => 1 ])
-> mapReduce ( $mapper , $reducer );
foreach
( $articlesByStatus
as
$status => $articles ) {
echo
sprintf ( "There are %d %s articles" , count ( $articles ), $status );
}
The above will ouput the following lines:
More Information 447
CakePHP Cookbook Documentation, Release 3.5
There are 4 published articles
There are 5 unpublished articles
Of course, this is a simplistic example that could actually be solved in another way without the help of a map-reduce process. Now, let’s take a look at another example in which the reducer function will be needed to do something more than just emitting the results.
Calculating the most commonly mentioned words, where the articles contain information about CakePHP, as usual we need a mapper function:
$mapper =
function
( $article , $key , $mapReduce ) {
if
( stripos ( $article [ 'body' ], 'cakephp' ) ===
false
) {
return
;
}
};
$words = array_map ( 'strtolower' , explode ( ' ' , $article [ 'body' ]));
foreach
( $words
as
$word ) {
$mapReduce -> emitIntermediate ( $article [ 'id' ], $word );
}
It first checks for whether the “cakephp” word is in the article’s body, and then breaks the body into individual words.
Each word will create its own bucket where each article id will be stored. Now let’s reduce our results to only extract the count:
$reducer =
function
( $occurrences , $word , $mapReduce ) {
$mapReduce -> emit ( count ( $occurrences ), $word );
}
Finally, we put everything together:
$wordCount = $articles -> find ()
-> where ([ 'published' =>
true
])
-> andWhere ([ 'published_date >=' =>
new
DateTime( '2014-01-01' )])
-> enableHydrate (
false
) // Prior to 3.4.0 use hydrate(false)
-> mapReduce ( $mapper , $reducer )
-> toArray ();
This could return a very large array if we don’t clean stop words, but it could look something like this:
[
'cakephp' => 100 ,
'awesome' => 39 ,
'impressive' => 57 ,
'outstanding' => 10 ,
'mind-blowing' => 83
]
One last example and you will be a map-reduce expert. Imagine you have a friends table and you want to find
“fake friends” in our database, or better said, people who do not follow each other. Let’s start with our mapper() function:
$mapper =
function
( $rel , $key , $mr ) {
$mr -> emitIntermediate ( $rel [ 'target_user_id' ], $rel [ 'source_user_id' ]);
$mr -> emitIntermediate ( $rel [ 'source_user_id' ], $rel [ 'target_user_id' ]);
};
The intermediate array will be like the following:
448 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
[
1 => [ 2 , 3 , 4 , 5 , 3 , 5 ],
2 => [ 1 ],
3 => [ 1 , 1 , 6 ],
4 => [ 1 ],
5 => [ 1 , 1 ],
6 => [ 3 ],
...
]
Positive numbers mean that a user, indicated with the first-level key, is following them, and negative numbers mean that the user is followed by them.
Now it’s time to reduce it. For each call to the reducer, it will receive a list of followers per user:
$reducer =
function
( $friends , $user , $mr ) {
$fakeFriends = [];
};
foreach
( $friends
as
$friend ) {
if
( $friend > 0 && !
in_array ( $friend , $friends )) {
$fakeFriends [] = $friend ;
}
}
if
( $fakeFriends ) {
$mr -> emit ( $fakeFriends , $user );
}
And we supply our functions to a query:
$fakeFriends = $friends -> find ()
-> enableHydrate (
false
) // Prior to 3.4.0 use hydrate(false)
-> mapReduce ( $mapper , $reducer )
-> toArray ();
This would return an array similar to this:
[
1 => [ 2 , 4 ],
3 => [ 6 ]
...
]
The resulting array means, for example, that user with id 1 follows users 2 and 4, but those do not follow 1 back.
Stacking Multiple Operations
Using mapReduce in a query will not execute it immediately. The operation will be registered to be run as soon as the first result is attempted to be fetched. This allows you to keep chaining additional methods and filters to the query even after adding a map-reduce routine:
$query = $articles -> find ()
-> where ([ 'published' =>
true
])
-> mapReduce ( $mapper , $reducer );
More Information 449
CakePHP Cookbook Documentation, Release 3.5
// At a later point in your app:
$query -> where ([ 'created >=' =>
new
DateTime( '1 day ago' )]);
This is particularly useful for building custom finder methods as described in the
Custom Finder Methods
section:
public function
findPublished (Query $query ,
array
$options )
{
return
$query -> where ([ 'published' =>
true
]);
}
public function
findRecent (Query $query ,
array
$options )
{
return
$query -> where ([ 'created >=' =>
new
DateTime( '1 day ago' )]);
}
public function
findCommonWords (Query $query ,
array
$options )
{
// Same as in the common words example in the previous section
$mapper = ...
;
$reducer = ...
;
return
$query -> mapReduce ( $mapper , $reducer );
}
$commonWords = $articles
-> find ( 'commonWords' )
-> find ( 'published' )
-> find ( 'recent' );
Moreover, it is also possible to stack more than one mapReduce operation for a single query. For example, if we wanted to have the most commonly used words for articles, but then filter it to only return words that were mentioned more than 20 times across all articles:
$mapper =
function
( $count , $word , $mr ) {
if
( $count > 20 ) {
$mr -> emit ( $count , $word );
}
};
$articles -> find ( 'commonWords' ) -> mapReduce ( $mapper );
Removing All Stacked Map-reduce Operations
Under some circumstances you may want to modify a Query object so that no mapReduce operations are executed at all. This can be done by calling the method with both parameters as null and the third parameter (overwrite) as true
:
$query -> mapReduce (
null
,
null
,
true
);
Validating Data
Before you
save your data
you will probably want to ensure the data is correct and consistent. In CakePHP we have two stages of validation:
1. Before request data is converted into entities, validation rules around data types and formatting can be applied.
450 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
2. Before data is saved, domain or application rules can be applied. These rules help ensure that your application’s data remains consistent.
Validating Data Before Building Entities
When marshalling data into entities, you can validate data. Validating data allows you to check the type, shape and size of data. By default request data will be validated before it is converted into entities. If any validation rules fail, the returned entity will contain errors. The fields with errors will not be present in the returned entity:
$article = $articles -> newEntity ( $this -> request -> getData ());
if
( $article -> errors ()) {
// Entity failed validation.
}
New in version 3.4.0: The getErrors() function was added.
When building an entity with validation enabled the following occurs:
1. The validator object is created.
2. The table and default validation provider are attached.
3. The named validation method is invoked. For example validationDefault.
4. The Model.buildValidator event will be triggered.
5. Request data will be validated.
6. Request data will be type-cast into types that match the column types.
7. Errors will be set into the entity.
8. Valid data will be set into the entity, while fields that failed validation will be excluded.
If you’d like to disable validation when converting request data, set the validate option to false:
$article = $articles -> newEntity (
$this -> request -> getData (),
[ 'validate' =>
false
]
);
The same can be said about the patchEntity() method:
$article = $articles -> patchEntity ( $article , $newData , [
'validate' =>
false
]);
Creating A Default Validation Set
Validation rules are defined in the Table classes for convenience. This defines what data should be validated in conjunction with where it will be saved.
To create a default validation object in your table, create the validationDefault() function:
use
Cake\ORM\Table;
use
Cake\Validation\Validator;
class ArticlesTable extends
Table
{
More Information 451
CakePHP Cookbook Documentation, Release 3.5
public function
validationDefault (Validator $validator )
{
$validator
-> requirePresence ( 'title' , 'create' )
-> notEmpty ( 'title' );
$validator
-> allowEmpty ( 'link' )
-> add ( 'link' , 'valid-url' , [ 'rule' => 'url' ]);
...
return
$validator ;
}
}
The available validation methods and rules come from the Validator class and are documented in the
Creating
Validators
section.
Note: Validation objects are intended primarily for validating user input, i.e. forms and any other posted request data.
Using A Different Validation Set
In addition to disabling validation you can choose which validation rule set you want applied:
$article = $articles -> newEntity (
$this -> request -> getData (),
[ 'validate' => 'update' ]
);
The above would call the validationUpdate() method on the table instance to build the required rules. By default the validationDefault() method will be used. An example validator for our articles table would be:
class ArticlesTable extends
Table
{
public function
validationUpdate ( $validator )
{
$validator
-> add ( 'title' , 'notEmpty' , [
'rule' => 'notEmpty' ,
'message' => __( 'You need to provide a title' ),
])
-> add ( 'body' , 'notEmpty' , [
'rule' => 'notEmpty' ,
'message' => __( 'A body is required' )
]);
return
$validator ;
}
}
You can have as many validation sets as necessary. See the
validation chapter
for more information on building validation rule-sets.
452 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Using A Different Validation Set For Associations
Validation sets can also be defined per association. When using the newEntity() or patchEntity() methods, you can pass extra options to each of the associations to be converted:
$data = [
'title' => 'My title' ,
'body' => 'The text' ,
'user_id' => 1 ,
'user' => [
'username' => 'mark'
],
'comments' => [
[ 'body' => 'First comment' ],
[ 'body' => 'Second comment' ],
]
];
$article = $articles -> patchEntity ( $article , $data , [
'validate' => 'update' ,
'associated' => [
'Users' => [ 'validate' => 'signup' ],
'Comments' => [ 'validate' => 'custom' ]
]
]);
Combining Validators
Because of how validator objects are built, it is easy to break their construction process into multiple reusable steps:
// UsersTable.php
public function
validationDefault (Validator $validator )
{
$validator -> notEmpty ( 'username' );
$validator -> notEmpty ( 'password' );
$validator -> add ( 'email' , 'valid-email' , [ 'rule' => 'email' ]);
...
return
$validator ;
}
public function
validationHardened (Validator $validator )
{
$validator = $this -> validationDefault ( $validator );
$validator -> add ( 'password' , 'length' , [ 'rule' => [ 'lengthBetween' , 8 , 100 ]]);
return
$validator ;
}
Given the above setup, when using the hardened validation set, it will also contain the validation rules declared in the default set.
Validation Providers
Validation rules can use functions defined on any known providers. By default CakePHP sets up a few providers:
More Information 453
CakePHP Cookbook Documentation, Release 3.5
1. Methods on the table class or its behaviors are available on the table provider.
2. The core Validation\Validation class is setup as the default provider.
When a validation rule is created you can name the provider of that rule.
For example, if your table has an isValidRole method you can use it as a validation rule:
use
Cake\ORM\Table;
use
Cake\Validation\Validator;
class UsersTable extends
Table
{
public function
validationDefault (Validator $validator )
{
$validator
-> add ( 'role' , 'validRole' , [
'rule' => 'isValidRole' ,
'message' => __( 'You need to provide a valid role' ),
'provider' => 'table' ,
]);
return
$validator ;
}
public function
isValidRole ( $value ,
array
$context )
{
return
in_array ( $value , [ 'admin' , 'editor' , 'author' ],
true
);
}
}
You can also use closures for validation rules:
$validator -> add ( 'name' , 'myRule' , [
'rule' =>
function
( $data , $provider ) {
if
( $data > 1 ) {
return true
;
}
return
'Not a good value.' ;
}
]);
Validation methods can return error messages when they fail. This is a simple way to make error messages dynamic based on the provided value.
Getting Validators From Tables
Once you have created a few validation sets in your table class, you can get the resulting object by name:
$defaultValidator = $usersTable -> validator ( 'default' );
$hardenedValidator = $usersTable -> validator ( 'hardened' );
Deprecated since version 3.5.0: validator() is deprecated. Use getValidator() instead.
454 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Default Validator Class
As stated above, by default the validation methods receive an instance of Cake\Validation\Validator. Instead, if you want your custom validator’s instance to be used each time, you can use table’s $_validatorClass property:
// In your table class
public function
initialize (
array
$config )
{
$this -> _validatorClass = '\FullyNamespaced\Custom\Validator' ;
}
Applying Application Rules
While basic data validation is done when
request data is converted into entities
, many applications also have more
complex validation that should only be applied after basic validation has completed.
Where validation ensures the form or syntax of your data is correct, rules focus on comparing data against the existing state of your application and/or network.
These types of rules are often referred to as ‘domain rules’ or ‘application rules’. CakePHP exposes this concept through ‘RulesCheckers’ which are applied before entities are persisted. Some example domain rules are:
• Ensuring email uniqueness
• State transitions or workflow steps (e.g., updating an invoice’s status).
• Preventing the modification of soft deleted items.
• Enforcing usage/rate limit caps.
Domain rules are checked when calling the Table save() and delete() methods.
Creating a Rules Checker
Rules checker classes are generally defined by the buildRules() method in your table class. Behaviors and other event subscribers can use the Model.buildRules event to augment the rules checker for a given Table class:
use
Cake\ORM\RulesChecker;
// In a table class
public function
buildRules (RulesChecker $rules )
{
// Add a rule that is applied for create and update operations
$rules -> add (
function
( $entity , $options ) {
// Return a boolean to indicate pass/failure
}, 'ruleName' );
// Add a rule for create.
$rules -> addCreate (
function
( $entity , $options ) {
// Return a boolean to indicate pass/failure
}, 'ruleName' );
// Add a rule for update
$rules -> addUpdate (
function
( $entity , $options ) {
// Return a boolean to indicate pass/failure
}, 'ruleName' );
More Information 455
CakePHP Cookbook Documentation, Release 3.5
// Add a rule for the deleting.
$rules -> addDelete (
function
( $entity , $options ) {
// Return a boolean to indicate pass/failure
}, 'ruleName' );
return
$rules ;
}
Your rules functions can expect to get the Entity being checked and an array of options. The options array will contain errorField
, message, and repository. The repository option will contain the table class the rules are attached to. Because rules accept any callable, you can also use instance functions:
$rules -> addCreate ([ $this , 'uniqueEmail' ], 'uniqueEmail' ); or callable classes:
$rules -> addCreate (
new
IsUnique([ 'email' ]), 'uniqueEmail' );
When adding rules you can define the field the rule is for and the error message as options:
$rules -> add ([ $this , 'isValidState' ], 'validState' , [
'errorField' => 'status' ,
'message' => 'This invoice cannot be moved to that status.'
]);
The error will be visible when calling the errors() method on the entity:
$entity -> errors (); // Contains the domain rules error messages
Creating Unique Field Rules
Because unique rules are quite common, CakePHP includes a simple Rule class that allows you to define unique field sets:
use
Cake\ORM\Rule\IsUnique;
// A single field.
$rules -> add ( $rules -> isUnique ([ 'email' ]));
// A list of fields
$rules -> add ( $rules -> isUnique (
[ 'username' , 'account_id' ],
'This username & account_id combination has already been used.'
));
When setting rules on foreign key fields it is important to remember, that only the fields listed are used in the rule.
This means that setting $user->account->id will not trigger the above rule.
Foreign Key Rules
While you could rely on database errors to enforce constraints, using rules code can help provide a nicer user experience. Because of this CakePHP includes an ExistsIn rule class:
456 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
// A single field.
$rules -> add ( $rules -> existsIn ( 'article_id' , 'articles' ));
// Multiple keys, useful for composite primary keys.
$rules -> add ( $rules -> existsIn ([ 'site_id' , 'article_id' ], 'articles' ));
The fields to check existence against in the related table must be part of the primary key.
You can enforce existsIn to pass when nullable parts of your composite foreign key are null:
// Example: A composite primary key within NodesTable is (id, site_id).
// A Node may reference a parent Node but does not need to. In latter case, parent_id
˓→ is null.
// Allow this rule to pass, even if fields that are nullable, like parent_id, are
˓→ null:
$rules -> add ( $rules -> existsIn (
[ 'parent_id' , 'site_id' ], // Schema: parent_id NULL, site_id NOT NULL
'ParentNodes' ,
[ 'allowNullableNulls' =>
true
]
));
// A Node however should in addition also always reference a Site.
$rules -> add ( $rules -> existsIn ([ 'site_id' ], 'Sites' ));
In most SQL databases multi-column UNIQUE indexes allow multiple null values to exist as NULL is not equal to itself. While, allowing multiple null values is the default behavior of CakePHP, you can include null values in your unique checks using allowMultipleNulls:
// Only one null value can exist in `parent_id` and `site_id`
$rules -> add ( $rules -> existsIn (
[ 'parent_id' , 'site_id' ],
'ParentNodes' ,
[ 'allowMultipleNulls' =>
false
]
));
New in version 3.3.0: The allowNullableNulls and allowMultipleNulls options were added.
Association Count Rules
If you need to validate that a property or association contains the correct number of values, you can use the validCount() rule:
// In the ArticlesTable.php file
// No more than 5 tags on an article.
$rules -> add ( $rules -> validCount ( 'tags' , 5 , '<=' , 'You can only have 5 tags' ));
When defining count based rules, the third parameter lets you define the comparison operator to use. ==, >=, <=, >,
< , and != are the accepted operators. To ensure a property’s count is within a range, use two rules:
// In the ArticlesTable.php file
// Between 3 and 5 tags
$rules -> add ( $rules -> validCount ( 'tags' , 3 , '>=' , 'You must have at least 3 tags' ));
$rules -> add ( $rules -> validCount ( 'tags' , 5 , '<=' , 'You must have at most 5 tags' ));
Note that validCount returns false if the property is not countable or does not exist:
More Information 457
CakePHP Cookbook Documentation, Release 3.5
// The save operation will fail if tags is null.
$rules -> add ( $rules -> validCount ( 'tags' , 0 , '<=' , 'You must not have any tags' ));
New in version 3.3.0: The validCount() method was added in 3.3.0.
Using Entity Methods as Rules
You may want to use entity methods as domain rules:
$rules -> add (
function
( $entity , $options ) {
return
$entity -> isOkLooking ();
}, 'ruleName' );
Creating Custom re-usable Rules
You may want to re-use custom domain rules. You can do so by creating your own invokable rule:
use
App\ORM\Rule\IsUniqueWithNulls;
// ...
public function
buildRules (RulesChecker $rules )
{
}
$rules -> add (
new
IsUniqueWithNulls([ 'parent_id' , 'instance_id' , 'name' ]),
˓→
'uniqueNamePerParent' , [
'errorField' => 'name' ,
'message' => 'Name must be unique per parent.'
]);
return
$rules ;
See the core rules for examples on how to create such rules.
Creating Custom Rule Objects
If your application has rules that are commonly reused, it is helpful to package those rules into re-usable classes:
// in src/Model/Rule/CustomRule.php
namespace
App\Model\Rule;
use
Cake\Datasource\EntityInterface;
class CustomRule
{
public function
__invoke (EntityInterface $entity ,
array
$options )
{
// Do work
return false
;
}
}
// Add the custom rule
use
App\Model\Rule\CustomRule;
458 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$rules -> add (
new
CustomRule( ...
), 'ruleName' );
By creating custom rule classes you can keep your code DRY and make your domain rules easy to test.
Disabling Rules
When saving an entity, you can disable the rules if necessary:
$articles -> save ( $article , [ 'checkRules' =>
false
]);
Validation vs. Application Rules
The CakePHP ORM is unique in that it uses a two-layered approach to validation.
The first layer is validation. Validation rules are intended to operate in a stateless way. They are best leveraged to ensure that the shape, data types and format of data is correct.
The second layer is application rules. Application rules are best leveraged to check stateful properties of your entities.
For example, validation rules could ensure that an email address is valid, while an application rule could ensure that the email address is unique.
As you already discovered, the first layer is done through the Validator objects when calling newEntity() or patchEntity()
:
$validatedEntity = $articlesTable -> newEntity (
$unsafeData ,
[ 'validate' => 'customName' ]
);
$validatedEntity = $articlesTable -> patchEntity (
$entity ,
$unsafeData ,
[ 'validate' => 'customName' ]
);
In the above example, we’ll use a ‘custom’ validator, which is defined using the validationCustomName() method:
public function
validationCustomName ( $validator )
{
$validator -> add ( ...
);
return
$validator ;
}
Validation assumes strings or array are passed since that is what is received from any request:
// In src/Model/Table/UsersTable.php
public function
validatePasswords ( $validator )
{
$validator -> add ( 'confirm_password' , 'no-misspelling' , [
'rule' => [ 'compareWith' , 'password' ],
'message' => 'Passwords are not equal' ,
]);
...
More Information 459
CakePHP Cookbook Documentation, Release 3.5
return
$validator ;
}
Validation is not triggered when directly setting properties on your entities:
$userEntity -> email = 'not an email!!' ;
$usersTable -> save ( $userEntity );
In the above example the entity will be saved as validation is only triggered for the newEntity() and patchEntity() methods. The second level of validation is meant to address this situation.
Application rules as explained above will be checked whenever save() or delete() are called:
// In src/Model/Table/UsersTable.php
public function
buildRules (RulesChecker $rules )
{
$rules -> add ( $rules -> isUnique ( 'email' ));
return
$rules ;
}
// Elsewhere in your application code
$userEntity -> email = '[email protected]' ;
$usersTable -> save ( $userEntity ); // Returns false
While Validation is meant for direct user input, application rules are specific for data transitions generated inside your application:
// In src/Model/Table/OrdersTable.php
public function
buildRules (RulesChecker $rules )
{
$check =
function
( $order ) {
return
$order -> price < 100 && $order -> shipping_mode === 'free' ;
};
$rules -> add ( $check , [
'errorField' => 'shipping_mode' ,
'message' => 'No free shipping for orders under 100!'
]);
return
$rules ;
}
// Elsewhere in application code
$order -> price = 50 ;
$order -> shipping_mode = 'free' ;
$ordersTable -> save ( $order ); // Returns false
Using Validation as Application Rules
In certain situations you may want to run the same data validation routines for data that was both generated by users and inside your application. This could come up when running a CLI script that directly sets properties on entities:
// In src/Model/Table/UsersTable.php
public function
validationDefault (Validator $validator )
{
$validator -> add ( 'email' , 'valid' , [
'rule' => 'email' ,
'message' => 'Invalid email'
460 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
]);
...
return
$validator ;
}
public function
buildRules (RulesChecker $rules )
{
// Add validation rules
$rules -> add (
function
( $entity ) {
$data = $entity -> extract ( $this -> schema () -> columns (),
true
);
$validator = $this -> validator ( 'default' );
$errors = $validator -> errors ( $data , $entity -> isNew ());
$entity -> errors ( $errors );
});
return empty
( $errors );
...
return
$rules ;
}
When executed the save will fail thanks to the new application rule that was added:
$userEntity -> email = 'not an email!!!' ;
$usersTable -> save ( $userEntity );
$userEntity -> errors ( 'email' ); // Invalid email
The same result can be expected when using newEntity() or patchEntity():
$userEntity = $usersTable -> newEntity ([ 'email' => 'not an email!!' ]);
$userEntity -> errors ( 'email' ); // Invalid email
Saving Data
class Cake\ORM\Table
After you have
loaded your data
you will probably want to update and save the changes.
A Glance Over Saving Data
Applications will usually have a couple of ways in which data is saved. The first one is obviously through web forms and the other is by directly generating or changing data in the code to be sent to the database.
Inserting Data
The easiest way to insert data in the database is by creating a new entity and passing it to the save() method in the
Table class:
use
Cake\ORM\TableRegistry;
$articlesTable = TableRegistry :: get ( 'Articles' );
$article = $articlesTable -> newEntity ();
More Information 461
CakePHP Cookbook Documentation, Release 3.5
$article -> title = 'A New Article' ;
$article -> body = 'This is the body of the article' ;
if
( $articlesTable -> save ( $article )) {
// The $article entity contains the id now
$id = $article -> id ;
}
Updating Data
Updating your data is equally easy, and the save() method is also used for that purpose:
use
Cake\ORM\TableRegistry;
$articlesTable = TableRegistry :: get ( 'Articles' );
$article = $articlesTable -> get ( 12 ); // Return article with id 12
$article -> title = 'CakePHP is THE best PHP framework!' ;
$articlesTable -> save ( $article );
CakePHP will know whether to perform an insert or an update based on the return value of the isNew() method.
Entities that were retrieved with get() or find() will always return false when isNew() is called on them.
Saving With Associations
By default the save() method will also save one level of associations:
$articlesTable = TableRegistry :: get ( 'Articles' );
$author = $articlesTable -> Authors -> findByUserName ( 'mark' ) -> first ();
$article = $articlesTable -> newEntity ();
$article -> title = 'An article by mark' ;
$article -> author = $author ;
if
( $articlesTable -> save ( $article )) {
// The foreign key value was set automatically.
echo
$article -> author_id ;
}
The save() method is also able to create new records for associations:
$firstComment = $articlesTable -> Comments -> newEntity ();
$firstComment -> body = 'The CakePHP features are outstanding' ;
$secondComment = $articlesTable -> Comments -> newEntity ();
$secondComment -> body = 'CakePHP performance is terrific!' ;
$tag1 = $articlesTable -> Tags -> findByName ( 'cakephp' ) -> first ();
$tag2 = $articlesTable -> Tags -> newEntity ();
$tag2 -> name = 'awesome' ;
$article = $articlesTable -> get ( 12 );
$article -> comments = [ $firstComment , $secondComment ];
$article -> tags = [ $tag1 , $tag2 ];
462 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$articlesTable -> save ( $article );
Associate Many To Many Records
The previous example demonstrates how to associate a few tags to an article. Another way of accomplishing the same thing is by using the link() method in the association:
$tag1 = $articlesTable -> Tags -> findByName ( 'cakephp' ) -> first ();
$tag2 = $articlesTable -> Tags -> newEntity ();
$tag2 -> name = 'awesome' ;
$articlesTable -> Tags -> link ( $article , [ $tag1 , $tag2 ]);
Unlink Many To Many Records
Unlinking many to many records is done via the unlink() method:
$tags = $articlesTable
-> Tags
-> find ()
-> where ([ 'name IN' => [ 'cakephp' , 'awesome' ]])
-> toArray ();
$articlesTable -> Tags -> unlink ( $article , $tags );
When modifying records by directly setting or changing the properties no validation happens, which is a problem when accepting form data. The following sections will demonstrate how to efficiently convert form data into entities so that they can be validated and saved.
Converting Request Data into Entities
Before editing and saving data back to your database, you’ll need to convert the request data from the array format held in the request, and the entities that the ORM uses. The Table class provides an easy and efficient way to convert one or many entities from request data. You can convert a single entity using:
// In a controller
$articles = TableRegistry :: get ( 'Articles' );
// Validate and convert to an Entity object
$entity = $articles -> newEntity ( $this -> request -> getData ());
Note: If you are using newEntity() and the resulting entities are missing some or all of the data they were passed, double check that the columns you want to set are listed in the $_accessible property of your entity. See
Mass
Assignment
The request data should follow the structure of your entities. For example if you have an article, which belonged to a user, and had many comments, your request data should resemble:
More Information 463
CakePHP Cookbook Documentation, Release 3.5
$data = [
'title' => 'CakePHP For the Win' ,
'body' => 'Baking with CakePHP makes web development fun!' ,
'user_id' => 1 ,
'user' => [
'username' => 'mark'
],
'comments' => [
[ 'body' => 'The CakePHP features are outstanding' ],
[ 'body' => 'CakePHP performance is terrific!' ],
]
];
By default, the newEntity() method validates the data that gets passed to it, as explained in the
Validating Data
Before Building Entities
section. If you wish to bypass data validation pass the 'validate' => false option:
$entity = $articles -> newEntity ( $data , [ 'validate' =>
false
]);
When building forms that save nested associations, you need to define which associations should be marshalled:
// In a controller
$articles = TableRegistry :: get ( 'Articles' );
// New entity with nested associations
$entity = $articles -> newEntity ( $this -> request -> getData (), [
'associated' => [
'Tags' , 'Comments' => [ 'associated' => [ 'Users' ]]
]
]);
The above indicates that the ‘Tags’, ‘Comments’ and ‘Users’ for the Comments should be marshalled. Alternatively, you can use dot notation for brevity:
// In a controller
$articles = TableRegistry :: get ( 'Articles' );
// New entity with nested associations using dot notation
$entity = $articles -> newEntity ( $this -> request -> getData (), [
'associated' => [ 'Tags' , 'Comments.Users' ]
]);
You may also disable marshalling of possible nested associations like so:
$entity = $articles -> newEntity ( $data , [ 'associated' => []]);
// or...
$entity = $articles -> patchEntity ( $entity , $data , [ 'associated' => []]);
Associated data is also validated by default unless told otherwise. You may also change the validation set to be used per association:
// In a controller
$articles = TableRegistry :: get ( 'Articles' );
// Bypass validation on Tags association and
// Designate 'signup' validation set for Comments.Users
$entity = $articles -> newEntity ( $this -> request -> getData (), [
'associated' => [
'Tags' => [ 'validate' =>
false
],
464 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
'Comments.Users' => [ 'validate' => 'signup' ]
]);
]
The
Using A Different Validation Set For Associations
chapter has more information on how to use different validators for associated marshalling.
The following diagram gives an overview of what happens inside the newEntity() or patchEntity() method: idation fails your entity will contain errors, and any invalid fields will not be populated in the created entity.
You can always count on getting an entity back from newEntity()
.
If val-
More Information
Converting
BelongsToMany
Data
If you are
465
CakePHP Cookbook Documentation, Release 3.5
saving belongsToMany associations you can either use a list of entity data or a list of ids. When using a list of entity data your request data should look like:
$data = [
'title' => 'My title' ,
'body' => 'The text' ,
'user_id' => 1 ,
'tags' => [
[ 'tag' => 'CakePHP' ],
[ 'tag' => 'Internet' ],
]
];
The above will create 2 new tags. If you want to link an article with existing tags you can use a list of ids. Your request data should look like:
$data = [
'title' => 'My title' ,
'body' => 'The text' ,
'user_id' => 1 ,
'tags' => [
'_ids' => [ 1 , 2 , 3 , 4 ]
]
];
If you need to link against some existing belongsToMany records, and create new ones at the same time you can use an expanded format:
$data = [
'title' => 'My title' ,
'body' => 'The text' ,
'user_id' => 1 ,
'tags' => [
[ 'name' => 'A new tag' ],
[ 'name' => 'Another new tag' ],
[ 'id' => 5 ],
[ 'id' => 21 ]
]
];
When the above data is converted into entities, you will have 4 tags. The first two will be new objects, and the second two will be references to existing records.
When converting belongsToMany data, you can disable the new entity creation, by using the onlyIds option. When enabled, this option restricts belongsToMany marshalling to only use the _ids key and ignore all other data.
New in version 3.1.0: The onlyIds option was added in 3.1.0
Converting HasMany Data
If you want to update existing hasMany associations and update their properties, you should first ensure your entity is loaded with the hasMany association populated. You can then use request data similar to:
$data = [
'title' => 'My Title' ,
'body' => 'The text' ,
'comments' => [
466 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
[ 'id' => 1 , 'comment' => 'Update the first comment' ],
[ 'id' => 2 , 'comment' => 'Update the second comment' ],
[ 'comment' => 'Create a new comment' ],
];
]
If you are saving hasMany associations and want to link existing records to a new parent record you can use the _ids format:
$data = [
'title' => 'My new article' ,
'body' => 'The text' ,
'user_id' => 1 ,
'comments' => [
'_ids' => [ 1 , 2 , 3 , 4 ]
]
];
When converting hasMany data, you can disable the new entity creation, by using the onlyIds option. When enabled, this option restricts hasMany marshalling to only use the _ids key and ignore all other data.
New in version 3.1.0: The onlyIds option was added in 3.1.0
Converting Multiple Records
When creating forms that create/update multiple records at once you can use newEntities():
// In a controller.
$articles = TableRegistry :: get ( 'Articles' );
$entities = $articles -> newEntities ( $this -> request -> getData ());
In this situation, the request data for multiple articles should look like:
$data = [
[
'title' => 'First post' ,
'published' => 1
],
[
'title' => 'Second post' ,
'published' => 1
],
];
Once you’ve converted request data into entities you can save() or delete() them:
// In a controller.
foreach
( $entities
as
$entity ) {
// Save entity
$articles -> save ( $entity );
// Delete entity
$articles -> delete ( $entity );
}
The above will run a separate transaction for each entity saved. If you’d like to process all the entities as a single transaction you can use transactional():
More Information 467
CakePHP Cookbook Documentation, Release 3.5
// In a controller.
$articles -> getConnection () -> transactional (
function
()
use
( $articles , $entities ) {
foreach
( $entities
as
$entity ) {
$articles -> save ( $entity , [ 'atomic' =>
false
]);
}
});
Changing Accessible Fields
It’s also possible to allow newEntity() to write into non accessible fields. For example, id is usually absent from the _accessible property. In such case, you can use the accessibleFields option. It could be useful to keep ids of associated entities:
// In a controller
$articles = TableRegistry :: get ( 'Articles' );
$entity = $articles -> newEntity ( $this -> request -> getData (), [
'associated' => [
'Tags' , 'Comments' => [
'associated' => [
'Users' => [
'accessibleFields' => [ 'id' =>
true
]
]
]
]
]
]);
The above will keep the association unchanged between Comments and Users for the concerned entity.
Note: If you are using newEntity() and the resulting entities are missing some or all of the data they were passed, double check that the columns you want to set are listed in the $_accessible property of your entity. See
Mass
Assignment
Merging Request Data Into Entities
In order to update entities you may choose to apply request data directly to an existing entity. This has the advantage that only the fields that actually changed will be saved, as opposed to sending all fields to the database to be persisted.
You can merge an array of raw data into an existing entity using the patchEntity() method:
// In a controller.
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> get ( 1 );
$articles -> patchEntity ( $article , $this -> request -> getData ());
$articles -> save ( $article );
Validation and patchEntity
Similar to newEntity(), the patchEntity method will validate the data before it is copied to the entity. The mechanism is explained in the
Validating Data Before Building Entities
section. If you wish to disable validation while patching an entity, pass the validate option as follows:
468 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
// In a controller.
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> get ( 1 );
$articles -> patchEntity ( $article , $data , [ 'validate' =>
false
]);
You may also change the validation set used for the entity or any of the associations:
$articles -> patchEntity ( $article , $this -> request -> getData (), [
'validate' => 'custom' ,
'associated' => [ 'Tags' , 'Comments.Users' => [ 'validate' => 'signup' ]]
]);
Patching HasMany and BelongsToMany
As explained in the previous section, the request data should follow the structure of your entity.
The patchEntity() method is equally capable of merging associations, by default only the first level of associations are merged, but if you wish to control the list of associations to be merged or merge deeper to deeper levels, you can use the third parameter of the method:
// In a controller.
$associated = [ 'Tags' , 'Comments.Users' ];
$article = $articles -> get ( 1 , [ 'contain' => $associated ]);
$articles -> patchEntity ( $article , $this -> request -> getData (), [
'associated' => $associated
]);
$articles -> save ( $article );
Associations are merged by matching the primary key field in the source entities to the corresponding fields in the data array. Associations will construct new entities if no previous entity is found for the association’s target property.
For example give some request data like the following:
$data = [
'title' => 'My title' ,
'user' => [
'username' => 'mark'
]
];
Trying to patch an entity without an entity in the user property will create a new user entity:
// In a controller.
$entity = $articles -> patchEntity (
new
Article, $data );
echo
$entity -> user -> username ; // Echoes 'mark'
The same can be said about hasMany and belongsToMany associations, with an important caveat:
Note: For belongsToMany associations, ensure the relevant entity has a property accessible for the associated entity.
If a Product belongsToMany Tag:
// in the Product Entity
protected
$_accessible = [
// .. other properties
More Information 469
CakePHP Cookbook Documentation, Release 3.5
];
'tags' =>
true
,
Note: For hasMany and belongsToMany associations, if there were any entities that could not be matched by primary key to a record in the data array, then those records will be discarded from the resulting entity.
Remember that using either patchEntity() or patchEntities() does not persist the data, it just edits (or creates) the given entities. In order to save the entity you will have to call the table’s save() method.
For example, consider the following case:
$data = [
'title' => 'My title' ,
'body' => 'The text' ,
'comments' => [
[ 'body' => 'First comment' , 'id' => 1 ],
[ 'body' => 'Second comment' , 'id' => 2 ],
]
];
$entity = $articles -> newEntity ( $data );
$articles -> save ( $entity );
$newData = [
'comments' => [
[ 'body' => 'Changed comment' , 'id' => 1 ],
[ 'body' => 'A new comment' ],
]
];
$articles -> patchEntity ( $entity , $newData );
$articles -> save ( $entity );
At the end, if the entity is converted back to an array you will obtain the following result:
[
'title' => 'My title' ,
'body' => 'The text' ,
'comments' => [
[ 'body' => 'Changed comment' , 'id' => 1 ],
[ 'body' => 'A new comment' ],
]
];
As you can see, the comment with id 2 is no longer there, as it could not be matched to anything in the $newData array. This happens because CakePHP is reflecting the new state described in the request data.
Some additional advantages of this approach is that it reduces the number of operations to be executed when persisting the entity again.
Please note that this does not mean that the comment with id 2 was removed from the database, if you wish to remove the comments for that article that are not present in the entity, you can collect the primary keys and execute a batch delete for those not in the list:
// In a controller.
$comments = TableRegistry :: get ( 'Comments' );
$present = (
new
Collection( $entity -> comments )) -> extract ( 'id' ) -> filter () -> toArray ();
$comments -> deleteAll ([
'article_id' => $article -> id ,
470 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
]);
'id NOT IN' => $present
As you can see, this also helps creating solutions where an association needs to be implemented like a single set.
You can also patch multiple entities at once. The consideration made for patching hasMany and belongsToMany associations apply for patching multiple entities: Matches are done by the primary key field value and missing matches in the original entities array will be removed and not present in the result:
// In a controller.
$articles = TableRegistry :: get ( 'Articles' );
$list = $articles -> find ( 'popular' ) -> toArray ();
$patched = $articles -> patchEntities ( $list , $this -> request -> getData ());
foreach
( $patched
as
$entity ) {
$articles -> save ( $entity );
}
Similarly to using patchEntity(), you can use the third argument for controlling the associations that will be merged in each of the entities in the array:
// In a controller.
$patched = $articles -> patchEntities (
$list ,
$this -> request -> getData (),
[ 'associated' => [ 'Tags' , 'Comments.Users' ]]
);
Modifying Request Data Before Building Entities
If you need to modify request data before it is converted into entities, you can use the Model.beforeMarshal
event. This event lets you manipulate the request data just before entities are created:
// Include use statements at the top of your file.
use
Cake\Event\Event;
use
ArrayObject;
// In a table or behavior class
public function
beforeMarshal (Event $event , ArrayObject $data , ArrayObject $options )
{
if
( isset ( $data [ 'username' ])) {
$data [ 'username' ] = mb_strtolower ( $data [ 'username' ]);
}
}
The $data parameter is an ArrayObject instance, so you don’t have to return it to change the data used to create entities.
The main purpose of beforeMarshal is to assist the users to pass the validation process when simple mistakes can be automatically resolved, or when data needs to be restructured so it can be put into the right fields.
The Model.beforeMarshal event is triggered just at the start of the validation process, one of the reasons is that beforeMarshal is allowed to change the validation rules and the saving options, such as the field whitelist.
Validation is triggered just after this event is finished. A common example of changing the data before it is validated is trimming all fields before saving:
More Information 471
CakePHP Cookbook Documentation, Release 3.5
// Include use statements at the top of your file.
use
Cake\Event\Event;
use
ArrayObject;
// In a table or behavior class
public function
beforeMarshal (Event $event , ArrayObject $data , ArrayObject $options )
{
foreach
( $data
as
$key => $value ) {
if
( is_string ( $value )) {
$data [ $key ] = trim ( $value );
}
}
}
Because of how the marshalling process works, if a field does not pass validation it will automatically be removed from the data array and not be copied into the entity. This is to prevent inconsistent data from entering the entity object.
Moreover, the data in beforeMarshal is a copy of the passed data. This is because it is important to preserve the original user input, as it may be used elsewhere.
Validating Data Before Building Entities
The
Validating Data
chapter has more information on how to use the validation features of CakePHP to ensure your data stays correct and consistent.
Avoiding Property Mass Assignment Attacks
When creating or merging entities from request data you need to be careful of what you allow your users to change or add in the entities. For example, by sending an array in the request containing the user_id an attacker could change the owner of an article, causing undesirable effects:
// Contains ['user_id' => 100, 'title' => 'Hacked!'];
$data = $this -> request -> getData ();
$entity = $this -> patchEntity ( $entity , $data );
$this -> save ( $entity );
There are two ways of protecting you against this problem. The first one is by setting the default columns that can be safely set from a request using the
Mass Assignment
feature in the entities.
The second way is by using the fieldList option when creating or merging data into an entity:
// Contains ['user_id' => 100, 'title' => 'Hacked!'];
$data = $this -> request -> getData ();
// Only allow title to be changed
$entity = $this -> patchEntity ( $entity , $data , [
'fieldList' => [ 'title' ]
]);
$this -> save ( $entity );
You can also control which properties can be assigned for associations:
// Only allow changing the title and tags
// and the tag name is the only column that can be set
472 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$entity = $this -> patchEntity ( $entity , $data , [
'fieldList' => [ 'title' , 'tags' ],
'associated' => [ 'Tags' => [ 'fieldList' => [ 'name' ]]]
]);
$this -> save ( $entity );
Using this feature is handy when you have many different functions your users can access and you want to let your users edit different data based on their privileges.
The fieldList options is also accepted by the newEntity(), newEntities() and patchEntities() methods.
Saving Entities
Cake\ORM\Table::save(
Entity $entity , array $options = [])
When saving request data to your database you need to first hydrate a new entity using newEntity() for passing into save(). For example:
// In a controller
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> newEntity ( $this -> request -> getData ());
if
( $articles -> save ( $article )) {
// ...
}
The ORM uses the isNew() method on an entity to determine whether or not an insert or update should be performed.
If the isNew() method returns true and the entity has a primary key value, an ‘exists’ query will be issued. The
‘exists’ query can be suppressed by passing 'checkExisting' => false in the $options argument:
$articles -> save ( $article , [ 'checkExisting' =>
false
]);
Once you’ve loaded some entities you’ll probably want to modify them and update your database. This is a pretty simple exercise in CakePHP:
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> find ( 'all' ) -> where ([ 'id' => 2 ]) -> first ();
$article -> title = 'My new title' ;
$articles -> save ( $article );
When saving, CakePHP will
apply your rules
, and wrap the save operation in a database transaction. It will also only
update properties that have changed. The above save() call would generate SQL like:
UPDATE articles SET title = 'My new title' WHERE id = 2 ;
If you had a new entity, the following SQL would be generated:
INSERT INTO articles (title) VALUES ( 'My new title' );
When an entity is saved a few things happen:
1. Rule checking will be started if not disabled.
2. Rule checking will trigger the Model.beforeRules event. If this event is stopped, the save operation will fail and return false.
More Information 473
CakePHP Cookbook Documentation, Release 3.5
3. Rules will be checked. If the entity is being created, the create rules will be used. If the entity is being updated, the update rules will be used.
4. The Model.afterRules event will be triggered.
5. The Model.beforeSave event is dispatched. If it is stopped, the save will be aborted, and save() will return false
.
6. Parent associations are saved. For example, any listed belongsTo associations will be saved.
7. The modified fields on the entity will be saved.
8. Child associations are saved. For example, any listed hasMany, hasOne, or belongsToMany associations will be saved.
9. The Model.afterSave event will be dispatched.
10. The Model.afterSaveCommit event will be dispatched.
The following diagram illustrates the above process:
See
Applying
the
Application
Rules
section for more information on creating and rules.
using
Warning:
If no changes are made to entity the when it is saved, the callbacks will not fire because no save is performed.
The save() method will return modified the entity on success, and false on failure.
You can disable
474 Chapter 12. Database Access & ORM
More Information
CakePHP Cookbook Documentation, Release 3.5
rules and/or transactions using the
$options argument for save:
Saving Associations
When you are saving an entity, you can also elect to save some or all of the associated entities.
By default first all level entities will be saved.
For example saving an
Article, will also automatically update any dirty entities that are directly related to articles table.
You
475
/
˓→
/
˓→
˓→
In
˓→ a
˓→ controller
˓→ or
˓→ table
˓→ method.
$articles -
˓→
>
˓→ save (
˓→
$article ,
˓→
[
˓→
'checkRules
˓→
' =>
˓→
false
,
˓→
'atomic
˓→
' =>
˓→
false
]);
CakePHP Cookbook Documentation, Release 3.5
option:
// In a controller.
// Only save the comments association
$articles -> save ( $entity , [ 'associated' => [ 'Comments' ]]);
You can define save distant or deeply nested associations by using dot notation:
// Save the company, the employees and related addresses for each of them.
$companies -> save ( $entity , [ 'associated' => [ 'Employees.Addresses' ]]);
Moreover, you can combine the dot notation for associations with the options array:
$companies -> save ( $entity , [
'associated' => [
'Employees' ,
'Employees.Addresses'
]
]);
Your entities should be structured in the same way as they are when loaded from the database. See the form helper documentation for
how to build inputs for associations
If you are building or modifying association data after building your entities you will have to mark the association property as modified with dirty():
$company -> author -> name = 'Master Chef' ;
$company -> dirty ( 'author' ,
true
); can fine tune which associations are saved by using the associated
Saving BelongsTo Associations
When saving belongsTo associations, the ORM expects a single nested entity named with the singular,
underscored
version of the association name. For example:
// In a controller.
$data = [
'title' => 'First Post' ,
'user' => [
476 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
'id' => 1 ,
'username' => 'mark'
]
];
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> newEntity ( $data , [
'associated' => [ 'Users' ]
]);
$articles -> save ( $article );
Saving HasOne Associations
When saving hasOne associations, the ORM expects a single nested entity named with the singular,
underscored
version of the association name. For example:
// In a controller.
$data = [
'id' => 1 ,
'username' => 'cakephp' ,
'profile' => [
'twitter' => '@cakephp'
]
];
$users = TableRegistry :: get ( 'Users' );
$user = $users -> newEntity ( $data , [
'associated' => [ 'Profiles' ]
]);
$users -> save ( $user );
Saving HasMany Associations
When saving hasMany associations, the ORM expects an array of entities named with the plural,
underscored
version of the association name. For example:
// In a controller.
$data = [
'title' => 'First Post' ,
'comments' => [
[ 'body' => 'Best post ever' ],
[ 'body' => 'I really like this.' ]
]
];
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> newEntity ( $data , [
'associated' => [ 'Comments' ]
]);
$articles -> save ( $article );
When saving hasMany associations, associated records will either be updated, or inserted. For the case that the record already has associated records in the database, you have the choice between two saving strategies: append Associated records are updated in the database or, if not matching any existing record, inserted.
More Information 477
CakePHP Cookbook Documentation, Release 3.5
replace Any existing records that do not match the records provided will be deleted from the database. Only provided records will remain (or be inserted).
By default the append saving strategy is used.
See
HasMany Associations
for details on defining the saveStrategy .
Whenever you add new records into an existing association you should always mark the association property as ‘dirty’.
This lets the ORM know that the association property has to be persisted:
$article -> comments [] = $comment ;
$article -> dirty ( 'comments' ,
true
);
Without the call to dirty() the updated comments will not be saved.
Saving BelongsToMany Associations
When saving belongsToMany associations, the ORM expects an array of entities named with the plural,
underscored
version of the association name. For example:
// In a controller.
$data = [
'title' => 'First Post' ,
'tags' => [
[ 'tag' => 'CakePHP' ],
[ 'tag' => 'Framework' ]
]
];
$articles = TableRegistry :: get ( 'Articles' );
$article = $articles -> newEntity ( $data , [
'associated' => [ 'Tags' ]
]);
$articles -> save ( $article );
When converting request data into entities, the newEntity() and newEntities() methods will handle both arrays of properties, as well as a list of ids at the _ids key. Using the _ids key makes it easy to build a select box or checkbox based form controls for belongs to many associations. See the
Converting Request Data into Entities
section for more information.
When saving belongsToMany associations, you have the choice between two saving strategies: append Only new links will be created between each side of this association. This strategy will not destroy existing links even though they may not be present in the array of entities to be saved.
replace When saving, existing links will be removed and new links will be created in the junction table. If there are existing link in the database to some of the entities intended to be saved, those links will be updated, not deleted and then re-saved.
See
BelongsToMany Associations
for details on defining the saveStrategy.
By default the replace strategy is used. Whenever you add new records into an existing association you should always mark the association property as ‘dirty’. This lets the ORM know that the association property has to be persisted:
$article -> tags [] = $tag ;
$article -> dirty ( 'tags' ,
true
);
Without the call to dirty() the updated tags will not be saved.
478 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Often you’ll find yourself wanting to make an association between two existing entities, eg. a user coauthoring an article. This is done by using the method link(), like this:
$article = $this -> Articles -> get ( $articleId );
$user = $this -> Users -> get ( $userId );
$this -> Articles -> Users -> link ( $article , [ $user ]);
When saving belongsToMany Associations, it can be relevant to save some additional data to the junction Table. In the previous example of tags, it could be the vote_type of person who voted on that article. The vote_type can be either upvote or downvote and is represented by a string. The relation is between Users and Articles.
Saving that association, and the vote_type is done by first adding some data to _joinData and then saving the association with link(), example:
$article = $this -> Articles -> get ( $articleId );
$user = $this -> Users -> get ( $userId );
$user -> _joinData =
new
Entity([ 'vote_type' => $voteType ], [ 'markNew' =>
true
]);
$this -> Articles -> Users -> link ( $article , [ $user ]);
Saving Additional Data to the Join Table
In some situations the table joining your BelongsToMany association, will have additional columns on it. CakePHP makes it simple to save properties into these columns. Each entity in a belongsToMany association has a _joinData property that contains the additional columns on the junction table. This data can be either an array or an Entity instance. For example if Students BelongsToMany Courses, we could have a junction table that looks like: id | student_id | course_id | days_attended | grade
When saving data you can populate the additional columns on the junction table by setting data to the _joinData property:
$student -> courses [ 0 ] -> _joinData -> grade = 80.12
;
$student -> courses [ 0 ] -> _joinData -> days_attended = 30 ;
$studentsTable -> save ( $student );
The _joinData property can be either an entity, or an array of data if you are saving entities built from request data.
When saving junction table data from request data your POST data should look like:
$data = [
'first_name' => 'Sally' ,
'last_name' => 'Parker' ,
'courses' => [
[
'id' => 10 ,
'_joinData' => [
'grade' => 80.12
,
'days_attended' => 30
]
],
// Other courses.
]
];
$student = $this -> Students -> newEntity ( $data , [
More Information 479
CakePHP Cookbook Documentation, Release 3.5
]);
'associated' => [ 'Courses._joinData' ]
See the
Creating Inputs for Associated Data
documentation for how to build inputs with FormHelper correctly.
Saving Complex Types
Tables are capable of storing data represented in basic types, like strings, integers, floats, booleans, etc. But It can also be extended to accept more complex types such as arrays or objects and serialize this data into simpler types that can be saved in the database.
This functionality is achieved by using the custom types system. See the
Adding Custom Types
section to find out how to build custom column Types:
// In config/bootstrap.php
use
Cake\Database\Type;
Type :: map ( 'json' , 'Cake\Database\Type\JsonType' );
// In src/Model/Table/UsersTable.php
use
Cake\Database\Schema\TableSchema;
class UsersTable extends
Table
{
protected function
_initializeSchema (TableSchema $schema )
{
$schema -> columnType ( 'preferences' , 'json' );
return
$schema ;
}
}
The code above maps the preferences column to the json custom type. This means that when retrieving data for that column, it will be unserialized from a JSON string in the database and put into an entity as an array.
Likewise, when saved, the array will be transformed back into its JSON representation:
$user =
new
User([
'preferences' => [
'sports' => [ 'football' , 'baseball' ],
'books' => [ 'Mastering PHP' , 'Hamlet' ]
]
]);
$usersTable -> save ( $user );
When using complex types it is important to validate that the data you are receiving from the end user is the correct type. Failing to correctly handle complex data could result in malicious users being able to store data they would not normally be able to.
Strict Saving
Cake\ORM\Table::saveOrFail(
$entity , $options = [])
Using this method will throw an
Cake\ORM\Exception\PersistenceFailedException
if:
• the application rules checks failed
480 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• the entity contains errors
• the save was aborted by a callback.
Using this can be helpful when you performing complex database operations without human monitoring, for example, inside a Shell task.
Note: If you use this method in a controller, be sure to catch the PersistenceFailedException that could be raised.
If you want to track down the entity that failed to save, you can use the
Cake\ORMException\PersistenceFailedException::getEntity() method:
try
{
$table -> saveOrFail ( $entity );
}
catch
(\Cake\ORM\Exception\PersistenceFailedException $e ) {
echo
$e -> getEntity ();
}
As this internally perfoms a
Cake\ORM\Table::save()
call, all corresponding save events will be triggered.
New in version 3.4.1.
Saving Multiple Entities
Cake\ORM\Table::saveMany(
$entities , $options = [])
Using this method you can save multiple entities atomically. $entities can be an array of entities created using newEntities()
/ patchEntities(). $options can have the same options as accepted by save():
$data = [
[
'title' => 'First post' ,
'published' => 1
],
[
'title' => 'Second post' ,
'published' => 1
],
];
$articles = TableRegistry :: get ( 'Articles' );
$entities = $articles -> newEntities ( $data );
$result = $articles -> saveMany ( $entities );
The result will be updated entities on success or false on failure.
New in version 3.2.8.
Bulk Updates
Cake\ORM\Table::updateAll(
$fields , $conditions)
There may be times when updating rows individually is not efficient or necessary. In these cases it is more efficient to use a bulk-update to modify many rows at once:
More Information 481
CakePHP Cookbook Documentation, Release 3.5
// Publish all the unpublished articles.
function
publishAllUnpublished ()
{
$this -> updateAll (
[ 'published' =>
true
], // fields
[ 'published' =>
false
]); // conditions
}
If you need to do bulk updates and use SQL expressions, you will need to use an expression object as updateAll() uses prepared statements under the hood:
use
Cake\Database\Expression\QueryExpression;
...
function
incrementCounters ()
{
$expression =
new
QueryExpression( 'view_count = view_count + 1' );
$this -> updateAll ([ $expression ], [ 'published' =>
true
]);
}
A bulk-update will be considered successful if 1 or more rows are updated.
Warning: updateAll will not trigger beforeSave/afterSave events. If you need those first load a collection of records and update them.
updateAll() is for convenience only. You can use this more flexible interface as well:
// Publish all the unpublished articles.
function
publishAllUnpublished ()
{
$this -> query ()
-> update ()
-> set ([ 'published' =>
true
])
-> where ([ 'published' =>
false
])
-> execute ();
}
Also see:
Updating Data
Deleting Data
class Cake\ORM\Table
Cake\ORM\Table::delete(
Entity $entity , $options = [])
Once you’ve loaded an entity you can delete it by calling the originating table’s delete method:
// In a controller.
$entity = $this -> Articles -> get ( 2 );
$result = $this -> Articles -> delete ( $entity );
When deleting entities a few things happen:
1. The
delete rules
will be applied. If the rules fail, deletion will be prevented.
482 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
2. The Model.beforeDelete event is triggered. If this event is stopped, the delete will be aborted and the event’s result will be returned.
3. The entity will be deleted.
4. All dependent associations will be deleted. If associations are being deleted as entities, additional events will be dispatched.
5. Any junction table records for BelongsToMany associations will be removed.
6. The Model.afterDelete event will be triggered.
By default all deletes happen within a transaction. You can disable the transaction with the atomic option:
$result = $this -> Articles -> delete ( $entity , [ 'atomic' =>
false
]);
Cascading Deletes
When deleting entities, associated data can also be deleted. If your HasOne and HasMany associations are configured as dependent, delete operations will ‘cascade’ to those entities as well. By default entities in associated tables are removed using
Cake\ORM\Table::deleteAll()
. You can elect to have the ORM load related entities, and
delete them individually by setting the cascadeCallbacks option to true. A sample HasMany association with both these options enabled would be:
// In a Table's initialize method.
$this -> hasMany ( 'Comments' , [
'dependent' =>
true
,
'cascadeCallbacks' =>
true
,
]);
Note: Setting cascadeCallbacks to true, results in considerably slower deletes when compared to bulk deletes.
The cascadeCallbacks option should only be enabled when your application has important work handled by event listeners.
Bulk Deletes
Cake\ORM\Table::deleteAll(
$conditions)
There may be times when deleting rows one by one is not efficient or useful. In these cases it is more performant to use a bulk-delete to remove many rows at once:
// Delete all the spam
function
destroySpam ()
{
return
$this -> deleteAll ([ 'is_spam' =>
true
]);
}
A bulk-delete will be considered successful if 1 or more rows are deleted.
Warning: deleteAll will not trigger beforeDelete/afterDelete events. If you need those first load a collection of records and delete them.
More Information 483
CakePHP Cookbook Documentation, Release 3.5
Strict Deletes
Cake\ORM\Table::deleteOrFail(
$entity , $options = [])
Using this method will throw an
Cake\ORM\Exception\PersistenceFailedException
if :
• the entity is new
• the entity has no primary key value
• application rules checks failed
• the delete was aborted by a callback.
If you want to track down the entity that failed to save, you can use the
Cake\ORMException\PersistenceFailedException::getEntity() method:
try
{
$table -> deleteOrFail ( $entity );
}
catch
(\Cake\ORM\Exception\PersistenceFailedException $e ) {
echo
$e -> getEntity ();
}
As this internally performs a
Cake\ORM\Table::delete()
call, all corresponding delete events will be triggered.
New in version 3.4.1.
Associations - Linking Tables Together
Defining relations between different objects in your application should be a natural process. For example, an article may have many comments, and belong to an author. Authors may have many articles and comments. CakePHP makes managing these associations easy. The four association types in CakePHP are: hasOne, hasMany, belongsTo, and belongsToMany.
Relationship Association Type Example one to one hasOne A user has one profile.
one to many hasMany A user can have multiple articles.
many to one belongsTo many to many belongsToMany
Many articles belong to a user.
Tags belong to many articles.
Associations are defined during the initialize() method of your table object. Methods matching the association type allow you to define the associations in your application. For example if we wanted to define a belongsTo association in our ArticlesTable:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsTo ( 'Authors' );
}
}
484 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
The simplest form of any association setup takes the table alias you want to associate with. By default all of the details of an association will use the CakePHP conventions. If you want to customize how your associations are handled you can modify them with setters:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsTo ( 'Authors' , [
'className' => 'Publishing.Authors'
])
-> setForeignKey ( 'authorid' )
-> setProperty ( 'person' );
}
}
You can also use arrays to customize your associations:
$this -> belongsTo ( 'Authors' , [
'className' => 'Publishing.Authors' ,
'foreignKey' => 'authorid' ,
'propertyName' => 'person'
]);
Arrays however do not offer the typehinting and autocomplete benefit the fluent interface does.
The same table can be used multiple times to define different types of associations. For example consider a case where you want to separate approved comments and those that have not been moderated yet:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> hasMany ( 'Comments' )
-> setConditions ([ 'approved' =>
true
]);
$this -> hasMany ( 'UnapprovedComments' , [
'className' => 'Comments'
])
-> setConditions ([ 'approved' =>
false
])
-> setProperty ( 'unapproved_comments' );
}
}
As you can see, by specifying the className key, it is possible to use the same table as different associations for the same table. You can even create self-associated tables to create parent-child relationships:
class CategoriesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> hasMany ( 'SubCategories' , [
'className' => 'Categories'
]);
$this -> belongsTo ( 'ParentCategories' , [
'className' => 'Categories'
]);
More Information 485
CakePHP Cookbook Documentation, Release 3.5
}
}
You can also setup associations in mass by making a single call to Table::addAssociations() which accepts an array containing a set of table names indexed by association type as an argument:
class PostsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addAssociations ([
'belongsTo' => [
'Users' => [ 'className' => 'App\Model\Table\UsersTable' ]
],
'hasMany' => [ 'Comments' ],
'belongsToMany' => [ 'Tags' ]
]);
}
}
Each association type accepts multiple associations where the keys are the aliases, and the values are association config data. If numeric keys are used the values will be treated as association aliases.
HasOne Associations
Let’s set up a Users Table with a hasOne relationship to an Addresses Table.
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 addresses table will contain a field called user_id.
The basic pattern is: hasOne: the other model contains the foreign key.
Relation Schema
Users hasOne Addresses addresses.user_id
Doctors hasOne Mentors mentors.doctor_id
Note: It is not mandatory to follow CakePHP conventions, you can override the use of any foreignKey in your associations definitions. Nevertheless sticking to conventions will make your code less repetitive, easier to read and to maintain.
If we had the UsersTable and AddressesTable classes made we could make the association with the following code:
class UsersTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> hasOne ( 'Addresses' );
}
}
If you need more control, you can define your associations using the setters. For example, you might want to limit the association to include only certain records:
486 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
class UsersTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> hasOne ( 'Addresses' )
-> setName ( 'Addresses' )
-> setConditions ([ 'Addresses.primary' => '1' ])
-> setDependent (
true
);
}
}
Possible keys for hasOne association arrays include:
• className: the class name of the table being associated to the current model. If you’re defining a ‘User hasOne
Address’ relationship, the className key should equal ‘Addresses’.
• foreignKey: the name of the foreign key found in the other table. 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’.
• bindingKey: The name of the column in the current table, that will be used for matching the foreignKey. If not specified, the primary key (for example the id column of the Users table) will be used.
• conditions: an array of find() compatible conditions such as ['Addresses.primary' => true]
• joinType: the type of the join to use in the SQL query, default is LEFT. You can use INNER if your hasOne association is always present.
• dependent: When the dependent key is set to true, and an entity is deleted, the associated model records are also deleted. In this case we set it to true so that deleting a User will also delete her associated Address.
• cascadeCallbacks: When this and dependent are true, cascaded deletes will load and delete entities so that callbacks are properly triggered. When false, deleteAll() is used to remove associated data and no callbacks are triggered.
• propertyName: The property name that should be filled with data from the associated table into the source table results. By default this is the underscored & singular name of the association so address in our example.
• strategy: Defines the query strategy to use. Defaults to ‘join’. The other valid value is ‘select’, which utilizes a separate query instead.
• finder: The finder method to use when loading associated records.
Once this association has been defined, find operations on the Users table can contain the Address record if it exists:
// In a controller or table method.
$query = $users -> find ( 'all' ) -> contain ([ 'Addresses' ]);
foreach
( $query
as
$user ) {
echo
$user -> address -> street ;
}
The above would emit SQL that is similar to:
SELECT
*
FROM users INNER JOIN addresses ON addresses .
user_id = users .
id;
BelongsTo Associations
Now that we have Address data access from the User table, let’s define a belongsTo association in the Addresses table 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 related data from the other direction.
More Information 487
CakePHP Cookbook Documentation, Release 3.5
When keying your database tables for a belongsTo relationship, follow this convention: belongsTo: the current model contains the foreign key.
Relation Schema
Addresses belongsTo Users addresses.user_id
Mentors belongsTo Doctors mentors.doctor_id
Tip: If a Table contains a foreign key, it belongs to the other Table.
We can define the belongsTo association in our Addresses table as follows:
class AddressesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsTo ( 'Users' );
}
}
We can also define a more specific relationship using the setters:
class AddressesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsTo ( 'Users' )
-> setForeignKey ( 'user_id' )
-> setJoinType ( 'INNER' );
}
}
Possible keys for belongsTo association arrays include:
• className: the class name of the model being associated to the current model. If you’re defining a ‘Profile belongsTo User’ relationship, the className key should equal ‘Users’.
• foreignKey: the name of the foreign key found in the current table. This is especially handy if you need to define multiple belongsTo relationships to the same model. The default value for this key is the underscored, singular name of the other model, suffixed with _id.
• bindingKey: The name of the column in the other table, that will be used for matching the foreignKey. If not specified, the primary key (for example the id column of the Users table) will be used.
• conditions: an array of find() compatible conditions or SQL strings such as ['Users.active' => true]
• joinType: the type of the join to use in the SQL query, default is LEFT which may not fit your needs in all situations, INNER may be helpful when you want everything from your main and associated models or nothing at all.
• propertyName: The property name that should be filled with data from the associated table into the source table results. By default this is the underscored & singular name of the association so user in our example.
• strategy: Defines the query strategy to use. Defaults to ‘join’. The other valid value is ‘select’, which utilizes a separate query instead.
• finder: The finder method to use when loading associated records.
488 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Once this association has been defined, find operations on the Addresses table can contain the User record if it exists:
// In a controller or table method.
$query = $addresses -> find ( 'all' ) -> contain ([ 'Users' ]);
foreach
( $query
as
$address ) {
echo
$address -> user -> username ;
}
The above would emit SQL that is similar to:
SELECT
*
FROM addresses LEFT JOIN users ON addresses .
user_id = users .
id;
HasMany Associations
An example of a hasMany association is “Article hasMany Comments”. Defining this association will allow us to fetch an article’s comments when the article is loaded.
When creating your database tables for a hasMany relationship, follow this convention: hasMany: the other model contains the foreign key.
Relation Schema
Article hasMany Comment Comment.article_id
Product hasMany Option
Doctor hasMany Patient
Option.product_id
Patient.doctor_id
We can define the hasMany association in our Articles model as follows:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> hasMany ( 'Comments' );
}
}
We can also define a more specific relationship using the setters:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> hasMany ( 'Comments' )
-> setForeignKey ( 'article_id' )
-> setDependent (
true
);
}
}
Sometimes you may want to configure composite keys in your associations:
// Within ArticlesTable::initialize() call
$this -> hasMany ( 'Reviews' )
-> setForeignKey ([
'article_id' ,
'article_hash'
]);
More Information 489
CakePHP Cookbook Documentation, Release 3.5
Relying on the example above, we have passed an array containing the desired composite keys to setForeignKey()
. By default the bindingKey would be automatically defined as id and hash respectively, but let’s assume that you need to specify different binding fields than the defaults, you can setup it manually with setBindingKey() :
// Within ArticlesTable::initialize() call
$this -> hasMany ( 'Reviews' )
-> setForeignKey ([
'article_id' ,
'article_hash'
])
-> setBindingKey ([
'whatever_id' ,
'whatever_hash'
]);
It is important to note that foreignKey values refers to the reviews table and bindingKey values refers to the articles table.
Possible keys for hasMany association arrays include:
• className: the class name of the model being associated to the current model. If you’re defining a ‘User hasMany Comment’ relationship, the className key should equal ‘Comments’.
• foreignKey: the name of the foreign key found in the other table. 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’.
• bindingKey: The name of the column in the current table, that will be used for matching the foreignKey. If not specified, the primary key (for example the id column of the Articles table) will be used.
• conditions: an array of find() compatible conditions or SQL strings such as ['Comments.visible' => true]
• sort: an array of find() compatible order clauses or SQL strings such as ['Comments.created' =>
'ASC']
• dependent: When dependent is set to true, recursive model deletion is possible. In this example, Comment records will be deleted when their associated Article record has been deleted.
• cascadeCallbacks: When this and dependent are true, cascaded deletes will load and delete entities so that callbacks are properly triggered. When false, deleteAll() is used to remove associated data and no callbacks are triggered.
• propertyName: The property name that should be filled with data from the associated table into the source table results. By default this is the underscored & plural name of the association so comments in our example.
• strategy: Defines the query strategy to use. Defaults to ‘select’. The other valid value is ‘subquery’, which replaces the IN list with an equivalent subquery.
• saveStrategy: Either ‘append’ or ‘replace’. Defaults to ‘append’. When ‘append’ the current records are appended to any records in the database. When ‘replace’ associated records not in the current set will be removed. If the foreign key is a null able column or if dependent is true records will be orphaned.
• finder: The finder method to use when loading associated records.
Once this association has been defined, find operations on the Articles table can contain the Comment records if they exist:
// In a controller or table method.
$query = $articles -> find ( 'all' ) -> contain ([ 'Comments' ]);
foreach
( $query
as
$article ) {
490 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
echo
$article -> comments [ 0 ] -> text ;
}
The above would emit SQL that is similar to:
SELECT
*
FROM articles;
SELECT
*
FROM comments WHERE article_id IN ( 1 , 2 , 3 , 4 , 5 );
When the subquery strategy is used, SQL similar to the following will be generated:
SELECT
*
FROM articles;
SELECT
*
FROM comments WHERE article_id IN (SELECT id FROM articles);
You may want to cache the counts for your hasMany associations. This is useful when you often need to show the number of associated records, but don’t want to load all the records just to count them. For example, the comment count on any given article is often cached to make generating lists of articles more efficient. You can use the
Counter-
CacheBehavior
to cache counts of associated records.
You should make sure that your database tables do not contain columns that match association property names. If for example you have counter fields that conflict with association properties, you must either rename the association property, or the column name.
BelongsToMany Associations
Note: In 3.0 and onward hasAndBelongsToMany / HABTM has been renamed to belongsToMany / BTM.
An example of a BelongsToMany association is “Article BelongsToMany Tags”, where the tags from one article are shared with other articles. BelongsToMany is often referred to as “has and belongs to many”, and is a classic “many to many” association.
The main difference between hasMany and BelongsToMany is that the link between the models in a BelongsToMany association are not exclusive. For example, we are joining our Articles table with a Tags table. Using ‘funny’ as a Tag for my Article, doesn’t “use up” the tag. I can also use it on the next article I write.
Three database tables are required for a BelongsToMany association. In the example above we would need tables for articles
, tags and articles_tags. The articles_tags table contains the data that links tags and articles together. The joining table is named after the two tables involved, separated with an underscore by convention. In its simplest form, this table consists of article_id and tag_id.
belongsToMany requires a separate join table that includes both model names.
Relationship
Article belongsToMany Tag
Join Table Fields articles_tags.id, articles_tags.tag_id, articles_tags.article_id
Patient belongsToMany Doctor doctors_patients.id, doctors_patients.doctor_id, doctors_patients.patient_id.
We can define the belongsToMany association in both our models as follows:
// In src/Model/Table/ArticlesTable.php
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsToMany ( 'Tags' );
}
}
More Information 491
CakePHP Cookbook Documentation, Release 3.5
// In src/Model/Table/TagsTable.php
class TagsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsToMany ( 'Articles' );
}
}
We can also define a more specific relationship using configuration:
// In src/Model/Table/TagsTable.php
class TagsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsToMany ( 'Articles' , [
'joinTable' => 'articles_tags' ,
]);
}
}
Possible keys for belongsToMany association arrays include:
• className: the class name of the model being associated to the current model. If you’re defining a ‘Article belongsToMany Tag’ relationship, the className key should equal ‘Tags’.
• joinTable: The name of the join table used in this association (if the current table doesn’t adhere to the naming convention for belongsToMany join tables). By default this table name will be used to load the Table instance for the join table.
• foreignKey: The name of the foreign key that references the current model found on the join table, or list in case of composite foreign keys. This is especially handy if you need to define multiple belongsToMany relationships.
The default value for this key is the underscored, singular name of the current model, suffixed with ‘_id’.
• bindingKey: The name of the column in the current table, that will be used for matching the foreignKey.
Defaults to the primary key.
• targetForeignKey: The name of the foreign key that references the target model found on the join model, or list in case of composite foreign keys. The default value for this key is the underscored, singular name of the target model, suffixed with ‘_id’.
• conditions: an array of find() compatible conditions. If you have conditions on an associated table, you should use a ‘through’ model, and define the necessary belongsTo associations on it.
• sort: an array of find() compatible order clauses.
• dependent: When the dependent key is set to false, and an entity is deleted, the data of the join table will not be deleted.
• through: Allows you to provide either the name of the Table instance you want used on the join table, or the instance itself. This makes customizing the join table keys possible, and allows you to customize the behavior of the pivot table.
• cascadeCallbacks: When this is true, cascaded deletes will load and delete entities so that callbacks are properly triggered on join table records. When false, deleteAll() is used to remove associated data and no callbacks are triggered. This defaults to false to help reduce overhead.
492 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• propertyName: The property name that should be filled with data from the associated table into the source table results. By default this is the underscored & plural name of the association, so tags in our example.
• strategy: Defines the query strategy to use. Defaults to ‘select’. The other valid value is ‘subquery’, which replaces the IN list with an equivalent subquery.
• saveStrategy: Either ‘append’ or ‘replace’. Defaults to ‘replace’. Indicates the mode to be used for saving associated entities. The former will only create new links between both side of the relation and the latter will do a wipe and replace to create the links between the passed entities when saving.
• finder: The finder method to use when loading associated records.
Once this association has been defined, find operations on the Articles table can contain the Tag records if they exist:
// In a controller or table method.
$query = $articles -> find ( 'all' ) -> contain ([ 'Tags' ]);
foreach
( $query
as
$article ) {
echo
$article -> tags [ 0 ] -> text ;
}
The above would emit SQL that is similar to:
SELECT
*
FROM articles;
SELECT
*
FROM tags
INNER JOIN articles_tags ON (
); tags .
id = article_tags .
tag_id
AND
article_id IN ( 1 , 2 , 3 , 4 , 5 )
When the subquery strategy is used, SQL similar to the following will be generated:
SELECT
*
FROM articles;
SELECT
*
FROM tags
INNER JOIN articles_tags ON (
); tags .
id = article_tags .
tag_id
AND
article_id IN (SELECT id FROM articles)
Using the ‘through’ Option
If you plan on adding extra information to the join/pivot table, or if you need to use join columns outside of the conventions, you will need to define the through option. The through option provides you full control over how the belongsToMany association will be created.
It is sometimes desirable to store additional data with a many to many association. Consider the following:
Student BelongsToMany Course
Course BelongsToMany Student
A Student can take many Courses and a Course can be taken by many Students. This is a simple many to many association. The following table would suffice: id | student_id | course_id
Now what if we want to store the number of days that were attended by the student on the course and their final grade?
The table we’d want would be:
More Information 493
CakePHP Cookbook Documentation, Release 3.5
id | student_id | course_id | days_attended | grade
The way to implement our requirement is to use a join model, otherwise known as a hasMany through association.
That is, the association is a model itself. So, we can create a new model CoursesMemberships. Take a look at the following models:
class StudentsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsToMany ( 'Courses' , [
'through' => 'CoursesMemberships' ,
]);
}
}
class CoursesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsToMany ( 'Students' , [
'through' => 'CoursesMemberships' ,
]);
}
}
class CoursesMembershipsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> belongsTo ( 'Students' );
$this -> belongsTo ( 'Courses' );
}
}
The CoursesMemberships join table uniquely identifies a given Student’s participation on a Course in addition to extra meta-information.
Default Association Conditions
The finder option allows you to use a
custom finder
to load associated record data. This lets you encapsulate your queries better and keep your code DRY’er. There are some limitations when using finders to load data in associations that are loaded using joins (belongsTo/hasOne). Only the following aspects of the query will be applied to the root query:
• WHERE conditions.
• Additional joins.
• Contained associations.
Other aspects of the query, such as selected columns, order, group by, having and other sub-statements, will not be applied to the root query. Associations that are not loaded through joins (hasMany/belongsToMany), do not have the above restrictions and can also use result formatters or map/reduce functions.
494 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Loading Associations
Once you’ve defined your associations you can
eager load associations
when fetching results.
Behaviors
Behaviors are a way to organize and enable horizontal re-use of Model layer logic. Conceptually they are similar to traits. However, behaviors are implemented as separate classes. This allows them to hook into the life-cycle callbacks that models emit, while providing trait-like features.
Behaviors provide a convenient way to package up behavior that is common across many models. For example,
CakePHP includes a TimestampBehavior. Many models will want timestamp fields, and the logic to manage these fields is not specific to any one model. It is these kinds of scenarios that behaviors are a perfect fit for.
Using Behaviors
Behaviors provide an easy way to create horizontally re-usable pieces of logic related to table classes. You may be wondering why behaviors are regular classes and not traits. The primary reason for this is event listeners. While traits would allow for re-usable pieces of logic, they would complicate binding events.
To add a behavior to your table you can call the addBehavior() method. Generally the best place to do this is in the initialize() method:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Timestamp' );
}
}
As with associations, you can use
plugin syntax
and provide additional configuration options:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Timestamp' , [
'events' => [
'Model.beforeSave' => [
'created_at' => 'new' ,
'modified_at' => 'always'
]
]
]);
}
}
More Information 495
CakePHP Cookbook Documentation, Release 3.5
Core Behaviors
CounterCache
class Cake\ORM\Behavior\CounterCacheBehavior
Often times web applications need to display counts of related objects. For example, when showing a list of articles you may want to display how many comments it has. Or when showing a user you might want to show how many friends/followers she has. The CounterCache behavior is intended for these situations. CounterCache will update a field in the associated models assigned in the options when it is invoked. The fields should exist in the database and be of the type INT.
Basic Usage
You enable the CounterCache behavior like any other behavior, but it won’t do anything until you configure some relations and the field counts that should be stored on each of them. Using our example below, we could cache the comment count for each article with the following:
class CommentsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'CounterCache' , [
'Articles' => [ 'comment_count' ]
]);
}
}
The CounterCache configuration should be a map of relation names and the specific configuration for that relation.
The counter’s value will be updated each time an entity is saved or deleted. The counter will not be updated when you use updateAll() or deleteAll(), or execute SQL you have written.
Advanced Usage
If you need to keep a cached counter for less than all of the related records, you can supply additional conditions or finder methods to generate a counter value:
// Use a specific find method.
// In this case find(published)
$this -> addBehavior ( 'CounterCache' , [
'Articles' => [
'comment_count' => [
'finder' => 'published'
]
]
]);
If you don’t have a custom finder method you can provide an array of conditions to find records instead:
$this -> addBehavior ( 'CounterCache' , [
'Articles' => [
'comment_count' => [
'conditions' => [ 'Comments.spam' =>
false
]
]
496 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
]);
]
If you want CounterCache to update multiple fields, for example both showing a conditional count and a basic count you can add these fields in the array:
$this -> addBehavior ( 'CounterCache' , [
'Articles' => [ 'comment_count' ,
'published_comment_count' => [
'finder' => 'published'
]
]
]);
If you want to calculate the CounterCache field value on your own, you can set the ignoreDirty option to true.
This will prevent the field from being recalculated if you’ve set it dirty before:
$this -> addBehavior ( 'CounterCache' , [
'Articles' => [
'comment_count' => [
'ignoreDirty' =>
true
]
]
]);
Lastly, if a custom finder and conditions are not suitable you can provide a callback method. This callable must return the count value to be stored:
$this -> addBehavior ( 'CounterCache' , [
'Articles' => [
'rating_avg' =>
function
( $event , $entity , $table ) {
return
4.5
;
}
]
]);
Note: The CounterCache behavior works for belongsTo associations only. For example for “Comments belongsTo Articles”, you need to add the CounterCache behavior to the CommentsTable in order to generate comment_count for Articles table.
It is possible though to make this work for belongsToMany associations. You need to enable the CounterCache behavior in a custom through table configured in association options. See how to configure a custom join table
Using the ‘through’ Option
Timestamp
class Cake\ORM\Behavior\TimestampBehavior
The timestamp behavior allows your table objects to update one or more timestamps on each model event. This is primarily used to populate data into created and modified fields. However, with some additional configuration, you can update any timestamp/datetime column on any event a table publishes.
More Information 497
CakePHP Cookbook Documentation, Release 3.5
Basic Usage
You enable the timestamp behavior like any other behavior:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Timestamp' );
}
}
The default configuration will do the following:
• When a new entity is saved the created and modified fields will be set to the current time.
• When an entity is updated, the modified field is set to the current time.
Using and Configuring the Behavior
If you need to modify fields with different names, or want to update additional timestamp fields on custom events you can use some additional configuration:
class OrdersTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Timestamp' , [
'events' => [
'Model.beforeSave' => [
'created_at' => 'new' ,
'updated_at' => 'always' ,
],
'Orders.completed' => [
'completed_at' => 'always'
]
]
]);
}
}
As you can see above, in addition to the standard Model.beforeSave event, we are also updating the completed_at column when orders are completed.
Updating Timestamps on Entities
Sometimes you’ll want to update just the timestamps on an entity without changing any other properties. This is sometimes referred to as ‘touching’ a record. In CakePHP you can use the touch() method to do exactly this:
// Touch based on the Model.beforeSave event.
$articles -> touch ( $article );
// Touch based on a specific event.
$orders -> touch ( $order , 'Orders.completed' );
498 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
After you have saved the entity, the field is updated.
Touching records can be useful when you want to signal that a parent resource has changed when a child resource is created/updated. For example: updating an article when a new comment is added.
Saving Updates Without Modifying Timestamps
To disable the automatic modification of the updated timestamp column when saving an entity you can mark the attribute as ‘dirty’:
// Mark the modified column as dirty making
// the current value be set on update.
$order -> dirty ( 'modified' ,
true
);
Translate
class Cake\ORM\Behavior\TranslateBehavior
The Translate behavior allows you to create and retrieve translated copies of your entities in multiple languages. It does so by using a separate i18n table where it stores the translation for each of the fields of any given Table object that it’s bound to.
Warning: The TranslateBehavior does not support composite primary keys at this point in time.
A Quick Tour
After creating the i18n table in your database attach the behavior to any Table object you want to make translatable:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Translate' , [ 'fields' => [ 'title' ]]);
}
}
Now, select a language to be used for retrieving entities by changing the application language, which will affect all translations:
// In a controller. Change the locale, e.g. to Spanish
I18n :: setLocale ( 'es' );
$this -> loadModel ( 'Articles' );
Then, get an existing entity:
$article = $this -> Articles -> get ( 12 );
echo
$article -> title ; // Echoes 'A title', not translated yet
Next, translate your entity:
$article -> title = 'Un Artículo' ;
$this -> Articles -> save ( $article );
More Information 499
CakePHP Cookbook Documentation, Release 3.5
You can try now getting your entity again:
$article = $this -> Articles -> get ( 12 );
echo
$article -> title ; // Echoes 'Un Artículo', yay piece of cake!
Working with multiple translations can be done by using a special trait in your Entity class:
use
Cake\ORM\Behavior\Translate\TranslateTrait;
use
Cake\ORM\Entity;
class Article extends
Entity
{
use
TranslateTrait;
}
Now you can find all translations for a single entity:
$article = $this -> Articles -> find ( 'translations' ) -> first ();
echo
$article -> translation ( 'es' ) -> title ; // 'Un Artículo'
echo
$article -> translation ( 'en' ) -> title ; // 'An Article';
It is equally easy to save multiple translations at once:
$article -> translation ( 'es' ) -> title = 'Otro Título' ;
$article -> translation ( 'fr' ) -> title = 'Un autre Titre' ;
$this -> Articles -> save ( $article );
If you want to go deeper on how it works or how to tune the behavior for your needs, keep on reading the rest of this chapter.
Initializing the i18n Database Table
In order to use the behavior, you need to create a i18n table with the correct schema. Currently the only way of loading the i18n table is by manually running the following SQL script in your database:
CREATE TABLE
i18n ( id int NOT NULL auto_increment, locale varchar ( 6 )
NOT NULL
, model varchar ( 255 )
NOT NULL
, foreign_key int ( 10 )
NOT NULL
, field varchar ( 255 )
NOT NULL
, content text ,
PRIMARY KEY
(id),
UNIQUE INDEX
I18N_LOCALE_FIELD(locale, model, foreign_key, field),
INDEX
I18N_FIELD(model, foreign_key, field)
);
The schema is also available as sql file in /config/schema/i18n.sql.
A note on language abbreviations: The Translate Behavior doesn’t impose any restrictions on the language identifier, the possible values are only restricted by the locale column type/size. locale is defined as varchar(6) in case you want to use abbreviations like es-419 (Spanish for Latin America, language abbreviation with area code UN
M.49
130
130 https://en.wikipedia.org/wiki/UN_M.49
500 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Tip: It’s wise to use the same language abbreviations as required for
Internationalization and Localization
Thus you are consistent and switching the language works identical for both, the Translate Behaviour and
Internationalization and Localization .
So it’s recommended to use either the two letter ISO code of the language like en, fr, de or the full locale name such as fr_FR, es_AR, da_DK which contains both the language and the country where it is spoken.
Attaching the Translate Behavior to Your Tables
Attaching the behavior can be done in the initialize() method in your Table class:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Translate' , [ 'fields' => [ 'title' , 'body' ]]);
}
}
The first thing to note is that you are required to pass the fields key in the configuration array. This list of fields is needed to tell the behavior what columns will be able to store translations.
Using a Separate Translations Table
If you wish to use a table other than i18n for translating a particular repository, you can specify it in the behavior’s configuration. This is common when you have multiple tables to translate and you want a cleaner separation of the data that is stored for each different table:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Translate' , [
'fields' => [ 'title' , 'body' ],
'translationTable' => 'ArticlesI18n'
]);
}
}
You need to make sure that any custom table you use has the columns field, foreign_key, locale and model.
Reading Translated Content
As shown above you can use the locale() method to choose the active translation for entities that are loaded:
// Load I18n core functions at the beginning of your Controller:
use
Cake\I18n\I18n;
// Then you can change the language in your action:
I18n :: setLocale ( 'es' );
More Information 501
CakePHP Cookbook Documentation, Release 3.5
$this -> loadModel ( 'Articles' );
// All entities in results will contain spanish translation
$results = $this -> Articles -> find () -> all ();
This method works with any finder in your tables. For example, you can use TranslateBehavior with find('list'):
I18n :: setLocale ( 'es' );
$data = $this -> Articles -> find ( 'list' ) -> toArray ();
// Data will contain
[ 1 => 'Mi primer artículo' , 2 => 'El segundo artículo' , 15 => 'Otro articulo' ...
]
Retrieve All Translations For An Entity
When building interfaces for updating translated content, it is often helpful to show one or more translation(s) at the same time. You can use the translations finder for this:
// Find the first article with all corresponding translations
$article = $this -> Articles -> find ( 'translations' ) -> first ();
In the example above you will get a list of entities back that have a _translations property set. This property will contain a list of translation data entities. For example the following properties would be accessible:
// Outputs 'en'
echo
$article -> _translations [ 'en' ] -> locale ;
// Outputs 'title'
echo
$article -> _translations [ 'en' ] -> field ;
// Outputs 'My awesome post!'
echo
$article -> _translations [ 'en' ] -> body ;
A more elegant way for dealing with this data is by adding a trait to the entity class that is used for your table:
use
Cake\ORM\Behavior\Translate\TranslateTrait;
use
Cake\ORM\Entity;
class Article extends
Entity
{
use
TranslateTrait;
}
This trait contains a single method called translation, which lets you access or create new translation entities on the fly:
// Outputs 'title'
echo
$article -> translation ( 'en' ) -> title ;
// Adds a new translation data entity to the article
$article -> translation ( 'de' ) -> title = 'Wunderbar' ;
502 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Limiting the Translations to be Retrieved
You can limit the languages that are fetched from the database for a particular set of records:
$results = $this -> Articles -> find ( 'translations' , [
'locales' => [ 'en' , 'es' ]
]);
$article = $results -> first ();
$spanishTranslation = $article -> translation ( 'es' );
$englishTranslation = $article -> translation ( 'en' );
Preventing Retrieval of Empty Translations
Translation records can contain any string, if a record has been translated and stored as an empty string (‘’) the translate behavior will take and use this to overwrite the original field value.
If this is undesired, you can ignore translations which are empty using the allowEmptyTranslations config key:
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Translate' , [
'fields' => [ 'title' , 'body' ],
'allowEmptyTranslations' =>
false
]);
}
}
The above would only load translated data that had content.
Retrieving All Translations For Associations
It is also possible to find translations for any association in a single find operation:
$article = $this -> Articles -> find ( 'translations' ) -> contain ([
'Categories' =>
function
( $query ) {
return
$query -> find ( 'translations' );
}
]) -> first ();
// Outputs 'Programación'
echo
$article -> categories [ 0 ] -> translation ( 'es' ) -> name ;
This assumes that Categories has the TranslateBehavior attached to it. It simply uses the query builder function for the contain clause to use the translations custom finder in the association.
Retrieving one language without using I18n::locale
calling I18n::setLocale('es'); changes the default locale for all translated finds, there may be times you wish to retrieve translated content without modifying the application’s state. For these scenarios use the behavior setLocale() method:
More Information 503
CakePHP Cookbook Documentation, Release 3.5
I18n :: setLocale ( 'en' ); // reset for illustration
$this -> loadModel ( 'Articles' );
$this -> Articles -> locale ( 'es' ); // specific locale
$article = $this -> Articles -> get ( 12 );
echo
$article -> title ; // Echoes 'Un Artículo', yay piece of cake!
Note that this only changes the locale of the Articles table, it would not affect the langauge of associated data. To affect associated data it’s necessary to call locale on each table for example:
I18n :: setLocale ( 'en' ); // reset for illustration
$this -> loadModel ( 'Articles' );
$this -> Articles -> locale ( 'es' );
$this -> Articles -> Categories -> locale ( 'es' );
$data = $this -> Articles -> find ( 'all' , [ 'contain' => [ 'Categories' ]]);
This example also assumes that Categories has the TranslateBehavior attached to it.
Querying Translated Fields
TranslateBehavior does not substitute find conditions by default. You need to use translationField() method to compose find conditions on translated fields:
$this -> Articles -> locale ( 'es' );
$data = $this -> Articles -> find () -> where ([
$this -> Articles -> translationField ( 'title' ) => 'Otro Título'
]);
Saving in Another Language
The philosophy behind the TranslateBehavior is that you have an entity representing the default language, and multiple translations that can override certain fields in such entity. Keeping this in mind, you can intuitively save translations for any given entity. For example, given the following setup:
// in src/Model/Table/ArticlesTable.php
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Translate' , [ 'fields' => [ 'title' , 'body' ]]);
}
}
// in src/Model/Entity/Article.php
class Article extends
Entity
{
use
TranslateTrait;
}
// In a Controller
$articles = $this -> loadModel ( 'Articles' );
504 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
$article =
new
Article([
'title' => 'My First Article' ,
'body' => 'This is the content' ,
'footnote' => 'Some afterwords'
]);
$this -> Articles -> save ( $article );
So, after you save your first article, you can now save a translation for it, there are a couple ways to do it. The first one is setting the language directly into the entity:
$article -> _locale = 'es' ;
$article -> title = 'Mi primer Artículo' ;
$this -> Articles -> save ( $article );
After the entity has been saved, the translated field will be persisted as well, one thing to note is that values from the default language that were not overridden will be preserved:
// Outputs 'This is the content'
echo
$article -> body ;
// Outputs 'Mi primer Artículo'
echo
$article -> title ;
Once you override the value, the translation for that field will be saved and can be retrieved as usual:
$article -> body = 'El contendio' ;
$this -> Articles -> save ( $article );
The second way to use for saving entities in another language is to set the default language directly to the table:
I18n :: setLocale ( 'es' );
$article -> title = 'Mi Primer Artículo' ;
$this -> Articles -> save ( $article );
Setting the language directly in the table is useful when you need to both retrieve and save entities for the same language or when you need to save multiple entities at once.
Saving Multiple Translations
It is a common requirement to be able to add or edit multiple translations to any database record at the same time. This can be done using the TranslateTrait:
use
Cake\ORM\Behavior\Translate\TranslateTrait;
use
Cake\ORM\Entity;
class Article extends
Entity
{
use
TranslateTrait;
}
Now, You can populate translations before saving them:
$translations = [
'fr' => [ 'title' => "Un article" ],
More Information 505
CakePHP Cookbook Documentation, Release 3.5
'es' => [ 'title' => 'Un artículo' ]
];
foreach
( $translations
as
$lang => $data ) {
$article -> translation ( $lang ) -> set ( $data , [ 'guard' =>
false
]);
}
$this -> Articles -> save ( $article );
As of 3.3.0, working with multiple translations has been streamlined. You can create form controls for your translated fields:
// In a view template.
<?= $this -> Form -> create ( $article ); ?>
<fieldset>
<legend>French</legend>
<?
= $this -> Form -> control ( '_translations.fr.title' ); ?>
<?
= $this -> Form -> control ( '_translations.fr.body' ); ?>
</fieldset>
<fieldset>
<legend>Spanish</legend>
<?
= $this -> Form -> control ( '_translations.es.title' ); ?>
<?
= $this -> Form -> control ( '_translations.es.body' ); ?>
</fieldset>
In your controller, you can marshal the data as normal, but with the translations option enabled:
$article = $this -> Articles -> newEntity ( $this -> request -> getData (), [
'translations' =>
true
]);
$this -> Articles -> save ( $article );
This will result in your article, the french and spanish translations all being persisted. You’ll need to remember to add
_translations into the $_accessible fields of your entity as well.
Validating Translated Entities
When attaching TranslateBehavior to a model, you can define the validator that should be used when translation records are created/modified by the behavior during newEntity() or patchEntity():
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Translate' , [
'fields' => [ 'title' ],
'validator' => 'translated'
]);
}
}
The above will use the validator created by validationTranslated to validated translated entities.
New in version 3.3.0: Validating translated entities, and streamlined translation saving was added in 3.3.0
506 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Tree
class Cake\ORM\Behavior\TreeBehavior
It’s fairly common to want to store hierarchical data in a database table. Examples of such data might be categories with unlimited subcategories, data related to a multilevel menu system or a literal representation of hierarchy such as departments in a company.
Relational databases are usually not well suited for storing and retrieving this type of data, but there are a few known techniques that can make them effective for working with multi-level information.
The TreeBehavior helps you maintain a hierarchical data structure in the database that can be queried without much overhead and helps reconstruct the tree data for finding and displaying processes.
Requirements
This behavior requires the following columns in your table:
• parent_id (nullable) The column holding the ID of the parent row
• lft (integer, signed) Used to maintain the tree structure
• rght (integer, signed) Used to maintain the tree structure
You can configure the name of those fields should you need to customize them. More information on the meaning of the fields and how they are used can be found in this article describing the MPTT logic
131
Warning: The TreeBehavior does not support composite primary keys at this point in time.
A Quick Tour
You enable the Tree behavior by adding it to the Table you want to store hierarchical data in:
class CategoriesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Tree' );
}
}
Once added, you can let CakePHP build the internal structure if the table is already holding some rows:
$categories = TableRegistry :: get ( 'Categories' );
$categories -> recover ();
You can verify it works by getting any row from the table and asking for the count of descendants it has:
$node = $categories -> get ( 1 );
echo
$categories -> childCount ( $node );
Getting a flat list of the descendants for a node is equally easy:
131 http://www.sitepoint.com/hierarchical-data-database-2/
More Information 507
CakePHP Cookbook Documentation, Release 3.5
$descendants = $categories -> find ( 'children' , [ 'for' => 1 ]);
foreach
( $descendants
as
$category ) {
echo
$category -> name .
"\n" ;
}
If you need to pass conditions you do so as per normal:
$descendants = $categories
-> find ( 'children' , [ 'for' => 1 ])
-> where ([ 'name LIKE' => '%Foo%' ]);
foreach
( $descendants
as
$category ) {
echo
$category -> name .
"\n" ;
}
If you instead need a threaded list, where children for each node are nested in a hierarchy, you can stack the ‘threaded’ finder:
$children = $categories
-> find ( 'children' , [ 'for' => 1 ])
-> find ( 'threaded' )
-> toArray ();
foreach
( $children
as
$child ) {
echo
" { $child -> name } has " .
count ( $child -> children ) .
" direct children" ;
}
Traversing threaded results usually requires recursive functions in, but if you only require a result set containing a single field from each level so you can display a list, in an HTML select for example, it is better to use the ‘treeList’ finder:
$list = $categories -> find ( 'treeList' );
// In a CakePHP template file:
echo
$this -> Form -> control ( 'categories' , [ 'options' => $list ]);
// Or you can output it in plain text, for example in a CLI script
foreach
( $list
as
$categoryName ) {
echo
$categoryName .
"\n" ;
}
The output will be similar to:
My Categories
_Fun
__Sport
___Surfing
___Skating
_Trips
__National
__International
The treeList finder takes a number of options:
• keyPath: A dot separated path to fetch the field to use for the array key, or a closure to return the key out of the provided row.
508 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
• valuePath: A dot separated path to fetch the field to use for the array value, or a closure to return the value out of the provided row.
• spacer: A string to be used as prefix for denoting the depth in the tree for each item
An example of all options in use is:
$query = $categories -> find ( 'treeList' , [
'keyPath' => 'url' ,
'valuePath' => 'id' ,
'spacer' => ' '
]);
One very common task is to find the tree path from a particular node to the root of the tree. This is useful, for example, for adding the breadcrumbs list for a menu structure:
$nodeId = 5 ;
$crumbs = $categories -> find ( 'path' , [ 'for' => $nodeId ]);
foreach
( $crumbs
as
$crumb ) {
echo
$crumb -> name .
' > ' ;
}
Trees constructed with the TreeBehavior cannot be sorted by any column other than lft, this is because the internal representation of the tree depends on this sorting. Luckily, you can reorder the nodes inside the same level without having to change their parent:
$node = $categories -> get ( 5 );
// Move the node so it shows up one position up when listing children.
$categories -> moveUp ( $node );
// Move the node to the top of the list inside the same level.
$categories -> moveUp ( $node ,
true
);
// Move the node to the bottom.
$categories -> moveDown ( $node ,
true
);
Configuration
If the default column names that are used by this behavior don’t match your own schema, you can provide aliases for them:
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Tree' , [
'parent' => 'ancestor_id' , // Use this instead of parent_id
'left' => 'tree_left' , // Use this instead of lft
'right' => 'tree_right' // Use this instead of rght
]);
}
More Information 509
CakePHP Cookbook Documentation, Release 3.5
Node Level (Depth)
Knowing the depth of tree nodes can be useful when you want to retrieve nodes only upto a certain level for e.g. when generating menus. You can use the level option to specify the field that will save level of each node:
$this -> addBehavior ( 'Tree' , [
'level' => 'level' , // Defaults to null, i.e. no level saving
]);
If you don’t want to cache the level using a db field you can use TreeBehavior::getLevel() method to get level of a node.
Scoping and Multi Trees
Sometimes you want to persist more than one tree structure inside the same table, you can achieve that by using the
‘scope’ configuration. For example, in a locations table you may want to create one tree per country:
class LocationsTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Tree' , [
'scope' => [ 'country_name' => 'Brazil' ]
]);
}
}
In the previous example, all tree operations will be scoped to only the rows having the column country_name set to ‘Brazil’. You can change the scoping on the fly by using the ‘config’ function:
$this -> behaviors () -> Tree -> config ( 'scope' , [ 'country_name' => 'France' ]);
Optionally, you can have a finer grain control of the scope by passing a closure as the scope:
$this -> behaviors () -> Tree -> config ( 'scope' ,
function
( $query ) {
$country = $this -> getConfigureContry (); // A made-up function
return
$query -> where ([ 'country_name' => $country ]);
});
Recovering with custom sort field
New in version 3.0.14.
By default, recover() sorts the items using the primary key. This works great if this is a numeric (auto increment) column, but can lead to weird results if you use UUIDs.
If you need custom sorting for the recovery, you can set a custom order clause in your config:
$this -> addBehavior ( 'Tree' , [
'recoverOrder' => [ 'country_name' => 'DESC' ],
]);
510 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Saving Hierarchical Data
When using the Tree behavior, you usually don’t need to worry about the internal representation of the hierarchical structure. The positions where nodes are placed in the tree are deduced from the ‘parent_id’ column in each of your entities:
$aCategory = $categoriesTable -> get ( 10 );
$aCategory -> parent_id = 5 ;
$categoriesTable -> save ( $aCategory );
Providing inexistent parent ids when saving or attempting to create a loop in the tree (making a node child of itself) will throw an exception.
You can make a node a root in the tree by setting the ‘parent_id’ column to null:
$aCategory = $categoriesTable -> get ( 10 );
$aCategory -> parent_id =
null
;
$categoriesTable -> save ( $aCategory );
Children for the new root node will be preserved.
Deleting Nodes
Deleting a node and all its sub-tree (any children it may have at any depth in the tree) is trivial:
$aCategory = $categoriesTable -> get ( 10 );
$categoriesTable -> delete ( $aCategory );
The TreeBehavior will take care of all internal deleting operations for you. It is also possible to only delete one node and re-assign all children to the immediately superior parent node in the tree:
$aCategory = $categoriesTable -> get ( 10 );
$categoriesTable -> removeFromTree ( $aCategory );
$categoriesTable -> delete ( $aCategory );
All children nodes will be kept and a new parent will be assigned to them.
The deletion of a node is based off of the lft and rght values of the entity. This is important to note when looping through the various children of a node for conditional deletes:
$descendants = $teams -> find ( 'children' , [ 'for' => 1 ]);
foreach
( $descendants
as
$descendant ) {
$team = $teams -> get ( $descendant -> id ); // search for the up-to-date entity object
if
( $team -> expired ) {
}
$teams -> delete ( $team ); // deletion reorders the lft and rght of database
˓→ entries
}
The TreeBehavior reorders the lft and rght values of records in the table when a node is deleted. As such, the lft and rght values of the entities inside $descendants (saved before the delete operation) will be inaccurate. Entities will have to be loaded and modified on the fly to prevent inconsistencies in the table.
More Information 511
CakePHP Cookbook Documentation, Release 3.5
Creating a Behavior
In the following examples we will create a very simple SluggableBehavior. This behavior will allow us to populate a slug field with the results of Text::slug() based on another field.
Before we create our behavior we should understand the conventions for behaviors:
• Behavior files are located in src/Model/Behavior, or MyPlugin\Model\Behavior.
• Behavior classes should be in the App\Model\Behavior namespace, or MyPlugin\Model\Behavior namespace.
• Behavior class names end in Behavior.
• Behaviors extend Cake\ORM\Behavior.
To create our sluggable behavior. Put the following into src/Model/Behavior/SluggableBehavior.php:
namespace
App\Model\Behavior;
use
Cake\ORM\Behavior;
class SluggableBehavior extends
Behavior
{
}
Similar to tables, behaviors also have an initialize() hook where you can put your behavior’s initialization code, if required:
public function
initialize (
array
$config )
{
// Some initialization code here
}
We can now add this behavior to one of our table classes. In this example we’ll use an ArticlesTable, as articles often have slug properties for creating friendly URLs:
namespace
App\Model\Table;
use
Cake\ORM\Table;
class ArticlesTable extends
Table
{
public function
initialize (
array
$config )
{
$this -> addBehavior ( 'Sluggable' );
}
}
Our new behavior doesn’t do much of anything right now. Next, we’ll add a mixin method and an event listener so that when we save entities we can automatically slug a field.
Defining Mixin Methods
Any public method defined on a behavior will be added as a ‘mixin’ method on the table object it is attached to. If you attach two behaviors that provide the same methods an exception will be raised. If a behavior provides the same method as a table class, the behavior method will not be callable from the table. Behavior mixin methods will receive
512 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
the exact same arguments that are provided to the table. For example, if our SluggableBehavior defined the following method:
public function
slug ( $value )
{
return
Text :: slug ( $value , $this -> _config [ 'replacement' ]);
}
It could be invoked using:
$slug = $articles -> slug ( 'My article name' );
Limiting or Renaming Exposed Mixin Methods
When creating behaviors, there may be situations where you don’t want to expose public methods as mixin methods.
In these cases you can use the implementedMethods configuration key to rename or exclude mixin methods. For example if we wanted to prefix our slug() method we could do the following:
protected
$_defaultConfig = [
'implementedMethods' => [
'superSlug' => 'slug' ,
]
];
Applying this configuration will make slug() not callable, however it will add a superSlug() mixin method to the table. Notably if our behavior implemented other public methods they would not be available as mixin methods with the above configuration.
Since the exposed methods are decided by configuration you can also rename/remove mixin methods when adding a behavior to a table. For example:
// In a table's initialize() method.
$this -> addBehavior ( 'Sluggable' , [
'implementedMethods' => [
'superSlug' => 'slug' ,
]
]);
Defining Event Listeners
Now that our behavior has a mixin method to slug fields, we can implement a callback listener to automatically slug a field when entities are saved. We’ll also modify our slug method to accept an entity instead of just a plain value. Our behavior should now look like:
namespace
App\Model\Behavior;
use
ArrayObject;
use
Cake\Datasource\EntityInterface;
use
Cake\Event\Event;
use
Cake\ORM\Behavior;
use
Cake\ORM\Entity;
use
Cake\ORM\Query;
use
Cake\Utility\Text;
More Information 513
CakePHP Cookbook Documentation, Release 3.5
class SluggableBehavior extends
Behavior
{
protected
$_defaultConfig = [
'field' => 'title' ,
'slug' => 'slug' ,
'replacement' => '-' ,
];
public function
slug (Entity $entity )
{
$config = $this -> config ();
$value = $entity -> get ( $config [ 'field' ]);
$entity -> set ( $config [ 'slug' ], Text :: slug ( $value , $config [ 'replacement' ]));
}
}
public function
beforeSave (Event $event , EntityInterface $entity , ArrayObject
˓→
$options )
{
$this -> slug ( $entity );
}
The above code shows a few interesting features of behaviors:
• Behaviors can define callback methods by defining methods that follow the
Lifecycle Callbacks
conventions.
• Behaviors can define a default configuration property. This property is merged with the overrides when a behavior is attached to the table.
To prevent the saving from continuing simply stop event propagation in your callback:
public function
beforeSave (Event $event , EntityInterface $entity , ArrayObject
{
˓→
$options )
if
( ...
) {
$event -> stopPropagation ();
return
;
}
$this -> slug ( $entity );
}
Defining Finders
Now that we are able to save articles with slug values, we should implement a finder method so we can fetch articles by their slug. Behavior finder methods, use the same conventions as
Custom Finder Methods
do. Our find('slug') method would look like:
public function
findSlug (Query $query ,
array
$options )
{
return
$query -> where ([ 'slug' => $options [ 'slug' ]]);
}
Once our behavior has the above method we can call it:
$article = $articles -> find ( 'slug' , [ 'slug' => $value ]) -> first ();
514 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
Limiting or Renaming Exposed Finder Methods
When creating behaviors, there may be situations where you don’t want to expose finder methods, or you need to rename finders to avoid duplicated methods. In these cases you can use the implementedFinders configuration key to rename or exclude finder methods. For example if we wanted to rename our find(slug) method we could do the following:
protected
$_defaultConfig = [
'implementedFinders' => [
'slugged' => 'findSlug' ,
]
];
Applying this configuration will make find('slug') trigger an error. However it will make find('slugged') available. Notably if our behavior implemented other finder methods they would not be available, as they are not included in the configuration.
Since the exposed methods are decided by configuration you can also rename/remove finder methods when adding a behavior to a table. For example:
// In a table's initialize() method.
$this -> addBehavior ( 'Sluggable' , [
'implementedFinders' => [
'slugged' => 'findSlug' ,
]
]);
Transforming Request Data into Entity Properties
Behaviors can define logic for how the custom fields they provide are marshalled by implementing the
Cake\ORM\PropertyMarshalInterface
. This interface requires a single method to be implemented:
public function
buildMarshalMap ( $marshaller , $map , $options )
{
return
[
'custom_behavior_field' =>
function
( $value , $entity ) {
// Transform the value as necessary
return
$value .
'123' ;
}
];
}
The TranslateBehavior has a non-trivial implementation of this interface that you might want to refer to.
New in version 3.3.0: The ability for behaviors to participate in marshalling was added in 3.3.0
Removing Loaded Behaviors
To remove a behavior from your table you can call the removeBehavior() method:
// Remove the loaded behavior
$this -> removeBehavior ( 'Sluggable' );
More Information 515
CakePHP Cookbook Documentation, Release 3.5
Accessing Loaded Behaviors
Once you’ve attached behaviors to your Table instance you can introspect the loaded behaviors, or access specific behaviors using the BehaviorRegistry:
// See which behaviors are loaded
$table -> behaviors () -> loaded ();
// Check if a specific behavior is loaded.
// Remember to omit plugin prefixes.
$table -> behaviors () -> has ( 'CounterCache' );
// Get a loaded behavior
// Remember to omit plugin prefixes
$table -> behaviors () -> get ( 'CounterCache' );
Re-configuring Loaded Behaviors
To modify the configuration of an already loaded behavior you can combine the BehaviorRegistry::get command with config command provided by the InstanceConfigTrait trait.
For example if a parent (e.g. an AppTable) class loaded the Timestamp behavior you could do the following to add, modify or remove the configurations for the behavior. In this case, we will add an event we want Timestamp to respond to:
namespace
App\Model\Table;
use
App\Model\Table\AppTable; // similar to AppController
class UsersTable extends
AppTable
{
public function
initialize (
array
$options )
{
parent
:: initialize ( $options );
// e.g. if our parent calls $this->addBehavior('Timestamp');
// and we want to add an additional event
if
( $this -> behaviors () -> has ( 'Timestamp' )) {
$this -> behaviors () -> get ( 'Timestamp' ) -> config ([
'events' => [
'Users.login' => [
'last_login' => 'always'
],
],
]);
}
}
}
Schema System
CakePHP features a schema system that is capable of reflecting and generating schema information for tables in SQL datastores. The schema system can generate/reflect a schema for any SQL platform that CakePHP supports.
516 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
The main pieces of the schema system are
Cake\Database\Schema\Collection and
Cake\Database\Schema\TableSchema
.
These classes give you access to database-wide and individual
Table object features respectively.
The primary use of the schema system is for
Fixtures
. However, it can also be used in your application if required.
Schema\TableSchema Objects
class Cake\Database\Schema\TableSchema
The schema subsystem provides a simple TableSchema object to hold data about a table in a database. This object is returned by the schema reflection features:
use
Cake\Database\Schema\TableSchema;
// Create a table one column at a time.
$schema =
new
TableSchema( 'posts' );
$schema -> addColumn ( 'id' , [
'type' => 'integer' ,
'length' => 11 ,
'null' =>
false
,
'default' =>
null
,
]) -> addColumn ( 'title' , [
'type' => 'string' ,
'length' => 255 ,
// Create a fixed length (char field)
'fixed' =>
true
]) -> addConstraint ( 'primary' , [
'type' => 'primary' ,
'columns' => [ 'id' ]
]);
// Schema\TableSchema classes could also be created with array data
$schema =
new
TableSchema( 'posts' , $columns );
Schema\TableSchema objects allow you to build up information about a table’s schema. It helps to normalize and validate the data used to describe a table. For example, the following two forms are equivalent:
$schema -> addColumn ( 'title' , 'string' );
// and
$schema -> addColumn ( 'title' , [
'type' => 'string'
]);
While equivalent, the 2nd form allows more detail and control. This emulates the existing features available in Schema files + the fixture schema in 2.x.
Accessing Column Data
Columns are either added as constructor arguments, or via addColumn(). Once fields are added information can be fetched using column() or columns():
// Get the array of data about a column
$c = $schema -> column ( 'title' );
More Information 517
CakePHP Cookbook Documentation, Release 3.5
// Get the list of all columns.
$cols = $schema -> columns ();
Indexes and Constraints
Indexes are added using the addIndex(). Constraints are added using addConstraint(). Indexes and constraints cannot be added for columns that do not exist, as it would result in an invalid state. Indexes are different from constraints, and exceptions will be raised if you try to mix types between the methods. An example of both methods is:
$schema =
new
TableSchema( 'posts' );
$schema -> addColumn ( 'id' , 'integer' )
-> addColumn ( 'author_id' , 'integer' )
-> addColumn ( 'title' , 'string' )
-> addColumn ( 'slug' , 'string' );
// Add a primary key.
$schema -> addConstraint ( 'primary' , [
'type' => 'primary' ,
'columns' => [ 'id' ]
]);
// Add a unique key
$schema -> addConstraint ( 'slug_idx' , [
'columns' => [ 'slug' ],
'type' => 'unique' ,
]);
// Add index
$schema -> addIndex ( 'slug_title' , [
'columns' => [ 'slug' , 'title' ],
'type' => 'index'
]);
// Add a foreign key
$schema -> addConstraint ( 'author_id_idx' , [
'columns' => [ 'author_id' ],
'type' => 'foreign' ,
'references' => [ 'authors' , 'id' ],
'update' => 'cascade' ,
'delete' => 'cascade'
]);
If you add a primary key constraint to a single integer column it will automatically be converted into a autoincrement/serial column depending on the database platform:
$schema =
new
TableSchema( 'posts' );
$schema -> addColumn ( 'id' , 'integer' )
-> addConstraint ( 'primary' , [
'type' => 'primary' ,
'columns' => [ 'id' ]
]);
In the above example the id column would generate the following SQL in MySQL:
CREATE TABLE `posts` (
`id` INTEGER AUTO_INCREMENT,
PRIMARY KEY ( `id` )
)
518 Chapter 12. Database Access & ORM
CakePHP Cookbook Documentation, Release 3.5
If your primary key contains more than one column, none of them will automatically be converted to an auto-increment value. Instead you will need to tell the table object which column in the composite key you want to auto-increment:
$schema =
new
TableSchema( 'posts' );
$schema -> addColumn ( 'id' , [
'type' => 'integer' ,
'autoIncrement' =>
true
,
])
-> addColumn ( 'account_id' , 'integer' )
-> addConstraint ( 'primary' , [
'type' => 'primary' ,
'columns' => [ 'id' , 'account_id' ]
]);
The autoIncrement option only works with integer and biginteger columns.
Reading Indexes and Constraints
Indexes and constraints can be read out of a table object using accessor methods. Assuming that $schema is a populated TableSchema instance you could do the following:
// Get contraints. Will return the
// names of all constraints.
$constraints = $schema -> constraints ()
// Get data about a single constraint.
$constraint = $schema -> constraint ( 'author_id_idx' )
// Get indexes. Will return the
// names of all indexes.
$indexes = $schema -> indexes ()
// Get data about a single index.
$index = $schema -> index ( 'author_id_idx' )
Adding Table Options
Some drivers (primarily MySQL) support and require additional table metadata. In the case of MySQL the CHARSET,
COLLATE and ENGINE properties are required for maintaining a table’s structure in MySQL. The following could be used to add table options:
$schema -> options ([
'engine' => 'InnoDB' ,
'collate' => 'utf8_unicode_ci' ,
]);
Platform dialects only handle the keys they are interested in and ignore the rest. Not all options are supported on all platforms.
Converting Tables into SQL
Using the createSql() or dropSql() you can get platform specific SQL for creating or dropping a specific table:
More Information 519
CakePHP Cookbook Documentation, Release 3.5
$db = ConnectionManager :: get ( 'default' );
$schema =
new
TableSchema( 'posts' , $fields , $indexes );
// Create a table
$queries = $schema -> createSql ( $db );
foreach
( $queries
as
$sql ) {
$db -> execute ( $sql );
}
// Drop a table
$sql = $schema -> dropSql ( $db );
$db -> execute ( $sql );
By using a connection’s driver the schema data can be converted into platform specific SQL. The return of createSql and dropSql is a list of SQL queries required to create a table and the required indexes. Some platforms may require multiple statements to create tables with comments and/or indexes. An array of queries is always returned.
Schema Collections
class Cake\Database\Schema\Collection
Collection provides access to the various tables available on a connection. You can use it to get the list of tables or reflect tables into
TableSchema
objects. Basic usage of the class looks like:
$db = ConnectionManager :: get ( 'default' );
// Create a schema collection.
$collection = $db -> schemaCollection ();
// Get the table names
$tables = $collection -> listTables ();
// Get a single table (instance of Schema\TableSchema)
$tableSchema = $collection -> describe ( 'posts' );
ORM Cache Shell
The OrmCacheShell provides a simple CLI tool for managing your application’s metadata caches. In deployment situations it is helpful to rebuild the metadata cache in-place without clearing the existing cache data. You can do this by running: bin / cake orm_cache build -connection
default
This will rebuild the metadata cache for all tables on the default connection. If you only need to rebuild a single table you can do that by providing its name: bin / cake orm_cache build -connection
default
articles
In addition to building cached data, you can use the OrmCacheShell to remove cached metadata as well:
# Clear all metadata bin / cake orm_cache clear
520 Chapter 12. Database Access & ORM
# Clear a single table bin / cake orm_cache clear articles
CakePHP Cookbook Documentation, Release 3.5
More Information 521
CakePHP Cookbook Documentation, Release 3.5
522 Chapter 12. Database Access & ORM
Download
Advertisement
Key features
Flexible ORM
Robust Routing System
Powerful Templating Engine
Components for common tasks
Authentication
Caching
Email
API building
Frequently asked questions
CakePHP is a free, open-source, rapid development framework for PHP. It is a framework that provides a structured way to organize your code and build web applications.
The main features of CakePHP are: a flexible Object Relational Mapper (ORM), a robust routing system, a powerful templating engine, and a comprehensive set of components for common tasks.
CakePHP can help you build web applications faster. By utilizing the framework’s features, you can avoid writing repetitive code and focus on building the unique aspects of your application.