Init commit
This commit is contained in:
parent
36ff5c4ad7
commit
a51a9f2916
3
LICENSE
3
LICENSE
|
@ -18,5 +18,4 @@ 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.
|
||||
|
||||
SOFTWARE.
|
63
README.md
63
README.md
|
@ -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)
|
|
@ -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"
|
||||
}
|
|
@ -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())))
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
namespace Nodes\CounterCache\Traits;
|
||||
|
||||
/**
|
||||
* Trait CounterCache
|
||||
*
|
||||
* @trait
|
||||
* @package Nodes\CounterCache\Traits
|
||||
*/
|
||||
trait CounterCache
|
||||
{
|
||||
use CounterCacheSaved,
|
||||
CounterCacheDeleted,
|
||||
CounterCacheRestored;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue