├── .gitignore ├── .travis.yml ├── LICENSE ├── composer.json ├── config └── taggable.php ├── docs ├── how-to-override-util.md └── suggesting.md ├── migrations └── 2016_05_09_154236_create_tags_table.php ├── phpunit.xml ├── readme.cn.md ├── readme.md ├── src ├── Contracts │ ├── TaggableContract.php │ └── TaggingUtility.php ├── Events │ ├── TagAdded.php │ └── TagRemoved.php ├── Model │ ├── Tag.php │ └── Tagged.php ├── Providers │ ├── LumenTaggingServiceProvider.php │ └── TaggingServiceProvider.php ├── Taggable.php └── Util.php └── tests ├── CommonUsageTest.php ├── TagTest.php ├── TestCase.php └── UtilTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | !empty 4 | .idea 5 | .buildpath 6 | composer.phar 7 | composer.lock 8 | /vendor 9 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - hhvm 8 | 9 | before_script: 10 | - travis_retry composer self-update 11 | - travis_retry composer install 12 | 13 | script: vendor/bin/phpunit --verbose -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Robert Conner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estgroupe/laravel-taggable", 3 | "description": "Taggable Trait for using tag inside Laravel Eloquent models, with Baum's Nested Set pattern support.", 4 | "license": "MIT", 5 | "homepage": "https://github.com/summerblue/laravel-taggable", 6 | "keywords": ["tag", "tags", "tagging", "laravel", "taggable", "tagged", "eloquent", "laravel5"], 7 | "authors": [ 8 | { 9 | "name": "Charlie Jade", 10 | "email": "cj@estgroupe.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.5.0", 15 | "illuminate/database": ">= 5.0", 16 | "illuminate/support": ">= 5.0", 17 | "baum/baum": "~1.1", 18 | "overtrue/pinyin" : "~3.0" 19 | }, 20 | "require-dev": { 21 | "orchestra/testbench": "~3.0", 22 | "phpunit/phpunit": "~4.0", 23 | "mockery/mockery": "~0.9", 24 | "vlucas/phpdotenv": "~2.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "EstGroupe\\Taggable\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "classmap": [ 33 | "tests/TestCase.php" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/taggable.php: -------------------------------------------------------------------------------- 1 | 'integer', // 'string' or 'integer' 8 | 9 | // Value of are passed through this before save of tags 10 | 'normalizer' => '\EstGroupe\Taggable\Util::slug', 11 | 12 | // Display value of tags are passed through (for front end display) 13 | 'displayer' => '\EstGroupe\Taggable\Util::tagName', 14 | 15 | // Database connection for Conner\Taggable\Tag model to use 16 | // 'connection' => 'mysql', 17 | 18 | // When deleting a model, remove all the tags relationship 19 | 'untag_on_delete' => true, 20 | 21 | // Auto-delete unused tags from the 'tags' database table, 22 | // when untaged, and they are used zero times. 23 | 'delete_unused_tags'=>false, 24 | 25 | // Model to use to store the tags in the database. 26 | // You can create your own and inherit the Taggable Tag. 27 | 'tag_model'=>'\EstGroupe\Taggable\Model\Tag', 28 | 29 | // Whether wan to keep track of the Model is tagged or not. 30 | // You have to set the field in your model like: 31 | // $table->enum('is_tagged', array('yes', 'no'))->default('no'); 32 | // Then you can call: Article::where('is_tagged', 'yes')->get() 33 | // to get all tagged $articles. 34 | 'is_tagged_label_enable' => false, 35 | 36 | // customize table name 37 | 'tags_table_name' => 'tags', 38 | 'taggables_table_name' => 'taggables', 39 | 40 | ); 41 | -------------------------------------------------------------------------------- /docs/how-to-override-util.md: -------------------------------------------------------------------------------- 1 | How do I override the Util class? 2 | ============ 3 | 4 | You'll need to create your own service provider. It should look something like this. 5 | 6 | ```php 7 | namespace My\Project\Providers; 8 | 9 | use EstGroupe\Taggable\Providers\TaggingServiceProvider as ServiceProvider; 10 | use EstGroupe\Taggable\Contracts\TaggingUtility; 11 | 12 | class TaggingServiceProvider extends ServiceProvider { 13 | 14 | /** 15 | * Register the service provider. 16 | * 17 | * @return void 18 | */ 19 | public function register() 20 | { 21 | $this->app->singleton(TaggingUtility::class, function () { 22 | return new MyNewUtilClass; 23 | }); 24 | } 25 | 26 | } 27 | ``` 28 | 29 | Where `MyNewUtilClass` is a class you have written. Your new Util class obviously needs to implement the `EstGroupe\Taggable\Contracts\TaggingUtility` interface. -------------------------------------------------------------------------------- /docs/suggesting.md: -------------------------------------------------------------------------------- 1 | Suggesting 2 | ============ 3 | 4 | Suggesting is a small little feature you could use if you wanted to have "suggested" tags that stand out. 5 | 6 | There is not much to it. You simply set the 'suggest' field in the database to true 7 | 8 | ```php 9 | $tag = EstGroupe\Taggable\Model\Tag::where('slug', '=', 'blog')->first(); 10 | $tag->suggest = true; 11 | $tag->save(); 12 | ``` 13 | 14 | And then you can fetch a list of suggested tags when you need it. 15 | 16 | ```php 17 | $suggestedTags = EstGroupe\Taggable\Model\Tag::suggested()->get(); 18 | ``` -------------------------------------------------------------------------------- /migrations/2016_05_09_154236_create_tags_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 12 | $table->string('slug', 255)->unique(); 13 | $table->string('name', 255)->unique(); 14 | $table->text('description')->nullable(); 15 | $table->boolean('suggest')->default(false); 16 | $table->integer('count')->unsigned()->default(0); // count of how many times this tag was used 17 | 18 | // For: Baum Nested Set 19 | // See: https://github.com/etrepat/baum#migration-configuration 20 | $table->integer('parent_id')->nullable(); 21 | $table->integer('lft')->nullable(); 22 | $table->integer('rgt')->nullable(); 23 | $table->integer('depth')->nullable(); 24 | }); 25 | 26 | Schema::create(config('taggable.taggables_table_name'), function(Blueprint $table) { 27 | $table->increments('id'); 28 | if(config('taggable.primary_keys_type') == 'string') { 29 | $table->string('taggable_id', 36)->index(); 30 | } else { 31 | $table->integer('taggable_id')->unsigned()->index(); 32 | } 33 | $table->string('taggable_type', 255)->index(); 34 | $table->integer('tag_id')->unsigned()->index(); 35 | 36 | $table->foreign('tag_id') 37 | ->references('id')->on(config('taggable.tags_table_name')) 38 | ->onDelete('cascade'); 39 | }); 40 | } 41 | 42 | public function down() 43 | { 44 | Schema::drop(config('taggable.tags_table_name')); 45 | Schema::drop(config('taggable.taggables_table_name')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /readme.cn.md: -------------------------------------------------------------------------------- 1 | Laravel Taggable 2 | ============ 3 | 4 | ## 功能说明 5 | 6 | 使用最简便的方式,为你的数据模型提供强大「打标签」功能。 7 | 8 | 本项目修改于 [rtconner/laravel-tagging](https://github.com/rtconner/laravel-tagging) 项目,增加了一下功能: 9 | 10 | * 标签名唯一; 11 | * 增加 [etrepat/baum](https://github.com/etrepat/baum) 依赖,让标签支持无限级别标签嵌套; 12 | * 中文 slug 拼音自动生成支持,感谢超哥的 [overtrue/pinyin](https://github.com/overtrue/pinyin); 13 | * 提供完整的测试用例,保证代码质量。 14 | 15 | > 注意: 本项目只支持 5.1 LTS 16 | 17 | :heart: 此项目由 [The EST Group](http://estgroupe.com) 团队的 [@Summer](https://github.com/summerblue) 维护。 18 | 19 | ## 无限级别标签嵌套 20 | 21 | 集成 [etrepat/baum](https://github.com/etrepat/baum) 让标签具备从属关系。 22 | 23 | ```php 24 | 25 | $root = Tag::create(['name' => 'Root']); 26 | 27 | // 创建子标签 28 | $child1 = $root->children()->create(['name' => 'Child1']); 29 | 30 | $child = Tag::create(['name' => 'Child2']); 31 | $child->makeChildOf($root); 32 | 33 | // 批量构建树 34 | $tagTree = [ 35 | 'name' => 'RootTag', 36 | 'children' => [ 37 | ['name' => 'L1Child1', 38 | 'children' => [ 39 | ['name' => 'L2Child1'], 40 | ['name' => 'L2Child1'], 41 | ['name' => 'L2Child1'], 42 | ] 43 | ], 44 | ['name' => 'L1Child2'], 45 | ['name' => 'L1Child3'], 46 | ] 47 | ]; 48 | 49 | Tag::buildTree($tagTree); 50 | ``` 51 | 52 | 更多关联操作请查看:[etrepat/baum](https://github.com/etrepat/baum) 。 53 | 54 | ## 标签名称规则说明 55 | 56 | * 标签名里的特殊符号和空格会被 `-` 替代; 57 | * 智能标签 slug 生成,会生成 name 对应的中文拼音 slug ,如:`标签` -> `biao-qian`,拼音一样的时候会被加上随机值; 58 | 59 | > 标签名清理使用:`$normalize_string = EstGroupe\Taggable\Util::tagName($name)`。 60 | 61 | ``` 62 | Tag::create(['标签名']); 63 | // name: 标签名 64 | // slug: biao-qian-ming 65 | 66 | Tag::create(['表签名']); 67 | // name: 表签名 68 | // slug: biao-qian-ming-3243 (后面 3243 为随机,解决拼音冲突) 69 | 70 | Tag::create(['标签 名']); 71 | // name: 标签-名 72 | // slug: biao-qian-ming 73 | 74 | Tag::create(['标签!名']); 75 | // name: 标签-名 76 | // slug: biao-qian-ming 77 | ``` 78 | 79 | ## 安装说明: 80 | 81 | ### 安装 82 | 83 | ```shell 84 | composer require estgroupe/laravel-taggable "5.1.*" 85 | ``` 86 | 87 | ### 安装和执行迁移 88 | 89 | 在 `config/app.php` 的 `providers` 数组中加入: 90 | ```php 91 | 'providers' => array( 92 | \EstGroupe\Taggable\Providers\TaggingServiceProvider::class, 93 | ); 94 | ``` 95 | ```bash 96 | php artisan vendor:publish --provider="EstGroupe\Taggable\Providers\TaggingServiceProvider" 97 | php artisan migrate 98 | ``` 99 | 100 | > 请仔细阅读 `config/tagging.php` 文件。 101 | 102 | ### 创建 Tag.php 103 | 104 | 不是必须的,不过建议你创建自己项目专属的 Tag.php 文件。 105 | 106 | ```php 107 | '\App\Models\Tag', 121 | ``` 122 | 123 | ### 加入 Taggable Trait 124 | 125 | ```php 126 | is_tagged 143 | 144 | // `yes` 145 | $article->tag('Tag1'); 146 | $article->is_tagged; 147 | 148 | // `no` 149 | $article->unTag(); 150 | $article->is_tagged 151 | 152 | // This is fast 153 | $taggedArticles = Article::where('is_tagged', 'yes')->get() 154 | ``` 155 | 156 | 首先你需要修改 `config/tagging.php` 文件中: 157 | 158 | ```php 159 | 'is_tagged_label_enable' => true, 160 | ``` 161 | 162 | 然后在你的模型的数据库创建脚本里加上: 163 | 164 | ```php 165 | increments('id'); 176 | ... 177 | // Add this line 178 | $table->enum('is_tagged', array('yes', 'no'))->default('no'); 179 | ... 180 | $table->timestamps(); 181 | }); 182 | } 183 | } 184 | ``` 185 | 186 | ### 「推荐标签」标示 187 | 188 | 方便你实现「推荐标签」功能,只需要把 `suggest` 字段标示为 `true`: 189 | 190 | ```php 191 | $tag = EstGroupe\Taggable\Model\Tag::where('slug', '=', 'blog')->first(); 192 | $tag->suggest = true; 193 | $tag->save(); 194 | ``` 195 | 196 | 即可以用以下方法读取: 197 | 198 | ```php 199 | $suggestedTags = EstGroupe\Taggable\Model\Tag::suggested()->get(); 200 | ``` 201 | 202 | ### 重写 Util 类? 203 | 204 | 大部分的通用操作都发生在 Util 类,你想获取更多的定制权力,请创建自己的 Util 类,并注册服务提供者: 205 | 206 | ```php 207 | namespace My\Project\Providers; 208 | 209 | use EstGroupe\Taggable\Providers\TaggingServiceProvider as ServiceProvider; 210 | use EstGroupe\Taggable\Contracts\TaggingUtility; 211 | 212 | class TaggingServiceProvider extends ServiceProvider { 213 | 214 | /** 215 | * Register the service provider. 216 | * 217 | * @return void 218 | */ 219 | public function register() 220 | { 221 | $this->app->singleton(TaggingUtility::class, function () { 222 | return new MyNewUtilClass; 223 | }); 224 | } 225 | 226 | } 227 | ``` 228 | 229 | 然后在 230 | 231 | > 注意 `MyNewUtilClass` 必须实现 `EstGroupe\Taggable\Contracts\TaggingUtility` 接口。 232 | 233 | ## 使用范例 234 | 235 | ```php 236 | $article = Article::with('tags')->first(); // eager load 237 | 238 | // 获取所有标签 239 | foreach($article->tags as $tag) { 240 | echo $tag->name . ' with url slug of ' . $tag->slug; 241 | } 242 | 243 | // 打标签 244 | $article->tag('Gardening'); // attach the tag 245 | $article->tag('Gardening, Floral'); // attach the tag 246 | $article->tag(['Gardening', 'Floral']); // attach the tag 247 | $article->tag('Gardening', 'Floral'); // attach the tag 248 | 249 | // 批量通过 tag ids 打标签 250 | $article->tagWithTagIds([1,2,3]); 251 | 252 | // 去掉标签 253 | $article->untag('Cooking'); // remove Cooking tag 254 | $article->untag(); // remove all tags 255 | 256 | // 重打标签 257 | $article->retag(['Fruit', 'Fish']); // delete current tags and save new tags 258 | $article->retag('Fruit', 'Fish'); 259 | $article->retag('Fruit, Fish'); 260 | 261 | $tagged = $article->tagged; // return Collection of rows tagged to article 262 | $tags = $article->tags; // return Collection the actual tags (is slower than using tagged) 263 | 264 | // 获取绑定的标签名称数组 265 | $article->tagNames(); // get array of related tag names 266 | 267 | // 获取打了「任意」标签的 Article 对象 268 | Article::withAnyTag('Gardening, Cooking')->get(); // fetch articles with any tag listed 269 | Article::withAnyTag(['Gardening','Cooking'])->get(); // different syntax, same result as above 270 | Article::withAnyTag('Gardening','Cooking')->get(); // different syntax, same result as above 271 | 272 | // 获取打了「全包含」标签的 Article 对象 273 | Article::withAllTags('Gardening, Cooking')->get(); // only fetch articles with all the tags 274 | Article::withAllTags(['Gardening', 'Cooking'])->get(); 275 | Article::withAllTags('Gardening', 'Cooking')->get(); 276 | 277 | EstGroupe\Taggable\Model\Tag::where('count', '>', 2)->get(); // return all tags used more than twice 278 | 279 | Article::existingTags(); // return collection of all existing tags on any articles 280 | ``` 281 | 282 | 如果你 [创建了 Tag.php](#),即可使用以下标签读取功能: 283 | 284 | ```php 285 | 286 | // 通过 slug 获取标签 287 | Tag::byTagSlug('biao-qian-ming')->first(); 288 | 289 | // 通过名字获取标签 290 | Tag::byTagName('标签名')->first(); 291 | 292 | // 通过名字数组获取标签数组 293 | Tag::byTagNames(['标签名', '标签2', '标签3'])->first(); 294 | 295 | // 通过 Tag ids 数组获取标签数组 296 | Tag::byTagIds([1,2,3])->first(); 297 | 298 | // 通过名字数组获取 ID 数组 299 | $ids = Tag::idsByNames(['标签名', '标签2', '标签3'])->all(); 300 | // [1,2,3] 301 | 302 | ``` 303 | 304 | ## 标签事件 305 | 306 | `Taggable` trait 提供以下两个事件: 307 | 308 | ```php 309 | EstGroupe\Taggable\Events\TagAdded; 310 | 311 | EstGroupe\Taggable\Events\TagRemoved; 312 | ``` 313 | 314 | 监听标签事件: 315 | 316 | ```php 317 | \Event::listen(EstGroupe\Taggable\Events\TagAdded::class, function($article){ 318 | \Log::debug($article->title . ' was tagged'); 319 | }); 320 | ``` 321 | 322 | ## 单元测试 323 | 324 | 基本用例测试请见: `tests/CommonUsageTest.php`。 325 | 326 | 运行测试: 327 | 328 | ``` 329 | composer install 330 | vendor/bin/phpunit --verbose 331 | ``` 332 | 333 | ## Thanks 334 | 335 | - Special Thanks to: Robert Conner - http://smartersoftware.net 336 | - [overtrue/pinyin](https://github.com/overtrue/pinyin) 337 | - [etrepat/baum](https://github.com/etrepat/baum) 338 | - Made with love by The EST Group - http://estgroupe.com/ 339 | 340 | 341 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Laravel Taggable 2 | ============ 3 | 4 | ## Introduction 5 | 6 | 7 | 8 | Tag support for Laravel Eloquent models using Taggable Trait. 9 | 10 | This project extends [rtconner/laravel-tagging](https://github.com/rtconner/laravel-tagging) , add the following feature specially for Chinese 11 | User: 12 | 13 | * Tag name unique, and using `tag_id` for query data. 14 | * Add [etrepat/baum](https://github.com/etrepat/baum) support complicated tag tree; 15 | * Chinese Pinyin slug support using [overtrue/pinyin](https://github.com/overtrue/pinyin); 16 | * Full test coverage。 17 | 18 | > Notice: This projcet only tested and intended only support 5.1 LTS. 19 | 20 | :heart: This project is maintained by [@Summer](https://github.com/summerblue), member of [The EST Group](http://estgroupe.com). 21 | 22 | 中文文档和讨论请见这里:https://phphub.org/topics/2123 23 | 24 | ## Baum Nested Sets 25 | 26 | Integarated [etrepat/baum](https://github.com/etrepat/baum), what is Nested Sets? 27 | 28 | > A nested set is a smart way to implement an ordered tree that allows for fast, non-recursive queries. For example, you can fetch all descendants of a node in a single query, no matter how deep the tree. 29 | 30 | ```php 31 | 32 | $root = Tag::create(['name' => 'Root']); 33 | 34 | // Create Child Tag 35 | $child1 = $root->children()->create(['name' => 'Child1']); 36 | 37 | $child = Tag::create(['name' => 'Child2']); 38 | $child->makeChildOf($root); 39 | 40 | // Batch create Tag Tree 41 | $tagTree = [ 42 | 'name' => 'RootTag', 43 | 'children' => [ 44 | ['name' => 'L1Child1', 45 | 'children' => [ 46 | ['name' => 'L2Child1'], 47 | ['name' => 'L2Child1'], 48 | ['name' => 'L2Child1'], 49 | ] 50 | ], 51 | ['name' => 'L1Child2'], 52 | ['name' => 'L1Child3'], 53 | ] 54 | ]; 55 | 56 | Tag::buildTree($tagTree); 57 | ``` 58 | 59 | Please refer the Official Project for more advance usage - [etrepat/baum](https://github.com/etrepat/baum) 60 | 61 | ## Tag name rules 62 | 63 | * Any special charactor and empty space will be replace by `-`; 64 | * Automatically smart slug generation, generate Chinese Pinyin slug, fore example: `标签` -> `biao-qian`, will add random value when there is a conflict. 65 | 66 | > Tag name normalizer:`$normalize_string = EstGroupe\Taggable\Util::tagName($name)`。 67 | 68 | ``` 69 | Tag::create(['标签名']); 70 | // name: 标签名 71 | // slug: biao-qian-ming 72 | 73 | Tag::create(['表签名']); 74 | // name: 表签名 75 | // slug: biao-qian-ming-3243 (3243 is random string) 76 | 77 | Tag::create(['标签 名']); 78 | // name: 标签-名 79 | // slug: biao-qian-ming 80 | 81 | Tag::create(['标签!名']); 82 | // name: 标签-名 83 | // slug: biao-qian-ming 84 | ``` 85 | 86 | ## Installation: 87 | 88 | ### Composer install package 89 | 90 | ```shell 91 | composer require estgroupe/laravel-taggable "5.1.*" 92 | ``` 93 | 94 | ### Config and Migration 95 | 96 | Change `providers` array at `config/app.php`: 97 | 98 | ```php 99 | 'providers' => array( 100 | \EstGroupe\Taggable\Providers\TaggingServiceProvider::class, 101 | ); 102 | ``` 103 | ```bash 104 | php artisan vendor:publish --provider="EstGroupe\Taggable\Providers\TaggingServiceProvider" 105 | php artisan migrate 106 | ``` 107 | 108 | > Please take a close look at file: `config/taggable.php` 109 | 110 | ### Create your own Tag.php 111 | 112 | It's optional but suggested to use your own `Tag` Model: 113 | 114 | ```php 115 | '\App\Models\Tag', 129 | ``` 130 | 131 | ### Adding Taggable Trait 132 | 133 | ```php 134 | is_tagged 151 | 152 | // `yes` 153 | $article->tag('Tag1'); 154 | $article->is_tagged; 155 | 156 | // `no` 157 | $article->unTag(); 158 | $article->is_tagged 159 | 160 | // This is fast 161 | $taggedArticles = Article::where('is_tagged', 'yes')->get() 162 | ``` 163 | 164 | First modify `config/taggable.php`: 165 | 166 | ```php 167 | 'is_tagged_label_enable' => true, 168 | ``` 169 | 170 | Add `is_tagged` filed to you model Migration file: 171 | 172 | ```php 173 | increments('id'); 184 | ... 185 | // Add this line 186 | $table->enum('is_tagged', array('yes', 'no'))->default('no'); 187 | ... 188 | $table->timestamps(); 189 | }); 190 | } 191 | } 192 | ``` 193 | 194 | ## `Suggesting` tags 195 | 196 | Suggesting is a small little feature you could use if you wanted to have "suggested" tags that stand out. 197 | 198 | There is not much to it. You simply set the 'suggest' field in the database to true 199 | 200 | ```php 201 | $tag = EstGroupe\Taggable\Model\Tag::where('slug', '=', 'blog')->first(); 202 | $tag->suggest = true; 203 | $tag->save(); 204 | ``` 205 | 206 | And then you can fetch a list of suggested tags when you need it. 207 | 208 | ```php 209 | $suggestedTags = EstGroupe\Taggable\Model\Tag::suggested()->get(); 210 | ``` 211 | 212 | ### Rewrite Util class? 213 | How do I override the Util class? 214 | ============ 215 | 216 | You'll need to create your own service provider. It should look something like this. 217 | 218 | ```php 219 | namespace My\Project\Providers; 220 | 221 | use EstGroupe\Taggable\Providers\TaggingServiceProvider as ServiceProvider; 222 | use EstGroupe\Taggable\Contracts\TaggingUtility; 223 | 224 | class TaggingServiceProvider extends ServiceProvider { 225 | 226 | /** 227 | * Register the service provider. 228 | * 229 | * @return void 230 | */ 231 | public function register() 232 | { 233 | $this->app->singleton(TaggingUtility::class, function () { 234 | return new MyNewUtilClass; 235 | }); 236 | } 237 | 238 | } 239 | ``` 240 | 241 | > Notice: Where `MyNewUtilClass` is a class you have written. Your new Util class obviously needs to implement the `EstGroupe\Taggable\Contracts\TaggingUtility` interface. 242 | 243 | ## Usage 244 | 245 | ```php 246 | $article = Article::with('tags')->first(); // eager load 247 | 248 | // Get all the article tagged tags 249 | foreach($article->tags as $tag) { 250 | echo $tag->name . ' with url slug of ' . $tag->slug; 251 | } 252 | 253 | // Tag some tag/tags 254 | $article->tag('Gardening'); // attach the tag 255 | $article->tag('Gardening, Floral'); // attach the tag 256 | $article->tag(['Gardening', 'Floral']); // attach the tag 257 | $article->tag('Gardening', 'Floral'); // attach the tag 258 | 259 | // Using tag_id batch tag 260 | $article->tagWithTagIds([1,2,3]); 261 | 262 | // Remove tags 263 | $article->untag('Cooking'); // remove Cooking tag 264 | $article->untag(); // remove all tags 265 | 266 | // Retag 267 | $article->retag(['Fruit', 'Fish']); // delete current tags and save new tags 268 | $article->retag('Fruit', 'Fish'); 269 | $article->retag('Fruit, Fish'); 270 | 271 | $tagged = $article->tagged; // return Collection of rows tagged to article 272 | $tags = $article->tags; // return Collection the actual tags (is slower than using tagged) 273 | 274 | // Get array of related tag names 275 | $article->tagNames(); 276 | 277 | // Fetch articles with any tag listed 278 | Article::withAnyTag('Gardening, Cooking')->get(); 279 | Article::withAnyTag(['Gardening','Cooking'])->get(); // different syntax, same result as above 280 | Article::withAnyTag('Gardening','Cooking')->get(); // different syntax, same result as above 281 | 282 | // Only fetch articles with all the tags 283 | Article::withAllTags('Gardening, Cooking')->get(); 284 | Article::withAllTags(['Gardening', 'Cooking'])->get(); 285 | Article::withAllTags('Gardening', 'Cooking')->get(); 286 | 287 | // Return all tags used more than twice 288 | EstGroupe\Taggable\Model\Tag::where('count', '>', 2)->get(); 289 | 290 | // Return collection of all existing tags on any articles 291 | Article::existingTags(); 292 | ``` 293 | 294 | `EstGroupe\Taggable\Model\Tag` has the following functions: 295 | 296 | ```php 297 | 298 | // By tag slug 299 | Tag::byTagSlug('biao-qian-ming')->first(); 300 | 301 | // By tag name 302 | Tag::byTagName('tag1')->first(); 303 | 304 | // Using names 305 | Tag::byTagNames(['tag1', 'tag12', 'tag13'])->first(); 306 | 307 | // Using Tag ids array 308 | Tag::byTagIds([1,2,3])->first(); 309 | 310 | // Using name to get tag ids array 311 | $ids = Tag::idsByNames(['标签名', '标签2', '标签3'])->all(); 312 | // [1,2,3] 313 | 314 | ``` 315 | 316 | ## Tagging events 317 | 318 | `Taggable` trait offer you two events: 319 | 320 | ```php 321 | EstGroupe\Taggable\Events\TagAdded; 322 | 323 | EstGroupe\Taggable\Events\TagRemoved; 324 | ``` 325 | 326 | You can listen to it as you want: 327 | 328 | ```php 329 | \Event::listen(EstGroupe\Taggable\Events\TagAdded::class, function($article){ 330 | \Log::debug($article->title . ' was tagged'); 331 | }); 332 | ``` 333 | 334 | ## Unit testing 335 | 336 | Common usage are tested at `tests/CommonUsageTest.php` file. 337 | 338 | Running test: 339 | 340 | ``` 341 | composer install 342 | vendor/bin/phpunit --verbose 343 | ``` 344 | 345 | ## Thanks 346 | 347 | - Special Thanks to: Robert Conner - http://smartersoftware.net 348 | - [overtrue/pinyin](https://github.com/overtrue/pinyin) 349 | - [etrepat/baum](https://github.com/etrepat/baum) 350 | - Made with love by The EST Group - http://estgroupe.com/ 351 | 352 | 353 | -------------------------------------------------------------------------------- /src/Contracts/TaggableContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * Copyright (C) 2015 Robert Conner 9 | */ 10 | interface TaggingUtility 11 | { 12 | /** 13 | * Converts input into array 14 | * 15 | * @param $tagName string or array 16 | * @return array 17 | */ 18 | public function makeTagArray($tagNames); 19 | 20 | /** 21 | * Create a web friendly URL slug from a string. 22 | * 23 | * Although supported, transliteration is discouraged because 24 | * 1) most web browsers support UTF-8 characters in URLs 25 | * 2) transliteration causes a loss of information 26 | * 27 | * @author Sean Murphy 28 | * 29 | * @param string $str 30 | * @return string 31 | */ 32 | public static function slug($str); 33 | public static function tagName($str); 34 | 35 | /** 36 | * Private! Please do not call this function directly, just let the Tag library use it. 37 | * Increment count of tag by one. This function will create tag record if it does not exist. 38 | * 39 | * @param Tag Object $tag 40 | */ 41 | public function incrementCount($tag, $count); 42 | 43 | /** 44 | * Private! Please do not call this function directly, let the Tag library use it. 45 | * Decrement count of tag by one. This function will create tag record if it does not exist. 46 | * 47 | * @param Tag Object $tag 48 | */ 49 | public function decrementCount($tag, $count); 50 | 51 | /** 52 | * Look at the tags table and delete any tags that are no londer in use by any taggable database rows. 53 | * Does not delete tags where 'suggest' is true 54 | * 55 | * @return int 56 | */ 57 | public function deleteUnusedTags(); 58 | 59 | /** 60 | * Return string with full namespace of the Tag model 61 | * 62 | * @return string 63 | */ 64 | public function tagModelString(); 65 | 66 | public function normalizeTagName($string); 67 | public function normalizeAndUniqueSlug($string); 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Events/TagAdded.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Events/TagRemoved.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Model/Tag.php: -------------------------------------------------------------------------------- 1 | table = config('taggable.tags_table_name'); 25 | 26 | parent::__construct($attributes); 27 | 28 | if(function_exists('config') && $connection = config('taggable.connection')) { 29 | $this->connection = $connection; 30 | } 31 | 32 | $this->taggingUtility = app(TaggingUtility::class); 33 | } 34 | 35 | public function isUserUpdateSlug($options) 36 | { 37 | // If slug in dirty, it mean user manual setting `$tag->slug = 'foo'` 38 | if (in_array('slug', array_keys($this->getDirty()))) { 39 | return true; 40 | } 41 | 42 | // If slug in $options array, it mean user use `$tag->save(['slug' => 'foo'])` to update slug 43 | if (in_array('slug', array_keys($options))) { 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | /** 50 | * (non-PHPdoc) 51 | * @see \Illuminate\Database\Eloquent\Model::save() 52 | */ 53 | public function save(array $options = array()) 54 | { 55 | $validator = app('validator')->make( 56 | array('name' => $this->name), 57 | array('name' => 'required|min:1') 58 | ); 59 | 60 | if($validator->passes()) { 61 | 62 | if (!$this->isUserUpdateSlug($options)) { 63 | // If user has been set slug,it do not need set slug by automatically 64 | $this->slug = $this->taggingUtility->normalizeAndUniqueSlug($this->name); 65 | } 66 | 67 | // $this->name = $this->taggingUtility->normalizeTagName($this->name); 68 | return parent::save($options); 69 | } else { 70 | throw new \Exception('Tag Name is required'); 71 | } 72 | } 73 | 74 | /** 75 | * Get suggested tags 76 | */ 77 | public function scopeSuggested($query) 78 | { 79 | return $query->where('suggest', true); 80 | } 81 | 82 | /** 83 | * Set the name of the tag : $tag->name = 'myname'; 84 | * 85 | * @param string $value 86 | */ 87 | public function setNameAttribute($value) 88 | { 89 | $this->attributes['name'] = $this->taggingUtility->normalizeTagName($value); 90 | } 91 | 92 | /** 93 | * Look at the tags table and delete any tags that are no londer in use by any taggable database rows. 94 | * Does not delete tags where 'suggest'value is true 95 | * 96 | * @return int 97 | */ 98 | public static function deleteUnused() 99 | { 100 | return (new static)->newQuery() 101 | ->where('count', '=', 0) 102 | ->where('suggest', false) 103 | ->delete(); 104 | } 105 | 106 | 107 | // Get one Tag item by tag name 108 | public function scopeByTagName($query, $tag_name) 109 | { 110 | // mormalize string 111 | $tag_name = app(TaggingUtility::class)->normalizeTagName(trim($tag_name)); 112 | return $query->where('name', $tag_name); 113 | } 114 | 115 | public function scopeByTagSlug($query, $tag_slug) 116 | { 117 | return $query->where('slug', $tag_slug); 118 | } 119 | 120 | // Get Tag collection by tag name array 121 | public function scopeByTagNames($query, $tag_names) 122 | { 123 | $normalize_tag_names = []; 124 | foreach ($tag_names as $tag_name) { 125 | // mormalize string 126 | $normalize_tag_names[] = app(TaggingUtility::class)->normalizeTagName(trim($tag_name)); 127 | } 128 | return $query->whereIn('name', $normalize_tag_names); 129 | } 130 | 131 | // Get Tag collection by tag id array 132 | public function scopeByTagIds($query, $tag_ids) 133 | { 134 | return $query->whereIn('id', $tag_ids); 135 | } 136 | 137 | // Get tag ids tag name array 138 | public function scopeIdsByNames($query, $tagNames) 139 | { 140 | return $query->whereIn('name', $tagNames)->lists('id'); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/Model/Tagged.php: -------------------------------------------------------------------------------- 1 | table = config('taggable.taggables_table_name'); 19 | 20 | parent::__construct($attributes); 21 | 22 | $this->taggingUtility = app(TaggingUtility::class); 23 | } 24 | 25 | /** 26 | * Morph to the tag 27 | * 28 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo 29 | */ 30 | public function taggable() 31 | { 32 | return $this->morphTo(); 33 | } 34 | 35 | /** 36 | * Get instance of tag linked to the tagged value 37 | * 38 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 39 | */ 40 | public function tag() 41 | { 42 | $model = $this->taggingUtility->tagModelString(); 43 | return $this->belongsTo($model, 'tag_slug', 'slug'); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/Providers/LumenTaggingServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(TaggingUtility::class, function () { 29 | return new Util; 30 | }); 31 | } 32 | 33 | /** 34 | * (non-PHPdoc) 35 | * @see \Illuminate\Support\ServiceProvider::provides() 36 | */ 37 | public function provides() 38 | { 39 | return [TaggingUtility::class]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Providers/TaggingServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../../config/taggable.php' => config_path('taggable.php') 21 | ], 'config'); 22 | 23 | $this->publishes([ 24 | __DIR__.'/../../migrations/' => database_path('migrations') 25 | ], 'migrations'); 26 | } 27 | 28 | /** 29 | * Register the service provider. 30 | * 31 | * @return void 32 | */ 33 | public function register() 34 | { 35 | $this->app->singleton(TaggingUtility::class, function () { 36 | return new Util; 37 | }); 38 | } 39 | 40 | /** 41 | * (non-PHPdoc) 42 | * @see \Illuminate\Support\ServiceProvider::provides() 43 | */ 44 | public function provides() 45 | { 46 | return [TaggingUtility::class]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Taggable.php: -------------------------------------------------------------------------------- 1 | untag(); 45 | }); 46 | } 47 | 48 | static::saved(function ($model) { 49 | $model->autoTagPostSave(); 50 | }); 51 | 52 | static::$taggingUtility = app(TaggingUtility::class); 53 | } 54 | 55 | /** 56 | * Return collection of tagged rows related to the tagged model 57 | * 58 | * @return Illuminate\Database\Eloquent\Collection 59 | */ 60 | public function tagged() 61 | { 62 | return $this->morphMany('EstGroupe\Taggable\Model\Tagged', 'taggable')->with('tag'); 63 | } 64 | 65 | public function tags() 66 | { 67 | return $this->morphToMany(static::$taggingUtility->tagModelString(), 'taggable'); 68 | } 69 | 70 | /** 71 | * Set the tag names via attribute, example $model->tag_names = 'foo, bar'; 72 | * 73 | * @param string $value 74 | */ 75 | public function getTagNamesAttribute($value) 76 | { 77 | return implode(', ', $this->tagNames()); 78 | } 79 | 80 | /** 81 | * Perform the action of tagging the model with the given string 82 | * 83 | * @param $tagName string or array 84 | */ 85 | public function tag($tagNames) 86 | { 87 | if(!is_array($tagNames)) { 88 | $tagNames = func_get_args(); 89 | } 90 | $tagNames = static::$taggingUtility->makeTagArray($tagNames); 91 | 92 | foreach($tagNames as $tagName) { 93 | $this->addTag($tagName); 94 | } 95 | } 96 | 97 | /** 98 | * Return array of the tag names related to the current model 99 | * 100 | * @return array 101 | */ 102 | public function tagNames() 103 | { 104 | return $this->tags()->lists('name')->all(); 105 | } 106 | 107 | /** 108 | * Return array of the tag slugs related to the current model 109 | * 110 | * @return array 111 | */ 112 | public function tagSlugs() 113 | { 114 | return $this->tags()->lists('slug')->all(); 115 | } 116 | 117 | /** 118 | * Remove the tag from this model 119 | * 120 | * @param $tagName string or array (or null to remove all tags) 121 | */ 122 | public function untag($tagNames=null) 123 | { 124 | if(is_null($tagNames)) { 125 | $tagNames = $this->tagNames(); 126 | } 127 | 128 | $tagNames = static::$taggingUtility->makeTagArray($tagNames); 129 | 130 | foreach($tagNames as $tagName) { 131 | $this->removeTag($tagName); 132 | } 133 | 134 | if(static::shouldDeleteUnused()) { 135 | static::$taggingUtility->deleteUnusedTags(); 136 | } 137 | } 138 | 139 | /** 140 | * Replace the tags from this model 141 | * 142 | * @param $tagName string or array 143 | */ 144 | public function retag($tagNames) 145 | { 146 | if(!is_array($tagNames)) { 147 | $tagNames = func_get_args(); 148 | } 149 | $tagNames = static::$taggingUtility->makeTagArray($tagNames); 150 | $currentTagNames = $this->tagNames(); 151 | 152 | $deletions = array_diff($currentTagNames, $tagNames); 153 | $additions = array_diff($tagNames, $currentTagNames); 154 | 155 | $this->untag($deletions); 156 | 157 | foreach($additions as $tagName) { 158 | $this->addTag($tagName); 159 | } 160 | } 161 | 162 | /** 163 | * Filter model to subset with the given tags 164 | * 165 | * @param $tagNames array|string 166 | */ 167 | public function scopeWithAllTags($query, $tagNames) 168 | { 169 | if(!is_array($tagNames)) { 170 | $tagNames = func_get_args(); 171 | array_shift($tagNames); 172 | } 173 | $tagNames = static::$taggingUtility->makeTagArray($tagNames); 174 | 175 | $model = static::$taggingUtility->tagModelString(); 176 | $tagids = $model::byTagNames($tagNames)->lists('id')->all(); 177 | 178 | $className = $query->getModel()->getMorphClass(); 179 | $primaryKey = $this->getKeyName(); 180 | 181 | $tagid_count = count($tagids); 182 | if ($tagid_count > 0) { 183 | $ids = Tagged::where('taggable_type', $className) 184 | ->whereIn('tag_id', $tagids) 185 | ->whereRaw('`tag_id` in (' .implode(',', $tagids). ') group by taggable_id having count(taggable_id) ='.$tagid_count) 186 | ->lists('taggable_id'); 187 | 188 | $query->whereIn($this->getTable().'.'.$primaryKey, $ids); 189 | } 190 | 191 | return $query; 192 | } 193 | 194 | /** 195 | * Filter model to subset with the given tags 196 | * 197 | * @param $tagNames array|string 198 | */ 199 | public function scopeWithAnyTag($query, $tagNames) 200 | { 201 | if(!is_array($tagNames)) { 202 | $tagNames = func_get_args(); 203 | array_shift($tagNames); 204 | } 205 | $tagNames = static::$taggingUtility->makeTagArray($tagNames); 206 | 207 | $model = static::$taggingUtility->tagModelString(); 208 | $tagids = $model::byTagNames($tagNames)->lists('id')->all(); 209 | 210 | $className = $query->getModel()->getMorphClass(); 211 | $primaryKey = $this->getKeyName(); 212 | 213 | $tags = Tagged::whereIn('tag_id', $tagids) 214 | ->where('taggable_type', $className) 215 | ->lists('taggable_id'); 216 | 217 | return $query->whereIn($this->getTable().'.'.$primaryKey, $tags); 218 | } 219 | 220 | /** 221 | * Adds a single tag 222 | * 223 | * @param $tagName string 224 | */ 225 | private function addTag($tagName) 226 | { 227 | $model = static::$taggingUtility->tagModelString(); 228 | $tag = $model::byTagName($tagName)->first(); 229 | 230 | if($tag) { 231 | // If tag is exists, do not create 232 | $count = $this->tagged()->where('tag_id', '=', $tag->id)->take(1)->count(); 233 | if($count >= 1) { 234 | return; 235 | } else { 236 | $this->tags()->attach($tag->id); 237 | } 238 | } else { 239 | // If tag is not exists, create tag and attach to object 240 | $tag = new $model; 241 | $tag->name = $tagName; 242 | $tag->save(); 243 | 244 | $this->tags()->attach($tag->id); 245 | } 246 | static::$taggingUtility->incrementCount($tag, 1); 247 | 248 | if (config('taggable.is_tagged_label_enable') 249 | && $this->is_tagged != 'yes' 250 | ) { 251 | $this->is_tagged = 'yes'; 252 | $this->save(); 253 | } 254 | 255 | unset($this->relations['tagged']); 256 | event(new TagAdded($this)); 257 | } 258 | 259 | /** 260 | * Removes a single tag 261 | * 262 | * @param $tagName string 263 | */ 264 | private function removeTag($tagName) 265 | { 266 | $tag = $this->tags()->byTagName($tagName)->first(); 267 | 268 | if ($tag) { 269 | $this->tags()->detach($tag->id); 270 | static::$taggingUtility->decrementCount($tag, 1); 271 | } 272 | 273 | if (config('taggable.is_tagged_label_enable') 274 | && $this->is_tagged != 'no' 275 | && $this->tags()->count() <= 0 276 | ) { 277 | $this->is_tagged = 'no'; 278 | $this->save(); 279 | } 280 | 281 | unset($this->relations['tagged']); 282 | event(new TagRemoved($this)); 283 | } 284 | 285 | /** 286 | * Return an array of all of the tags that are in use by this model 287 | * 288 | * @return Collection 289 | */ 290 | public static function existingTags() 291 | { 292 | $tags_table_name = config('taggable.tags_table_name'); 293 | 294 | return Tagged::distinct() 295 | ->join($tags_table_name, 'tag_id', '=', $tags_table_name.'.id') 296 | ->where('taggable_type', '=', (new static)->getMorphClass()) 297 | ->orderBy('tag_id', 'ASC') 298 | ->get(array($tags_table_name.'.slug as slug', $tags_table_name.'.name as name', $tags_table_name.'.count as count')); 299 | } 300 | 301 | /** 302 | * Should untag on delete 303 | */ 304 | public static function untagOnDelete() 305 | { 306 | return isset(static::$untagOnDelete) 307 | ? static::$untagOnDelete 308 | : config('taggable.untag_on_delete'); 309 | } 310 | 311 | /** 312 | * Delete tags that are not used anymore 313 | */ 314 | public static function shouldDeleteUnused() 315 | { 316 | return config('taggable.delete_unused_tags'); 317 | } 318 | 319 | /** 320 | * Set tag names to be set on save 321 | * 322 | * @param mixed $value Data for retag 323 | * 324 | * @return void 325 | * 326 | * @access public 327 | */ 328 | public function setTagNamesAttribute($value) 329 | { 330 | $this->autoTagTmp = $value; 331 | $this->autoTagSet = true; 332 | } 333 | 334 | /** 335 | * AutoTag post-save hook 336 | * 337 | * Tags model based on data stored in tmp property, or untags if manually 338 | * set to falsey value 339 | * 340 | * @return void 341 | * 342 | * @access public 343 | */ 344 | public function autoTagPostSave() 345 | { 346 | if ($this->autoTagSet) { 347 | if ($this->autoTagTmp) { 348 | $this->retag($this->autoTagTmp); 349 | } else { 350 | $this->untag(); 351 | } 352 | } 353 | } 354 | 355 | /** 356 | * by @CJ 357 | * Sync tags with tag_id array 358 | * 359 | * @param $tag_ids tag_id array 360 | */ 361 | public function tagWithTagIds($tag_ids = []) 362 | { 363 | if (count($tag_ids) <= 0) { 364 | return; 365 | } 366 | 367 | $model = static::$taggingUtility->tagModelString(); 368 | $tag_names = $model::byTagIds($tag_ids)->lists('name')->all(); 369 | 370 | $this->retag($tag_names); 371 | } 372 | 373 | 374 | } 375 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * Copyright (C) 2014 Robert Conner 13 | */ 14 | class Util implements TaggingUtility 15 | { 16 | /** 17 | * Converts input into array 18 | * 19 | * @param $tagName string or array 20 | * @return array 21 | */ 22 | public function makeTagArray($tagNames) 23 | { 24 | if(is_array($tagNames) && count($tagNames) == 1) { 25 | $tagNames = reset($tagNames); 26 | } 27 | 28 | if(is_string($tagNames)) { 29 | $tagNames = explode(',', $tagNames); 30 | } elseif(!is_array($tagNames)) { 31 | $tagNames = array(null); 32 | } 33 | 34 | $tagNames = array_map('trim', $tagNames); 35 | 36 | return array_values($tagNames); 37 | } 38 | 39 | /** 40 | * Create a web friendly URL slug from a string. 41 | * 42 | * Although supported, transliteration is discouraged because 43 | * 1) most web browsers support UTF-8 characters in URLs 44 | * 2) transliteration causes a loss of information 45 | * 46 | * @author Sean Murphy 47 | * 48 | * @param string $str 49 | * @return string 50 | */ 51 | public static function slug($str) 52 | { 53 | // Make sure string is in UTF-8 and strip invalid UTF-8 characters 54 | $str = mb_convert_encoding((string)$str, 'UTF-8'); 55 | 56 | $options = array( 57 | 'delimiter' => '-', 58 | 'limit' => '255', 59 | 'lowercase' => true, 60 | 'replacements' => array(), 61 | 'transliterate' => true, 62 | ); 63 | 64 | $char_map = array( 65 | // Latin 66 | 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C', 67 | 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 68 | 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O', 69 | 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH', 70 | 'ß' => 'ss', 71 | 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 72 | 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 73 | 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o', 74 | 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th', 75 | 'ÿ' => 'y', 76 | 77 | // Latin symbols 78 | '©' => '(c)', 79 | 80 | // Greek 81 | 'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8', 82 | 'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P', 83 | 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W', 84 | 'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I', 85 | 'Ϋ' => 'Y', 86 | 'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8', 87 | 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p', 88 | 'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w', 89 | 'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's', 90 | 'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i', 91 | 92 | // Turkish 93 | 'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G', 94 | 'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g', 95 | 96 | // Russian 97 | 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh', 98 | 'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', 99 | 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C', 100 | 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu', 101 | 'Я' => 'Ya', 102 | 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 103 | 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 104 | 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 105 | 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', 106 | 'я' => 'ya', 107 | 108 | // Ukrainian 109 | 'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G', 110 | 'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g', 111 | 112 | // Czech 113 | 'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U', 114 | 'Ž' => 'Z', 115 | 'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u', 116 | 'ž' => 'z', 117 | 118 | // Polish 119 | 'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z', 120 | 'Ż' => 'Z', 121 | 'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z', 122 | 'ż' => 'z', 123 | 124 | // Latvian 125 | 'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N', 126 | 'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z', 127 | 'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', 128 | 'š' => 's', 'ū' => 'u', 'ž' => 'z', 129 | 130 | //Romanian 131 | 'Ă' => 'A', 'ă' => 'a', 'Ș' => 'S', 'ș' => 's', 'Ț' => 'T', 'ț' => 't' 132 | ); 133 | 134 | // Make custom replacements 135 | $str = preg_replace(array_keys($options['replacements']), $options['replacements'], $str); 136 | 137 | // Transliterate characters to ASCII 138 | if ($options['transliterate']) { 139 | $str = str_replace(array_keys($char_map), $char_map, $str); 140 | } 141 | // Replace non-alphanumeric characters with our delimiter 142 | $str = preg_replace('/[^\p{L}\p{Nd}]+/u', $options['delimiter'], $str); 143 | 144 | // Remove duplicate delimiters 145 | $str = preg_replace('/(' . preg_quote($options['delimiter'], '/') . '){2,}/', '$1', $str); 146 | 147 | // Truncate slug to max. characters 148 | $str = mb_substr($str, 0, ($options['limit'] ? $options['limit'] : mb_strlen($str, 'UTF-8')), 'UTF-8'); 149 | 150 | // Remove delimiter from ends 151 | $str = trim($str, $options['delimiter']); 152 | 153 | // Normalizer tag name 154 | $str = static::tagName($str); 155 | 156 | $str = app()->make(Pinyin::class)->permlink($str); 157 | 158 | return $options['lowercase'] ? mb_strtolower($str, 'UTF-8') : $str; 159 | } 160 | 161 | public static function tagName($string) 162 | { 163 | // from http://stackoverflow.com/a/8483919/689832 164 | $string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens. 165 | return preg_replace("/[^\p{L}\p{N}]/u", '-', $string); // Removes special chars. 166 | } 167 | 168 | /** 169 | * Private! Please do not call this function directly, just let the Tag library use it. 170 | * Increment count of tag by one. This function will create tag record if it does not exist. 171 | * 172 | * @param Tag Object $tag 173 | */ 174 | public function incrementCount($tag, $count) 175 | { 176 | if($count <= 0) { return; } 177 | 178 | $tag->count = $tag->count + $count; 179 | $tag->save(); 180 | } 181 | 182 | /** 183 | * Private! Please do not call this function directly, let the Tag library use it. 184 | * Decrement count of tag by one. This function will create tag record if it does not exist. 185 | * 186 | * @param Tag Object $tag 187 | */ 188 | public function decrementCount($tag, $count) 189 | { 190 | if($count <= 0) { return; } 191 | 192 | $tag->count = $tag->count - $count; 193 | if($tag->count < 0) { 194 | $tag->count = 0; 195 | $model = $this->tagModelString(); 196 | \Log::warning("The '.$model.' count for `$tag->name` was a negative number. This probably means your data got corrupted. Please assess your code and report an issue if you find one."); 197 | } 198 | $tag->save(); 199 | } 200 | 201 | /** 202 | * Look at the tags table and delete any tags that are no londer in use by any taggable database rows. 203 | * Does not delete tags where 'suggest' is true 204 | * 205 | * @return int 206 | */ 207 | public function deleteUnusedTags() 208 | { 209 | $model = $this->tagModelString(); 210 | return $model::deleteUnused(); 211 | } 212 | 213 | /** 214 | * @return string 215 | */ 216 | public function tagModelString() 217 | { 218 | return config('taggable.tag_model', '\EstGroupe\Taggable\Model\Tag'); 219 | } 220 | 221 | // Check DB Slug Dulplication 222 | public function uniqueSlug($slug_str, $tag_name) 223 | { 224 | $model = $this->tagModelString(); 225 | if (!empty($slug_str) && $tag = $model::where('slug', $slug_str)->first()) { 226 | // 只有当 slug 一样但 tagname 不一样的情况下,才自动设置随机 slug 后缀 227 | if ($tag->name != $this->normalizeTagName($tag_name)) { 228 | $slug_str .= '-'. mt_rand(1000, 9999); 229 | } 230 | } 231 | return $slug_str; 232 | } 233 | 234 | // Should be call before insert into database 235 | public function normalizeAndUniqueSlug($tag_name) 236 | { 237 | $normalizer = config('taggable.normalizer', static::class.'::slug'); 238 | // Normalize 239 | $slug_string = call_user_func($normalizer, $tag_name); 240 | // Make sure slug is unique 241 | return $this->uniqueSlug($slug_string, $tag_name); 242 | } 243 | 244 | // Should be call before insert into database 245 | public function normalizeTagName($string) 246 | { 247 | $string = call_user_func('\Illuminate\Support\Str::title', $string); 248 | $normalizer = config('taggable.displayer', static::class.'::tagName'); 249 | // Normalize 250 | return call_user_func($normalizer, $string); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /tests/CommonUsageTest.php: -------------------------------------------------------------------------------- 1 | artisan('migrate', [ 15 | '--database' => 'testbench', 16 | '--realpath' => realpath(__DIR__.'/../migrations'), 17 | ]); 18 | } 19 | 20 | protected function getEnvironmentSetUp($app) 21 | { 22 | $app['config']->set('taggable.tags_table_name', 'tags'); 23 | $app['config']->set('taggable.taggables_table_name', 'taggables'); 24 | 25 | $app['config']->set('database.default', 'testbench'); 26 | $app['config']->set('database.connections.testbench', [ 27 | 'driver' => 'sqlite', 28 | 'database' => ':memory:', 29 | 'prefix' => '', 30 | ]); 31 | 32 | \Schema::create('books', function ($table) { 33 | $table->increments('id'); 34 | $table->string('name'); 35 | // is_tagged_label_enable == true 36 | $table->enum('is_tagged', array('yes', 'no'))->default('no'); 37 | $table->timestamps(); 38 | }); 39 | } 40 | 41 | public function tearDown() 42 | { 43 | \Schema::drop('books'); 44 | } 45 | 46 | public function test_tag_call() 47 | { 48 | $stub = Stub::create(['name'=>123]); 49 | 50 | $stub->tag('test123'); 51 | $stub->tag('456'); 52 | $stub->tag('third'); 53 | $this->assertSame(['Test123', '456', 'Third'], $stub->tagNames()); 54 | } 55 | 56 | public function test_untag_call() 57 | { 58 | $stub = Stub::create(['name'=>'Stub']); 59 | 60 | $stub->tag('one'); 61 | $stub->tag('two'); 62 | $stub->tag('three'); 63 | 64 | $stub->untag('two'); 65 | 66 | $this->assertArraysEqual(['Three', 'One'], $stub->tagNames()); 67 | 68 | $stub->untag('ONE'); 69 | $this->assertArraysEqual(['Three'], $stub->tagNames()); 70 | } 71 | 72 | public function test_retag() 73 | { 74 | $stub = Stub::create(['name'=>123]); 75 | 76 | $stub->tag('first'); 77 | $stub->tag('second'); 78 | 79 | $stub->retag('foo, bar, another'); 80 | $this->assertEquals(['Foo', 'Bar', 'Another'], $stub->tagNames()); 81 | } 82 | 83 | public function test_unique() 84 | { 85 | $stub = Stub::create(['name'=>123]); 86 | 87 | $stub->tag('first'); 88 | $stub->tag('first'); 89 | $stub->tag('second'); 90 | $stub->tag('bar'); 91 | 92 | $stub->retag('first, foo, bar, another'); 93 | $this->assertEquals(['First', 'Foo', 'Bar', 'Another'], $stub->tagNames()); 94 | } 95 | 96 | public function test_tag_names_attribute() 97 | { 98 | $stub = Stub::create(['name'=>123, 'tag_names'=>'foo, bar']); 99 | 100 | $stub->save(); 101 | 102 | $this->assertEquals(['Foo', 'Bar'], $stub->tagNames()); 103 | } 104 | 105 | // Test Counter 106 | public function test_counter() 107 | { 108 | $stub1 = Stub::create(['name'=>123]); 109 | $stub2 = Stub::create(['name'=>123]); 110 | $stub3 = Stub::create(['name'=>123]); 111 | $stub4 = Stub::create(['name'=>123]); 112 | 113 | $stub1->tag('foo'); 114 | 115 | $stub2->retag('foo', 'bar'); 116 | 117 | $stub3->tag('foo'); 118 | $stub3->untag('foo'); 119 | $stub3->retag('foo'); 120 | 121 | $tag = $stub1->tags()->first(); 122 | $stub4->tagWithTagIds([$tag->id]); 123 | 124 | $this->assertEquals(4, $stub1->tags()->first()->count); 125 | } 126 | 127 | // Test is_tagged 128 | public function test_is_tagged_label() 129 | { 130 | config(['taggable.is_tagged_label_enable' => true]); 131 | 132 | $stub = Stub::create(['name'=>123]); 133 | 134 | $stub->tag('foo'); 135 | $this->assertEquals('yes', $stub->is_tagged); 136 | 137 | $stub->untag('foo'); 138 | $this->assertEquals('no', $stub->is_tagged); 139 | 140 | $stub->retag('foo'); 141 | $this->assertEquals('yes', $stub->is_tagged); 142 | 143 | $stub->untag(); // remove all tags 144 | $this->assertEquals('no', $stub->is_tagged); 145 | } 146 | 147 | // Test existingTags 148 | public function test_existing_tags() 149 | { 150 | $stub1 = Stub::create(['name'=>123]); 151 | $stub2 = Stub::create(['name'=>123]); 152 | 153 | $stub1->tag('foo'); 154 | $stub2->retag('bar'); 155 | 156 | $tag_names = Stub::existingTags()->lists('name')->all(); 157 | $this->assertEquals(['Foo', 'Bar'], $tag_names); 158 | } 159 | 160 | // Test Name with special character 161 | public function test_normalize_name() 162 | { 163 | $stub = Stub::create(['name'=>123]); 164 | $stub->tag('标签名'); 165 | $this->assertEquals('标签名', $stub->tags()->first()->name); 166 | $stub->untag(); 167 | 168 | $stub->tag('标签 名'); 169 | $this->assertEquals('标签-名', $stub->tags()->first()->name); 170 | $stub->untag(); 171 | 172 | $stub->tag('标签$名'); 173 | $this->assertEquals('标签-名', $stub->tags()->first()->name); 174 | $stub->untag(); 175 | 176 | $stub->tag('标签,名'); 177 | $this->assertEquals('标签-名', $stub->tags()->first()->name); 178 | $stub->untag(); 179 | 180 | $stub->tag('标签-名'); 181 | $this->assertEquals('标签-名', $stub->tags()->first()->name); 182 | $stub->untag(); 183 | 184 | $stub->tag('标签?名'); 185 | $this->assertEquals('标签-名', $stub->tags()->first()->name); 186 | $stub->untag(); 187 | 188 | $stub->tag('标签!名'); 189 | $this->assertEquals('标签-名', $stub->tags()->first()->name); 190 | $stub->untag(); 191 | } 192 | 193 | // Test Slug phoneticize conflict 194 | public function test_smart_slug() 195 | { 196 | $stub = Stub::create(['name'=>123]); 197 | 198 | $stub->tag('标签名'); 199 | $this->assertEquals('biao-qian-ming', $stub->tags()->first()->slug); 200 | $stub->untag(); 201 | 202 | $stub->tag('表签名'); 203 | $this->assertNotEquals('biao-qian-ming', $stub->tags()->first()->slug); 204 | } 205 | 206 | // Test scopeWithAllTags scopeWithAnyTag 207 | public function test_scope_with_tags() 208 | { 209 | $stub1 = Stub::create(['name'=>'stub1']); 210 | $stub2 = Stub::create(['name'=>'stub2']); 211 | 212 | $stub1->tag('tag1', 'tag2'); 213 | $stub2->tag('tag2', 'tag3'); 214 | 215 | $result = Stub::withAllTags('tag1', 'tag2'); 216 | 217 | $this->assertEquals(1, $result->count()); 218 | $this->assertEquals('stub1', $result->first()->name); 219 | 220 | $result2 = Stub::withAnyTag('tag100', 'tag2'); 221 | $this->assertEquals(2, $result2->count()); 222 | $this->assertEquals(['stub1', 'stub2'], $result2->lists('name')->all()); 223 | } 224 | } 225 | 226 | class Stub extends Eloquent 227 | { 228 | use Taggable; 229 | 230 | protected $connection = 'testbench'; 231 | 232 | public $table = 'books'; 233 | } 234 | -------------------------------------------------------------------------------- /tests/TagTest.php: -------------------------------------------------------------------------------- 1 | artisan('migrate', [ 16 | '--database' => 'testbench', 17 | '--realpath' => realpath(__DIR__.'/../migrations'), 18 | ]); 19 | } 20 | 21 | protected function getEnvironmentSetUp($app) 22 | { 23 | $app['config']->set('taggable.tags_table_name', 'tags'); 24 | $app['config']->set('taggable.taggables_table_name', 'taggables'); 25 | 26 | $app['config']->set('database.default', 'testbench'); 27 | $app['config']->set('database.connections.testbench', [ 28 | 'driver' => 'sqlite', 29 | 'database' => ':memory:', 30 | 'prefix' => '', 31 | ]); 32 | } 33 | 34 | public function test_instantiation() 35 | { 36 | $tag = new Tag(); 37 | 38 | $this->assertInternalType('object', $tag); 39 | } 40 | 41 | public function test_save() 42 | { 43 | $tag = $this->creat_test_tag(); 44 | $this->assertInternalType('object', $tag); 45 | } 46 | 47 | 48 | public function test_scopeSuggested() 49 | { 50 | $tag = $this->creat_test_tag(); 51 | $new_tag = Tag::suggested()->first(); 52 | $this->assertSame($this->testTagName, $new_tag->name); 53 | } 54 | 55 | public function test_deleteUnused() 56 | { 57 | $tag = new Tag(); 58 | $tag->name = $this->testTagName; 59 | $tag->count = 0; 60 | $tag->suggest = false; 61 | $tag->save(); 62 | 63 | Tag::deleteUnused(); 64 | 65 | $this->assertSame([], Tag::all()->toArray()); 66 | } 67 | 68 | public function test_scopeByTagName() 69 | { 70 | $tag = $this->creat_test_tag(); 71 | $new_tag = Tag::byTagName($this->testTagName)->first(); 72 | $this->assertSame($this->testTagName, $new_tag->name); 73 | } 74 | 75 | public function test_scopeByTagSlug() 76 | { 77 | $tag = $this->creat_test_tag(); 78 | $new_tag = Tag::byTagSlug(strtolower($this->testTagName))->first(); 79 | } 80 | 81 | public function test_scopeByTagNames() 82 | { 83 | $this->create_multiple_test_tag(); 84 | $tags = Tag::byTagNames($this->testTagNames)->get()->toArray(); 85 | $this->assertSame(4, count($tags)); 86 | } 87 | 88 | public function test_scopeByTagIds() 89 | { 90 | $this->create_multiple_test_tag(); 91 | $tags = Tag::byTagIds([1, 2, 3, 4])->get()->toArray(); 92 | $this->assertSame(4, count($tags)); 93 | } 94 | 95 | public function test_scopeIdsByNames() 96 | { 97 | $this->create_multiple_test_tag(); 98 | $tags = Tag::idsByNames($this->testTagNames)->all(); 99 | sort($tags); 100 | 101 | $this->assertSame(['1', '2', '3', '4'], $tags); 102 | } 103 | 104 | public function creat_test_tag($tagName = '') 105 | { 106 | $tag = new Tag(); 107 | $tag->name = $tagName ? $tagName : $this->testTagName; 108 | $tag->suggest = true; 109 | $tag->save(); 110 | return $tag; 111 | } 112 | 113 | public function create_multiple_test_tag() 114 | { 115 | foreach ($this->testTagNames as $k => $v) { 116 | $this->creat_test_tag($v); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertEquals(count($expected), count($actual), 'Failed to assert that two arrays have the same length.'); 29 | 30 | // sort arrays if order is irrelevant 31 | if (!$regard_order) { 32 | if ($check_keys) { 33 | $this->assertTrue(ksort($expected), 'Failed to sort array.'); 34 | $this->assertTrue(ksort($actual), 'Failed to sort array.'); 35 | } else { 36 | $this->assertTrue(sort($expected), 'Failed to sort array.'); 37 | $this->assertTrue(sort($actual), 'Failed to sort array.'); 38 | } 39 | } 40 | 41 | $this->assertEquals($expected, $actual); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /tests/UtilTest.php: -------------------------------------------------------------------------------- 1 | makeTagArray($tagStrings); 19 | $second = $util->makeTagArray(implode(', ', $tagStrings)); 20 | 21 | $this->assertEquals($tagStrings, $first); 22 | $this->assertEquals($tagStrings, $second); 23 | 24 | $result = $util->makeTagArray([1=>'tag1', 3=>'tag2']); 25 | $this->assertEquals(['tag1', 'tag2'], $result); 26 | 27 | $result = $util->makeTagArray([1=>'tag1']); 28 | $this->assertEquals(['tag1'], $result); 29 | } 30 | 31 | public function test_make_tag_array_single() 32 | { 33 | $util = new Util; 34 | $tagStrings = ['tag']; 35 | 36 | $result = $util->makeTagArray($tagStrings); 37 | 38 | $this->assertEquals($result, ['tag']); 39 | } 40 | 41 | public function test_make_tag_array_from_strings() 42 | { 43 | $util = new Util; 44 | 45 | $result = $util->makeTagArray('tag'); 46 | $this->assertEquals($result, ['tag']); 47 | 48 | $result = $util->makeTagArray('tag1,tag2'); 49 | $this->assertEquals($result, ['tag1', 'tag2']); 50 | 51 | $result = $util->makeTagArray('One, Two, Three'); 52 | $this->assertEquals($result, ['One', 'Two', 'Three']); 53 | } 54 | 55 | public function test_slug() 56 | { 57 | $util = new Util; 58 | $this->assertEquals('sugar-free', $util->slug('Sugar Free')); 59 | 60 | $str = 'ПЧÑ�Цщ'; 61 | $this->assertNotEquals($util->slug($str), $str); 62 | 63 | $str = 'quiénsí'; 64 | $this->assertNotEquals($util->slug($str), $str); 65 | 66 | $str = 'ČĢ'; 67 | $this->assertNotEquals($util->slug($str), $str); 68 | 69 | $str = 'same-slug'; 70 | $this->assertEquals($util->slug($str), $str); 71 | 72 | $str = '&=*!$&&,'; 73 | $this->assertNotEquals($util->slug($str), $str); 74 | } 75 | 76 | } --------------------------------------------------------------------------------