Giter Site home page Giter Site logo

rinvex / laravel-repositories Goto Github PK

View Code? Open in Web Editor NEW
666.0 25.0 114.0 498 KB

⚠️ [ABANDONED] Rinvex Repository is a simple, intuitive, and smart implementation of Active Repository with extremely flexible & granular caching system for Laravel, used to abstract the data layer, making applications more flexible to maintain.

Home Page: https://rinvex.com

License: MIT License

PHP 100.00%
rinvex laravel repository granular cache eloquent

laravel-repositories's Introduction

Rinvex Repository

⚠️ This package is abandoned and no longer maintained. No replacement package was suggested. ⚠️

👉 Contact me if you are interested in maintaining it!

Rinvex Repository Diagram

Rinvex Repository is a simple, intuitive, and smart implementation of Active Repository with extremely flexible & granular caching system for Laravel, used to abstract the data layer, making applications more flexible to maintain.

Packagist Scrutinizer Code Quality Travis StyleCI License

💡 If you are looking for Laravel 5.5 support, use the dev-develop branch. It's stable but not tagged yet since test suites isn't complete. 💡

Features

  • Cache, Cache, Cache!
  • Prevent code duplication.
  • Reduce potential programming errors.
  • Granularly cache queries with flexible control.
  • Apply centrally managed, consistent access rules and logic.
  • Implement and centralize a caching strategy for the domain model.
  • Improve the code’s maintainability and readability by separating client objects from domain models.
  • Maximize the amount of code that can be tested with automation and to isolate both the client object and the domain model to support unit testing.
  • Associate a behavior with the related data. For example, calculate fields or enforce complex relationships or business rules between the data elements within an entity.

Quick Example (TL;DR)

The Rinvex\Repository\Repositories\BaseRepository is an abstract class with bare minimum that concrete implementations must extend.

The Rinvex\Repository\Repositories\EloquentRepository is currently the only available repository implementation (more to come in the future and you can develop your own), it makes it easy to create new eloquent model instances and to manipulate them easily. To use EloquentRepository your repository MUST extend it first:

namespace App\Repositories;

use Rinvex\Repository\Repositories\EloquentRepository;

class FooRepository extends EloquentRepository
{
    protected $repositoryId = 'rinvex.repository.uniqueid';

    protected $model = 'App\Models\User';
}

That's it, you're done! Yes, it's that simple.

But if you'd like more control over the container instance, or would like to pass model name dynamically you can alternatively do as follow:

namespace App\Repositories;

use Illuminate\Contracts\Container\Container;
use Rinvex\Repository\Repositories\EloquentRepository;

class FooRepository extends EloquentRepository
{
    // Instantiate repository object with required data
    public function __construct(Container $container)
    {
        $this->setContainer($container)
             ->setModel(\App\Models\User::class)
             ->setRepositoryId('rinvex.repository.uniqueid');

    }
}

Now inside your controller, you can either instantiate the repository traditionally through $repository = new \App\Repositories\FooRepository(); or to use Laravel's awesome dependency injection and let the IoC do the magic:

namespace App\Http\Controllers;

use App\Repositories\FooRepository;

class BarController
{
    // Inject `FooRepository` from the IoC
    public function baz(FooRepository $repository)
    {
        // Find entity by primary key
        $repository->find(1);

        // Find all entities
        $repository->findAll();

        // Create a new entity
        $repository->create(['name' => 'Example']);
    }
}

Rinvex Repository Workflow - Create Repository Rinvex Repository Workflow - Create Repository

Rinvex Repository Workflow - Use In Controller Rinvex Repository Workflow - Use In Controller

UML Diagram


Mission accomplished! You're good to use this package right now! ✅

Unless you need to dig deeper & know some advanced stuff, you can skip the following steps! 😉


Table Of Contents

Installation

The best and easiest way to install this package is through Composer.

Compatibility

This package fully compatible with Laravel 5.1.*, 5.2.*, and 5.3.*.

While this package tends to be framework-agnostic, it embraces Laravel culture and best practices to some extent. It's tested mainly with Laravel but you still can use it with other frameworks or even without any framework if you want.

Require Package

Open your application's composer.json file and add the following line to the require array:

"rinvex/laravel-repositories": "3.0.*"

Note: Make sure that after the required changes your composer.json file is valid by running composer validate.

Install Dependencies

On your terminal run composer install or composer update command according to your application's status to install the new requirements.

Note: Checkout Composer's Basic Usage documentation for further details.

Integration

Rinvex Repository package is framework-agnostic and as such can be integrated easily natively or with your favorite framework.

Native Integration

Integrating the package outside of a framework is incredibly easy, just require the vendor/autoload.php file to autoload the package.

Note: Checkout Composer's Autoloading documentation for further details.

Run the following command on your terminal to publish config files:

php artisan vendor:publish --tag="rinvex-repository-config"

Note: Checkout Laravel's Configuration documentation for further details.

You are good to go. Integration is done and you can now use all the available methods, proceed to the Usage section for an example.

Configuration

If you followed the previous integration steps, then your published config file reside at config/rinvex.repository.php.

Config options are very expressive and self explanatory, as follows:

