Init commit

This commit is contained in:
Morten Rugaard 2015-12-06 17:02:48 +01:00
parent 36ff5c4ad7
commit a51a9f2916
16 changed files with 562 additions and 3 deletions

View File

@ -19,4 +19,3 @@ 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.

View File

@ -1 +1,62 @@
# counter-cache
## Counter Cache
Brings the ruby concept of "counter caching" to [Laravel](http://laravel.com/docs).
[![Total downloads](https://img.shields.io/packagist/dt/nodes/core.svg)](https://packagist.org/packages/nodes/core)
[![Monthly downloads](ttps://img.shields.io/packagist/dm/nodes/core.svg)](https://packagist.org/packages/nodes/core)
[![Latest release](https://img.shields.io/packagist/v/nodes/core.svg)](https://packagist.org/packages/nodes/core)
[![Open issues](https://img.shields.io/github/issues/nodes-php/core.svg)](https://github.com/nodes-php/core/issues)
[![License](https://img.shields.io/packagist/l/nodes/core.svg)](https://packagist.org/packages/nodes/core)
[![Star repository on GitHub](https://img.shields.io/github/stars/nodes-php/core.svg?style=social&label=Star)](https://github.com/nodes-php/core)
[![Watch repository on GitHub](https://img.shields.io/github/watchers/nodes-php/core.svg?style=social&label=Watch)](https://github.com/nodes-php/core)
[![Fork repository on GitHub](https://img.shields.io/github/forks/nodes-php/core.svg?style=social&label=Fork)](https://github.com/nodes-php/core)
## Introduction
One thing we at [Nodes](http://nodesagency.com) have been missing in [Laravel](http://laravel.com/docs) is the concept of "counter caching".
Laravel comes "out of the box" with the [increment](http://laravel.com/docs/5.1/queries#updates)/[decrement](http://laravel.com/docs/5.1/queries#updates) methods on it's [Eloquent](http://laravel.com/docs/5.1/eloquent) models. But you'll need to manually execute these methods everytime, you've saved/delete stuff with your model.
Since the [increment](http://laravel.com/docs/5.1/queries#updates)/[decrement](http://laravel.com/docs/5.1/queries#updates) methods always +1/-1, you can't 100% rely on these as cached value.
What if you forgot to execute the decrement method when you deleted a row. Or what if someone deleted a row directly from the database, then your count would be "out of sync".
Therefore we've created this package which brings "counter caching" to [Laravel](http://laravel.com/docs).
The difference between this package and Laravel's [increment](http://laravel.com/docs/5.1/queries#updates)/[decrement](http://laravel.com/docs/5.1/queries#updates) is that our package actually generates and fires a SQL count statement, that counts the entries and updates the desired column with the result.
This way you're always 100% sure that the value in your "counter cache" column is correct.
## Installation
To install this package you will need:
* Laravel 5.1+
* PHP 5.5.9+
You must then modify your `composer.json` file and run `composer update` to include the latest version of the package in your project.
```
"require": {
"dingo/api": "1.0.*@dev"
}
```
Or you can run the composer require command from your terminal.
```
composer require nodes/counter-cache
```
## Usage
To do.
## Developers / Maintainers
This package is developed and maintained by the PHP team at [Nodes Agency](http://nodesagency.com)
[![Follow Nodes PHP on Twitter](https://img.shields.io/twitter/follow/nodesphp.svg?style=social)](https://twitter.com/nodesphp) [![Tweet Nodes PHP](https://img.shields.io/twitter/url/http/nodesphp.svg?style=social)](https://twitter.com/nodesphp)
### License
This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)

31
composer.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "nodes/counter-cache",
"description": "Counter caching for Laravel",
"keywords": [
"nodes",
"counter cache",
"counter-cache",
"laravel",
"database",
"model"
],
"license": "MIT",
"homepage": "http://nodesagency.com",
"authors": [
{
"name": "Morten Rugaard",
"email": "moru@nodes.dk",
"role": "Developer"
}
],
"require": {
"php": ">=5.5.9",
"nodes/core": "^0.1"
},
"autoload": {
"psr-4": {
"Nodes\\CounterCache\\": "src"
}
},
"minimum-stability": "stable"
}

165
src/CounterCache.php Normal file
View File

@ -0,0 +1,165 @@
<?php
namespace Nodes\CounterCache;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Illuminate\Database\Eloquent\Relations\Relation as IlluminateRelation;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Nodes\CounterCache\Exceptions\NoCounterCachesFound;
use Nodes\CounterCache\Exceptions\NoEntitiesFoundException;
use Nodes\CounterCache\Exceptions\NotCounterCacheableException;
use Nodes\CounterCache\Exceptions\RelationNotFoundException;
/**
* Class CounterCache
*
* @package Nodes\CounterCache
*/
class CounterCache
{
/**
* Perform counter caching on model
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param \Illuminate\Database\Eloquent\Model $model
* @return boolean
* @throws \Nodes\CounterCache\Exceptions\NoCounterCachesFound
* @throws \Nodes\CounterCache\Exceptions\NotCounterCacheableException
* @throws \Nodes\CounterCache\Exceptions\RelationNotFoundException
*/
public function count(IlluminateModel $model)
{
// If model does not implement the CounterCacheable
// interface, we'll jump ship and abort.
if (!$model instanceof CounterCacheable) {
Log::error(sprintf('[%s] Model [%s] does not implement CounterCacheable.', __CLASS__, get_class($model)));
throw new NotCounterCacheableException(sprintf('Model [%s] does not implement CounterCacheable.', __CLASS__, get_class($model)));
}
// Retrieve array of available counter caches
$counterCaches = (array) $model->counterCaches();
// Validate counter caches
if (empty($counterCaches)) {
Log::error(sprintf('[%s] No counter caches found on model [%s].', __CLASS__, get_class($model)));
throw new NoCounterCachesFound(sprintf('No counter caches found on model [%s].', __CLASS__, get_class($model)));
}
// Handle each available counter caches
foreach ($counterCaches as $counterCacheColumnName => $relations) {
// Since an available counter cache could be found
// in multiple tables, we'll need to support multiple relations.
foreach ((array) $relations as $relationName => $counterCacheConditions) {
// Sometimes our counter cache might require additional conditions
// which means, we need to support both scenarios
$relationName = !is_array($counterCacheConditions) ? $counterCacheConditions : $relationName;
// When we've figured out the name of our relation
// we'll just make a quick validation, that it actually exists
if (!method_exists($model, $relationName)) {
Log::error(sprintf('[%s] Relation [%s] was not found on model [%s]', __CLASS__, $relationName, get_class($model)));
throw new RelationNotFoundException(sprintf('Relation [%s] was not found on model [%s]', __CLASS__, $relationName, get_class($model)));
}
// Retrieve relation query builder
$relation = $model->{$relationName}();
// Update the count value for counter cache column
$this->updateCount($model, $relation, $counterCacheConditions, $model->getAttribute($relation->getForeignKey()), $counterCacheColumnName);
// If our model's foreign key has been updated,
// we need to update the counter cache for the previous value as well
if (!is_null($model->getOriginal($relation->getForeignKey())) && $model->getOriginal($relation->getForeignKey()) != $model->getAttribute($relation->getForeignKey())) {
$this->updateCount($model, $relation, $counterCacheConditions, $model->getOriginal($relation->getForeignKey()), $counterCacheColumnName);
}
}
}
return true;
}
/**
* Perform counter caching on all entities of model
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param \Illuminate\Database\Eloquent\Model $model
* @return boolean
* @throws \Nodes\CounterCache\Exceptions\NoEntitiesFoundException
* @throws \Nodes\CounterCache\Exceptions\NoCounterCachesFound
* @throws \Nodes\CounterCache\Exceptions\NotCounterCacheableException
* @throws \Nodes\CounterCache\Exceptions\RelationNotFoundException
*/
public function countAll(IlluminateModel $model)
{
// Retrieve all entities of model
$entities = $model->get();
// If no entities found, we'll log the error,
// throw an exception and abort.
if (!$entities->isEmpty()) {
Log::error(sprintf('[%s] No entities found of model [%s]', __CLASS__, get_class($model)));
throw new NoEntitiesFoundException(sprintf('No entities found of model [%s]', get_class($model)));
}
// Perform counter caching on each found entity
foreach ($entities as $entry) {
$this->count($entry);
}
return true;
}
/**
* Update counter cache column
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access protected
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @param array|null $counterCacheConditions
* @param string $foreignKey
* @param string $counterCacheColumnName
* @return boolean
*/
protected function updateCount(IlluminateModel $model, IlluminateRelation $relation, $counterCacheConditions, $foreignKey, $counterCacheColumnName)
{
// Retrieve table name of relation
$relationTableName = $relation->getModel()->getTable();
// Generate query builder for counting entries
// on our model. Result will be used as value when
// we're updating the counter cache column on the relation
$countQuery = $model->newQuery()
->select(DB::raw(sprintf('COUNT(%s.id)', $model->getTable())))
->join(
DB::raw(sprintf('(SELECT %s.%s FROM %s) as relation', $relationTableName, $relation->getOtherKey(), $relationTableName)),
sprintf('%s.%s', $model->getTable(), $relation->getForeignKey()), '=', sprintf('relation.%s', $relation->getOtherKey())
)
->where(sprintf('%s.%s', $model->getTable(), $relation->getForeignKey()), '=', $foreignKey);
// If our relation has additional conditions, we'll need
// to add them to our query builder that counts the entries
if (is_array($counterCacheConditions)) {
foreach ($counterCacheConditions as $conditionType => $conditionParameters) {
foreach ($conditionParameters as $parameters) {
call_user_func_array([$countQuery, $conditionType], $parameters);
}
}
}
// Retrieve countQuery SQL
// and prepare for binding replacements
$countQuerySql = str_replace(['%', '?'], ['%%', '%s'], $countQuery->toSql());
// Fire the update query
// to update counter cache column
return (bool) $relation->getBaseQuery()->update([
sprintf('%s.%s', $relationTableName, $counterCacheColumnName) => DB::raw(sprintf('(%s)', vsprintf($countQuerySql, $countQuery->getBindings())))
]);
}
}

21
src/CounterCacheable.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Nodes\CounterCache;
/**
* Interface CounterCacheable
*
* @interface
* @package Nodes\CounterCache
*/
interface CounterCacheable
{
/**
* Retrieve array of counter caches
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @return array
*/
public function counterCaches();
}

View File

@ -0,0 +1,29 @@
<?php
namespace Nodes\CounterCache\Exceptions;
use Nodes\Exceptions\Exception as NodesException;
/**
* Class CounterCacheException
*
* @package Nodes\CounterCache\Exceptions
*/
class CounterCacheException extends NodesException
{
/**
* CounterCacheException constructor
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param string $message
* @param integer $statusCode
* @param string|null $statusMessage
* @param array $headers
* @param boolean $report
*/
public function __construct($message = 'Counter cache failed', $statusCode = 500, $statusMessage = 'Counter cache failed', array $headers = [], $report = true)
{
parent::__construct($message, $statusCode, $statusMessage, $headers, $report);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Nodes\CounterCache\Exceptions;
/**
* Class NoCounterCachesFound
*
* @package Nodes\CounterCache\Exceptions
*/
class NoCounterCachesFound extends CounterCacheException
{
/**
* NoCounterCachesFound constructor
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param string $message
* @param integer $statusCode
* @param string|null $statusMessage
* @param array $headers
* @param boolean $report
*/
public function __construct($message = 'No counter caches found on model', $statusCode = 500, $statusMessage = 'Counter cache failed', array $headers = [], $report = true)
{
parent::__construct($message, $statusCode, $statusMessage, $headers, $report);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Nodes\CounterCache\Exceptions;
/**
* Class NoEntitiesFoundException
*
* @package Nodes\CounterCache\Exceptions
*/
class NoEntitiesFoundException extends CounterCacheException
{
/**
* NoEntitiesFoundException constructor
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param string $message
* @param integer $statusCode
* @param string|null $statusMessage
* @param array $headers
* @param boolean $report
*/
public function __construct($message = 'No entities found', $statusCode = 500, $statusMessage = 'Counter cache failed', array $headers = [], $report = true)
{
parent::__construct($message, $statusCode, $statusMessage, $headers, $report);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Nodes\CounterCache\Exceptions;
/**
* Class NotCounterCacheableException
*
* @package Nodes\CounterCache\Exceptions
*/
class NotCounterCacheableException extends CounterCacheException
{
/**
* NotCounterCacheableException constructor
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param string $message
* @param integer $statusCode
* @param string|null $statusMessage
* @param array $headers
* @param boolean $report
*/
public function __construct($message = 'Model does not implement CounterCacheable', $statusCode = 500, $statusMessage = 'Counter cache failed', array $headers = [], $report = true)
{
parent::__construct($message, $statusCode, $statusMessage, $headers, $report);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Nodes\CounterCache\Exceptions;
/**
* Class RelationNotFoundException
*
* @package Nodes\CounterCache\Exceptions
*/
class RelationNotFoundException extends CounterCacheException
{
/**
* RelationNotFoundException constructor
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @access public
* @param string $message
* @param integer $statusCode
* @param string|null $statusMessage
* @param array $headers
* @param boolean $report
*/
public function __construct($message = 'Relation not found on model', $statusCode = 500, $statusMessage = 'Counter cache failed', array $headers = [], $report = true)
{
parent::__construct($message, $statusCode, $statusMessage, $headers, $report);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Nodes\CounterCache\Traits;
/**
* Trait CounterCache
*
* @trait
* @package Nodes\CounterCache\Traits
*/
trait CounterCache
{
use CounterCacheSaved,
CounterCacheDeleted,
CounterCacheRestored;
}

View File

@ -0,0 +1,26 @@
<?php
namespace Nodes\CounterCache\Traits;
/**
* Trait CounterCacheCreated
*
* @trait
* @package Nodes\CounterCache\Traits
*/
trait CounterCacheCreated
{
/**
* The "booting" of trait
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @static
* @return void
*/
public static function bootCounterCacheCreated()
{
static::created(function($model) {
app('Nodes\CounterCache\CounterCache')->count($model);
});
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Nodes\CounterCache\Traits;
/**
* Trait CounterCacheDeleted
*
* @trait
* @package Nodes\CounterCache\Traits
*/
trait CounterCacheDeleted
{
/**
* The "booting" of trait
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @static
* @return void
*/
public static function bootCounterCacheDeleted()
{
static::deleted(function($model) {
app('Nodes\CounterCache\CounterCache')->count($model);
});
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Nodes\CounterCache\Traits;
/**
* Trait CounterCacheRestored
*
* @trait
* @package Nodes\CounterCache\Traits
*/
trait CounterCacheRestored
{
/**
* The "booting" of trait
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @static
* @return void
*/
public static function bootCounterCacheRestored()
{
static::restored(function($model) {
app('Nodes\CounterCache\CounterCache')->count($model);
});
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Nodes\CounterCache\Traits;
/**
* Trait CounterCacheSaved
*
* @trait
* @package Nodes\CounterCache\Traits
*/
trait CounterCacheSaved
{
/**
* The "booting" of trait
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @static
* @return void
*/
public static function bootCounterCacheSaved()
{
static::saved(function($model) {
app('Nodes\CounterCache\CounterCache')->count($model);
});
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Nodes\CounterCache\Traits;
/**
* Trait CounterCacheUpdated
*
* @trait
* @package Nodes\CounterCache\Traits
*/
trait CounterCacheUpdated
{
/**
* The "booting" of trait
*
* @author Morten Rugaard <moru@nodes.dk>
*
* @static
* @return void
*/
public static function bootCounterCacheUpdated()
{
static::updated(function($model) {
app('Nodes\CounterCache\CounterCache')->count($model);
});
}
}