Rinvex Repository
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.
Table Of Contents
- Features
- Installation
- Integration
- Configuration
- Usage
- Quick Example
- Detailed Documentation
setContainer()
,getContainer()
setModel()
,getModel()
setRepositoryId()
,getRepositoryId()
setCacheLifetime()
,getCacheLifetime()
setCacheDriver()
,getCacheDriver()
enableCacheClear()
,isCacheClearEnabled()
createModel()
forgetCache()
with()
where()
whereIn()
whereNotIn()
offset()
limit()
orderBy()
find()
findBy()
findAll()
paginate()
simplePaginate()
findWhere()
findWhereIn()
findWhereNotIn()
create()
update()
delete()
- Code To An Interface
- Add Custom Implementation
- EloquentRepository Fired Events
- Mandatory Repository Conventions
- Automatic Guessing
- Flexible & Granular Caching
- Final Thoughts
- Changelog
- Support
- Contributing & Protocols
- Security Vulnerabilities
- About Rinvex
- License
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.
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.
Prerequisites
"php": ">=5.5.9",
"illuminate/events": "5.1.*|5.2.*|5.3.*",
"illuminate/support": "5.1.*|5.2.*|5.3.*",
"illuminate/database": "5.1.*|5.2.*|5.3.*",
"illuminate/container": "5.1.*|5.2.*|5.3.*",
"illuminate/contracts": "5.1.*|5.2.*|5.3.*"
Require Package
Open your application's composer.json
file and add the following line to the require
array:
"rinvex/repository": "2.0.*"
Note: Make sure that after the required changes your
composer.json
file is valid by runningcomposer 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.
Laravel Integration
Rinvex Repository package supports Laravel by default and it comes bundled with a Service Provider for easy integration with the framework.
After installing the package, open your Laravel config file located at config/app.php
and add the following service provider to the $providers
array:
Rinvex\Repository\Providers\RepositoryServiceProvider::class,
Note: Checkout Laravel's Service Providers and Service Container documentation for further details.
Run the following command on your terminal to publish config files:
php artisan vendor:publish --provider="Rinvex\Repository\Providers\RepositoryServiceProvider" --tag="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 [
/*
|--------------------------------------------------------------------------
| 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 minutes 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
Quick Example
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 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\User::class)
->setRepositoryId('rinvex.repository.uniqueid');
}
}
Now inside your controller, you can either instantiate the repository traditionaly 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 - Use In Controller
You're good to go! That's pretty enough knowledge to use this package.
A good programmer is someone who always looks both ways before crossing a one-way street. -Doug Linder
So, you decided to proceed, ha?! Awesome!! :D
Detailed Documentation
setContainer()
, getContainer()
The setContainer
method sets the IoC container instance, while getContainer
returns it:
// Set the IoC container instance
$this->setContainer(new \Illuminate\Container\Container());
// Get the IoC container instance:
$container = $this->getContainer();
setModel()
, getModel()
The setModel
method sets the repository model, while getModel
returns it:
// Set the repository model
$repository->setModel(\App\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:
$repository->with(['relationship']);
where()
The with
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);
Note: All of the
where
,whereIn
, andwhereNotIn
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);
findBy()
The findBy
method finds an entity by one of it's attributes:
$entity = $repository->findBy('id', 1);
findAll()
The findAll
method finds all entities:
$allEntities = $repository->findAll();
paginate()
The paginate
method paginates all entities:
$entitiesPagination = $repository->paginate(15);
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);
Notes:
- Signature of all of the
findWhere
,findWhereIn
, andfindWhereNotIn
methods has been changed since v2.0.0.- All of the
findWhere
,findWhereIn
, andfindWhereNotIn
methods utilize thewhere
,whereIn
, andwhereNotIn
methods respectively, and thus takes first argument as an array of same parameters required by the later ones.
create()
The create
method creates a new entity with the given attributes:
$createdEntity = $repository->create(['name' => 'Example']);
// Assign created entity status and instance variables
list($status, $instance) = $createdEntity;
update()
The update
method updates an entity with the given attributes:
$updatedEntity = $repository->update(1, ['name' => 'Example2']);
// Assign updated entity status and instance variables
list($status, $instance) = $updatedEntity;
delete()
The delete
method deletes an entity with the given id:
$deletedEntity = $repository->delete(1);
// Assign deleted entity status and instance variables
list($status, $instance) = $deletedEntity;
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
, anddelete
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\RepositoryContract;
interface UserRepositoryContract extends RepositoryContract
{
//
}
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 relashions 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
|
├── src --> self explanatory directories
| ├── Console
| | └── Commands
| |
| ├── Http
| | ├── Controllers
| | ├── Middleware
| | ├── Requests
| | └── routes.php
| |
| ├── 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 recomended 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 result ofget_called_class()
function). - Repository Model Conventionally repositories are namespaced like this
Rinvex\Demos\Repositories\ItemRepository
, so corresponding model supposed to be namespaced like thisRinvex\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 minutes
$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 help@rinvex.com. 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 Rinvex LLC, Some rights reserved.
Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at help@rinvex.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4
This project adheres to the following standards and practices.
Versioning
This project is versioned under the Semantic Versioning guidelines as much as possible.
Releases will be numbered with the following format:
<major>.<minor>.<patch>
<breaking>.<feature>.<fix>
And constructed with the following guidelines:
- Breaking backward compatibility bumps the major and resets the minor and patch.
- New additions without breaking backward compatibility bumps the minor and resets the patch.
- Bug fixes and misc changes bumps the patch.
Support Policy
- This package adheres to the following release cycle:
- LTS Releases:
- Released Every 2 years
- Gets 2 years bug fixes
- Gets 3 years security fixes
- General Releases:
- Released Every 6 months
- Gets 6 months bug fixes
- Gets 12 months security fixes
- LTS Releases:
- As of next Laravel LTS release, long term support (LTS) will be provided for this package. This support and maintenance window is the largest ever provided for Rinvex packages and provides stability and peace of mind for larger, enterprise clients and customers.
Coding Standards
This project 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. If you notice any compliance oversights, please send a patch via pull request.
Pull Requests
The pull request process differs for new features and bugs.
Pull requests for bugs may be sent without creating any proposal issue. If you believe that you know of a solution for a bug that has been filed, please leave a comment detailing your proposed fix or create a pull request with the fix mentioning that issue id.
Proposal / Feature Requests
If you have a proposal or a feature request, you may create an issue with [Proposal]
in the title.
The proposal should also describe the new feature, as well as implementation ideas. The proposal will then be reviewed and either approved or denied. Once a proposal is approved, a pull request may be created implementing the new feature.
Which Branch?
This project follows Git-Flow, and as such has master
(latest stable releases), develop
(latest WIP development) and X.Y support branches.
Note: Pull requests which do not follow these guidelines will be closed without any further notice.
The MIT License (MIT)
Copyright © 2016, Rinvex LLC,
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
v1.0.2 - 2016-06-22
- Fix
findWhere
wrong results and fix docs mistakes (close #15) - Enable/disable cache per query (close #16)
- Revamp the entire documentation (close #17)
v1.0.1 - 2016-06-21
- Update docs, docblocks, and fix homepage link
- Add per repository cache lifetime/driver support (Close #10)
v1.0.0 - 2016-06-18
- Commit first draft