CakePHP’s database Query Builder provides a simple to use fluent interface for creating and operating database queries. It can be used to accomplish most database operations in your application, and works on all supported database systems.
Query builders allow us to create database queries that work with any supported database, without having to worry about differences between SQL implementations in the different database systems.
The CakePHP query builder uses PDO parameter binding to protect your application against SQL injection attacks. There is no need to clean strings being passed as bindings.
For creating a Query object in CakePHP, the easiest way is using find() from a Table object. In CakePHP, queries are lazily evaluated, which means that they are not evaluated until any of the following actions happens: the query is iterated through a foreach, it’s called first(), all(), toList(), toArray().
You can check all the SQL queries that CakePHP is generating, you just need to enable the Database Logging. See here: https://book.cakephp.org/4/en/orm/database-basics.html#query-logging.
Let’s do a few samples using the Query Builder - this is the ER diagram of the database that we will be using for the queries. We have Orders, Products, Users and Items that will store the products sold in each order and the quantity sold.
Let’s create some queries using the Query Builder SQL Functions: https://book.cakephp.org/4/en/orm/query-builder.html#using-sql-functions. SQL Functions as part of the Query Builder are abstractions of some commonly used SQL functions, and they allow the ORM to choose the specific implementation your application needs based on the Database that is being used. For example, CONCAT is implemented in a different way in MySQL and Postgres, using concat() function will work if you use MySQL or Postgres
Imagine we want to build a report of the products sold, including the following:
-
Display the product in the following format “Products.name - Products.description”.
-
Total of products sold.
-
Total of products in stock..
-
Total amount on sold products.
First, we need to build the Query object using find(), this needs to be done from the Table. $query = Table->find();
.
We want to get a report of the products sold and the current stock. Initially, we would need to build a subquery using ItemsTable where the information related to the products sold is present. Don’t forget to use identifier() when referencing any column. This will tell us the items sold per product.
$itemsQuery = $this->Items->find() ->where(['Items.product_id' => $query->identifier('Products.id')]);
Now, let’s build each query needed for the information required:
-
Let’s start displaying the product in the following format: “Products.name - Products.description”. Using concat() we could get it:
$query->select([
'display_name' => $query->func()->concat([
$query->identifier('Products.name'), ' - ', $query->identifier('Products.description')]),
]);
-
Total of products sold. Using sum() we could get it, we just need to sum Items.quantity per each product, this is why we use $itemsQuery and then we sum the quantity per each product.
$query->select([
'quantity_sold' => $itemsQuery->select(['sum' => $itemsQuery->func()->sum($query->identifier('Items.quantity'))]),
]); -
To get the income, we need to multiply the Quantity of items * Price and sum all of them.
$query->select([
'income' => $itemsQuery->select(['sum' => $itemsQuery->func()->sum($query->newExpr('Items.quantity * Items.unit_price'))
])]); -
Stock. to get the stock we need to take advantage of the quantity_sold we just got in #2, and subtract it from the real quantity Products.quantity. For getting this, we would need to create a subquery in the FROM clause using the previous queries, and then subtract in the target query something like quantity - quantity_sold.
$products = $query->cleanCopy()->select([
'id' => 'ProductDetails.id',
'price' => 'ProductDetails.price',
'quantity' => 'ProductDetails.quantity',
'display_name' => 'ProductDetails.displaye_name',
'quantity_sold' => 'ProductDetails.quantity_sold',
'income' => 'ProductDetails.income',
'stock' => $query->newExpr('ProductDetails.quantity - ProductDetails.quantity_sold'),
])->from([
'ProductDetails' => $query->cleanCopy()->select([
'id' => 'Products.id',
'price' => 'Products.price',
'quantity' => 'Products.quantity',
'display_name' => $query->func()->concat([$query->identifier('Products.name'), ' - ', $query->identifier('Products.description')]),
'quantity_sold' => $itemsQuery->select(['sum' => $itemsQuery->func()->sum($query->identifier('Items.quantity'))]),
'income' => $itemsQuery->cleanCopy()->select(['sum' => $itemsQuery->func()->sum($query->newExpr('Items.quantity * Items.unit_price'))]), ])
]);
Notice that we are using cleanCopy(), this is useful when the same Query object is affected previously by a select(), from() or where clauses, if you run this same example without using cleanCopy() you will get unexpected results.
The SQL query generated is this:
SELECT ProductDetails.id AS id,
ProductDetails.price AS price,
ProductDetails.quantity AS quantity,
ProductDetails.display_name AS display_name,
ProductDetails.quantity_sold AS quantity_sold,
ProductDetails.income AS income,
(ProductDetails.quantity - ProductDetails.quantity_sold) AS stock
FROM
(SELECT Products.id AS id,
Products.price AS price,
Products.quantity AS quantity,
(CONCAT(Products.name, :param0, Products.description)) AS display_name,
(SELECT (SUM(Items.quantity)) AS SUM
FROM items Items
WHERE Items.product_id = (Products.id)) AS quantity_sold,
(SELECT (SUM(Items.quantity * Items.unit_price)) AS SUM
FROM items Items
WHERE Items.product_id = (Products.id)) AS income
FROM products Products) ProductDetails
CakePHP QueryBuilder gives a ton of possibilities to create queries with clean and robust code. So go play with it and enjoy!