yuriitatur/repository

A simple framework/db agnostic repository package

dev-master 2025-08-09 14:07 UTC

This package is auto-updated.

Last update: 2025-08-09 19:00:58 UTC


README

Quality Gate Status Coverage

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.

  1. 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 the QueryBuilder instance for unlimited manipulations.
  2. orWhere(array|callable $filters) - the same as where, but will always apply OR operator.
  3. orderBy(array $orderFields) - an associative array of column and direction of sorting.
  4. limit(int $limit) - page size
  5. offset(int $offset) - page offset
  6. page(int $page) - page number, if specified with offset, offset always prevails.
  7. withMeta(array $metaData) - array of metadata, usually marks some driver-specific stuff.
  8. getQueryBuilder - return query builder, which has all those methods + more.
    1. withQueryAst(QueryAST $newQuery) - sets the whole query object.
    2. fromPlainQuery(string $plainQuery) - applies a query from a string, read more

Also, there are plenty of repositories to choose from:

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:

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.