return [

    /*
    |--------------------------------------------------------------------------
    | Models Directory
    |--------------------------------------------------------------------------
    |
    | Here you may specify the default models directory, just write
    | directory name, like 'Models' not the full path.
    |
    | Default: 'Models'
    |
    */

    'models' => 'Models',

    /*
    |--------------------------------------------------------------------------
    | Caching Strategy
    |--------------------------------------------------------------------------
    */

    'cache' => [

        /*
        |--------------------------------------------------------------------------
        | Cache Keys File
        |--------------------------------------------------------------------------
        |
        | Here you may specify the cache keys file that is used only with cache
        | drivers that does not support cache tags. It is mandatory to keep
        | track of cache keys for later usage on cache flush process.
        |
        | Default: storage_path('framework/cache/rinvex.repository.json')
        |
        */

        'keys_file' => storage_path('framework/cache/rinvex.repository.json'),

        /*
        |--------------------------------------------------------------------------
        | Cache Lifetime
        |--------------------------------------------------------------------------
        |
        | Here you may specify the number of seconds that you wish the cache
        | to be remembered before it expires. If you want the cache to be
        | remembered forever, set this option to -1. 0 means disabled.
        |
        | Default: -1
        |
        */

        'lifetime' => -1,

        /*
        |--------------------------------------------------------------------------
        | Cache Clear
        |--------------------------------------------------------------------------
        |
        | Specify which actions would you like to clear cache upon success.
        | All repository cached data will be cleared accordingly.
        |
        | Default: ['create', 'update', 'delete']
        |
        */

        'clear_on' => [
            'create',
            'update',
            'delete',
        ],

        /*
        |--------------------------------------------------------------------------
        | Cache Skipping URI
        |--------------------------------------------------------------------------
        |
        | For testing purposes, or maybe some certain situations, you may wish
        | to skip caching layer and get fresh data result set just for the
        | current request. This option allows you to specify custom
        | URL parameter for skipping caching layer easily.
        |
        | Default: 'skipCache'
        |
        */

        'skip_uri' => 'skipCache',

    ],

];

Usage

Detailed Documentation

setContainer(), getContainer()

The setContainer method sets the IoC container instance, while getContainer returns it:

// Set the IoC container instance
$repository->setContainer(new \Illuminate\Container\Container());

// Get the IoC container instance
$container = $repository->getContainer();

setConnection(), getConnection()

The setConnection method sets the connection associated with the repository, while getConnection returns it:

// Set the connection associated with the repository
$repository->setConnection('mysql');

// Get the current connection for the repository
$connection = $repository->getConnection();

Note: The name passed to the setConnection method should correspond to one of the connections listed in your config/database.php configuration file.

setModel(), getModel()

The setModel method sets the repository model, while getModel returns it:

// Set the repository model
$repository->setModel(\App\Models\User::class);

// Get the repository model
$repositoryModel = $repository->getModel();

setRepositoryId(), getRepositoryId()

The setRepositoryId method sets the repository identifier, while getRepositoryId returns it (it could be anything you want, but must be unique per repository):

// Set the repository identifier
$repository->setRepositoryId('rinvex.repository.uniqueid');

// Get the repository identifier
$repositoryId = $repository->getRepositoryId();

setCacheLifetime(), getCacheLifetime()

The setCacheLifetime method sets the repository cache lifetime, while getCacheLifetime returns it:

// Set the repository cache lifetime
$repository->setCacheLifetime(123);

// Get the repository cache lifetime
$cacheLifetime = $repository->getCacheLifetime();

setCacheDriver(), getCacheDriver()

The setCacheDriver method sets the repository cache driver, while getCacheDriver returns it:

// Set the repository cache driver
$repository->setCacheDriver('redis');

// Get the repository cache driver
$cacheDriver = $repository->getCacheDriver();

enableCacheClear(), isCacheClearEnabled()

The enableCacheClear method enables repository cache clear, while isCacheClearEnabled determines it's state:

// Enable repository cache clear
$repository->enableCacheClear(true);

// Disable repository cache clear
$repository->enableCacheClear(false);

// Determine if repository cache clear is enabled
$cacheClearStatus = $repository->isCacheClearEnabled();

createModel()

The createModel() method creates a new repository model instance:

$repositoryModelInstance = $repository->createModel();

forgetCache()

The forgetCache() method forgets the repository cache:

$repository->forgetCache();

with()

The with method sets the relationships that should be eager loaded:

// Pass a string
$repository->with('relationship');

// Or an array
$repository->with(['relationship1', 'relationship2']);

where()

The where method adds a basic where clause to the query:

$repository->where('slug', '=', 'example');

whereIn()

The whereIn method adds a "where in" clause to the query:

$repository->whereIn('id', [1, 2, 5, 8]);

whereNotIn()

The whereNotIn method adds a "where not in" clause to the query:

$repository->whereNotIn('id', [1, 2, 5, 8]);

whereHas()

The whereHas method adds a "where has relationship" clause to the query:

use Illuminate\Database\Eloquent\Builder;

$repository->whereHas('attachments', function (Builder $builder) use ($attachment) {
    $builder->where('attachment_id', $attachment->id);
});

Note: All of the where* methods are chainable & could be called multiple times in a single request. It will hold all where clauses in an array internally and apply them all before executing the query.

