ODM 3.0.0
Breeze uses MongoDB for database. That ODM is called Breeze, it provides a very smooth experience to create models and migrations and work with the database.
Connecting
By default, in your .env
file, you have a DATABASE_DSN
. This is where you put your mongodb dsn string. But one thing that is very cool, is that if for some reason, you need to connect to more than one database. Breeze offers multi database support. You can see this as a very low cost way to scale your database if needed.
To connect to multiple sources, simply use multiple DATABASE_DSN
by given them ids, like so:
DATABASE_DSN=mongodb://...
DATABASE_DSN_2=mongodb://...
The main DSN should never have a number. In code, your main DSN is refered to as connection 0
.
class YourModel extends Model
{
protected int $connection = 0; // or 1 if you have 2 DSNs. 0 is the defaut value
}
First steps
To create your first model, use the CLI:
php sail create:model <module_or_container> <container_or_module_name> <model_name>
So for example:
php sail create:model container Spec Product
This would create the Product
model in the Spec
container space. The standard is naming your model using singular grammar. Automatically, Breeze will know to use the plural for the collection in the database. In this case, Product
would store data in the products
collection.
Custom collection name
By default, when creating a model, Breeze will take the name of your model and pluralize it and set it to snakecase. so MyModel
would be in collection my_models
. Sometimes, that's perfect, but sometimes that is not what you want. To change it, simply define it like this:
class YourModel extends Model
{
protected string $collection = 'my_super_models';
}
Helping your IDE help you
To make sure your IDE helps you for code completion and doesn't go crazy saying "accessed property using magic methods", we suggest adding a phpdoc block to define your properties, here is an example:
/**
*
* @property string $name
* @property int $number
* @property Collection $list
*
*/
class YourModel extends Model
{
// ...
}
Fields
the field
property let's Breeze know what fields are allowed to be in the json representation of your model. by default, the value is set to ['*']
, meaning, all fields will be allowed for json representation.
If you want to keep sensitive information out of json representations, use the guards
property.
Guards
Guards are used to protect sensitive information from leaving the cms. That means that whenever a model is encoded to json, the fields in the model's guard
property will be omitted.
Casting
Breeze offers a more advanced concept that is basically this:
When you store an array or object in MongoDB, it comes back as simple array and object. But most times, you'll want to cast an array to Collection
or an object to one of your custom objects. Breeze does not how to do that unless you tell it how.
Enter, Casting. Casting is a property in your model called casting
. This is an array where you define what should be cast to what type. The type you want it to be casted to must implement the Castable
interface and implement it's 2 methods.
Out of the box, Breeze supports Carbon
(date), ObjectId
(mongodb id), string
(for ObjetId), Collection
, DateTime
(for integers like timestamps) and encrypted
.
It's also important to note that the casting is done automatically for you when you fetch records from the database. But it's also done automatically when you write, the inverse conversion is done, thanks to the Castable
interface.
So for example, if your field is cast as encrypted
, that means that if you fetch a record from the database, it will be decrypted for you, if you write a value to the database, it will be encrypted just before being writen to the database. So you don't need to encrypt it yourself.
Here is an example of casting:
class YourModel extends Model
{
protected array $casting = [
'keyname' => 'encrypted',
'otherkey' => Collection::class,
'date' => Carbon::class
];
}
Validators
Validators are used to validate the data before being inserted or updated in the database. For example, you want to make sure that the title of your content to be "not empty", the validators would do this check for you automatically.
Here is the list of built-in validators:
Identifier | Description |
---|---|
not-empty | Validates that property is not empty |
string | Validates that property is a string |
numeric | Validates that property is of a numeric value (float or int) |
boolean | Validates that property is a boolean |
Provided validators can be used together to perform multiple validations, for example,
"not-empty,string" would validate that the property is not empty and is of type string.
Custom Validator
If you want, you can build your own custom validator. To do that, you need to have a class that implements the Validator
interface and have the validate
method. to register it in the $validators
property in your model.
protected array $validators = [
'propA' => MyValidator::class
];
And your validator looks like this
use Breeze\Contracts\Validator;
class MyValidator implements Validator
{
public static function validate(string $key, mixed $value): void
{
// Your code validates the value, if everything is ok
// you do and return nothing. In the case that something
// is wrong, you throw an exception
// Note: if you do not throw an exception, the database will save your
// data as is and act like everything was validated.
}
}
Permission Checking
If you would like to check for permissions for a specific call, you have access to the hasPermissions
method. If you do not specify anything, it will check for write permission. If you provide true, it will check for read permission.
Before you can use this method, you need to define what ACL group your model works with. Here is an example for model that uses a custom products
ACL group, using the permissionGroup
property:
class YourModel extends Model
{
protected string $permissionGroup = 'the_name_of_you_acl';
}
Then anywhere in your model, you can call:
$this->hasPermission(); // check write
if your code makes it passed this method, that means permission is allowed. otherwise, the code will throw an exception saying permission denied
.
Separation of Concern
Breeze enforces separation of concern. What that mean is that Breeze forces you to do your database queries from within a model. You cannot perform any queries from outside. Well, you can but it's extra work for you to get that going.
Querying
One thing to know is that Breeze uses chaining to simplify and make developer experience better when querying. Here is a basic get by id.
public function getById(ObjectId|string $id): ?YourModel
{
return $this->findById($id)->exec();
}
Breeze offers the classic options for querying that mongodb has. find
, findOne
, count
, aggregate
but it also has the very useful findById
which accepts a string or ObjectId. The other 2 find calls require that _id
be an ObjectId.
One special method also supported is distinct
to perform a distinct queries much like MySQL does.
Query Options
Breeze offers a simple of gathering all possible options of a call into a simple object. This gathers sort
, skip
, limit
and projection
. Here are examples of how to use it:
// Init with all options
$this->find(['some_field' => true], QueryOptions::init(['yourfield' => 1], 0, 100, ['sortkey' => 1]));
// Init with only sort
$this->find(['some_field' => true], QueryOptions::initWithsort(['sortkey' => 1]);
// Init with only pagination (skip and limit)
$this->find(['some_field' => true], QueryOptions::initWithPagination(0, 100));
// Init with only projection
$this->find(['some_field' => true], QueryOptions::initWithProjection(['yourfield' => 1]));
// You can add that to a query
$this->find([], QueryOptions::initWithPagination(0, 100))->exec();
QueryOptions alternative
The QueryOptions
are made for better code reuse and to tighten up large queries. But if you want to do "one-off" settings like skip documents or limit the amount of documents to return, you can use the inline methods:
$this->find()->skip(5)->limit(20)->exec();
Every option in QueryOptions
is available as an inline method:
$this->find()->skip(5)
->limit(10)
->collation('fr')
->sort(['field' => 1])
->project(['fieldA' => 1])
->exec();
Populating
Breeze implements a feature that is found in the popular orm Mongoose
for nodejs, populate. This enables you to populate fields that represent an id of another collection without having to do an extra query in your code.
For example, let's take this collection structure and see how we can use populate to save on queries.
{
"_id": "63653ce2fc482bfbb70d4f76",
"name": "Apple",
"author_id": "634ed5a2af9ad9278209b245",
"category_id": "6346e9e988bac135e7018214"
}
$this->find([])
->populate('author_id', 'author', User::class)
->populate('category_id', 'category', Category::class)
->exec();
This would add author
and category
properties to your returned documents. Don't worry if the value of the field you are targeting for population (eg: author_id
) is empty or null, Breeze will know to not call populate on it. Instead, it will create the property and set it to null
. If you are using this for GraphQL, we suggest having those fields be optional in case a value would be null.
Like Mongoose, Breeze supports nested population. Here is an example of that in action:
$this->find([])
->populate('something_id', 'something', Something:class, [
['somefield_in_something', 'somefield', SomeField::class]
])->exec();
Caching
Breeze offers fully-managed caching using Redis within models. This means that if you provide a key and optionally a time to live, the model system will read/write to cache if it's active. To use caching, simply define your key in the exec method.
$this->find([...])->exec('mySpecialKey', Cache::TTL_MONTH);
By default, if no TTL given, Cache::TTL_WEEK
will be used.
A thing to note is that model and list of models when returning from cache use what we call CastBack
which casts the value of the model (json) back to its original model, so you don't have to.
Inserting
To insert a record into the database, you have access to 2 methods, insert
and insertMany
for bulk operations.
$this->insert([
'field' => $value
]);
Insert methods return the id of the records created
Updating
To perform updates, you have either updateMany
or updateOne
. Here is how you can update a document.
$this->updateOne(['_id' => $id], ['$set' => ['fielda' => 1, 'fieldb' => 2]]);
Update methods return the number of records affected by the operation.
But you can also use any of mongodb's update operators (ex: $push
, $pull
, etc.).
Deleting
Like updates, Breeze supports deleteMany
, deleteOne
and deleteById
.
Delete methods return the number of records affected by the operation.
Bulk Operations
Breeze has support for MongoDB's bulkWrite
command. This enables high effiency and performance when you have bulk of data to update. This is preferred to the writing as many times as the number of records you are updating methodology. Here is an example of a bulkwrite for updating records.
foreach ($loopEl as $num => $loop) {
$ops = [
'updateOne' => [
['_id' => $loop->_id],
['$set' => ['value' => ($num + 1)]]
]
];
}
$this->bulkWrite($ops);
This loops through records to build an array of operations to perform, once we are ready, we perform only 1 database operation. This will always be more efficient than doing 1 operation per record.
Adding indexes using code
If your code needs to perform indexing of collections, you can easily perform this task with
// Optional options
$options = [
'unique' => true,
'name' => 'your_index_name',
// etc.. (all available mongodb options are available)
]
$this->addIndex(['field' => 1], $options); // 1 = Ascending, -1 Descending
// or
$this->addIndexes([['field' => 1], ['field2' => -1]]);
As you can add indexes, you can remove them with dropIndex
or dropIndexes
.
NOTE: Breeze supports creating basic indexes, if you need more advanced indexes, it's preferred to be done directly in your database.
Logging Queries
You can activate the logging
configuration (works only in development mode), this will log your queries to wherever you have configured the logger. We recommended database logging as it's clearer than flat file.
Utilities
ensureObjectId
This method takes an ObjectId or string as argument and makes sure to return an ObjectId. The reason this exists is because developing using the _id as string in the code is simpler than dealing the object everywhere. But the database expects the object, this makes sure that the variable you have is an ObjectId before you pass it to mongodb.
ensureObjectIds
This method is the same as ensureObjectId
but works on an array of ids.
timeToDate
This turns any timestamp into a UTCDateTime object.
safe
This method is there for you to filter out possible bad user input. Even if the database is not SQL, Document Injection is a very real thing. To prevent this, use safe
on the data your want to store, it will make sure it's safe to be used in a database call. This method is recursive, that means, all of the input will be sanitized.
NOTE: This is to process values, not entire mongodb object/insert/update/delete calls.