├── .gitignore ├── tests ├── Unit │ ├── Stubs │ │ ├── SumCache │ │ │ ├── Order.php │ │ │ └── Item.php │ │ ├── CountCache │ │ │ ├── User.php │ │ │ ├── Comment.php │ │ │ └── Post.php │ │ ├── PivotModelStub.php │ │ ├── ModelStub.php │ │ ├── ParentModelStub.php │ │ └── RealModelStub.php │ ├── TestCase.php │ └── Database │ │ └── Traits │ │ └── CamelCaseModelTest.php └── Acceptance │ ├── Models │ ├── Order.php │ ├── Comment.php │ ├── User.php │ ├── Item.php │ └── Post.php │ ├── SluggedTest.php │ ├── SumCacheTest.php │ ├── CountCacheTest.php │ └── AcceptanceTestCase.php ├── .travis.yml ├── src ├── Database │ └── Model.php ├── Behaviours │ ├── CountCache │ │ ├── Countable.php │ │ └── CountCache.php │ ├── Uuid.php │ ├── SumCache │ │ ├── Summable.php │ │ └── SumCache.php │ ├── Slug.php │ ├── Cacheable.php │ ├── Sluggable.php │ └── CamelCasing.php ├── Commands │ ├── FindCacheableClasses.php │ └── RebuildCaches.php └── EloquenceServiceProvider.php ├── phpunit.xml ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea* 6 | -------------------------------------------------------------------------------- /tests/Unit/Stubs/SumCache/Order.php: -------------------------------------------------------------------------------- 1 | 'Kirk', 12 | 'pivot_field' => 'whatever' 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /tests/Acceptance/Models/Order.php: -------------------------------------------------------------------------------- 1 | hasMany('Tests\Acceptance\Models\Item'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Unit/Stubs/ModelStub.php: -------------------------------------------------------------------------------- 1 | 'Kirk', 12 | 'last_name' => 'Bushell', 13 | 'address' => 'Home', 14 | 'country_of_origin' => 'Australia' 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/Database/Model.php: -------------------------------------------------------------------------------- 1 | attributes; 9 | } 10 | 11 | public function getAttribute($key) 12 | { 13 | return $this->attributes[$key]; 14 | } 15 | 16 | public function setAttribute($key, $value) 17 | { 18 | $this->attributes[$key] = $value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Stubs/CountCache/Comment.php: -------------------------------------------------------------------------------- 1 | 'Tests\Unit\Stubs\CountCache\Post', 15 | 'Tests\Unit\Stubs\CountCache\User' 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Unit/TestCase.php: -------------------------------------------------------------------------------- 1 | init(); 14 | } 15 | 16 | public function tearDown() 17 | { 18 | m::close(); 19 | } 20 | 21 | public function init() 22 | { 23 | // Nothing to do - for children to implement. 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Acceptance/Models/Comment.php: -------------------------------------------------------------------------------- 1 | hasMany(Post::class); 16 | } 17 | 18 | public function slugStrategy() 19 | { 20 | return ['firstName', 'lastName']; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Unit/Stubs/SumCache/Item.php: -------------------------------------------------------------------------------- 1 | 'Tests\Unit\Stubs\SumCache\Order', 17 | 'sumField' => 'itemTotalExplicit', 18 | 'columnToSum' => 'total', 19 | 'foreignKey' => 'itemId', 20 | 'key' => 'id', 21 | ] 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Stubs/CountCache/Post.php: -------------------------------------------------------------------------------- 1 | ['Tests\Unit\Stubs\CountCache\User', 'user_id', 'id'], 15 | [ 16 | 'model' => 'Tests\Unit\Stubs\CountCache\User', 17 | 'countField' => 'posts_count_explicit', 18 | 'foreignKey' => 'user_id', 19 | 'key' => 'id' 20 | ] 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Acceptance/SluggedTest.php: -------------------------------------------------------------------------------- 1 | firstName = 'Kirk'; 13 | $user->lastName = 'Bushell'; 14 | $user->save(); 15 | 16 | $this->assertEquals('kirk-bushell', (string) $user->slug); 17 | } 18 | 19 | public function testPostSlug() 20 | { 21 | $post = new Post; 22 | $post->save(); 23 | 24 | $this->assertRegExp('/^[a-z0-9]{8}$/i', (string) $post->slug); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Acceptance/Models/Item.php: -------------------------------------------------------------------------------- 1 | 'Tests\Acceptance\Models\Order', 19 | 'field' => 'itemTotalExplicit', 20 | 'columnToSum' => 'total', 21 | 'foreignKey' => 'orderId', 22 | 'key' => 'id', 23 | ] 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/Unit 16 | 17 | 18 | ./tests/Acceptance 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/Acceptance/Models/Post.php: -------------------------------------------------------------------------------- 1 | ['Tests\Acceptance\Models\User', 'userId', 'id'], 19 | [ 20 | 'model' => 'Tests\Acceptance\Models\User', 21 | 'field' => 'postCountExplicit', 22 | 'foreignKey' => 'userId', 23 | 'key' => 'id', 24 | ] 25 | ]; 26 | } 27 | 28 | public function slugStrategy() 29 | { 30 | return 'id'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kirkbushell/eloquence", 3 | "description": "A set of extensions adding additional functionality and consistency to Laravel's awesome Eloquent library.", 4 | "keywords": ["laravel", "eloquent", "camelcase", "camel", "case", "snake_case", "snake"], 5 | "authors": [ 6 | { 7 | "name": "Kirk Bushell", 8 | "email": "torm3nt@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.6.0", 13 | "hashids/hashids": "1.0.5", 14 | "illuminate/database": "~5.1", 15 | "ramsey/uuid": "~2.8", 16 | "hanneskod/classtools": "~1.0" 17 | }, 18 | "require-dev": { 19 | "illuminate/events": "~5.0", 20 | "mockery/mockery": "0.9.*@dev", 21 | "orchestra/testbench": "3.1.*", 22 | "phpunit/phpunit": "4.3.*" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Eloquence\\": "src/", 27 | "Tests\\": "tests/" 28 | } 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Kirk Bushell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Behaviours/CountCache/Countable.php: -------------------------------------------------------------------------------- 1 | apply(function ($config) use ($countCache, $model) { 14 | $countCache->updateCacheRecord($config, '+', 1, $model->{$config['foreignKey']}); 15 | }); 16 | }); 17 | 18 | static::updated(function ($model) { 19 | (new CountCache($model))->update(); 20 | }); 21 | 22 | static::deleted(function ($model) { 23 | $countCache = new CountCache($model); 24 | $countCache->apply(function ($config) use ($countCache, $model) { 25 | $countCache->updateCacheRecord($config, '-', 1, $model->{$config['foreignKey']}); 26 | }); 27 | }); 28 | } 29 | 30 | /** 31 | * Return the count cache configuration for the model. 32 | * 33 | * @return array 34 | */ 35 | abstract public function countCaches(); 36 | } 37 | -------------------------------------------------------------------------------- /src/Behaviours/Uuid.php: -------------------------------------------------------------------------------- 1 | getKeyName()) 28 | */ 29 | static::creating(function ($model) { 30 | $key = $model->getKeyName(); 31 | 32 | if (empty($model->$key)) { 33 | $model->$key = (string) $model->generateNewUuid(); 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Get a new version 4 (random) UUID. 40 | * 41 | * @return \Rhumsaa\Uuid\Uuid 42 | */ 43 | public function generateNewUuid() 44 | { 45 | return RhumsaaUuid::uuid4(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Behaviours/SumCache/Summable.php: -------------------------------------------------------------------------------- 1 | apply(function ($config) use ($model, $sumCache) { 18 | $sumCache->updateCacheRecord($config, '+', $model->{$config['columnToSum']}, $model->{$config['foreignKey']}); 19 | }); 20 | }); 21 | 22 | static::updated(function ($model) { 23 | (new SumCache($model))->update(); 24 | }); 25 | 26 | static::deleted(function ($model) { 27 | $sumCache = new SumCache($model); 28 | $sumCache->apply(function ($config) use ($model, $sumCache) { 29 | $sumCache->updateCacheRecord($config, '-', $model->{$config['columnToSum']}, $model->{$config['foreignKey']}); 30 | }); 31 | }); 32 | } 33 | 34 | /** 35 | * Return the sum cache configuration for the model. 36 | * 37 | * @return array 38 | */ 39 | abstract public function sumCaches(); 40 | } 41 | -------------------------------------------------------------------------------- /src/Commands/FindCacheableClasses.php: -------------------------------------------------------------------------------- 1 | directory = realpath($directory); 18 | } 19 | 20 | public function getAllCacheableClasses() 21 | { 22 | $finder = new Finder; 23 | $iterator = new ClassIterator($finder->in($this->directory)); 24 | $iterator->enableAutoloading(); 25 | 26 | $classes = []; 27 | 28 | foreach ($iterator->type(Model::class) as $className => $class) { 29 | if ($class->isInstantiable() && $this->usesCaching($class)) { 30 | $classes[] = $className; 31 | } 32 | } 33 | 34 | return $classes; 35 | } 36 | 37 | /** 38 | * Decide if the class uses any of the caching behaviours. 39 | * 40 | * @param \ReflectionClass $class 41 | * 42 | * @return bool 43 | */ 44 | private function usesCaching(\ReflectionClass $class) 45 | { 46 | return $class->hasMethod('bootCountable') || $class->hasMethod('bootSummable'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Behaviours/Slug.php: -------------------------------------------------------------------------------- 1 | slug = $slug; 23 | } 24 | 25 | /** 26 | * Generate a new 8-character slug. 27 | * 28 | * @param integer $id 29 | * @return Slug 30 | */ 31 | public static function fromId($id) 32 | { 33 | $salt = md5(uniqid().$id); 34 | $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 35 | $slug = with(new Hashids($salt, $length = 8, $alphabet))->encode($id); 36 | 37 | return new Slug($slug); 38 | } 39 | 40 | /** 41 | * Creates a new slug from a string title. 42 | * 43 | * @param string $title 44 | * @return Slug 45 | */ 46 | public static function fromTitle($title) 47 | { 48 | return new Slug(Str::slug($title)); 49 | } 50 | 51 | /** 52 | * Returns a string value for the Slug. 53 | * 54 | * @return string 55 | */ 56 | public function __toString() 57 | { 58 | return (string) $this->slug; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/EloquenceServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('Illuminate\Database\Eloquent\Model', 'Eloquence\Database\Model'); 27 | } 28 | 29 | /** 30 | * Register the service provider. 31 | * 32 | * @return void 33 | */ 34 | public function register() 35 | { 36 | $this->app->bind('command.eloquence:rebuild', RebuildCaches::class); 37 | 38 | $this->commands(['command.eloquence:rebuild']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Acceptance/SumCacheTest.php: -------------------------------------------------------------------------------- 1 | data = $this->setupOrderAndItem(); 14 | } 15 | 16 | public function testOrderSumCache() 17 | { 18 | $order = Order::first(); 19 | 20 | $this->assertEquals(34, $order->itemTotal); 21 | $this->assertEquals(34, $order->itemTotalExplicit); 22 | } 23 | 24 | public function testAdditionalSumCache() 25 | { 26 | $order = new Order; 27 | $order->save(); 28 | 29 | $item = new Item; 30 | $item->orderId = $this->data['order']->id; 31 | $item->total = 45; 32 | $item->save(); 33 | 34 | $this->assertEquals(79, Order::first()->itemTotal); 35 | $this->assertEquals(0, Order::get()[1]->itemTotal); 36 | 37 | $this->assertEquals(79, Order::first()->itemTotalExplicit); 38 | $this->assertEquals(0, Order::get()[1]->itemTotalExplicit); 39 | 40 | $item->orderId = $order->id; 41 | $item->save(); 42 | 43 | $this->assertEquals(34, Order::first()->itemTotal); 44 | $this->assertEquals(45, Order::get()[1]->itemTotal); 45 | 46 | $this->assertEquals(34, Order::first()->itemTotalExplicit); 47 | $this->assertEquals(45, Order::get()[1]->itemTotalExplicit); 48 | } 49 | 50 | private function setupOrderAndItem() 51 | { 52 | $order = new Order; 53 | $order->save(); 54 | 55 | $item = new Item; 56 | $item->total = 34; 57 | $item->orderId = $order->id; 58 | $item->save(); 59 | 60 | return compact('order', 'item'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Acceptance/CountCacheTest.php: -------------------------------------------------------------------------------- 1 | data = $this->setupUserAndPost(); 15 | } 16 | 17 | public function testUserCountCache() 18 | { 19 | $user = User::first(); 20 | 21 | $this->assertEquals(1, $user->postCount); 22 | $this->assertEquals(1, $user->postCountExplicit); 23 | } 24 | 25 | public function testComplexCountCache() 26 | { 27 | $post = new Post; 28 | $post->userId = $this->data['user']->id; 29 | $post->save(); 30 | 31 | $comment = new Comment; 32 | $comment->userId = $this->data['user']->id; 33 | $comment->postId = $this->data['post']->id; 34 | $comment->save(); 35 | 36 | $this->assertEquals(2, User::first()->postCount); 37 | $this->assertEquals(2, User::first()->postCountExplicit); 38 | 39 | $this->assertEquals(1, User::first()->commentCount); 40 | $this->assertEquals(1, Post::first()->commentCount); 41 | 42 | $comment->postId = $post->id; 43 | $comment->save(); 44 | 45 | $this->assertEquals(0, Post::first()->commentCount); 46 | $this->assertEquals(1, Post::get()[1]->commentCount); 47 | } 48 | 49 | private function setupUserAndPost() 50 | { 51 | $user = new User; 52 | $user->firstName = 'Kirk'; 53 | $user->lastName = 'Bushell'; 54 | $user->save(); 55 | 56 | $post = new Post; 57 | $post->userId = $user->id; 58 | $post->save(); 59 | 60 | return compact('user', 'post'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Commands/RebuildCaches.php: -------------------------------------------------------------------------------- 1 | option('class')) { 45 | $classes = [$class]; 46 | } else { 47 | $directory = $this->option('dir') ?: app_path(); 48 | $classes = (new FindCacheableClasses($directory))->getAllCacheableClasses(); 49 | } 50 | foreach ($classes as $className) { 51 | $this->rebuild($className); 52 | } 53 | } 54 | 55 | /** 56 | * Rebuilds the caches for the given class. 57 | * 58 | * @param string $className 59 | */ 60 | private function rebuild($className) 61 | { 62 | $instance = new $className; 63 | 64 | if (method_exists($instance, 'countCaches')) { 65 | $this->info("Rebuilding [$className] count caches"); 66 | $countCache = new CountCache($instance); 67 | $countCache->rebuild(); 68 | } 69 | 70 | if (method_exists($instance, 'sumCaches')) { 71 | $this->info("Rebuilding [$className] sum caches"); 72 | $sumCache = new SumCache($instance); 73 | $sumCache->rebuild(); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /tests/Unit/Stubs/RealModelStub.php: -------------------------------------------------------------------------------- 1 | [ 'Role', 'role_id', 'id' ] ]; 33 | * 34 | * So, to extend, the first argument should be an index representing the counter cache 35 | * field on the associated model. Next is a numerical array: 36 | * 37 | * 0 = The model to be used for the update 38 | * 1 = The foreign_key for the relationship that RelatedCount will watch *optional 39 | * 2 = The remote field that represents the key *optional 40 | * 41 | * If the latter 2 options are not provided, or if the counter cache option is a string representing 42 | * the model, then RelatedCount will assume the ID fields based on conventional standards. 43 | * 44 | * Ie. another way to setup a counter cache is like below. This is an identical configuration to above. 45 | * 46 | * return [ 'user_count' => 'Role' ]; 47 | * 48 | * This can be simplified even further, like this: 49 | * 50 | * return [ 'Role' ]; 51 | * 52 | * @return array 53 | */ 54 | public function countCaches() 55 | { 56 | return [ 57 | 'users_count' => ['Role', 'role_id', 'id'], 58 | 'comments_count' => 'Post', 59 | 'User' 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Acceptance/AcceptanceTestCase.php: -------------------------------------------------------------------------------- 1 | migrate(); 15 | $this->init(); 16 | } 17 | 18 | protected function getEnvironmentSetUp($app) 19 | { 20 | $app['config']->set('database.default', 'test'); 21 | $app['config']->set('database.connections.test', array( 22 | 'driver' => 'sqlite', 23 | 'database' => ':memory:' 24 | )); 25 | } 26 | 27 | protected function init() 28 | { 29 | // Overload 30 | } 31 | 32 | private function migrate() 33 | { 34 | Schema::create('users', function (Blueprint $table) { 35 | $table->increments('id'); 36 | $table->string('first_name'); 37 | $table->string('last_name'); 38 | $table->string('slug')->nullable(); 39 | $table->integer('comment_count')->default(0); 40 | $table->integer('post_count')->default(0); 41 | $table->integer('post_count_explicit')->default(0); 42 | $table->timestamps(); 43 | }); 44 | 45 | Schema::create('posts', function (Blueprint $table) { 46 | $table->increments('id'); 47 | $table->integer('user_id')->nullable(); 48 | $table->string('slug')->nullable(); 49 | $table->integer('comment_count')->default(0); 50 | $table->timestamps(); 51 | }); 52 | 53 | Schema::create('comments', function (Blueprint $table) { 54 | $table->increments('id'); 55 | $table->integer('user_id'); 56 | $table->integer('post_id'); 57 | $table->timestamps(); 58 | }); 59 | 60 | Schema::create('orders', function (Blueprint $table) { 61 | $table->increments('id'); 62 | $table->integer('item_total')->default(0); 63 | $table->integer('item_total_explicit')->default(0); 64 | $table->timestamps(); 65 | }); 66 | 67 | Schema::create('items', function (Blueprint $table) { 68 | $table->increments('id'); 69 | $table->integer('order_id'); 70 | $table->integer('total'); 71 | $table->timestamps(); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Behaviours/Cacheable.php: -------------------------------------------------------------------------------- 1 | processConfig($config); 25 | 26 | $sql = "UPDATE `{$config['table']}` SET `{$config['field']}` = `{$config['field']}` {$operation} {$amount} WHERE `{$config['key']}` = {$foreignKey}"; 27 | 28 | return DB::update($sql); 29 | } 30 | 31 | public function rebuildCacheRecord(array $config, Model $model, $command, $aggregateField = null) 32 | { 33 | $config = $this->processConfig($config); 34 | 35 | $modelTable = $this->getModelTable($model); 36 | 37 | if (is_null($aggregateField)) { 38 | $aggregateField = $config['foreignKey']; 39 | } else { 40 | $aggregateField = snake_case($aggregateField); 41 | } 42 | 43 | $sql = "UPDATE `{$config['table']}` INNER JOIN (" . 44 | "SELECT `{$config['foreignKey']}`, {$command}(`{$aggregateField}`) AS aggregate FROM `{$modelTable}` GROUP BY `{$config['foreignKey']}`) a " . 45 | "ON (a.`{$config['foreignKey']}` = `{$config['table']}`.`{$config['key']}`" . 46 | ") SET `{$config['field']}` = aggregate"; 47 | 48 | return DB::update($sql); 49 | } 50 | 51 | /** 52 | * Creates the key based on model properties and rules. 53 | * 54 | * @param string $model 55 | * @param string $field 56 | * 57 | * @return string 58 | */ 59 | protected function field($model, $field) 60 | { 61 | $class = strtolower(class_basename($model)); 62 | $field = $class . '_' . $field; 63 | 64 | return $field; 65 | } 66 | 67 | /** 68 | * Process configuration parameters to check key names, fix snake casing, etc.. 69 | * 70 | * @param array $config 71 | * 72 | * @return array 73 | */ 74 | protected function processConfig(array $config) 75 | { 76 | return [ 77 | 'model' => $config['model'], 78 | 'table' => $this->getModelTable($config['model']), 79 | 'field' => snake_case($config['field']), 80 | 'key' => snake_case($this->key($config['key'])), 81 | 'foreignKey' => snake_case($this->key($config['foreignKey'])), 82 | ]; 83 | } 84 | 85 | /** 86 | * Returns the true key for a given field. 87 | * 88 | * @param string $field 89 | * 90 | * @return mixed 91 | */ 92 | protected function key($field) 93 | { 94 | if (method_exists($this->model, 'getTrueKey')) { 95 | return $this->model->getTrueKey($field); 96 | } 97 | 98 | return $field; 99 | } 100 | 101 | /** 102 | * Returns the table for a given model. Model can be an Eloquent model object, or a full namespaced 103 | * class string. 104 | * 105 | * @param string|Model $model 106 | * 107 | * @return mixed 108 | */ 109 | protected function getModelTable($model) 110 | { 111 | if (!is_object($model)) { 112 | $model = new $model; 113 | } 114 | 115 | return $model->getTable(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Behaviours/CountCache/CountCache.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | 24 | /** 25 | * Applies the provided function to the count cache setup/configuration. 26 | * 27 | * @param \Closure $function 28 | */ 29 | public function apply(\Closure $function) 30 | { 31 | foreach ($this->model->countCaches() as $key => $cache) { 32 | $function($this->config($key, $cache)); 33 | } 34 | } 35 | 36 | /** 37 | * Update the cache for all operations. 38 | */ 39 | public function update() 40 | { 41 | $this->apply(function ($config) { 42 | $foreignKey = $this->key($config['foreignKey']); 43 | 44 | if ($this->model->getOriginal($foreignKey) && $this->model->{$foreignKey} != $this->model->getOriginal($foreignKey)) { 45 | $this->updateCacheRecord($config, '-', 1, $this->model->getOriginal($foreignKey)); 46 | $this->updateCacheRecord($config, '+', 1, $this->model->{$foreignKey}); 47 | } 48 | }); 49 | } 50 | 51 | /** 52 | * Rebuild the count caches from the database 53 | */ 54 | public function rebuild() 55 | { 56 | $this->apply(function($config) { 57 | $this->rebuildCacheRecord($config, $this->model, 'COUNT'); 58 | }); 59 | } 60 | 61 | /** 62 | * Takes a registered counter cache, and setups up defaults. 63 | * 64 | * @param string $cacheKey 65 | * @param array $cacheOptions 66 | * @return array 67 | */ 68 | protected function config($cacheKey, $cacheOptions) 69 | { 70 | $opts = []; 71 | 72 | if (is_numeric($cacheKey)) { 73 | if (is_array($cacheOptions)) { 74 | // Most explicit configuration provided 75 | $opts = $cacheOptions; 76 | $relatedModel = array_get($opts, 'model'); 77 | } else { 78 | // Smallest number of options provided, figure out the rest 79 | $relatedModel = $cacheOptions; 80 | } 81 | } else { 82 | // Semi-verbose configuration provided 83 | $relatedModel = $cacheOptions; 84 | $opts['field'] = $cacheKey; 85 | 86 | if (is_array($cacheOptions)) { 87 | if (isset($cacheOptions[2])) { 88 | $opts['key'] = $cacheOptions[2]; 89 | } 90 | if (isset($cacheOptions[1])) { 91 | $opts['foreignKey'] = $cacheOptions[1]; 92 | } 93 | if (isset($cacheOptions[0])) { 94 | $relatedModel = $cacheOptions[0]; 95 | } 96 | } 97 | } 98 | 99 | return $this->defaults($opts, $relatedModel); 100 | } 101 | 102 | /** 103 | * Returns necessary defaults, overwritten by provided options. 104 | * 105 | * @param array $options 106 | * @param string $relatedModel 107 | * @return array 108 | */ 109 | protected function defaults($options, $relatedModel) 110 | { 111 | $defaults = [ 112 | 'model' => $relatedModel, 113 | 'field' => $this->field($this->model, 'count'), 114 | 'foreignKey' => $this->field($relatedModel, 'id'), 115 | 'key' => 'id' 116 | ]; 117 | 118 | return array_merge($defaults, $options); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Behaviours/SumCache/SumCache.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | 24 | /** 25 | * Applies the provided function to the count cache setup/configuration. 26 | * 27 | * @param \Closure $function 28 | */ 29 | public function apply(\Closure $function) 30 | { 31 | foreach ($this->model->sumCaches() as $key => $cache) { 32 | $function($this->config($key, $cache)); 33 | } 34 | } 35 | 36 | /** 37 | * Rebuild the count caches from the database 38 | */ 39 | public function rebuild() 40 | { 41 | $this->apply(function($config) { 42 | $this->rebuildCacheRecord($config, $this->model, 'SUM', $config['columnToSum']); 43 | }); 44 | } 45 | 46 | /** 47 | * Update the cache for all operations. 48 | */ 49 | public function update() 50 | { 51 | $this->apply(function ($config) { 52 | $foreignKey = $this->key($config['foreignKey']); 53 | $amount = $this->model->{$config['columnToSum']}; 54 | 55 | if ($this->model->getOriginal($foreignKey) && $this->model->{$foreignKey} != $this->model->getOriginal($foreignKey)) { 56 | $this->updateCacheRecord($config, '-', $amount, $this->model->getOriginal($foreignKey)); 57 | $this->updateCacheRecord($config, '+', $amount, $this->model->{$foreignKey}); 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Takes a registered sum cache, and setups up defaults. 64 | * 65 | * @param string $cacheKey 66 | * @param array $cacheOptions 67 | * @return array 68 | */ 69 | protected function config($cacheKey, $cacheOptions) 70 | { 71 | $opts = []; 72 | 73 | if (is_numeric($cacheKey)) { 74 | if (is_array($cacheOptions)) { 75 | // Most explicit configuration provided 76 | $opts = $cacheOptions; 77 | $relatedModel = array_get($opts, 'model'); 78 | } else { 79 | // Smallest number of options provided, figure out the rest 80 | $relatedModel = $cacheOptions; 81 | } 82 | } else { 83 | // Semi-verbose configuration provided 84 | $relatedModel = $cacheOptions; 85 | $opts['field'] = $cacheKey; 86 | 87 | if (is_array($cacheOptions)) { 88 | if (isset($cacheOptions[3])) { 89 | $opts['key'] = $cacheOptions[3]; 90 | } 91 | if (isset($cacheOptions[2])) { 92 | $opts['foreignKey'] = $cacheOptions[2]; 93 | } 94 | if (isset($cacheOptions[1])) { 95 | $opts['columnToSum'] = $cacheOptions[1]; 96 | } 97 | if (isset($cacheOptions[0])) { 98 | $relatedModel = $cacheOptions[0]; 99 | } 100 | } 101 | } 102 | 103 | return $this->defaults($opts, $relatedModel); 104 | } 105 | 106 | /** 107 | * Returns necessary defaults, overwritten by provided options. 108 | * 109 | * @param array $options 110 | * @param string $relatedModel 111 | * @return array 112 | */ 113 | protected function defaults($options, $relatedModel) 114 | { 115 | $defaults = [ 116 | 'model' => $relatedModel, 117 | 'columnToSum' => 'total', 118 | 'field' => $this->field($this->model, 'total'), 119 | 'foreignKey' => $this->field($relatedModel, 'id'), 120 | 'key' => 'id' 121 | ]; 122 | 123 | return array_merge($defaults, $options); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Behaviours/Sluggable.php: -------------------------------------------------------------------------------- 1 | generateSlug(); 16 | }); 17 | 18 | static::created(function ($model) { 19 | if ($model->slugStrategy() == 'id') { 20 | $model->generateIdSlug(); 21 | $model->save(); 22 | } 23 | }); 24 | } 25 | 26 | /** 27 | * Generate a slug based on the main model key. 28 | */ 29 | public function generateIdSlug() 30 | { 31 | $this->setSlugValue(Slug::fromId($this->getKey())); 32 | } 33 | 34 | /** 35 | * Generate a slug string based on the fields required. 36 | */ 37 | public function generateTitleSlug(array $fields) 38 | { 39 | static $attempts = 0; 40 | 41 | $titleSlug = Slug::fromTitle(implode('-', $this->getTitleFields($fields))); 42 | 43 | // This is not the first time we've attempted to create a title slug, so - let's make it more unique 44 | if ($attempts > 0) { 45 | $titleSlug . "-{$attempts}"; 46 | } 47 | 48 | $this->setSlugValue($titleSlug); 49 | 50 | $attempts++; 51 | } 52 | 53 | /** 54 | * Because a title slug can be created from multiple sources (such as an article title, a category title.etc.), 55 | * this allows us to search out those fields from related objects and return the combined values. 56 | * 57 | * @param array $fields 58 | * @return array 59 | */ 60 | public function getTitleFields(array $fields) 61 | { 62 | $fields = array_map(function ($field) { 63 | if (str_contains($field, '.')) { 64 | return object_get($this, $field); // this acts as a delimiter, which we can replace with / 65 | } else { 66 | return $this->{$field}; 67 | } 68 | }, $fields); 69 | 70 | return $fields; 71 | } 72 | 73 | /** 74 | * Generate the slug for the model based on the model's slug strategy. 75 | */ 76 | public function generateSlug() 77 | { 78 | $strategy = $this->slugStrategy(); 79 | 80 | if ($strategy == 'uuid') { 81 | $this->generateIdSlug(); 82 | } elseif ($strategy != 'id') { 83 | $this->generateTitleSlug((array) $strategy); 84 | } 85 | } 86 | 87 | /** 88 | * Set the value of the slug. 89 | * 90 | * @param $value 91 | */ 92 | public function setSlugValue(Slug $value) 93 | { 94 | $this->{$this->slugField()} = $value; 95 | } 96 | 97 | /** 98 | * Allows laravel to start using the sluggable field as the string for routes. 99 | * 100 | * @return mixed 101 | */ 102 | public function getRouteKey() 103 | { 104 | $slug = $this->slugField(); 105 | 106 | return $this->$slug; 107 | } 108 | 109 | /** 110 | * Return the name of the field you wish to use for the slug. 111 | * 112 | * @return string 113 | */ 114 | protected function slugField() 115 | { 116 | return 'slug'; 117 | } 118 | 119 | /** 120 | * Return the strategy to use for the slug. 121 | * 122 | * When using id or uuid, simply return 'id' or 'uuid' from the method below. However, 123 | * for creating a title-based slug - simply return the field you want it to be based on 124 | * 125 | * Eg: 126 | * 127 | * return 'id'; 128 | * return 'uuid'; 129 | * return 'name'; 130 | * 131 | * If you'd like your slug to be based on more than one field, return it in dot-notation: 132 | * 133 | * return 'first_name.last_name'; 134 | * 135 | * If you're using the camelcase model trait, then you can use that format: 136 | * 137 | * return 'firstName.lastName'; 138 | * 139 | * @return string 140 | */ 141 | public function slugStrategy() 142 | { 143 | return 'id'; 144 | } 145 | 146 | /** 147 | * Sets the slug attribute with the Slug value object. 148 | * 149 | * @param Slug $slug 150 | */ 151 | public function setSlugAttribute(Slug $slug) 152 | { 153 | $this->attributes[$this->slugField()] = (string) $slug; 154 | } 155 | 156 | /** 157 | * Returns the slug attribute as a Slug value object. 158 | * 159 | * @return \Eloquence\Behaviours\Slug 160 | */ 161 | public function getSlugAttribute() 162 | { 163 | return new Slug($this->attributes[$this->slugField()]); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /tests/Unit/Database/Traits/CamelCaseModelTest.php: -------------------------------------------------------------------------------- 1 | model = new ModelStub; 19 | } 20 | 21 | public function testAttributesAsArray() 22 | { 23 | $attributes = $this->model->attributesToArray(); 24 | 25 | $this->assertArrayHasKey('firstName', $attributes); 26 | $this->assertArrayHasKey('lastName', $attributes); 27 | $this->assertArrayHasKey('address', $attributes); 28 | $this->assertArrayHasKey('firstName', $attributes); 29 | } 30 | 31 | public function testAttributeDeclaration() 32 | { 33 | $this->model->setAttribute('firstName', 'Andrew'); 34 | 35 | $this->assertEquals('Andrew', $this->model->getAttribute('firstName')); 36 | } 37 | 38 | public function testAttributeRetrieval() 39 | { 40 | $this->assertEquals('Kirk', $this->model->getAttribute('firstName')); 41 | } 42 | 43 | public function testArrayRetrievalOfAttributes() 44 | { 45 | $expectedArray = [ 46 | 'firstName' => 'Kirk', 47 | 'lastName' => 'Bushell', 48 | 'address' => 'Home', 49 | 'countryOfOrigin' => 'Australia' 50 | ]; 51 | 52 | $actualArray = $this->model->getAttributes(); 53 | 54 | $this->assertEquals($expectedArray, $actualArray); 55 | } 56 | 57 | public function testAttributeConversionOfAllAttributes() 58 | { 59 | $expectedAttributes = [ 60 | 'address' => 'Home', 61 | 'countryOfOrigin' => 'Australia', 62 | 'firstName' => 'Kirk', 63 | 'lastName' => 'Bushell' 64 | ]; 65 | 66 | $this->assertEquals($expectedAttributes, $this->model->attributesToArray()); 67 | } 68 | 69 | public function testAttributeConversionLeavesPivotFieldsAlone() 70 | { 71 | $model = new PivotModelStub; 72 | 73 | $expectedAttributes = [ 74 | 'firstName' => 'Kirk', 75 | 'pivot_field' => 'whatever' 76 | ]; 77 | 78 | $this->assertEquals($expectedAttributes, $model->attributesToArray()); 79 | } 80 | 81 | public function testModelFilling() 82 | { 83 | $model = new RealModelStub([ 84 | 'myField' => 'value', 85 | 'anotherField' => 'yeah', 86 | 'someField' => 'whatever' 87 | ]); 88 | 89 | $this->assertEquals($model->myField, 'value'); 90 | $this->assertEquals($model->anotherField, 'yeah'); 91 | $this->assertNull($model->someField); 92 | } 93 | 94 | public function testRelationalMethods() 95 | { 96 | $this->setExpectedException('LogicException'); 97 | 98 | $model = new RealModelStub; 99 | $model->fakeRelationship; 100 | } 101 | 102 | public function testModelHidesHiddenFields() 103 | { 104 | $model = new RealModelStub([ 105 | 'myField' => 'value', 106 | 'anotherField' => 'yeah', 107 | 'someField' => 'whatever', 108 | 'hiddenField' => 'secrets!', 109 | 'passwordHash' => '1234', 110 | ]); 111 | 112 | $modelArray = $model->toArray(); 113 | 114 | $this->assertFalse(isset($modelArray['hiddenField'])); 115 | $this->assertFalse(isset($modelArray['passwordHash'])); 116 | 117 | $this->assertEquals('secrets!', $model->getAttribute('hiddenField')); 118 | $this->assertEquals('1234', $model->getAttribute('passwordHash')); 119 | } 120 | 121 | public function testModelExposesHiddenFields() 122 | { 123 | $model = new RealModelStub([ 124 | 'myField' => 'value', 125 | 'anotherField' => 'yeah', 126 | 'someField' => 'whatever', 127 | 'hiddenField' => 'secrets!', 128 | 'passwordHash' => '1234', 129 | ]); 130 | 131 | $hidden = $model->withHidden(['hiddenField', 'passwordHash'])->toArray(); 132 | 133 | $this->assertTrue(isset($hidden['hiddenField'])); 134 | $this->assertTrue(isset($hidden['passwordHash'])); 135 | 136 | $this->assertEquals('secrets!', $hidden['hiddenField']); 137 | $this->assertEquals('1234', $hidden['passwordHash']); 138 | } 139 | 140 | public function testModelDateFieldHandling() 141 | { 142 | $model = new RealModelStub([ 143 | 'myField' => '2011-11-11T11:11:11Z', 144 | 'dateField' => '2011-11-11T11:11:11Z', 145 | ]); 146 | 147 | $this->assertFalse($model->myField instanceof Carbon); 148 | $this->assertTrue($model->dateField instanceof Carbon); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Behaviours/CamelCasing.php: -------------------------------------------------------------------------------- 1 | getSnakeKey($key), $value); 25 | } 26 | 27 | /** 28 | * Retrieve a given attribute but allow it to be accessed via alternative case methods (such as camelCase). 29 | * 30 | * @param string $key 31 | * @return mixed 32 | */ 33 | public function getAttribute($key) 34 | { 35 | if (method_exists($this, $key)) { 36 | return $this->getRelationValue($key); 37 | } 38 | 39 | return parent::getAttribute($this->getSnakeKey($key)); 40 | } 41 | 42 | /** 43 | * Return the attributes for the model, converting field casing if necessary. 44 | * 45 | * @return array 46 | */ 47 | public function attributesToArray() 48 | { 49 | return $this->toCamelCase(parent::attributesToArray()); 50 | } 51 | 52 | /** 53 | * Converts the attributes to a camel-case version, if applicable. 54 | * 55 | * @return array 56 | */ 57 | public function getAttributes() 58 | { 59 | return $this->attributesToArray(); 60 | } 61 | 62 | /** 63 | * Get the model's relationships, converting field casing if necessary. 64 | * 65 | * @return array 66 | */ 67 | public function relationsToArray() 68 | { 69 | return $this->toCamelCase(parent::relationsToArray()); 70 | } 71 | 72 | /** 73 | * Overloads eloquent's getHidden method to ensure that hidden fields declared 74 | * in camelCase are actually hidden and not exposed when models are turned 75 | * into arrays. 76 | * 77 | * @return array 78 | */ 79 | public function getHidden() 80 | { 81 | return array_map('snake_case', $this->hidden); 82 | } 83 | 84 | /** 85 | * Overloads the eloquent getDates method to ensure that date field declarations 86 | * can be made in camelCase but mapped to/from DB in snake_case. 87 | * 88 | * @return array 89 | */ 90 | public function getDates() 91 | { 92 | $dates = parent::getDates(); 93 | return array_map('snake_case', $dates); 94 | } 95 | 96 | /** 97 | * Converts a given array of attribute keys to the casing required by CamelCaseModel. 98 | * 99 | * @param mixed $attributes 100 | * @return array 101 | */ 102 | public function toCamelCase($attributes) 103 | { 104 | $convertedAttributes = []; 105 | 106 | foreach ($attributes as $key => $value) { 107 | $key = $this->getTrueKey($key); 108 | $convertedAttributes[$key] = $value; 109 | } 110 | 111 | return $convertedAttributes; 112 | } 113 | 114 | /** 115 | * Get the model's original attribute values. 116 | * 117 | * @param string $key 118 | * @param mixed $default 119 | * @return array 120 | */ 121 | public function getOriginal($key = null, $default = null) 122 | { 123 | return array_get($this->toCamelCase($this->original), $key, $default); 124 | } 125 | 126 | /** 127 | * Converts a given array of attribute keys to the casing required by CamelCaseModel. 128 | * 129 | * @param $attributes 130 | * @return array 131 | */ 132 | private function toSnakeCase($attributes) 133 | { 134 | $convertedAttributes = []; 135 | 136 | foreach ($attributes as $key => $value) { 137 | $convertedAttributes[$this->getSnakeKey($key)] = $value; 138 | } 139 | 140 | return $convertedAttributes; 141 | } 142 | 143 | /** 144 | * Retrieves the true key name for a key. 145 | * 146 | * @param $key 147 | * @return string 148 | */ 149 | public function getTrueKey($key) 150 | { 151 | // If the key is a pivot key, leave it alone - this is required internal behaviour 152 | // of Eloquent for dealing with many:many relationships. 153 | if ($this->isCamelCase() && strpos($key, 'pivot_') === false) { 154 | $key = camel_case($key); 155 | } 156 | 157 | return $key; 158 | } 159 | 160 | /** 161 | * Determines whether the model (or its parent) requires camelcasing. This is required 162 | * for pivot models whereby they actually depend on their parents for this feature. 163 | * 164 | * @return bool 165 | */ 166 | public function isCamelCase() 167 | { 168 | return $this->enforceCamelCase or (isset($this->parent) && method_exists($this->parent, 'isCamelCase') && $this->parent->isCamelCase()); 169 | } 170 | 171 | /** 172 | * If the field names need to be converted so that they can be accessed by camelCase, then we can do that here. 173 | * 174 | * @param $key 175 | * @return string 176 | */ 177 | protected function getSnakeKey($key) 178 | { 179 | return snake_case($key); 180 | } 181 | 182 | /** 183 | * Because we are changing the case of keys and want to use camelCase throughout the application, whenever 184 | * we do isset checks we need to ensure that we check using snake_case. 185 | * 186 | * @param $key 187 | * @return mixed 188 | */ 189 | public function __isset($key) 190 | { 191 | $key = snake_case($key); 192 | 193 | return parent::__isset($key); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eloquence 2 | 3 | ![Version](https://img.shields.io/packagist/v/kirkbushell/eloquence.svg) 4 | ![Downloads](https://img.shields.io/packagist/dt/kirkbushell/eloquence.svg) 5 | ![Status](https://img.shields.io/travis/kirkbushell/eloquence/master.svg) 6 | 7 | Eloquence is a package to extend Laravel 5's base Eloquent models and functionality. 8 | 9 | It provides a number of utilities and classes to work with Eloquent in new and useful ways, 10 | such as camel cased attributes (for JSON apis), count caching, uuids and more. 11 | 12 | ## Installation 13 | 14 | Install the package via composer: 15 | 16 | composer require kirkbushell/eloquence:~2.0 17 | 18 | For Laravel 4, please install the 1.1.5 release. Please note that this is no longer supported 19 | and won't receive any new features, only security updates. 20 | 21 | composer require kirkbushell/eloquence:1.1.5 22 | 23 | ## Usage 24 | 25 | First, add the eloquence service provider to your config/app.php file: 26 | 27 | 'Eloquence\EloquenceServiceProvider', 28 | 29 | It's important to note that this will automatically re-bind the Model class 30 | that Eloquent uses for many-to-many relationships. This is necessary because 31 | when the Pivot model is instantiated, we need it to utilise the parent model's 32 | information and traits that may be needed. 33 | 34 | You should now be good to go with your models. 35 | 36 | ## Camel case all the things! 37 | 38 | For those of us who prefer to work with a single coding standard right across our applications, 39 | using the CamelCaseModel trait will ensure that all those attributes, relationships and associated 40 | data from our Eloquent models persist through to our APIs in a camel-case manner. This is important 41 | if you are writing front-end applications, which are also using camelCase. This allows for a 42 | better standard across our application. To use: 43 | 44 | use Eloquence\Behaviours\CamelCasing; 45 | 46 | Put the above line in your models and that's it. 47 | 48 | ### Note! 49 | 50 | Eloquence DOES NOT CHANGE how you write your schema migrations. You should still be using snake_case 51 | when setting up your fields and tables in your database schema migrations. This is a good thing - 52 | snake_case of field names is the defacto standard within the Laravel community :) 53 | 54 | 55 | ## UUIDs 56 | 57 | Eloquence comes bundled with UUID capabilities that you can use in your models. 58 | 59 | Simply include the Uuid trait: 60 | 61 | use Eloquence\Behaviours\Uuid; 62 | 63 | And then disable auto incrementing ids: 64 | 65 | public $incrementing = false; 66 | 67 | This will turn off id auto-incrementing in your model, and instead automatically generate a UUID4 value for your id field. One 68 | benefit of this is that you can actually know the id of your record BEFORE it's saved! 69 | 70 | You must ensure that your id column is setup to handle UUID values. This can be done by creating a migration with the following 71 | properties: 72 | 73 | $table->char('id', $length = 36)->index(); 74 | 75 | It's important to note that you should do your research before using UUID functionality and whether it works for you. UUID 76 | field searches are much slower than indexed integer fields (such as autoincrement id fields). 77 | 78 | 79 | ### Custom UUIDs 80 | 81 | Should you need a custom UUID solution (aka, maybe you don't want to use a UUID4 id), you can simply define the value you wish on 82 | the id field. The UUID model trait will not set the id if it has already been defined. In this use-case however, it's probably no good 83 | to use the Uuid trait, as it's practically useless in this scenario. 84 | 85 | ## Behaviours 86 | 87 | Eloquence comes with a system for setting up behaviours, which are really just small libraries that you can use with your Eloquent models. 88 | The first of these is the count cache. 89 | 90 | ### Count cache 91 | 92 | Count caching is where you cache the result of a count of a related table's records. A simple example of this is where you have a user who 93 | has many posts. In this example, you may want to count the number of posts a user has regularly - and perhaps even order by this. In SQL, 94 | ordering by a counted field is slow and unable to be indexed. You can get around this by caching the count of the posts the user 95 | has created on the user's record. 96 | 97 | To get this working, you need to do two steps: 98 | 99 | 1. Use the Countable trait on the model and 100 | 2. Configure the count cache settings 101 | 102 | #### Configure the count cache 103 | 104 | To setup the count cache configuration, we need to have the model use Countable trait, like so: 105 | 106 | ```php 107 | class Post extends Eloquent { 108 | use Countable; 109 | 110 | public function countCaches() { 111 | return [User::class]; 112 | } 113 | } 114 | ``` 115 | 116 | This tells the count cache that the Post model has a count cache on the User model. So, whenever a post is added, or modified or 117 | deleted, the count cache behaviour will update the appropriate user's count cache for their posts. In this case, it would update `post_count` 118 | on the user model. 119 | 120 | The example above uses the following standard conventions: 121 | 122 | * `post_count` is a defined field on the User model table 123 | * `user_id` is the field representing the foreign key on the post model 124 | * `id` is the primary key on the user model table 125 | 126 | These are, however, configurable: 127 | 128 | ```php 129 | class Post extends Eloquent { 130 | use Countable; 131 | 132 | public function countCaches() { 133 | return [ 134 | 'num_posts' => ['User', 'users_id', 'id'] 135 | ]; 136 | } 137 | } 138 | ``` 139 | 140 | This example customises the count cache field, and the related foreign key, with `num_posts` and `users_id`, respectively. 141 | 142 | Alternatively, you can be very explicit about the configuration (useful if you are using count caching on several tables 143 | and use the same column name on each of them): 144 | 145 | ```php 146 | class Post extends { 147 | use Countable; 148 | 149 | public function countCaches() { 150 | return [ 151 | [ 152 | 'model' => 'User', 153 | 'field' => 'num_posts', 154 | 'foreignKey' => 'users_id', 155 | 'key' => 'id' 156 | ] 157 | ]; 158 | } 159 | } 160 | ``` 161 | 162 | If using the explicit configuration, at a minimum you will need to define the "model" parameter. The "countField", "foreignKey", 163 | and "key" parameters will be calculated using the standard conventions mentioned above if they are omitted. 164 | 165 | With this configuration now setup - you're ready to go! 166 | 167 | 168 | ### Sum cache 169 | 170 | Sum caching is similar to count caching, except that instead of caching a _count_ of a related table's records, you cache a _sum_ 171 | of a particular field on the related table's records. A simple example of this is where you have an order that has many items. 172 | Using sum caching, you can cache the sum of all the items' prices, and store that sum in the order table. 173 | 174 | To get this working -- just like count caching -- you need to do two steps: 175 | 176 | 1. Utilise the Summable trait on the model and 177 | 2. Configure the model for any sum caches 178 | 179 | #### Configure the sum cache 180 | 181 | To setup the sum cache configuration, simply do the following: 182 | 183 | ```php 184 | class Item extends Eloquent { 185 | use Summable; 186 | 187 | public function sumCaches() { 188 | return [Order::class]; 189 | } 190 | } 191 | ``` 192 | 193 | This tells the sum cache manager that the Item model has a sum cache on the Order model. So, whenever an item is added, modified, or 194 | deleted, the sum cache behaviour will update the appropriate order's sum cache for their items. In this case, it would update `item_total` 195 | on the Order model. 196 | 197 | The example above uses the following conventions: 198 | 199 | * `item_total` is a defined field on the Order model table 200 | * `total` is a defined field on the Item model table (the column we are summing) 201 | * `order_id` is the field representing the foreign key on the item model 202 | * `id` is the primary key on the order model table 203 | 204 | These are, however, configurable: 205 | 206 | ```php 207 | class Item extends Eloquent { 208 | use Summable; 209 | 210 | public function sumCaches() { 211 | return [ 212 | 'item_total' => ['Order', 'total', 'order_id', 'id'] 213 | ]; 214 | } 215 | } 216 | ``` 217 | 218 | Or using the verbose syntax: 219 | 220 | ```php 221 | class Item extends Eloquent { 222 | use Summable; 223 | 224 | public function sumCaches() { 225 | return [ 226 | [ 227 | 'model' => 'Order', 228 | 'columnToSum' => 'total', 229 | 'field' => 'item_total' 230 | 'foreignKey' => 'order_id', 231 | 'key' => 'id' 232 | ] 233 | ]; 234 | } 235 | } 236 | ``` 237 | 238 | Both of these examples implements the default settings. 239 | 240 | With these settings configured, you will now see the related model's sum cache updated every time an item is added, updated, or removed. 241 | 242 | ### Sluggable models 243 | 244 | Sluggable is another behaviour that allows for the easy addition of model slugs. To use, implement the Sluggable trait: 245 | 246 | ```php 247 | class User extends Eloquent { 248 | use Sluggable; 249 | 250 | public function slugStrategy() { 251 | return 'username'; 252 | } 253 | } 254 | ``` 255 | 256 | In the example above, a slug will be created based on the username field of the User model. There are two other 257 | slugs that are supported however, as well: 258 | 259 | * id and 260 | * uuid 261 | 262 | The only difference between the two above, is that if you're using UUIDs, the slug will be generated previous 263 | to the save, based on the uuid field. With ids, which are generally auto-increase strategies - the slug has 264 | to be generated after the record has been saved - which results in a secondary save call to the database. 265 | 266 | That's it! Easy huh? 267 | 268 | ## Changelog 269 | 270 | #### 2.0.2 271 | 272 | * Updated PHP dependency to 5.6+ 273 | * CountCache and SumCache behaviours now supported via a service layer 274 | 275 | #### 2.0.0 276 | 277 | * Sum cache model behaviour added 278 | * Booting of behaviours now done via Laravel trait booting 279 | * Simplification of all behaviours and their uses 280 | * Updated readme/configuration guide 281 | 282 | #### 1.4.0 283 | 284 | * Slugs when retrieved from a model now return Slug value objects. 285 | 286 | #### 1.3.4 287 | 288 | * More random, less predictable slugs for id strategies 289 | 290 | #### 1.3.3 291 | 292 | * Fixed a bug with relationships not being accessible via model properties 293 | 294 | #### 1.3.2 295 | 296 | * Slugged behaviour 297 | * Fix for fillable attributes 298 | 299 | #### 1.3.1 300 | 301 | * Relationship fixes 302 | * Fillable attributes bug fix 303 | * Count cache update for changing relationships fix 304 | * Small update for implementing count cache observer 305 | 306 | #### 1.3.0 307 | 308 | * Count cache model behaviour added 309 | * Many-many relationship casing fix 310 | * Fixed an issue when using ::create 311 | 312 | #### 1.2.0 313 | 314 | * Laravel 5 support 315 | * Readme updates 316 | 317 | #### 1.1.5 318 | 319 | * UUID model trait now supports custom UUIDs (instead of only generating them for you) 320 | 321 | #### 1.1.4 322 | 323 | * UUID fix 324 | 325 | #### 1.1.3 326 | 327 | * Removed the schema binding on the service provider 328 | 329 | #### 1.1.2 330 | 331 | * Removed the uuid column creation via custom blueprint 332 | 333 | #### 1.1.1 334 | 335 | * Dependency bug fix 336 | 337 | #### 1.1.0 338 | 339 | * UUIDModel trait added 340 | * CamelCaseModel trait added 341 | * Model class updated to use CamelCaseModel trait - deprecated, backwards-compatibility support only 342 | * Eloquence now its own namespace (breaking change) 343 | * EloquenceServiceProvider added use this if you want to overload the base model automatically (required for pivot model camel casing). 344 | 345 | #### 1.0.2 346 | 347 | * Relationships now support camelCasing for retrieval (thanks @linxgws) 348 | 349 | #### 1.0.1 350 | 351 | * Fixed an issue with dependency resolution 352 | 353 | #### 1.0.0 354 | 355 | * Initial implementation 356 | * Camel casing of model attributes now available for both setters and getters 357 | 358 | ## License 359 | 360 | The Laravel framework is open-sourced software licensed under the MIT license. 361 | --------------------------------------------------------------------------------