offset()

The offset method sets the "offset" value of the query:

$repository->offset(5);

limit()

The limit method sets the "limit" value of the query:

$repository->limit(9);

orderBy()

The orderBy method adds an "order by" clause to the query:

$repository->orderBy('id', 'asc');

find()

The find method finds an entity by it's primary key:

$entity = $repository->find(1);

findOrFail()

The findOrFail() method finds an entity by its primary key or throw an exception:

$entity = $repository->findOrFail(1);

findOrNew()

The findOrNew() method finds an entity by its primary key or return fresh entity instance:

$entity = $repository->findOrNew(1);

findBy()

The findBy method finds an entity by one of it's attributes:

$entity = $repository->findBy('id', 1);

findFirst()

The findFirst method finds first entity:

$firstEntity = $repository->findFirst();

findAll()

The findAll method finds all entities:

$allEntities = $repository->findAll();

paginate()

The paginate method paginates all entities:

$entitiesPagination = $repository->paginate(15, ['*'], 'page', 2);

As you can guess, this query the first 15 records, in the second page.

simplePaginate()

The simplePaginate method paginates all entities into a simple paginator:

$entitiesSimplePagination = $repository->simplePaginate(15);

findWhere()

The findWhere method finds all entities matching where conditions:

// Matching values with equal '=' operator
$repository->findWhere(['slug', '=', 'example']);

findWhereIn()

The findWhereIn method finds all entities matching whereIn conditions:

$includedEntities = $repository->findwhereIn(['id', [1, 2, 5, 8]]);

findWhereNotIn()

The findWhereNotIn method finds all entities matching whereNotIn conditions:

$excludedEntities = $repository->findWhereNotIn(['id', [1, 2, 5, 8]]);

findWhereHas()

The findWhereHas method finds all entities matching whereHas conditions:

use Illuminate\Database\Eloquent\Builder;

$entities = $repository->findWhereHas(['attachments', function (Builder $builder) use ($attachment) {
    $builder->where('attachment_id', $attachment->id);
}]);

Notes:

  • The findWhereHas method will return a collection of entities that match the condition inside the closure. If you need to embed the attachments relation, in this case, you'll need to call with() method before calling findWhereHas() like this: $repository->with('attachments')->findWhereHas([...]);
  • Signature of all of the findWhere, findWhereIn, and findWhereNotIn methods has been changed since v2.0.0.
  • All of the findWhere, findWhereIn, and findWhereNotIn methods utilize the where, whereIn, and whereNotIn methods respectively, and thus takes first argument as an array of same parameters required by the later ones.
  • All of the find* methods are could be filtered with preceding where clauses, which is chainable by the way. All where clauses been hold in an array internally and applied before executing the query. Check the following examples:

Example of filtered findAll method:

$allFilteredEntities = $repository->where('slug', '=', 'example')->findAll();

Another example of filtered findFirst method with chained clauses:

$allFilteredEntities = $repository->where('name', 'LIKE', '%TEST%')->where('slug', '=', 'example')->findFirst();

create()

The create method creates a new entity with the given attributes:

$createdEntity = $repository->create(['name' => 'Example']);

update()

The update method updates an entity with the given attributes:

$updatedEntity = $repository->update(1, ['name' => 'Example2']);

store()

The store method stores the entity with the given attributes:

// Existing Entity
$storedEntity = $repository->store(1, ['name' => 'Example2']);

// New Entity
$storedEntity = $repository->store(null, ['name' => 'Example2']);

Note: This method is just an alias for both create & update methods. It's useful in case where single form is used for both create & update processes.

delete()

The delete method deletes an entity with the given id:

$deletedEntity = $repository->delete(1);

beginTransaction()

The beginTransaction method starts a database transaction:

$repository->beginTransaction();

commit()

The commit method commits a database transaction:

$repository->commit();

rollBack()

The rollback method rollbacks a database transaction:

$repository->rollBack();

Notes:

  • All find* methods take one more optional parameter for selected attributes.
  • All set* methods returns an instance of the current repository, and thus can be chained.
  • create, update, and delete methods always return an array with two values, the first is action status whether it's success or fail as a boolean value, and the other is an instance of the model just operated upon.
  • It's recommended to set IoC container instance, repository model, and repository identifier explicitly through your repository constructor like the above example, but this package is smart enough to guess any missing requirements. Check Automatic Guessing Section

Code To An Interface

As a best practice, it's recommended to code for an interface, specifically for scalable projects. The following example explains how to do so.

First, create an interface (abstract) for every entity you've:

use Rinvex\Repository\Contracts\CacheableContract;
use Rinvex\Repository\Contracts\RepositoryContract;

interface UserRepositoryContract extends RepositoryContract, CacheableContract
{
    //
}

Second, create a repository (concrete implementation) for every entity you've:

use Rinvex\Repository\Repositories\EloquentRepository;

class UserEloquentRepository extends EloquentRepository implements UserRepositoryContract
{
    //
}

Now in a Laravel Service Provider bind both to the IoC (inside the register method):

$this->app->bind(UserRepositoryContract::class, UserEloquentRepository::class)

This way we don't have to instantiate the repository manually, and it's easy to switch between multiple implementations. The IoC Container will take care of the required dependencies.

