tobento / service-repository-storage
Storage repository implementation.
Package info
github.com/tobento-ch/service-repository-storage
pkg:composer/tobento/service-repository-storage
Requires
- php: >=8.4
- tobento/service-collection: ^2.0
- tobento/service-dater: ^2.0
- tobento/service-iterable: ^2.0
- tobento/service-repository: ^2.0
- tobento/service-storage: ^2.0
- tobento/service-support: ^2.0
Requires (Dev)
- phpunit/phpunit: ^12.3
- tobento/service-container: ^2.0
- tobento/service-database-storage: ^2.0
- tobento/service-filesystem: ^2.0
- tobento/service-migration: ^2.0
- vimeo/psalm: ^6.13
Suggests
- tobento/service-database-storage: Support for migration
- tobento/service-migration: Support for migration
README
Storage repository implementation.
Table of Contents
- Getting started
- Documentation
- Credits
Getting started
Add the latest version of the repository storage service project running this command.
composer require tobento/service-repository-storage
Requirements
- PHP 8.4 or greater
Highlights
- Framework-agnostic, will work with any project
- Decoupled design
Documentation
Check out the Storage Service - Storages for its available storages.
Check out the Repository Service for its documentation.
Storage Repository
To create a storage repository simply extend the StorageRepository::class:
You may also check out the Repository With Columns section to create the repository using columns.
use Tobento\Service\Repository\RepositoryInterface; use Tobento\Service\Repository\ReadRepositoryInterface; use Tobento\Service\Repository\WriteRepositoryInterface; use Tobento\Service\Repository\Storage\StorageRepository; use Tobento\Service\Repository\Storage\StorageEntityFactoryInterface; use Tobento\Service\Storage\InMemoryStorage; use Tobento\Service\Storage\Tables\Tables; class ProductRepository extends StorageRepository { // } $repository = new ProductRepository( storage: new InMemoryStorage( items: [], tables: new Tables()->add('products', ['id', 'sku', 'price'], 'id') ), table: 'products', // specify which storage table should be used. entityFactory: null, // null|StorageEntityFactoryInterface ); var_dump($repository instanceof RepositoryInterface); // bool(true) var_dump($repository instanceof ReadRepositoryInterface); // bool(true) var_dump($repository instanceof WriteRepositoryInterface); // bool(true)
By default, the read and write methods will return the following types depending on the method called:
Tobento\Service\Storage\ItemInterface::classStorage - ItemInterfaceTobento\Service\Storage\ItemsInterface::classStorage - ItemsInterfacenull
You may specify a custom Storage Entity Factory to return custom entities by the entityFactory parameter.
Storage Read Repository
To create a storage read repository simply extend the StorageReadRepository::class:
You may also check out the Repository With Columns section to create the repository using columns.
use Tobento\Service\Repository\ReadRepositoryInterface; use Tobento\Service\Repository\Storage\StorageReadRepository; use Tobento\Service\Repository\Storage\StorageEntityFactoryInterface; use Tobento\Service\Storage\InMemoryStorage; use Tobento\Service\Storage\Tables\Tables; class ProductReadRepository extends StorageReadRepository { // adding custom find methods } $repository = new ProductReadRepository( storage: new InMemoryStorage( items: [ 'products' => [ 1 => ['id' => 1, 'sku' => 'paper', 'price' => 1.2], 2 => ['id' => 2, 'sku' => 'pen', 'price' => 1.8], 3 => ['id' => 3, 'sku' => 'pencil', 'price' => 1.5], ], ], tables: new Tables()->add('products', ['id', 'sku', 'price'], 'id') ), table: 'products', entityFactory: null, // null|StorageEntityFactoryInterface ); var_dump($repository instanceof ReadRepositoryInterface); // bool(true)
By default, the find methods will return the following types depending on the method called:
Tobento\Service\Storage\ItemInterface::classStorage - ItemInterfaceTobento\Service\Storage\ItemsInterface::classStorage - ItemsInterfacenull
You may specify a custom Storage Entity Factory to return custom entities by the entityFactory parameter.
findById
use Tobento\Service\Storage\ItemInterface; $entity = $repository->findById(id: 2); var_dump($entity instanceof ItemInterface); // bool(true) var_dump($entity->get('sku')); // string(3) "pen" $entity = $repository->findById(id: 5); var_dump($entity); // NULL
You may check out the Storage Service - ItemInterface for its documentation.
findByIds
use Tobento\Service\Storage\ItemsInterface; use Tobento\Service\Storage\ItemInterface; $entities = $repository->findByIds(1, 2, 8); var_dump($entities instanceof ItemsInterface); // bool(true) var_dump($entities->count()); // int(2) foreach($entities as $entity) { var_dump($entity instanceof ItemInterface); // bool(true) }
You may check out the Storage Service - ItemsInterface for its documentation.
You may check out the Storage Service - ItemInterface for its documentation.
findOne
use Tobento\Service\Storage\ItemInterface; $entity = $repository->findOne(where: [ 'sku' => 'pen', ]); var_dump($entity instanceof ItemInterface); // bool(true) var_dump($entity->get('sku')); // string(3) "pen" $entity = $repository->findOne(where: [ 'sku' => 'foo', ]); var_dump($entity); // NULL
You may check out the Where Parameters for its supported parameters.
You may check out the Storage Service - ItemInterface for its documentation.
findAll
use Tobento\Service\Storage\ItemsInterface; use Tobento\Service\Storage\ItemInterface; $entities = $repository->findAll(where: [ 'price' => ['>' => 1.3], ]); var_dump($entities instanceof ItemsInterface); // bool(true) var_dump($entities->count()); // int(2) foreach($entities as $entity) { var_dump($entity instanceof ItemInterface); // bool(true) } $entities = $repository->findAll( where: [ 'price' => [ '>' => 1.3, '<' => 1.6, ], 'sku' => ['like' => 'pe%'], ], orderBy: [ 'sku' => 'DESC', // or 'ASC' ], limit: 20, // (number) // limit: [20, 5], // [20(number), 5(offset)] ); var_dump($entities->count()); // int(1)
You may check out the Where Parameters for its supported parameters.
You may check out the Storage Service - ItemsInterface for its documentation.
You may check out the Storage Service - ItemInterface for its documentation.
Storage Write Repository
To create a storage write repository simply extend the StorageWriteRepository::class:
You may also check out the Repository With Columns section to create the repository using columns.
use Tobento\Service\Repository\WriteRepositoryInterface; use Tobento\Service\Repository\Storage\StorageWriteRepository; use Tobento\Service\Repository\Storage\StorageEntityFactoryInterface; use Tobento\Service\Storage\InMemoryStorage; use Tobento\Service\Storage\Tables\Tables; class ProductWriteRepository extends StorageWriteRepository { // you may add custom write methods } $repository = new ProductWriteRepository( storage: new InMemoryStorage( items: [ 'products' => [ 1 => ['id' => 1, 'sku' => 'paper', 'price' => 1.2], 2 => ['id' => 2, 'sku' => 'pen', 'price' => 1.8], 3 => ['id' => 3, 'sku' => 'pencil', 'price' => 1.5], ], ], tables: new Tables()->add('products', ['id', 'sku', 'price'], 'id') ), table: 'products', entityFactory: null, // null|StorageEntityFactoryInterface ); var_dump($repository instanceof WriteRepositoryInterface); // bool(true)
By default, the methods will return the following types depending on the method called:
Tobento\Service\Storage\ItemInterface::classStorage - ItemInterfaceTobento\Service\Storage\ItemsInterface::classStorage - ItemsInterfacenull
You may specify a custom Storage Entity Factory to return custom entities by the entityFactory parameter.
create
use Tobento\Service\Storage\ItemInterface; $createdEntity = $repository->create(attributes: [ 'sku' => 'scissors', ]); var_dump($createdEntity instanceof ItemInterface); // bool(true) var_dump($createdEntity->all()); // array(2) { ["sku"]=> string(8) "scissors" ["id"]=> int(4) }
You may check out the Storage Service - ItemInterface for its documentation.
updateById
use Tobento\Service\Storage\ItemInterface; use Tobento\Service\Repository\RepositoryUpdateException; $updatedEntity = $repository->updateById( id: 2, attributes: [ 'price' => 2.5, ] ); var_dump($updatedEntity instanceof ItemInterface); // bool(true) var_dump($updatedEntity->all()); // array(2) { ["sku"]=> string(8) "scissors" ["id"]=> int(4) }
This method will throw a RepositoryUpdateException::class exception if the storage table has no primary key specified or the entity to update does not exist.
You may check out the Storage Service - ItemInterface for its documentation.
update
use Tobento\Service\Storage\ItemsInterface; use Tobento\Service\Storage\ItemInterface; $updatedEntities = $repository->update( where: [ 'id' => ['>' => 1], ], attributes: [ 'price' => 2.5, ], ); var_dump($updatedEntities instanceof ItemsInterface); // bool(true) var_dump($updatedEntities->count()); // int(2) foreach($updatedEntities as $entity) { var_dump($entity instanceof ItemInterface); // bool(true) }
You may check out the Where Parameters for its supported parameters.
You may check out the Storage Service - ItemsInterface for its documentation.
You may check out the Storage Service - ItemInterface for its documentation.
deleteById
use Tobento\Service\Storage\ItemInterface; use Tobento\Service\Repository\RepositoryDeleteException; $deletedEntity = $repository->deleteById(id: 2); var_dump($deletedEntity instanceof ItemInterface); // bool(true) var_dump($deletedEntity->all()); // array(3) { ["id"]=> int(2) ["sku"]=> string(3) "pen" ["price"]=> float(1.8) }
This method will throw a RepositoryDeleteException::class exception if the storage table has no primary key specified or the entity to delete does not exist.
You may check out the Storage Service - ItemInterface for its documentation.
delete
use Tobento\Service\Storage\ItemsInterface; use Tobento\Service\Storage\ItemInterface; $deletedEntities = $repository->delete(where: [ 'id' => ['>' => 1], ]); var_dump($deletedEntities instanceof ItemsInterface); // bool(true) var_dump($deletedEntities->count()); // int(2) foreach($deletedEntities as $entity) { var_dump($entity instanceof ItemInterface); // bool(true) }
You may check out the Where Parameters for its supported parameters.
You may check out the Storage Service - ItemsInterface for its documentation.
You may check out the Storage Service - ItemInterface for its documentation.
Where Parameters
The following where clauses are supported (for all read/write methods with where parameter):
$entities = $repository->findAll(where: [ 'sku' => 'pen', // is equal to: 'sku' => ['=' => 'pen'], 'sku' => ['!=' => 'pen'], 'sku' => ['null'], 'sku' => ['not null'], 'price' => ['>' => 1.5], 'price' => ['<' => 1.5], 'price' => ['>=' => 1.5], 'price' => ['<=' => 1.5], 'price' => ['<>' => 1.5], 'price' => ['<=>' => 1.5], 'price' => ['between' => [2, 5]], 'price' => ['not between' => [2, 5]], 'id' => ['in' => [2,5,6]], 'id' => ['not in' => [2,5,6]], // Finds any values that (not) start with "a" 'title' => ['like' => 'a%'], 'title' => ['not like' => 'a%'], // Finds any values that (not) end with "a" 'title' => ['like' => '%a'], 'title' => ['not like' => '%a'], // Finds any values that have (not) "a" in any position 'title' => ['like' => '%a%'], 'title' => ['not like' => '%a%'], // With multiple likes using an array 'title' => ['like' => ['a%', '%a']], 'title' => ['not like' => ['a%', '%a']], 'title' => ['or like' => ['a%', '%a']], // Json specific: 'options->color' => 'blue', 'options->colors' => ['contains' => 'blue'], 'options->colors' => ['contains' => ['blue']], 'options->colors' => ['contains one of' => ['blue', 'red']], 'options->color' => ['contains key'], ]);
Or Clauses
To create or clauses add the or keyword before any operator:
$entities = $repository->findAll(where: [ 'title' => ['like' => '%foo%', 'or like' => '%bar%'], // or 'title' => ['like' => '%foo%'], 'price' => ['or >' => 1.5], // or using an array 'id' => ['in' => [2,5,6]], [ 'title' => ['or like' => '%foo%'], 'price' => ['or >' => 1.5], ] ]);
Storage Entity Factory
You may create a custom entity factory to return custom entities by the Storage Repository, Storage Read Repository or Storage Write Repository.
To create a custom entity factory simply extend the EntityFactory::class and adjust the createEntityFromArray method:
use Tobento\Service\Repository\Storage\EntityFactory; use Tobento\Service\Repository\Storage\StorageEntityFactoryInterface; use Tobento\Service\Repository\EntityFactoryInterface; use Tobento\Service\Storage\ItemInterface; class ProductFactory extends EntityFactory { public function createEntityFromArray(array $attributes): Product { // Process the columns reading: $attributes = $this->columns->processReading($attributes); // Create entity: return new Product( id: $attributes['id'] ?? 0, sku: $attributes['sku'] ?? '', ); } } class Product { public function __construct( public readonly int $id, public readonly string $sku, ) {} } $productFactory = new ProductFactory(); $productFactory->setColumns([]); // will be set by the storage var_dump($productFactory instanceof StorageEntityFactoryInterface); // bool(true) var_dump($productFactory instanceof EntityFactoryInterface); // bool(true) $product = $productFactory->createEntityFromArray([ 'id' => 1, 'sku' => 'pen', ]); var_dump($product); // object(Product)#4 (2) { ["id"]=> int(1) ["sku"]=> string(3) "pen" }
Repository With Columns
Creating a storage repository with columns has the following advantages:
- casts values to primitive types on reading and writing
- specify a reader and writer to handle casting
- create database migration based on columns
use Tobento\Service\Repository\Storage\StorageRepository; use Tobento\Service\Repository\Storage\Column; use Tobento\Service\Storage\InMemoryStorage; class ProductRepository extends StorageRepository { // } $repository = new ProductRepository( storage: new InMemoryStorage(items: []), table: 'products', // specify the columns: columns: [ new Column\Id(), new Column\Text('sku'), new Column\Text('title') ->read(fn (string $value, array $attributes): string => ucfirst($value)) ->write(fn (string $value, array $attributes, string $action): string => ucfirst($value)), new Column\Boolean('active'), ], );
You may prefer to specify the columns on its class instead by using the configureColumns method:
use Tobento\Service\Repository\Storage\StorageRepository; use Tobento\Service\Repository\Storage\Column\ColumnsInterface; use Tobento\Service\Repository\Storage\Column\ColumnInterface; use Tobento\Service\Repository\Storage\Column; use Tobento\Service\Storage\InMemoryStorage; class ProductRepository extends StorageRepository { /** * Returns the configured columns. * * @return iterable<ColumnInterface>|ColumnsInterface */ protected function configureColumns(): iterable|ColumnsInterface { return [ new Column\Id(), new Column\Text('sku'), new Column\Text('title') ->read(fn (string $value, array $attributes): string => ucfirst($value)) ->write(fn (string $value, array $attributes, string $action): string => ucfirst($value)), new Column\Boolean('active'), ]; } } $repository = new ProductRepository( storage: new InMemoryStorage(items: []), table: 'products', );
Columns Common Methods
Type
The parameters set on the type method are used for Migration purpose only.
use Tobento\Service\Repository\Storage\Column\Text; use Tobento\Service\Repository\Storage\Column\Int; $column = new Text(name: 'name') ->type(length: 150, nullable: false, default: 'foo', parameters: ['charset' => 'utf8mb4']); $column = new Int(name: 'name') ->type( length: 20, unsigned: true, index: ['name' => 'index_name', 'column' => 'name', 'unique' => true, 'primary' => true], ); $column = new Float(name: 'name', type: 'decimal') ->type(precision: 10, scale: 0);
Check out the Service Database - Column Factory createColumnFromArray method for more detail.
Check out the Service Database - Index Factory createIndexFromArray method for more detail.
Read
You may use the read method to specify a reader (callable). The reader will automatically be called by the repository when attempting to retrieve the value.
use Tobento\Service\Repository\Storage\Column\Text; $column = new Text(name: 'name') ->read(fn (string $value, array $attributes): string => ucfirst($value));
The value is casted to its column type before being passed to the reader!
Write
You may use the write method to specify a writer (callable). The writer will automatically be called by the repository when attempting to write the value. The writer will only be called if the attribute exists on any write methods. If you you want the writer to be always called, check out the Force Writing section.
use Tobento\Service\Repository\Storage\Column\Text; $column = new Text(name: 'name') ->write(fn (string $value, array $attributes, string $action): string => ucfirst($value)); // $action = the action name processed such as 'create' or 'update'
The value is casted to its column type before being passed to the writer!
Force Writing
You may use the forceWriting method to specify if you want to force writing, meaning the writer gets always called even if the attribute was not passed on any write methods.
use Tobento\Service\Repository\Storage\Column\Text; $column = new Text(name: 'name') ->forceWriting(action: 'create|update') ->write(fn (string $value, array $attributes, string $action): string => ucfirst($value)); $column = new Text(name: 'name') ->forceWriting(action: 'create') ->write(fn (string $value, array $attributes, string $action): string => ucfirst($value));
Storable
use Tobento\Service\Repository\Storage\Column\Text; $column = new Text(name: 'name') ->storable(false);
Columns
Boolean
use Tobento\Service\Repository\Storage\Column\Boolean; $column = new Boolean(name: 'active'); $column = new Boolean(name: 'active') ->type(default: true);
Datetime
use Tobento\Service\Repository\Storage\Column\Datetime; $column = new Datetime(name: 'created_at'); // with datetime type (default): $column = new Datetime(name: 'created_at', type: 'datetime') ->type(nullable: true); // with date type: $column = new Datetime(name: 'created_at', type: 'date') ->type(nullable: true); // with time type: $column = new Datetime(name: 'created_time', type: 'time') ->type(nullable: true); // with timestamp type: $column = new Datetime(name: 'created_ts', type: 'timestamp') ->type(nullable: true);
read
You may use the read method to cast your value. Without specifying a read method, the value will be casted to a string only.
use Tobento\Service\Repository\Storage\Column\Datetime; use Tobento\Service\Dater\DateFormatter; use DateTimeImmutable; $column = new Datetime(name: 'created_at'); $read = fn (mixed $value, array $attributes, DateFormatter $df) : DateTimeImmutable => $df->toDateTime(value: $value); $column = new Datetime(name: 'created_at')->read($read);
Check out the Dater Service - DateFormatter for more detail.
write
You may use the write method to cast your value. Without specifying a write method, the value will be casted to the following formats:
- datetime type:
Y-m-d H:i:s - date type:
Y-m-d - time type:
H:i:s - timestamp type: timestamp string
use Tobento\Service\Repository\Storage\Column\Datetime; use Tobento\Service\Dater\DateFormatter; $column = new Datetime(name: 'created_at'); $write = fn (mixed $value, array $attributes, string $action, DateFormatter $df) : string => $df->format(value: $value, format: 'H:i:s'); $column = new Datetime(name: 'created_at')->read($read);
Check out the Dater Service - DateFormatter for more detail.
autoCreate
Use the autoCreate method if you want the date to be automatically created if no value was passed by any write methods.
use Tobento\Service\Repository\Storage\Column\Datetime; $column = new Datetime(name: 'created_at')->autoCreate();
autoUpdate
Use the autoUpdate method if you want the date to be automatically updated if no value was passed by any write methods.
use Tobento\Service\Repository\Storage\Column\Datetime; $column = new Datetime(name: 'updated_at')->autoUpdate();
Float
use Tobento\Service\Repository\Storage\Column\FloatCol; $column = new FloatCol(name: 'name'); // with float type (default): $column = new FloatCol(name: 'name', type: 'float') ->type(nullable: false, default: 0.5); // with double type: $column = new FloatCol(name: 'name', type: 'double') ->type(nullable: false, default: 0.5); // with decimal type: $column = new FloatCol(name: 'name', type: 'decimal') ->type(nullable: false, default: 0.5, precision: 10, scale: 0);
Id
use Tobento\Service\Repository\Storage\Column\Id; $column = new Id(); // with name (default is id): $column = new Id(name: 'some_id'); // with bigPrimary type (default): $column = new Id(type: 'bigPrimary') ->type( length: 18, unsigned: true, index: ['name' => 'index_name', 'primary' => true], ); // with primary: $column = new Id(type: 'primary') ->type( length: 5, unsigned: true, index: ['name' => 'index_name', 'primary' => true], );
Integer
use Tobento\Service\Repository\Storage\Column\Integer; $column = new Integer(name: 'name'); // with int type (default): $column = new Integer(name: 'name', type: 'int') ->type(length: 11, unsigned: true, nullable: false, default: 0); // with tinyInt type: $column = new Integer(name: 'name', type: 'tinyInt') ->type(length: 5, unsigned: true, nullable: false, default: 0); // with bigInt type: $column = new Integer(name: 'name', type: 'bigInt') ->type(length: 200, unsigned: true, nullable: false, default: 0);
Json
use Tobento\Service\Repository\Storage\Column\Json; $column = new Json(name: 'name'); $column = new Json(name: 'name') ->type(nullable: false, default: ['foo', 'bar']);
Text
use Tobento\Service\Repository\Storage\Column\Text; $column = new Text(name: 'sku'); // with string type (default): $column = new Text(name: 'sku', type: 'string') ->type(length: 100, nullable: false, default: ''); // with char type: $column = new Text(name: 'locale', type: 'char') ->type(length: 5, nullable: false, default: 'en'); // with text type: $column = new Text(name: 'desc', type: 'text') ->type(nullable: false, default: 'lorem ipsum');
Translatable
use Tobento\Service\Repository\Storage\Column\Translatable; $column = new Translatable(name: 'name'); // with string subtype (default): $column = new Translatable(name: 'name', subtype: 'string') ->type(nullable: false) ->read(fn (string $value, array $attributes, string $locale): string => strtoupper($value)) ->write(fn (string $value, array $attributes, string $action, string $locale): string => strtoupper($value)); // with array subtype: $column = new Translatable(name: 'name', subtype: 'array') ->type(nullable: false) ->read(fn (array $value, array $attributes, string $locale): array => $value) ->write(fn (array $value, array $attributes, string $action, string $locale): array => $value);
Read Attribute
After reading, a StringTranslations::class or ArrayTranslations::class is being created depending on its column subtype string or array.
use Tobento\Service\Repository\Storage\Attribute\StringTranslations; $repository->locale('en'); $repository->locales('en', 'de', 'fr'); $repository->localeFallbacks(['de' => 'en']); $entity = $repository->findById(id: 2); var_dump($entity->get('title') instanceof StringTranslations); // bool(true) // The title on the current locale set on the repository: $title = (string)$entity->get('title'); // or: $title = $entity->get('title')->get(); // specific locale: $title = $entity->get('title')->get(locale: 'de'); // specific locale with default value // if fallback locale value does not exist: $title = $entity->get('title')->get(locale: 'fr', default: 'title'); // check if translation exists: var_dump($entity->get('title')->has(locale: 'de')); // bool(true) // returns all translations: var_dump($entity->get('title')->all(); // array(2) {["en"]=> string(5) "Title" ["de"]=> string(5) "Titel"}
use Tobento\Service\Repository\Storage\Attribute\ArrayTranslations; $repository->locale('en'); $repository->locales('en', 'de', 'fr'); $repository->localeFallbacks(['de' => 'en']); $entity = $repository->findById(id: 2); var_dump($entity->get('meta') instanceof ArrayTranslations); // bool(true) // The meta on the current locale set on the repository: $meta = $entity->get('meta')->get(); // array(1) {["color"]=> string(3) "red"} // specific locale: $meta = $entity->get('meta')->get(locale: 'de'); // array(1) {["color"]=> string(3) "rot"} // specific locale with default value // if fallback locale value does not exist: $meta = $entity->get('meta')->get(locale: 'fr', default: ['color' => 'rot']); // array(1) {["color"]=> string(3) "red"} // check if translation exists: var_dump($entity->get('title')->has(locale: 'de')); // bool(true) // returns all translations: var_dump($entity->get('meta')->all(); // array(2) {["en"]=> array(1) {["color"]=> string(3) "red"} ["de"]=> array(1) {["color"]=> string(3) "rot"}} // The meta color on the current locale set on the repository: $color = $entity->get('meta')->get(key: 'color'); // string(3) "red" // specific locale: $color = $entity->get('meta')->get(locale: 'de', key: 'color'); // string(3) "rot" // specific locale with default value // if fallback locale value does not exist: $color = $entity->get('meta')->get(locale: 'fr', key: 'color', default: ['color' => 'rot']); // string(3) "red" // check if translation exists: var_dump($entity->get('title')->has(locale: 'de', key: 'color')); // bool(true)
Aliased Columns
Aliased Columns is an enhanced implementation of Columns that adds support for column name aliasing. Aliases allow you to expose alternative, domain-friendly attribute names for reading and writing, while keeping the underlying raw storage column names unchanged.
Note on Write-Safety
Aliases are read-only by default because alias names are not part of the column schema or validation rules.
If external input (e.g.$repo->create(['alias' => 'value'])) contains alias keys, write-through aliasing would allow those values to bypass the validation and type rules defined for the raw column.
Write-through aliasing can be explicitly enabled when alias names are intended to behave exactly like their raw counterparts.
Creating Aliased Columns
use Tobento\Service\Repository\Storage\Column\AliasedColumns; use Tobento\Service\Repository\Storage\Column; $columns = new AliasedColumns( new Column\Text('title'), new Column\Boolean('active'), )->withAliases([ 'name'=> 'title', 'is_active' => 'active', ]);
Aliases map alias to raw:
nametotitleis_activetoactive
Multiple aliases may point to the same raw column.
All such aliases mirror the same raw value and behave consistently in both reading and writing modes.
Alias Validation & Collision Rules
Aliases are validated when calling withAliases().
Invalid or conflicting aliases are silently ignored.
// Raw column exists, alias ignored ->withAliases(['title' => 'active']); // 'title' is already a real column name // Raw column missing, alias ignored ->withAliases(['display_name' => 'missing']); // 'missing' is not a raw column // Valid alias kept ->withAliases(['display_name' => 'title']); // ok // Multiple aliases allowed ->withAliases([ 'name_short' => 'title', // ok 'title_display' => 'title', // ok ]);
Reading (raw to alias)
Alias values are added after raw values have been processed by the parent Columns class.
$attributes = ['title' => 'Hello']; $result = $columns->processReading($attributes); // Result: [ 'title' => 'Hello', 'name' => 'Hello', // alias added ]
Aliases always mirror the processed raw values.
Aliases are only added when their raw column exists.
If the raw key is missing, the alias is silently omitted without error.
Writing (alias to raw)
Aliases are ignored by default because values provided under alias names are not part of the validated column schema.
This prevents external input (e.g. $repo->create(['alias' => 'Hello'])) from bypassing the raw column's validation and write-rules.
$columns = $columns->withAliases(['name' => 'title'], readonly: true); $result = $columns->processWriting(['name' => 'Hello'], 'create'); // Result: []
No raw value is written. Alias writes are removed before parent processing.
Writing (write-through mode)
Enable write-through aliasing explicitly:
$columns = $columns->withAliases(['name' => 'title'], readonly: false); $result = $columns->processWriting(['name' => 'Hello'], 'create'); // Result: [ 'title' => 'Hello', ]
Alias writes are rewritten to their raw column names.
Column Resolution
Alias names resolve to their raw column definitions:
$columns->get('name'); // returns the 'title' column $columns->get('title'); // same column
Translations
Confiure the locales for the repository:
// current locale: $repository->locale('en'); // only the defined locales are used: $repository->locales('en', 'de', 'fr'); // fallbacks: $repository->localeFallbacks(['de' => 'en']);
Where Parameters Translations
Where clauses for translation columns (for all read/write methods with where parameter):
$entities = $repository->findAll(where: [ // query current locale set on the repository: 'title' => ['like' => 'pe%'], // query specific locale using json syntax: 'title->de' => ['like' => 'pe%'], // Array translations: // query current locale set on the repository: 'options->color' => 'red', // same as: options->en->color // query specific locale using json syntax: 'options->de->color' => 'red', ]);
Write Translations
create
$createEntity = $repository->create([ 'title' => [ 'en' => 'Title', 'de' => 'Titel', ], ]);
update
// updates all: $updatedEntity = $repository->updateById(2, [ 'title' => [ 'en' => 'Title', 'de' => 'Titel', ], ]); // updates specific locale using json syntax: $updatedEntity = $repository->updateById(2, [ 'title->de' => 'Title', ]); // Array translations: // updates specific locale using json syntax: $updatedEntity = $repository->updateById(2, [ 'options->de->color' => 'red', ]);
Migration
If you have set up your Repository With Columns, you might use the migration RepositoryAction::class and RepositoryDeleteAction::class to create your database migration from the columns defined.
First, you will need to install:
composer require tobento/service-database-storagecomposer require tobento/service-migration
Example of migration class
use Tobento\Service\Repository\Storage\Migration\RepositoryAction; use Tobento\Service\Repository\Storage\Migration\RepositoryDeleteAction; use Tobento\Service\Migration\MigrationInterface; use Tobento\Service\Migration\ActionsInterface; use Tobento\Service\Migration\Action; use Tobento\Service\Migration\Actions; class UserMigration implements MigrationInterface { public function __construct( protected UserRepository $userRepository, ) {} /** * Return a description of the migration. * * @return string */ public function description(): string { return 'Users migration'; } /** * Return the actions to be processed on install. * * @return ActionsInterface */ public function install(): ActionsInterface { // you might check if repository is supported for the action: if (RepositoryAction::isSupportedRepository($this->userRepository)) { // create action } return new Actions( new RepositoryAction( repository: $this->userRepository, description: 'User migration', // you might set items to be migrated items: [ ['email' => 'demo@example.com'], ], // you may set if items should be created using a boolean: createItems: true, // default // or using a closure: createItems: fn ($repo): bool => is_null($repo->findOne()), ), // you might use the newOrNull method // if the defined repository is of any type. // If it is an unsupported repository // a Action\NullAction::class is created. RepositoryAction::newOrNull( repository: $this->userRepository, description: 'User migration', // you might set items to be migrated items: [ ['email' => 'demo@example.com'], ], createItems: true, // default ), // you might use the newOrFail method // if the defined repository is of any type. // If it is an unsupported repository // a Action\Fail::class is created. RepositoryAction::newOrFail( repository: $this->userRepository, description: 'User migration', // you might set items to be migrated items: [ ['email' => 'demo@example.com'], ], createItems: true, // default ), ); } /** * Return the actions to be processed on uninstall. * * @return ActionsInterface */ public function uninstall(): ActionsInterface { // you might check if repository is supported for the action: if (RepositoryDeleteAction::isSupportedRepository($this->userRepository)) { // create action } return new Actions( new RepositoryDeleteAction( repository: $this->userRepository, description: 'User migration', ), // you might use the newOrNull method // if the defined repository is of any type. // If it is an unsupported repository // a Action\NullAction::class is created. RepositoryDeleteAction::newOrNull( repository: $this->userRepository, description: 'User migration', ), // you might use the newOrFail method // if the defined repository is of any type. // If it is an unsupported repository // a Action\Fail::class is created. RepositoryDeleteAction::newOrFail( repository: $this->userRepository, description: 'User migration', ), ); } }
You may check out the Migration Service for more detail.