From b1af8a41dea897e135f899f84a8cab37ee8309b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sercan=20C=CC=A7ak=C4=B1r?= Date: Tue, 25 Jul 2017 20:56:21 +0300 Subject: [PATCH] initial commit --- .editorconfig | 16 ++ .gitattributes | 12 ++ .gitignore | 3 + .styleci.yml | 3 + .travis.yml | 17 ++ CHANGELOG.md | 7 + CONTRIBUTING.md | 35 ++++ LICENSE.md | 21 ++ README.md | 185 ++++++++++++++++++ composer.json | 49 +++++ config/categorizable.php | 17 ++ ...7_08_01_000000_create_categories_table.php | 38 ++++ ...8_01_000000_create_categorizable_table.php | 39 ++++ phpunit.xml.dist | 21 ++ src/Categorizable.php | 107 ++++++++++ src/CategorizableServiceProvider.php | 36 ++++ src/Category.php | 28 +++ tests/CategorizableTest.php | 94 +++++++++ tests/Category.php | 18 ++ tests/Post.php | 25 +++ tests/TestCase.php | 73 +++++++ tests/TestServiceProvider.php | 21 ++ tests/factories/ModelFactory.php | 30 +++ .../2017_08_01_000000_create_posts_table.php | 32 +++ 24 files changed, 927 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/categorizable.php create mode 100644 database/migrations/2017_08_01_000000_create_categories_table.php create mode 100644 database/migrations/2017_08_01_000000_create_categorizable_table.php create mode 100644 phpunit.xml.dist create mode 100644 src/Categorizable.php create mode 100644 src/CategorizableServiceProvider.php create mode 100644 src/Category.php create mode 100644 tests/CategorizableTest.php create mode 100644 tests/Category.php create mode 100644 tests/Post.php create mode 100644 tests/TestCase.php create mode 100644 tests/TestServiceProvider.php create mode 100644 tests/factories/ModelFactory.php create mode 100644 tests/migrations/2017_08_01_000000_create_posts_table.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1492202 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c4d7642 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto + +/.github export-ignore +/build export-ignore +/tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +CHANGELOG.md export-ignore +CONTRIBUTING.md export-ignore +README.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35cf7e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +composer.lock +/build +/vendor diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..916d27e --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,3 @@ +preset: laravel + +linting: true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4f1bdd0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: php + +php: + - 7.0 + - 7.1 + +matrix: + fast_finish: true + +sudo: false + +before_script: + - travis_retry composer self-update + - travis_retry composer install --prefer-source --no-interaction --dev + +script: + - vendor/bin/phpunit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..63be7e3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Release Notes + +All notable changes to `laravel-categorizable` will be documented in this file + +## 1.0.0 (2017-08-01) + +Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a2d4cae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +CONTRIBUTING +============ + +Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests. + +## Guidelines + +* Please follow the [PSR-2 Coding Style Guide](http://www.php-fig.org/psr/psr-2/), enforced by [StyleCI](https://styleci.io/). +* Ensure that the current tests pass, and if you've added something new, add the tests where relevant. +* Send a coherent commit history, making sure each individual commit in your pull request is meaningful. +* You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts. +* If you are changing the behavior, or the public api, you may need to update the docs. +* Please remember that we follow [SemVer](http://semver.org/). + +## Running Tests + +You will need an install of [Composer](https://getcomposer.org/) before continuing. + +First, install the dependencies: + +```bash +$ composer install +``` + +Then run PHPUnit: + +```bash +$ vendor/bin/phpunit +``` + +If the test suite passes on your local machine you should be good to go. + +When you make a pull request, the tests will automatically be run again by [Travis CI](https://travis-ci.org/). + +We also have [StyleCI](https://styleci.io/) setup to automatically fix any code style issues. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d83c025 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Sercan Çakır + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..510cc57 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# Laravel Categorizable + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/mayoz/laravel-categorizable.svg?style=flat-square)](https://packagist.org/packages/mayoz/laravel-categorizable) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/mayoz/laravel-categorizable/master.svg?style=flat-square)](https://travis-ci.org/mayoz/laravel-categorizable) +[![StyleCI](https://styleci.io/repos/71335427/shield?branch=master)](https://styleci.io/repos/71335427) + +Easily add the ability to category your Eloquent models in Laravel 5. + +* [Installation](#installation) +* [Configuration](#configuration) +* [Usage](#usage) +* [Extending](#extending) +* [License](#license) + +## Installation + +You can install the package via composer: + +``` bash +composer require mayoz/categorizable +``` + +Register the service provider in your `config/app.php` configuration file: + +```php +'providers' => [ + ... + Mayoz\Categorizable\CategorizableServiceProvider::class, + ... +]; +``` + +You can publish the migration with: + +```bash +php artisan vendor:publish --provider="Mayoz\Categorizable\CategorizableServiceProvider" --tag="migrations" +``` + +The migration has been published you can create the `categories` and `categorizable` tables. You are feel free for added new fields that you need. After, run the migrations: + +```bash +php artisan migrate +``` + +## Usage + +Suppose, you have the `Post` model as follows: + +```php +categorize([1, 2, 3, 4, 5]); + +return $post; +``` + +Now, the `post` model is associated with categories ids of `1`, `2`, `3`, `4` and `5`. + +Remove the existing category association for the `Post` model: + +```php +$post = Post::find(1); + +$post->uncategorize([3, 5]); + +return $post; +``` + +The `post` model is associated with categories ids of `1`, `2` and `4`. + +Rearrange the category relationships for the `Post` model: + +```php +$post = Post::find(1); + +$post->recategorize([1, 5]); + +return $post; +``` + +The `post` model is associated with categories ids of `1` and `5`. + +## Extending + +I suggest, you always extend the `Category` model to define your relationships directly. Create you own `Category` model: + +```php +categorized(Post::class); + } +} +``` + +You publish the package config: + +```bash +php artisan vendor:publish --provider="Mayoz\Categorizable\CategorizableServiceProvider" --tag="config" +``` + +This is the contents of the published config file: + +```php + App\Category::class, + +]; +``` + +That is all. Now let's play for relationship query with the category. + +```php +/** + * Respond the post + * + * @param \App\Category $category + * @return \Illuminate\Http\Response + */ +public function index(Category $category) +{ + return $category->posts()->paginate(10); +} +``` + +If we did not extend the `Category` model, as had to use; + +```php +/** + * Respond the post + * + * @param \App\Category $category + * @return \Illuminate\Http\Response + */ +public function index(Category $category) +{ + return $category->categorize(Post::class)->paginate(10); +} +``` + +## License + +This package is licensed under [The MIT License (MIT)](LICENSE.md). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8d46b34 --- /dev/null +++ b/composer.json @@ -0,0 +1,49 @@ +{ + "name": "mayoz/laravel-categorizable", + "description": "Polymorphic categorizable for Laravel", + "keywords": ["laravel", "polymorphic", "categorizable"], + "license": "MIT", + "support": { + "issues": "https://github.com/mayoz/laravel-categorizable/issues", + "source": "https://github.com/mayoz/laravel-categorizable" + }, + "authors": [ + { + "name": "Sercan Çakır", + "email": "srcnckr@gmail.com" + } + ], + "require": { + "php": "^7.0", + "illuminate/database": "~5.4|~5.5", + "illuminate/support": "~5.4|~5.5" + }, + "require-dev": { + "orchestra/testbench": "^3.4", + "phpunit/phpunit": "^6.0" + }, + "autoload": { + "psr-4": { + "Mayoz\\Categorizable\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mayoz\\Tests\\Categorizable\\": "tests" + } + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true + }, + "extra": { + "laravel": { + "providers": [ + "Mayoz\\Categorizable\\CategorizableServiceProvider" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/categorizable.php b/config/categorizable.php new file mode 100644 index 0000000..7fdcc71 --- /dev/null +++ b/config/categorizable.php @@ -0,0 +1,17 @@ + Mayoz\Categorizable\Category::class, + +]; diff --git a/database/migrations/2017_08_01_000000_create_categories_table.php b/database/migrations/2017_08_01_000000_create_categories_table.php new file mode 100644 index 0000000..6b16ee0 --- /dev/null +++ b/database/migrations/2017_08_01_000000_create_categories_table.php @@ -0,0 +1,38 @@ +engine = 'InnoDB'; + + // table columns + $table->increments('id'); + $table->string('name'); + $table->string('slug')->unique(); + $table->string('caption')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('categories'); + } +} diff --git a/database/migrations/2017_08_01_000000_create_categorizable_table.php b/database/migrations/2017_08_01_000000_create_categorizable_table.php new file mode 100644 index 0000000..2ac9919 --- /dev/null +++ b/database/migrations/2017_08_01_000000_create_categorizable_table.php @@ -0,0 +1,39 @@ +engine = 'InnoDB'; + + // table columns + $table->unsignedInteger('category_id'); + $table->morphs('categorizable'); + $table->timestamp('created_at'); + + // foreign keys + $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('categorizables'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8ae6724 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/src/Categorizable.php b/src/Categorizable.php new file mode 100644 index 0000000..bba3ed9 --- /dev/null +++ b/src/Categorizable.php @@ -0,0 +1,107 @@ +categories->each->delete(); + }); + } + + /** + * Get all categories for the relation. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany + */ + public function categories() + { + return $this->morphToMany( + config('categorizable.category'), 'categorizable' + )->withPivot('created_at'); + } + + /** + * Attach one or multiple category to the model. + * + * @example + * + * ``` + * $post = tap(Post::findOrFail($id), function ($post) { + * $post->categorize((array) request('categories')); + * }); + * ``` + * + * @param mixed $categories + * @return static + */ + public function categorize($categories) + { + $this->categories()->sync($categories, false); + + return $this; + } + + /** + * Remove all categories from the model and assign the given ones. + * + * @example + * + * ``` + * $post = tap(Post::findOrFail($id), function ($post) { + * $post->recategorize((array) request('categories')); + * }); + * ``` + * + * @param mixed $categories + * @return static + */ + public function recategorize($categories = []) + { + $this->categories()->sync($categories); + + return $this; + } + + /** + * Detach one or multiple category from the model. + * + * @example + * + * ``` + * $post = tap(Post::findOrFail($id), function ($post) { + * $post->uncategorize((array) request('categories')); + * }); + * ``` + * + * @param mixed $categories + * @return static + */ + public function uncategorize($categories) + { + $this->categories()->detach($categories); + + return $this; + } + + /** + * Get the number of categories for the relation. + * + * @return int + */ + public function getCategoriesCountAttribute() + { + if ($this->relationLoaded('categories')) { + return $this->categories->count(); + } + + return $this->categories()->count(); + } +} diff --git a/src/CategorizableServiceProvider.php b/src/CategorizableServiceProvider.php new file mode 100644 index 0000000..c3acefe --- /dev/null +++ b/src/CategorizableServiceProvider.php @@ -0,0 +1,36 @@ +publishes([ + __DIR__.'/../database/migrations' => database_path('migrations'), + ], 'migrations'); + + $this->publishes([ + __DIR__.'/../config/categorizable.php' => config_path('categorizable.php'), + ], 'config'); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->mergeConfigFrom( + __DIR__.'/../config/categorizable.php', 'categorizable' + ); + } +} diff --git a/src/Category.php b/src/Category.php new file mode 100644 index 0000000..13a5b8b --- /dev/null +++ b/src/Category.php @@ -0,0 +1,28 @@ +morphedByMany($related, 'categorizable'); + } +} diff --git a/tests/CategorizableTest.php b/tests/CategorizableTest.php new file mode 100644 index 0000000..e193d4d --- /dev/null +++ b/tests/CategorizableTest.php @@ -0,0 +1,94 @@ +categories = factory(Category::class, 5)->create(); + } + + /** + * A basic test example. + * + * @return void + */ + public function testCategorize() + { + $post = factory(Post::class)->create(); + + $post->categorize($this->categories); + + $this->assertEquals($this->categories->count(), $post->categories_count); + } + + /** + * A basic test example. + * + * @return void + */ + public function testRecategorize() + { + $post = factory(Post::class)->create(); + + $post->categorize($this->categories); + $post->recategorize($this->categories->random(2)); + + $this->assertEquals(2, $post->categories_count); + } + + /** + * A basic test example. + * + * @return void + */ + public function testRecategorizeForRemove() + { + $post = factory(Post::class)->create(); + + $post->categorize($this->categories); + $post->recategorize([]); + + $this->assertEquals(0, $post->categories_count); + } + + /** + * A basic test example. + * + * @return void + */ + public function testUncategorize() + { + $post = factory(Post::class)->create(); + + $post->categorize($this->categories); + $post->uncategorize($this->categories->random(3)); + + $this->assertEquals(2, $post->categories_count); + } + + /** + * A basic test example. + * + * @return void + */ + public function testRelationship() + { + $posts = factory(Post::class, 5)->create()->each(function ($post) { + $post->categorize($this->categories); + }); + + $category = Category::find(1); + + $this->assertEquals($category->posts, $category->categorized(Post::class)->get()); + $this->assertEquals($posts->count(), $category->posts->count()); + } +} diff --git a/tests/Category.php b/tests/Category.php new file mode 100644 index 0000000..b95be22 --- /dev/null +++ b/tests/Category.php @@ -0,0 +1,18 @@ +morphedByMany(Post::class, 'categorizable'); + } +} diff --git a/tests/Post.php b/tests/Post.php new file mode 100644 index 0000000..cdfd685 --- /dev/null +++ b/tests/Post.php @@ -0,0 +1,25 @@ +setUpDatabase($this->app); + $this->setupConfig($this->app); + } + + /** + * {@inheritdoc} + */ + protected function getPackageProviders($app) + { + return [ + CategorizableServiceProvider::class, + TestServiceProvider::class, + ]; + } + + /** + * {@inheritdoc} + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('database.default', 'test'); + + $app['config']->set('database.connections.test', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + /** + * Setup the application database. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function setUpDatabase($app) + { + $this->artisan('migrate', ['--database' => 'test']); + + $this->withFactories(__DIR__.'/factories'); + + $this->beforeApplicationDestroyed(function () { + $this->artisan('migrate:rollback'); + }); + } + + /** + * Setup the application config. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function setupConfig($app) + { + $app['config']->set('categorizable.category', Category::class); + } +} diff --git a/tests/TestServiceProvider.php b/tests/TestServiceProvider.php new file mode 100644 index 0000000..4fef86b --- /dev/null +++ b/tests/TestServiceProvider.php @@ -0,0 +1,21 @@ +loadMigrationsFrom([ + __DIR__.'/migrations', + __DIR__.'/../database/migrations', + ]); + } +} diff --git a/tests/factories/ModelFactory.php b/tests/factories/ModelFactory.php new file mode 100644 index 0000000..b67fc19 --- /dev/null +++ b/tests/factories/ModelFactory.php @@ -0,0 +1,30 @@ +define(Category::class, function (Faker $faker) { + return [ + 'name' => $name = $faker->unique()->word, + 'slug' => str_slug($name), + 'caption' => $faker->text(255), + ]; +}); + +$factory->define(Post::class, function (Faker $faker) { + return [ + 'name' => $faker->unique()->word, + ]; +}); diff --git a/tests/migrations/2017_08_01_000000_create_posts_table.php b/tests/migrations/2017_08_01_000000_create_posts_table.php new file mode 100644 index 0000000..80d90f3 --- /dev/null +++ b/tests/migrations/2017_08_01_000000_create_posts_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('posts'); + } +}