Note: Checkout Laravel's Service Providers and Service Container documentation for further details.

Add Custom Implementation

Since we're focusing on abstracting the data layer, and we're separating the abstract interface from the concrete implementation, it's easy to add your own implementation.

Say your domain model uses a web service, or a filesystem data store as it's data source, all you need to do is just extend the BaseRepository class, that's it. See:

class FilesystemRepository extends BaseRepository
{
    // Implement here all `RepositoryContract` methods that query/persist data to & from filesystem or whatever datastore
}

EloquentRepository Fired Events

Repositories fire events at every action, like create, update, delete. All fired events are prefixed with repository's identifier (you set before in your repository's constructor) like the following example:

  • rinvex.repository.uniqueid.entity.created
  • rinvex.repository.uniqueid.entity.updated
  • rinvex.repository.uniqueid.entity.deleted

For your convenience, the events suffixed with .entity.created, .entity.updated, or .entity.deleted have listeners that take actions accordingly. Usually we need to flush cache -if enabled & exists- upon every success action.

There's one more event rinvex.repository.uniqueid.entity.cache.flushed that's fired on cache flush. It has no listeners by default, but you may need to listen to it if you've model relations for further actions.

Mandatory Repository Conventions

Here some conventions important to know while using this package. This package adheres to best practices trying to make development easier for web artisans, and thus it has some conventions for standardization and interoperability.

  • All Fired Events has a unique suffix, like .entity.created for example. Note the .entity. which is mandatory for automatic event listeners to subscribe to.

  • Default directory structure of any package uses Rinvex Repository is as follows:

├── config                  --> config files
|
├── database
|   ├── factories           --> database factory files
|   ├── migrations          --> database migration files
|   └── seeds               --> database seed files
|
├── resources
|   └── lang
|       └── en              --> English language files
|
├── routes                  --> Routes files
|   ├── api.php
|   ├── console.php
|   └── web.php
|
├── src                     --> self explanatory directories
|   ├── Console
|   |   └── Commands
|   |
|   ├── Http
|   |   ├── Controllers
|   |   ├── Middleware
|   |   └── Requests
|   |
|   ├── Events
|   ├── Exceptions
|   ├── Facades
|   ├── Jobs
|   ├── Listeners
|   ├── Models
|   ├── Overrides
|   ├── Policies
|   ├── Providers
|   ├── Repositories
|   ├── Scopes
|   ├── Support
|   └── Traits
|
└── composer.json           --> composer dependencies file

Note: Rinvex Repository adheres to PSR-4: Autoloader and expects other packages that uses it to adhere to the same standard as well. It's required for Automatic Guessing, such as when repository model is missing, it will be guessed automatically and resolved accordingly, and while that full directory structure might not required, it's the standard for all Rinvex packages.

Automatic Guessing

While it's recommended to explicitly set IoC container, repository identifier, and repository model; This package is smart enough to guess any of these required data whenever missing.

  • IoC Container app() helper is used as a fallback if IoC container instance not provided explicitly.
  • Repository Identifier It's recommended to set repository identifier as a doted name like rinvex.repository.uniqueid, but if it's missing fully qualified repository class name will be used (actually the value of static::class).
  • Repository Model Conventionally repositories are namespaced like this Rinvex\Demos\Repositories\ItemRepository, so corresponding model supposed to be namespaced like this Rinvex\Demos\Models\Item. That's how this packages guess the model if it's missing according to the Default Directory Structure.

Flexible & Granular Caching

Rinvex Repository has a powerful, yet simple and granular caching system, that handles almost every edge case. While you can enable/disable your application's cache as a whole, you have the flexibility to enable/disable cache granularly for every individual query! That gives you the ability to except certain queries from being cached even if the method is normally cached by default or otherwise.

Let's see what caching levels we can control:

Whole Application Cache

Checkout Laravel's Cache documentation for more details.

Individual Query Cache

Change cache per query or disable it:

// Set cache lifetime for this individual query to 123 seconds
$repository->setCacheLifetime(123);

// Set cache lifetime for this individual query to forever
$repository->setCacheLifetime(-1);

// Disable cache for this individual query
$repository->setCacheLifetime(0);

Change cache driver per query:

// Set cache driver for this individual query to redis
$repository->setCacheDriver('redis');

Both setCacheLifetime & setCacheDriver methods are chainable:

// Change cache lifetime & driver on runtime
$repository->setCacheLifetime(123)->setCacheDriver('redis')->findAll();

// Use default cache lifetime & driver
$repository->findAll();

Unless disabled explicitly, cache is enabled for all repositories by default, and kept for as long as your rinvex.repository.cache.lifetime config value, using default application's cache driver cache.default (which could be changed per query as well).

Caching results is totally up to you, while all retrieval find* methods have cache enabled by default, you can enable/disable cache for individual queries or control how it's being cached, for how long, and using which driver as you wish.

Temporary Skip Individual HTTP Request Cache

Lastly, you can skip cache for an individual request by passing the following query string in your URL skipCache=true. You can modify this parameter to whatever name you may need through the rinvex.repository.cache.skip_uri config option.

