yuriitatur / repository
A simple framework/db agnostic repository package
Requires
- php: >=8.2
- psr/log: ^3.0
- symfony/cache-contracts: ^3.6
- symfony/event-dispatcher: ^7.3
- yuriitatur/exceptions: ^1.0
- yuriitatur/query-builder: dev-master
Requires (Dev)
- dg/bypass-finals: ^1.9
- doctrine/dbal: ^4.3
- elasticsearch/elasticsearch: ^9.0
- illuminate/database: ^12.0
- jms/serializer: ^3.32
- kint-php/kint: ^6.0
- meilisearch/meilisearch-php: ^1.0
- mongodb/mongodb: ^2.1
- phpunit/phpunit: ^12.0
- ramsey/uuid: ^4.1
- symfony/serializer: ^7.3
Suggests
- doctrine/dbal: Allows you to use Dbal driver
- elasticsearch/elasticsearch: Allows you to use ElasticSearch driver
- glopgar/monolog-timer-processor: Adds timers to measure query durarions
- jms/serializer: Adds ability to use JmsSerializerEntityMapper
- meilisearch/meilisearch-php: If you want to use meilisearch db driver
- mongodb/mongodb: Allows you to use MongoDB driver
- symfony/event-dispatcher: Allows to use events
- symfony/serializer: Allows to use SymfonySerializerArrayEntityHydrator
- trevorpe/laravel-symfony-cache: Adds Symfony to Laravel Cache bridge, useful when you want to use CachedRepository in Laravel
This package is auto-updated.
Last update: 2025-08-09 19:00:58 UTC
README
Repository
A framework and db agnostic repository and query builder library to speed up using your own entities. It also contains multiple db drivers, such as Eloquent, MeiliSearch, Php in memory. You are now not bound to any query builder, you can use filters, orders and pagination with any repository to query any entity.
Installation
Composer:
composer require yuriitatur/repository
Core Concepts
The core Contract behind this package is Entity
and BuilderRepositoryInterface
.
To mark your class as an entity, simply implement YuriiTatur\Repository\Contracts\Entity
interface.
class Product implements Entity
{
use HasId; # boilerplate to save you from writing id get/set
}
BuilderRepositoryInterface
is a base interface of repository, most of the time you will be using
CollectionRepository
, it's a stateless repository that can chain your calls to save the query between different contexts.
Usage
Repository
$repo = new CollectionRepository(
$dbDriver
);
$myEntity = $repo
->where([new FilterConditionNode('status', '=', 'active')])
->orWhere([new InNode('userId', [1,2,3])])
->orderBy('createdAt', 'desc')
->limit(10)
->page(3)
->get();
As you can see, the repo has a few methods that work with query builder.
where(array|callable $filters, LogicalOperator $operator = LogicalOperator::AND)
- applies filters to query builder. The first argument can accept an array of query nodes, combined with logical$operator
. Or a callable that will accept theQueryBuilder
instance for unlimited manipulations.orWhere(array|callable $filters)
- the same aswhere
, but will always apply OR operator.orderBy(array $orderFields)
- an associative array of column and direction of sorting.limit(int $limit)
- page sizeoffset(int $offset)
- page offsetpage(int $page)
- page number, if specified with offset, offset always prevails.withMeta(array $metaData)
- array of metadata, usually marks some driver-specific stuff.getQueryBuilder
- return query builder, which has all those methods + more.withQueryAst(QueryAST $newQuery)
- sets the whole query object.fromPlainQuery(string $plainQuery)
- applies a query from a string, read more
Also, there are plenty of repositories to choose from:
- CachedRepository - caches read queries
- ReadOnlyRepository - ensures only read queries are allowed
- MultiEntityRepository — coming soon
Drivers
Db driver is a bridge between your query and database. To use your own driver, it must implement
DatabaseDriverInterface. Additionally, you can implement
AdvancedDatabaseDriverInterface; it contains some methods,
that are very specific to some databases, for example, if your db can determine the total number of possible results in one query,
then you should probably implement this inside getWithTotalCount
method.
There are some drivers our of the box:
- MeiliSearchDriver - meilisearch driver
- InMemoryPhpDriver - in memory repo
- EloquentModelDriver - eloquent model repo
- TableDriver - eloquent table repo
ColumnMatcher
Many drivers require you to specify ColumnMatcherInterface. This is needed to
map your entity column to your db one. For example query like userId = 123
should be in your db user_id = 123
. The default implementation is ArrayColumnMatcher;
it simply gets an array map and returns the data by key.
When using Eloquent, it's very suitable to modify the eloquent query directly; For this purpose, it contains 2 methods:
getFilterableColumnOption
and getOrderableColumnOption
, for instance when you need modify the filter, but still be able to sort.
$matcher = new ArrayColumnMatcher([
'discount' => function (Builder $builder, $node) {
$builder->whereHas('discount', function (Builder $builder) use ($node) {
$builder->where('amount', $node->operator, $node->operand);
});
},
]);
Hydration TODO
EntityMapperInterface it's a simple concept of mapping your raw db
data to Entity
. The library contains JmsSerializerArrayEntityMapper that uses
jms/serializer
to hydrate objects. EloquentEntityMapper uses the same
jms mapper, but it fills the model. And MapperModelEntityMapper that
relies on models to implement MappedModelInterface that knows exactly how to hydrate raw model.
Transaction
This package doesn't force to use transactions, but it provides some boilerplate code to start using them. TransactionDriverInterface and TransactionRunnerInterface describes the transaction process, with different atomicity approach. You can use DriverRunnerTransaction or laravel's EloquentTransaction or EloquentTransactionDriver. You can also wrap your whole laravel request in TransactionMiddleware
LazyLoading
Entity class can use HasReferences trait to utilize lazy loading feature. You can inject a closure that will load your relations.
class User implements Entity
{
use HasId;
use HasReferences;
private ?Address $address = null; # it's important to set null
public function getAddress(): ?Address
{
return $this->loadReference('address');
}
}
$user = new User;
$user->injectReference('address', function (User $user) use ($addressRepo) {
return $addressRepo->where([new FilterConditionNode('id', '=', $user->getAddressId())])->findOne();
});
But beware of the N+1 problem.
if you are listening to events, you can use LazyReference
attribute to automatically inject references on
hydration. It accepts the service name that will be called to inject your reference, note it should be valid callable
that accepts your entity as a parameter.
class User implements Entity
{
use HasId;
use HasReferences;
#[LazyReference('MyAddressLoaderService')]
private ?Address $address = null;
public function getAddress(): ?Address
{
return $this->loadReference('address');
}
}
class MyAddressLoaderService
{
public function __invoke(User $user)
{
echo "Injecting the address";
}
}
Coming next
- MultiEntityRepository aka Repository Facade
- MongoDB driver
- Elasticsearch driver
Testing
composer test
License
This code is under MIT license, read more in the LICENSE file.