Database
Amarant framework features a hybrid active record object relational mapper (ORM). Data is mapped using data models. Each data model is bound to a single database table.
The supported database type is PostgreSQL. See system requirements.
Database migrations are written using pure SQL for maximum flexibility.
Note
When referring to field or column, we always mean a colum in a database table.
Connections
Connections are abstractions that enable access to the database. Internally, each connection uses an adapter to communicate with the database. An adapter then actually connects to a database, for example using a PDO.
By default, the application has at least two connections configured, using the configuration mentioned here.
The first one, using the binding identifier db.connection.default
represents the default connection while the second, db.connection.readonly is meant to be used
as a read-only connection.
Note
Although by default, both of these connections use the same database, you can reconfigure them using a dependency injection configurator.
Creating a connection
If needed, you can create additional connections using dependency injection configurator.
Check the annotations to know more.
- Create an adapter.
- The adapter implementation to use.
- Inject parameters from the environment.
- Optionally, add PDO options.
- Create a connection.
- The connection implementation to use.
- Inject the newly created adapter.
- Add connection to connection pool constructor parameter.
- The name of the new connection in the pool.
The connection can now be fetched from the pool like this:
$customConnection = $connectionPool->getConnection('custom');
Note
Connections will not establish a connection to the database in advance, but only when the connection is used to perform some query.
Data models
When data is loaded from the database, it's hydrated to a data model. Also, the data from a data model can be persisted to the database. Multiple hydrators and persisters can be used in this process.
When data models are loaded, saved or deleted, multiple events are fired, allowing event subscribers to access the data model, check what fields have changed, etc.
A data model can store data which is not a part of its table schema. By default, and when using only the default persister, only the properties that exist in the table schema will be persisted.
The reason for this is to allow multiple hydrators to enrich the data model with additional data. Conversely, to allow multiple persisters to save additional data, for example relational data to other tables.
Create a data model
Check the annotations to know more.
- Add method annotations for the IDE.
- Extend the base data model
Amarant\Framework\Data\Database\DataModel. - It's recommended that you define table fields as public constants or an enum to be able to use them when building queries.
- This is the only method that must be implemented when extending the base data model. It should correspond to a table name in the database that this data model will be bound to.
To additionally customize the data model, override methods from the base model.
Note
When getting or setting data on a data model, always use get or set and the name of a field in PascalCase as
the method name.
For example, a table field named some_field, its getter method is getSomeField, while the setter is
setSomeField.
You can also use the is or has methods.
The isSomeField would return the value of the field as boolean, while hasSomeField would return if
the data model has a value present for the field, as a boolean.
Primary key
To set the primary key field name, override the method getIdField.
By default, the name is id. To fetch the value of the primary key, use getIdValue method.
Discriminator
Defines a discriminator column (table field) name and a discriminator map which determine the data model class that the data will be hydrated into.
In the following example, when value of table field some_discriminator_field is value a,
the table row will be hydrated to SomeDataModelClass::class.
Otherwise, when the value is value b, the table row will be hydrated using SomeOtherDataModelClass::class class.
| Vendor/ModuleName/DataModel/Example.php | |
|---|---|
Default values
To set default data, override the method getDefaultData and return an array of field names mapped to a value.
This data will be set on the data model upon instantiation.
| Vendor/ModuleName/DataModel/Example.php | |
|---|---|
Tracking changes
A data model stores a copy of the data which can be used to check which values have changed.
Methods hasOriginalData, getOriginalData can be used to fetch the original data of the model.
Additionally, methods isModified and isDeleted can be used to check if the model was modified or deleted.
These flags can be reset using clearModified or changed using setDeleted.
Duplication
To make a copy of a data model, use the copy method. This creates a new instance using the same data, but
without the primary key field.
Important
This doesn't do a deep copy of any nested data models which might have been added to the model.
Events
All the following events are dispatched using
Amarant\Framework\Database\Event\DataModelEvent object
from Amarant\Framework\Database\Repository\AbstractRepository.
Important
When referring to these events, always use Amarant\Framework\Enum\EventEnum. Do not hardcode event names.
data_model_hydrate_after - dispatched after the data was fetched from the database and all hydrators were executed.
data_model_save_before , data_model_save_after - dispatched before and after all persisters are executed.
data_model_delete_before , data_model_delete_after - dispatched before and after the model was deleted.
data_model_delete_by_ids_before , data_model_delete_by_ids_after - dispatched before and after deleting rows using primary keys.
Note
If a transaction is retried, for example in case of a deadlock, each event won't be fired more than once.
Pausing events
By implementing Amarant\Framework\Contract\Database\DataModel\EventAwareDataModelInterface
on a data model, events can be paused.
If the method isPaused returns true, no events will be fired.
Repositories
The next thing we should do after creating a new data model is to create a repository for it.
Repositories expose a public interface the rest of the application can use to fetch, save and delete data for a particular data model.
Always use a default repository class, configured for a particular data model, and inject it into your repository, unless you have a good reason not to. The default repository takes care of events, hydration, persistence and more.
Creating a repository
First, create an interface for the repository. This will ease up testing, as you can swap to a different implementation. Also, you can make the implementation final.
Next, create the implementation.
Check the annotations to know more.
- We'll inject this default repository in the next step.
Configure the repository and inject the default repository.
- Inject a default repository for the
Exampledata model, using call injection on the repository pool.
You can now inject and use ExampleRepositoryInterface.
Migrations
Migrations are used to update the database schema.
They can be executed and reverted. Migrations are executed for each application module, in the order they were added in, and each module is migrated in the order of the module dependency chain.
In the case of revert, same logic applies but in reverse order.
Create a migration
Use the Amarant CLI to create a migration. You must specify the module name.
Note
The name parameter is optional. It's used to add a suffix to the migration file name.
We now have a new migration file created.
Let's create a table in the up method and drop it in the down method.
Execute and revert migrations
To execute pending migrations, use:
To revert migrations, use:
This is a partial revert. It will skip reverting all other migrations after this particular one. To revert
all migrations, remove the until parameter.
Note
You can add the --dry-run parameter to skip database queries, allowing you to see which migrations would
be executed or reverted and in which order.
Warning
Doing a full revert, without the until and dry-run parameters effectively removes all defined tables from the database and
truncates the migrations table.
Hydrators
Hydrators are used to normalize data loaded from the database and populate a data model with it. The default hydrator populates only the fields that are defined in a table a data model is bound to.
Multiple hydrators can be used to add or change the data model's data, for example to load some other data and enrich the model with it.
Create a hydrator
Check the annotations to know more.
- Return
trueif this hydrator supports the data model and the source data. Returningfalsewill result inhydratenot being called on this class. - Use the value from source if the tag priority of this hydrator is before the default hydrator.
- Use the value from the data model if the tag priority of this hydrator is after the default hydrator.
- Always clear the modified flag on the data model, because every change on the model results in the modified flag being set to
true.
Tag the hydrator using dependency injection configurator.
- Use priority lower than 500 to run this hydrator after the default one or use priority greater than 500 to run before it.
Persisters
Persisters are used to save data to the database, converting the values to database suitable format, depending on field types, in the process.
The default persister saves only the fields that are defined in a table a data model is bound to.
As with hydrators, multiple persisters can be used, most commonly to save data to related tables.
Create a persister
Check the annotations to know more.
- Return
trueif this persister supports the data model. Returningfalsewill result inpersistnot being called on this class. - Persisters are executed within a database transaction. Primary key value is already present on the data model if this persister is executed after the default one. It can be used to save data to some other, related tables using the connection or a repository that can be injected in the constructor.
Tag the persister using dependency injection configurator.
- Use priority lower than 500 to run this persister after the default one or use priority greater than 500 to run before it.