Final Thoughts

  • Since this is an evolving implementation that may change accordingly depending on real-world use cases.
  • Repositories intelligently pass missing called methods to the underlying model, so you actually can implement any kind of logic, or even complex queries by utilizing the repository model.
  • For more insights about the Active Repository implementation, I've published an article on the topic titled Active Repository is good & Awesomely Usable, read it if you're interested.
  • Repositories utilizes cache tags in a very smart way, even if your chosen cache driver doesn't support it. Repositories will manage it virtually on it's own for precise cache management. Behind scenes it uses a json file to store cache keys. Checkout the rinvex.repository.cache.keys_file config option to change file path.
  • Rinvex Repository follows the FIG PHP Standards Recommendations compliant with the PSR-1: Basic Coding Standard, PSR-2: Coding Style Guide and PSR-4: Autoloader to ensure a high level of interoperability between shared PHP code.
  • I don't see the benefit of adding a more complex layer by implementing the Criteria Pattern for filtration at the moment, rather I'd prefer to keep it as simple as it is now using traditional where clauses since we can achieve same results. (do you've different thoughts? explain please)

Changelog

Refer to the Changelog for a full history of the project.

Support

The following support channels are available at your fingertips:

Contributing & Protocols

Thank you for considering contributing to this project! The contribution guide can be found in CONTRIBUTING.md.

Bug reports, feature requests, and pull requests are very welcome.

Security Vulnerabilities

If you discover a security vulnerability within this project, please send an e-mail to [email protected]. All security vulnerabilities will be promptly addressed.

About Rinvex

Rinvex is a software solutions startup, specialized in integrated enterprise solutions for SMEs established in Alexandria, Egypt since June 2016. We believe that our drive The Value, The Reach, and The Impact is what differentiates us and unleash the endless possibilities of our philosophy through the power of software. We like to call it Innovation At The Speed Of Life. That’s how we do our share of advancing humanity.

License

This software is released under The MIT License (MIT).

(c) 2016-2020 Rinvex LLC, Some rights reserved.

laravel-repositories's People

Contributors

ahmedash95 avatar brunogaspar avatar ebisbe avatar hansen1416 avatar hasnayeen avatar ionut-tanasa avatar kevindierkx avatar liyu001989 avatar maherelgamil avatar ninjaparade avatar omranic avatar sdebacker avatar tzurbaev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-repositories's Issues

Reset cache lifetime and driver after query execution (?)

Steps to reproduce

Execute two or more queries in the same request.

$x = $this->repository->setCacheDriver('redis')->findWhere(['post_id' => 10]);
$y = $this->repository->findWhere(['post_id' => 20]);

Both queries are cached in redis but the second query doesn't say anything about the cache driver so it's expected to be cached in the default driver and with the default lifetime (?)

Cheers

Reset "wheres"

Steps to reproduce

  • Perform two or more repository queries in the same request
$x = $this->repository->setCacheLifetime(0)->findWhere(['post_id' => 10]);
$y = $this->repository->setCacheLifetime(0)->findWhere(['post_id' => 20]);

Expected executed queries

select * from `post` where `post_id` = '10'
select * from `post` where `post_id` = '20'

Executed queries

select * from `post` where `post_id` = '10'
select * from `post` where `post_id` = '10' and `post_id` = '20'

Didn't test other find* methods

Cheers

paginate() and simplePaginate() always return the first cached results even when a page is provided in the url

As discussed in #53 the paginate and simplePaginate always return the first cached result for every page when the paginate() or simplePaginate() arguments are left empty.

This is expected since the default for the $page argument is null and is cached as null.

This becomes a hassle when you want the unique results for different pages, the $page argument is the last argument you can specify forcing you to specify perPage, attributes and the pageName every time you want paginated results.

illuminate/database resolves this by checking the page parameter on the request and returning the first page by default when no page parameter is provided.

Looking at the EloquentRepository all request are piped through the executeCallback method. This method handles all the fancy caching logic and returns either the results of the provided callback or the already cached results.

This is where it goes wrong, executeCallback always uses the function arguments to determine the correct cache. This works for indexes and shows but breaks for paginated results.
This happens because the cached results are returned before the Eloquent model could check if it's handling the same page as before.

To resolve this we need to add some extra logic to the paginate and simplePaginate methods. They should append the requested page parameter from the url to the function arguments when it is not the default value as specified on the method.

This can be done using the illuminate/paginator this is already a requirement when you want paginated results.

I've already got a pull request made, this issue is just for justification

Help and improvements

Hi Abdelrahman Omran. Yesterday didn't had enough time to test your package and posted an issue based on code read only. Didn't mean to be rude and I'm sorry if the post sounds like it. I really appreciate your work.

Today got some time and started with a fresh laravel install and a simple database I use for development: Posts, Comments and Authors. Really simple structure with dummy data.

This is what I have and the errors that I found. Perhaps I am making some mistakes here but just followed your docs.

PostRepository

<?php

namespace App\Repositories;

use App\Models\Post;
use Illuminate\Container\Container as Application;
use Rinvex\Repository\Repositories\EloquentRepository;

class PostRepository extends EloquentRepository
{
    // Instantiate repository object with required data
    public function __construct(Application $app)
    {
        $this->setContainer($app)
             ->retrieveModel(Post::class)
             ->setRepositoryId('rinvex.repository');
    }

    public function getPost10()
    {
        return $this->findWhere(['post_id', '<=', 10]);
    }
}

IndexController

<?php

namespace App\Http\Controllers;

use App\Http\Requests;
use App\Repositories\PostRepository;

class IndexController extends Controller
{
    private $repository;

    public function __construct(PostRepository $repository)
    {
        $this->repository = $repository;
    }

    public function index()
    {
        $post20 = $this->repository->getPost10();
        return $post20;
    }
}

The application throws an error:

BadMethodCallException in Builder.php line 2345:
Call to undefined method Illuminate\Database\Query\Builder::setRepositoryId()

in Builder.php line 2345
at Builder->__call('setRepositoryId', array('rinvex.repository'))
at call_user_func_array(array(object(Builder), 'setRepositoryId'), array('rinvex.repository')) in Builder.php line 1402
at Builder->__call('setRepositoryId', array('rinvex.repository'))
at call_user_func_array(array(object(Builder), 'setRepositoryId'), array('rinvex.repository')) in Model.php line 3505
at Model->__call('setRepositoryId', array('rinvex.repository')) in PostRepository.php line 16
at PostRepository->__construct(object(Application))
at ReflectionClass->newInstanceArgs(array(object(Application))) in Container.php line 779
at Container->build('App\Repositories\PostRepository', array()) in Container.php line 629
at Container->make('App\Repositories\PostRepository', array()) in Application.php line 697
at Application->make('App\Repositories\PostRepository') in Container.php line 849
at Container->resolveClass(object(ReflectionParameter)) in Container.php line 804
at Container->getDependencies(array(object(ReflectionParameter)), array()) in Container.php line 773
...

Then read about Automatic Guessing in the docs and commented the __construct() method in PostRepository.php

Another error

FatalThrowableError in EloquentRepository.php line 150:
Call to a member function toSql() on null

in EloquentRepository.php line 150
at EloquentRepository->findWhere(array('post_id', '<=', '10')) in PostRepository.php line 21
at PostRepository->getPost10() in IndexController.php line 19
at IndexController->index()

Line 150: $cacheKey = md5(json_encode([$where, $columns, $with, $lifetime, $driver, $this->model->toSql()]));

$this->model is null

Can you give me some feedback? I would like to test the improvements on the caching system.
Thanks in advance.

Cheers

Reset model

I'm getting duplicate queries as the first issue before this refactor:

$x = $this->repository->setCacheLifetime(0)->findWhere(['post_id', '=', 10, 'or']);
$y = $this->repository->setCacheLifetime(0)->findWhere(['post_id', '=', 20, 'and']);

Queries executed

select * from post where post_id = '10'

select * from post where post_id = '10' and post_id = '20'

Updated local develop branch a few minutes ago.

Maybe you could check the number of elements in the where array before list(...) and set $boolean with a default. Otherwise there's an error if the fourth element is empty

Drop `addGlobalScope`, `withoutGlobalScopes` methods

It's recommended to decouple the repository from Laravel Eloquent for better abstraction, more adherence to the design patterns, and better consistency through dropping addGlobalScope, withoutGlobalScopes methods.

Laravel readme example - Bind interface to implementation

It would be nice to have an example on the readme to bind an interface to an implementation.

Give this interface:

interface UsersRepository extends \Rinvex\Repository\Contracts\RepositoryContract {}

And the following implementation :

class UsersEloquentRepository extends \Rinvex\Repository\Repositories\EloquentRepository implements UsersRepository {}

In a Laravel Service Provider we'd have $this->app->bind(UsersRepository::class, UsersEloquentRepository::class)

This way we don't have to new the repository implementation. Using dependency injection (i.e. injecting the UsersRepository in a controller) the IoC Container would take care of other dependencies that our app may depend on and new them up under the hood.

What do you think ?

Granular Cache - enable/disable cache per query

While this package currently works fine with adequate granular cache, it allows cache enable/disable on two levels: per repository as a whole / per method as an individual.

Current Limitation
But what if we need say the findAll method to be cached, but we need to exclude just a single call for some reasons due to the frequency of data update of the resource or whatever reason else? This is currently not supported, and thus requires more granular and flexible cache control.

Proposed Solution
Rather than setting cached methods from the config options rinvex.repository.cache.methods, we'll move that option to the queryable method (all find* methods) so that any method call could be specified whether it's cached individually or not, with default to yes.

Filtering? #data-grid

Hi.

It would be cool to have some kind of more advanced filtering based on query strings or using simple classes for each filter type.

A simple example would be :

example.com/users?search=field**:operator:**value ... and so on.

Since I'm planning to use your package intensively I would be cool to have this feature.

What do you think?

Wiki

Would you be kind enough to move the docs to the wiki? I think the readme file is getting too long. Since you are doing a great job with this package, I wouldn't want the docs to suck 😄

I know I asked you some time ago, but it would be cool to do it like this:

https://github.com/ionut-tanasa/repository/wiki

Much cleaner, imho.

If you want you can give me access and I can manage it. Or merge my wiki. I just took the readme and splitted it in sections.

Very good but caching system is not flexible enough

Hi. Thank you for this nice package. Had tried many 'repository' packages but none of them have a flexible/granular caching system.
It's ok to have a default storage and lifetime but that's not enough.

In a app I have different queries on the same repository that needs different lifetimes. The least accessed data can have a longer or infinite lifetime and can be stored in files. This cached data doesn't need to be deleted/regenerated every time the model changes.
Ex: Average product price data generated twice a month.

Ex: Product suplier list doesn't change often. This could be cached in memcached. The LRU algo from memcached doesn't cause performance issues if the this data is deleted from cache.

The most accessed data must be cached in redis with shorter lifetime.
Ex: Prices changes every week. Stock changes every day. Product descriptions are accessed every time.

A good caching system would allow to select the storage/lifetime for a specific query. If none is selected then the default is used.

Keep up the good work.

Cheers

About Criterias

Hi.
Just simple use case:
We want to rename a column in big application. Without criterias we also need to rename all usings of old column name in code. With criterias - we need to rename only in one place. Profit)
Also criterias give possibility to maintain database in most uniform view and reuse repeating parts of sql queries.

