Getting started
Routing and controllers
Command line
Databases (SQL)
Databases (NoSQL)
Security
Packages
Learn more
- Array helper
- Caching
- Collections
- Command, event and query buses
- Date and time
- File system
- HTML helper
- Humanizer
- Image manipulation
- Internationalization
- Logging
- Number helper
- Pagination
- Retry helper
- Sessions
- String helper
- URL builder
- UUID helper
- Validation
- Views
Official packages
Query builder
- Getting a query builder instance
- Fetching data
- Inserting data
- Updating data
- Deleting data
- Aggregates
- Clauses
- Common table expressions
- Set operations
- Row-level locking
- Dialect specific SQL
- JSON data
- Array and JSON representations of results
The query builder allows you to programmatically build SQL queries.
All queries executed by the query builder use prepared statements, thus mitigating the risk of SQL injections. However, you have to make sure that you don't create SQL injection vectors if you're using using raw SQL in your query builder queries!
The query builder currently supports the following dialects:
- Firebird
- MariaDB
- MySQL
- Oracle
- PostgreSQL
- SQLite
- SQLServer
The example SQL in the documentation is generated using the MySQL compiler.
Getting a query builder instance
You can create a query builder instance using the Connection::getQuery()
method.
$query = $connection->getQuery();
Fetching data
If you only want to retrieve a single row then you can use the first
method.
$person = $query->table('persons')->where('id', '=', 1)->first();
Note that the
first
method will returnnull
if nothing is found.
If you want to throw an exception if there isn't a matching record then you can use the firstOrThrow
method.
// By default if throws a mako\database\exceptions\NotFoundException
$person = $query->table('persons')->where('id', '=', 1)->firstOrThrow();
// But you can make it throw any exception you want
// You can for example throw a mako\http\exceptions\NotFoundException to display a 404 page
$person = $query->table('persons')->where('id', '=', 1)->firstOrThrow(NotFoundException::class);
Fetching all rows is done using the all
method.
$persons = $query->table('persons')->all();
You can also specify which columns you want to include in the result set
$persons = $query->table('persons')->select(['name', 'email'])->all();
To make a distinct selection use the distinct
method
$persons = $query->table('persons')->select(['name', 'email'])->distinct()->all();
Note that the
all
andpaginate
methods return a result set object and not an array so you'll need to use theisEmpty
method to check if it's empty.
Selecting from the results of a subquery is also possible.
$persons = $query->table(new Subquery(function ($query) {
$query->table('persons')->select(['name'])->distinct();
}, 'distinct_names'))
->where('name', '!=', 'John Doe')
->all();
You can also use the as
method of the Subquery
to set the subquery table alias.
$persons = $query->table((new Subquery(function ($query) {
$query->table('persons')->select(['name'])->distinct();
}))->as('distinct_names'))
->where('name', '!=', 'John Doe')
->all();
Advanced column selections can also be made using raw SQL and subqueries.
$persons = $query->table('persons')->select([
'name',
'email',
new Raw("CASE gender WHEN 'm' THEN 'male' ELSE 'female' END AS gender"),
new Subquery(function ($query) {
$query->table('persons')->select([new Raw('AVG(age)']));
}, 'average_age')
])
->all();
If you need to process a large dataset and don't want to put the entire result set in memory then you can use the yield
method. It returns a generator that lets you iterate over the result set.
$persons = $query->table('persons')->select(['name', 'email'])->yield();
foreach ($persons as $person) {
// Only a single row is kept in memory at a time
}
Note that when using MySQL you might have to configure PDO to use unbuffered queries for this to work as expected.
In addition to using the yield
method to process large amounts of data you can also use the batch
method. The default batch size is a 1000 records but you can override this using the optional second parameter.
You can also set the offset starting point and offset end point using the optional third and fourth parameters respectively. This is useful if you have parallel workers processing data.
$query->table('persons')->ascending('id')->batch(function ($batch) {
// Process the batch here
});
Fetching the value of a single column is done using the column
method.
$email = $query->table('persons')->select(['email'])->where('id', '=', 1)->column();
// You can also use the following syntax
$email = $query->table('persons')->where('id', '=', 1)->column('email');
It is also possible to fetch an array containing the values of a single column using the columns
method.
$emails = $query->table('persons')->select(['email'])->columns();
// You can also use the following syntax
$emails = $query->table('persons')->columns('email');
The pairs
method allows you to fetch an array where the first column is used as the array keys and the second is used as the array values.
$pairs = $query->table->('users')->pairs('id', 'email');
The blob
method allows you to stream the contents of a blob column. The method will return null
if no matching record is found.
$stream = $query->table->('images')->where('id', '=', 1)->blob('image');
if ($stream !== null) {
$image = fopen('image.jpg');
stream_copy_to_stream($stream, $image);
fclose($image);
fclose($stream);
}
Inserting data
Inserting data is done using the insert
method.
$query->table('foobars')
->insert(['field1' => 'foo', 'field2' => new DateTime]);
You can also insert data using the insertAndGetId
method. It will create the record and return the generated auto increment id.
$query->table('foobars')
->insertAndGetId(['field1' => 'foo', 'field2' => new DateTime]);
When working with PostgreSQL the
insertAndGetId
method assumes that the sequence follows the default naming convention (<table_name>_<primary_key_name>_seq
) You can override the default primary key name (id
) by using the optional second parameter.
Sometimes you want to insert data only if a matching record doesn't already exist. This is where the insertOrUpdate
method comes in handy. The first parameter is the values you want to insert when creating a new record while the second parameter is the values you want to update in case of a conflict.
$query->table('foobars')
->insertOrUpdate(
['field1' => 'foo', 'field2' => new DateTime],
['field2' => new DateTime]
);
There is also a optional third parameter that lets you define the conflict target (the column or set of columns that must be unique). This is not necessary when working with MySQL and MariaDB but it is required when working with PostgreSQL and SQLite.
It is advised to use the third optional parameter if you want your code to be database agnostic. The value will simply be ignored when querying databases that don't require it to be set.
$query->table('foobars')
->insertOrUpdate(
['field1' => 'foo', 'field2' => new DateTime],
['field2' => new DateTime]
['field1']
);
Updating data
Updating data is done using the update
method.
$query->table('foobars')
->where('id', '=', 10)
->update(['field1' => 'foo', 'field2' => new DateTime]);
There are also shortcuts for incrementing and decrementing column values:
$query->table('articles')->where('id', '=', 1)->increment('views');
$query->table('articles')->where('id', '=', 1)->increment('views', 10);
$query->table('shows')->where('id', '=', 1)->decrement('tickets')
$query->table('shows')->where('id', '=', 1)->decrement('tickets', 50);
Deleting data
Deleting data is done using the delete
method.
$query->table('articles')->where('id', '=', 10)->delete();
Aggregates
The query builder also includes a few handy shortcuts to the most common aggregate functions:
// Counting
$count = $query->table('persons')->count();
$count = $query->table('persons')->where('age', '>', 25)->count();
// Distinct counting
$count = $query->table('persons')->countDistinct('age');
$count = $query->table('persons')->countDistinct(['age', 'height']);
// Average value
$height = $query->table('persons')->avg('height');
$height = $query->table('persons')->where('age', '>', 25)->avg('height');
// Largest value
$height = $query->table('persons')->max('height');
$height = $query->table('persons')->where('age', '>', 25)->max('height');
// Smallest value
$height = $query->table('persons')->min('height');
$height = $query->table('persons')->where('age', '>', 25)->min('height');
// Sum
$height = $query->table('persons')->sum('height');
$height = $query->table('persons')->where('age', '>', 25)->sum('height');
Clauses
WHERE clauses
where(), orWhere(), whereColumn(), orWhereColumn(), whereRaw(), orWhereRaw(), whereDate(), orWhereDate()
// SELECT * FROM `persons` WHERE `age` > 25
$persons = $query->table('persons')->where('age', '>', 25)->all();
// SELECT * FROM `persons` WHERE `age` > 25 OR `age` < 20
$persons = $query->table('persons')->where('age', '>', 25)->orWhere('age', '<', 20)->all();
// SELECT * FROM `persons` WHERE (`username`, `email`) = ('foo', 'foobar@example.org') LIMIT 1
$person = $query->table('persons')->where(['username', 'email'], '=', ['foo', 'foo@example.org'])->first();
// SELECT * FROM `persons` WHERE (`age` > 25 AND `height` > 180)
$persons = $query->table('persons')
->where(function ($query) {
$query->where('age', '>', 25);
$query->where('height', '>', 180);
})
->all();
The whereColumn
and orWhereColumn
methods allow you to compare two columns.
// SELECT * FROM `persons` WHERE `first_name` = `last_name`
$persons = $query->table('persons')->whereColumn('first_name', '=', 'last_name')->all();
The wereRaw
and orWhereRaw
methods allow you to set a "raw" parameter value or to write an entire sql expression.
// SELECT * FROM `persons` WHERE `age` > AVG(`age`)
$persons = $query->table('persons')->whereRaw('age', '>', 'AVG(`age`)')->all();
// SELECT * FROM `persons` WHERE MATCH(`name`) AGAINST ('foobar' IN BOOLEAN MODE)
$persons = $query->table('persons')->whereRaw('MATCH(`name`) AGAINST (? IN BOOLEAN MODE)', ['foobar']);
The whereDate
and orWhereDate
methods allow you to easily match records based on the date portion of a datetime column. The methods accept dates in the YYYY-MM-DD
format and instances of DateTimeInterface
.
// SELECT * FROM `articles` WHERE `created_at` > '2019-07-08 23:59:59.999999'
$articles = $query->table('articles')->whereDate('created_at', '>', '2019-07-08')->all();
WHERE BETWEEN clauses
between(), orBetween(), notBetween(), orNotBetween(), betweenDate(), orBetweenDate(), notBetweenDate(), orNotBetweenDate()
// SELECT * FROM `persons` WHERE `age` BETWEEN 20 AND 25
$persons = $query->table('persons')->between('age', 20, 25)->all();
// SELECT * FROM `persons` WHERE `age` BETWEEN 20 AND 25 OR `age` BETWEEN 30 AND 35
$persons = $query->table('persons')->between('age', 20, 25)->orBetween('age', 30, 35)->all();
The betweenDate
, orBetweenDate
, notBetweenDate
and orNotBetweenDate
methods make it easy to match records between two dates using the date portion of a datetime column. The methods accept dates in the YYYY-MM-DD
format and instances of DateTimeInterface
.
// SELECT * FROM `articles` WHERE `created_at`
// BETWEEN '2019-07-01 00:00:00.000000' AND '2019-07-31 23:59:59.999999'
$articles = $query->table('articles')->betweenDate('created_at', '2019-07-01', '2019-07-31')->all();
WHERE IN clauses
in(), orIn(), notIn(), orNotIn()
// SELECT * FROM `persons` WHERE `id` IN (1, 2, 3, 4, 5)
$persons = $query->table('persons')->in('id', [1, 2, 3, 4, 5])->all();
// SELECT * FROM `persons` WHERE `id` IN (SELECT `id` FROM `persons` WHERE `id` != 1)
$persons = $query->table('persons')
->in('id', new Subquery(function ($query) {
$query->table('persons')->select(['id'])->where('id', '!=', 1);
}))
->all();
WHERE IS NULL clauses
isNull(), orIsNull(), isNotNull(), orIsNotNull()
// SELECT * FROM `persons` WHERE `address` IS NULL
$persons = $query->table('persons')->isNull('address')->all();
WHERE EXISTS clauses
exists(), orExists(), notExists(), orNotExists()
// SELECT * FROM `persons` WHERE EXISTS (SELECT * FROM `cars` WHERE `cars`.`person_id` = `persons`.`id`)
$persons = $query->table('persons')
->exists(new Subquery(function ($query) {
$query->table('cars')->whereRaw('cars.person_id', '=', 'persons.id');
}))
->all();
JOIN clauses
join(), joinRaw(), leftJoin(), leftJoinRaw(), rightJoin(), rightJoinRaw(), crossJoin(), lateralJoin()
// SELECT * FROM `persons` INNER JOIN `phones` ON `persons`.`id` = `phones`.`user_id`
$persons = $query->table('persons')->join('phones', 'persons.id', '=', 'phones.user_id')->all();
// SELECT * FROM `persons` AS `u` INNER JOIN `phones` AS `p` ON
// (`u`.`id` = `p`.`user_id` OR `u`.`phone_number` = `p`.`number`)
$persons = $query->table('persons as u')
->join('phones as p', function ($join) {
$join->on('u.id', '=', 'p.user_id');
$join->orOn('u.phone_number', '=', 'p.number');
})
->all();
// SELECT * FROM `drinks` CROSS JOIN `meals`
$menu = $query->table('drinks')->crossJoin('meals')->all();
// SELECT `customers`.*, `recent_sales`.* FROM `customers` LEFT OUTER JOIN LATERAL (
// SELECT * FROM `sales` WHERE `sales`.`customer_id` = `customers`.`id`
// ORDER BY `created_at` DESC LIMIT 3
// ) AS `recent_sales` ON TRUE
$customers = $query->table('customers')
->lateralJoin(new Subquery(function (Query $query): void {
$query->table('sales')
->whereRaw('sales.customer_id', '=', 'customers.id')
->descending('created_at')
->limit(3);
}, 'recent_sales'))
->select(['customers.*', 'recent_sales.*'])->all();
GROUP BY clauses
groupBy()
// SELECT `customer`, `order_date`, SUM(`order_price`) as `sum` FROM `orders` GROUP BY `customer`
$customers = $query->table('orders')
->select(['customer', new Raw('SUM(price) as sum')])
->groupBy('customer')
->all();
// SELECT `customer`, `order_date`, SUM(`order_price`) as `sum` FROM `orders` GROUP BY `customer`, `order_date`
$customers = $query->table('orders')
->select(['customer', 'order_date', new Raw('SUM(price) as sum')])
->groupBy(['customer', 'order_date'])
->all();
HAVING clauses
having(), havingRaw(), orHaving(), orHavingRaw()
// SELECT `customer`, SUM(`price`) AS `sum` FROM `orders` GROUP BY `customer` HAVING SUM(`price`) < 2000
$customers = $query->table('orders')
->select(['customer', new Raw('SUM(price) as sum')])
->groupBy('customer')
->havingRaw('SUM(price)', '<', 2000)
->all();
ORDER BY clauses
orderBy(), orderByRaw(), descending(), descendingRaw(), ascending(), ascendingRaw()
// SELECT * FROM `persons` ORDER BY `name` ASC
$persons = $query->table('persons')->orderBy('name', 'asc')->all();
// SELECT * FROM `persons` ORDER BY `name` ASC
$persons = $query->table('persons')->ascending('name')->all();
// SELECT * FROM `persons` ORDER BY `name` DESC
$persons = $query->table('persons')->descending('name')->all();
// SELECT * FROM `persons` ORDER BY `name` ASC, `age` DESC
$persons = $query->table('persons')->orderBy('name', 'asc')->orderBy('age', 'desc')->all();
// SELECT * FROM `persons` ORDER BY `name`, `age` ASC
$persons = $query->table('persons')->orderBy(['name', 'age'], 'asc')->all();
LIMIT and OFFSET clauses
limit(), offset(), paginate()
// SELECT * FROM `persons` LIMIT 10
$persons = $query->table('persons')->limit(10)->all();
// SELECT * FROM `persons` LIMIT 10 OFFSET 10
$persons = $query->table('persons')->limit(10)->offset(10)->all();
You can also use the paginate
method to limit your results.
// SELECT * FROM `persons` LIMIT 10 OFFSET 0
$persons = $query->table('persons')->paginate(10);
Common table expressions
with(), withRecursive()
The with
method allows you to add common table expressions to your queries.
// WITH `cte` AS (SELECT 1, 2, 3) SELECT * FROM `cte`
$result = $query->with('cte', [], new Subquery(function ($query) {
$query->selectRaw('1, 2, 3');
}))->table('cte')->all();
// WITH `cte` (`one`, `two`, `three`) AS (SELECT 1, 2, 3) SELECT * FROM `cte`
$result = $query->with('cte', ['one', 'two', 'three'], new Subquery(function ($query) {
$query->selectRaw('1, 2, 3');
}))
->table('cte')->all();
You can also add recursive common table expressions using the withRecursive
method.
// WITH RECURSIVE `cte` AS (SELECT 1, 2, 3) SELECT * FROM `cte`
$result = $query->withRecursive('cte', [], new Subquery(function ($query) {
$query->selectRaw('1, 2, 3');
}))->table('cte')->all();
// WITH RECURSIVE `cte` (`one`, `two`, `three`) AS (SELECT 1, 2, 3) SELECT * FROM `cte`
$result = $query->withRecursive('cte', ['one', 'two', 'three'], new Subquery(function ($query) {
$query->selectRaw('1, 2, 3');
}))
->table('cte')->all();
The query builder allows you add multiple common table expressions to your queries and it even supports nesting.
Set operations
union(), unionAll(), intersect(), intersectAll(), except(), exceptAll()
You can also combine the results of multiple queries into a single result set using set operations.
// SELECT * FROM `sales2015` UNION SELECT * FROM `sales2016`
$result = $query->table('sales2015')->union()->table('sales2016')->all();
Row-level locking
lock(), sharedLock()
The lock()
method can be used to enable row-level locking during database transactions.
// SELECT * FROM `persons` WHERE `age` = 30 FOR UPDATE
$persons = $query->table('persons')->where('age', '=', 30)->lock()->all();
It will use an exclusive lock by default but you can enable shared locking by passing false
to the lock()
method or by using the sharedLock()
method.
// SELECT * FROM `persons` WHERE `age` = 30 LOCK IN SHARE MODE
$persons = $query->table('persons')->where('age', '=', 30)->lock(false)->all();
It is also possible to provide a custom locking clause.
// SELECT * FROM `persons` WHERE `age` = 30 CUSTOM LOCK
$persons = $query->table('persons')->where('age', '=', 30)->lock('CUSTOM LOCK')->all();
Here's an overview of the locking clauses generated for the different RDBMSes that support row-level locking.
RDBMS | Exclusive lock | Shared lock |
---|---|---|
Firebird | FOR UPDATE WITH LOCK | WITH LOCK |
MySQL | FOR UPDATE | LOCK IN SHARE MODE |
Oracle | FOR UPDATE | FOR UPDATE |
PostgreSQL | FOR UPDATE | FOR SHARE |
SQLServer | WITH (UPDLOCK, ROWLOCK) | WITH (HOLDLOCK, ROWLOCK) |
Row-level locking will gracefully degrade for any RDBMS that doesn't support the feature.
Dialect specific SQL
Sometimes you'll find yourself in situations where you have to use dialect specific features in your queries while still supporting multiple databases. This is where the forCompiler
method comes in handy.
The first parameter is the compiler class name and the second one is a closure where you can build upon the query.
$events = $query->table('events')
->forCompiler(PostgreSQL::class, function ($query) {
$query->whereRaw('EXTRACT(YEAR FROM "date") = ?', ['1337']);
})
->forCompiler(MySQL::class, function ($query) {
$query->whereRaw('YEAR(`date`) = ?', ['1337']);
})
->all();
JSON data
The query builder features a unified syntax for querying JSON data and it currently supports MySQL
, Oracle
, PostgreSQL
, SQLServer
and SQLite
.
$foos = $query->table('articles')
->select(['meta->foo as foo'])
->where('meta->bar', '=', json_encode(1))
->all();
You can also use the unified syntax to update JSON values. This feature currently supports MySQL
, PostgreSQL
(jsonb), SQLServer
and SQLite
.
$query->table('articles')->update(['meta->bar' => json_encode(0)]);
Array and JSON representations of results
You can convert both single result and result set objects to arrays and JSON using the toArray
and toJson
methods respectively. JSON encoding of your results can also be achieved by using the json_encode
function or by casting the objects to strings.
The
toJson
method accepts the same optional option flags as thejson_encode
function.
$json = (string) $query->table('articles')->select(['id', 'title', 'content'])->limit(10)->all();
The code above will result in the following JSON:
[
{"id": 1, "title": "Article 1", "content": "Article 1 content"},
{"id": 2, "title": "Article 2", "content": "Article 2 content"},
...
{"id": 9, "title": "Article 9", "content": "Article 9 content"},
{"id": 10, "title": "Article 10", "content": "Article 10 content"}
]
Data fetched using the paginate
method will return a JSON object instead of an array. The records are available as data
while pagination information is available as pagination
:
{
"data": [
{"id": 1, "title": "Article 1", "content": "Article 1 content"},
{"id": 2, "title": "Article 2", "content": "Article 2 content"},
...
{"id": 9, "title": "Article 9", "content": "Article 9 content"},
{"id": 10, "title": "Article 10", "content": "Article 10 content"}
],
"pagination": {
"current_page": 1,
"number_of_pages": 1,
"items": 10,
"items_per_page": 50,
"first": "https:\/\/example.org\/api\/articles?page=1",
"last": "https:\/\/example.org\/api\/articles?page=1",
"next": null,
"previous": null
}
}