Get ready for laravel 5.3

Laravel 5.3 will be out in a month but I'm already testing version 5.3.0-dev for a project that will start in less than 2 weeks.
I've made a PR: #22
More to come.

Cheers

Return object instead of array for CrUD operations

Hey, me again ...

Wouldn't it be better to have something like this in the create, update and delete methods:

// Find the given instance
$instance = $id instanceof Model ? $id : $this->find($id);

if ($instance) {
    // Fill instance with data
    $instance->fill($attributes);

    // Update the instance
    $updated = $instance->save();

    // Fire the updated event
    $this->getContainer('events')->fire($this->getRepositoryId().'.entity.updated', [$this, $instance]);

}

return $updated ? $instance : $updated;

So why?

Lets take this controller method as an example:

/**
 * GuestsController::update
 *
 * Update the specified Guest in storage.
 *
 * @param GuestUpdateRequest $request
 * @param  int               $id
 *
 * @return \Illuminate\Http\Response
 */
public function update(GuestUpdateRequest $request, $id)
{
    $entity = $this->guestsRepository->update($id, $request->all());
    if (is_array($entity) && $entity[0]) {
        return $this->response->item($entity[1], new GuestTransformer);
    }

    return $this->response->errorInternal();
}

I have to use indexes if from the repository I'm receiving an array ... Using my suggestion I would do like this and it would be a lot more cleaner:

/**
 * GuestsController::update
 *
 * Update the specified Guest in storage.
 *
 * @param GuestUpdateRequest $request
 * @param  int               $id
 *
 * @return \Illuminate\Http\Response
 */
public function update(GuestUpdateRequest $request, $id)
{
    $entity = $this->guestsRepository->update($id, $request->all());
    if ($entity) {
        return $this->response->item($entity, new GuestTransformer);
    }

    return $this->response->errorInternal();
}

Another use case would be when I would like to do something else with my entity. Access some other properties or pass the whole object to something else.

What do you think?
Did you chose to return an array from the methods for a particular reason?

Update / Create related entities

Hey, me again :) Just throwing some ideas here...

It would be cool to have some kind of mechanism to allow us to save / create related models.

Given this payload:

{
  "user": {
    "email" : "[email protected]",
    "profile": {
      "name": "John",
      "surname": "Doe"
    }
  }
}

Calling $repository->create($data); the following things should happen:

  • create the user
  • create the profile
  • associate the profile to the user

The profile key, obviously, would be the name of the relation on the User entity.

Same for the update(). If user.id and profile.id have values the repository would updated them accordingly.

Do you think this would be doable?

(Possible) Issue when there's no rinvex.repository.json cache key file

Steps to reproduce

  1. Delete all cache folders/files and rinvex.repository.json file.
  2. Execute a query.

Expected result
Query is cached

Actual result

PHP Error

ErrorException in BaseRepository.php line 456:
file_get_contents(0): failed to open stream: No such file or directory

in BaseRepository.php line 456
at HandleExceptions->handleError('2', 'file_get_contents(0): failed to open stream: No such file or directory', 'C:\Work\www\projects\ae1\packages\repository\src\Repositories\BaseRepository.php', '456', array('file' => 'C:\Work\www\projects\ae1\storage\framework/cache/repository.json'))
at file_get_contents('0') in BaseRepository.php line 456
at BaseRepository->getCacheKeys('C:\Work\www\projects\ae1\storage\framework/cache/repository.json') in BaseRepository.php line 439
at BaseRepository->storeCacheKeys('App\Repositories\PostRepository', 'findWhere', '4e831f3f5eaf03c0ff46807f623cb9e2') in BaseRepository.php line 287
at BaseRepository->executeCallback('App\Repositories\PostRepository', 'findWhere', array(array('post_id' => '20')), object(Closure)) in EloquentRepository.php line 143
at EloquentRepository->findWhere(array('post_id' => '20')) in IndexController.php line 44

Hit F5 or hard refresh and everything is ok.

Don't know if it is a real issue or a Windows only issue
Tested with latest php 7 (7.0.8), latest nginx on a Windows 10 box.

Cheers

Pagination methods returns same results for any page

As of v2.0.0 paginate() and simplePaginate() methods are returning the same results if you're not providing $page argument explicitly (but there's no info about it in documentation).

This happens because both paginate and simplePaginate methods provides their arguments to BaseRepository::executeCallback via func_get_args but its documentation says:

Note: This function returns a copy of the passed arguments only, and does not account for default (non-passed) arguments.

So, all non-passed arguments are ignored and only $perPage (which is obviously the same for every request) is provided.

As a quick-fix I did this:

use Illuminate\Pagination\Paginator;

[...]

$args = [$perPage, $attributes, $pageName, $page ?: Paginator::resolveCurrentPage($pageName)];

and I'm sending this array to executeCallback instead of func_get_args.

Another fix without touching vendor code is to provide all arguments from client code:

$paginator = $repository->paginate(25, ['*'], 'page', intval($request->input('page', 1)));

If first fix is ok for you, I'll create PR, just let me know.

Ping

Still active ?

Add whereHas and findWhereHas methods

Thinking in a RESTful way I have the following Event entity that has many Guest.

Index method:

/**
 * Display a listing of the Event's Guests.
 *
 * @param $event
 *
 * @return \Illuminate\Http\Response
 */
public function index($event)
{
    $eventGuests = $this->guestsRepository->whereHas('events', function ($query) use ($event) {
        $query->where('event_id', $event);
    });

    return $this->response->paginator($eventGuests, new GuestTransformer);
}

In this case it's :

/**
 * @param array    $relation
 * @param \Closure $closure
 * @param bool     $paginated
 *
 * @return mixed|void
 */
public function whereHas($relation, \Closure $closure, $paginated = true)
{
    return $this->executeCallback(get_called_class(), __FUNCTION__, func_get_args(), function () use ($relation, $closure, $paginated) {
        if ($paginated) {
            return $this->prepareQuery($this->createModel())->whereHas($relation, $closure)->paginate();
        }

        return $this->prepareQuery($this->createModel())->whereHas($relation, $closure)->get();
    });
}

Show method:

/**
 * Display the specified Event's Guest.
 *
 * @param int $event
 * @param int $guest
 *
 * @return \Illuminate\Http\Response
 *
 */
public function show($event, $guest)
{
    $eventGuest = $this->guestsRepository->findWhereHas($guest, 'events', function ($query) use ($event) {
        $query->where('event_id', $event);
    });

    if (!$eventGuest) {
        return $this->response->errorNotFound();
    }

    return $this->response->item($eventGuest, new GuestTransformer);
}

For this one:

/**
 * @param integer  $id
 * @param string   $relation
 * @param \Closure $closure
 *
 * @return mixed
 */
public function findWhereHas($id, $relation, \Closure $closure)
{
    return $this->executeCallback(get_called_class(), __FUNCTION__, func_get_args(), function () use ($relation, $closure, $id) {
        return $this->prepareQuery($this->createModel())->whereHas($relation, $closure)->find($id);
    });
}

I'd be happy to submit a PR if you like it.

composer update: Class 'Rinvex\Repository\RepositoryServiceProvider' not found

Hi. good morning.
After a composer update i'm getting this error. Even on a fresh install.
Steps to reproduce:
1 - Create a new laravel project - laravel new projecttest
2 - Add rinvex/repository to composer.json - composer require rinvex/repository
3 - Add Rinvex\Repository\RepositoryServiceProvider::class to config/app.php
3 - composer update - throws the error

Any tip? It was working ok yesterday.

cheers

`findWhere` can only query on one column

the v1.0.* version allowed passing an array of more than 1 column to query against, where as now we can only pass one column and an operator.

Is it possible to allow being able to query more than 1?

example

$repository->findWhere(['id' => 1, 'active' => true]);

Caching relations?!

While working on rinvex/fort, which uses rinvex/repository we found ourselves dealing too much with relations, which results in direct model interaction and uncached result sets, which means more queries and database hits.

I'm wondering if we can cache relations somehow on the repository level, and limit direct model interactions .. Is it possible? Is it best practice? Any other suggestions?

New config option: default model directory

Received feedback:

A) I really like the "smart guess" feature that automatically tries to guess the Model and container if they are not set via the Repository constructor class, right now it seems to point to App\Model\GuessedName to search for the model, how about making this a config option to set the base "scan path" so we could have directories like App\Entities or just App (Which seems to be the default, considering that the User.php class is inside the root)

Database Transactions

Cool feature to have would be transactions.

Enabled globally or per repository, maybe?

Maybe for the EloquentRepository.

What do you think?

Container Usage

I'm not you really need to inject the container into the Eloquent repository. You can just "new" up Eloquent models manually. Typically they wouldn't / shouldn't have constructor dependencies. Unless you have a strong reason for needing the container?

Swap database connection on the fly

We could add a getter and a setter for a property on the BaseRepository and call setConnection on the model underneath.

A quick example could be $repository->setConnection('sqlsrv')->....

I'd add a default fallback configuration key to rinvex.repository.php

Cache not cleared on update

In some cases cache isn't cleared on update! Even cache tags are being managed correctly, the actual cache not cleared!! Thanks @ninjaparade for the report 👍 Fix in progress 😉

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.