commit 5253dcdf290a329d1be324d7cd72bf923330e161 Author: Marcel Pociot Date: Mon Sep 3 13:23:13 2018 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb6265e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/.editorconfig export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51c6ece --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +build +composer.lock +docs +vendor +coverage \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..df16b68 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,19 @@ +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true + diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..f4d3cbc --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +preset: laravel + +disabled: + - single_class_element_per_statement diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4084157 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php + +php: + - 7.1 + - 7.2 + +env: + matrix: + - COMPOSER_FLAGS="--prefer-lowest" + - COMPOSER_FLAGS="" + +before_script: + - travis_retry composer self-update + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..26054cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `laravel-comments` will be documented in this file + +## 1.0.0 - 201X-XX-XX + +- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b4ae1c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..08a23cf --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Beyond Code GmbH + +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..9dc8e49 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Add comments to your Laravel application + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/beyondcode/laravel-comments.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-comments) +[![Build Status](https://img.shields.io/travis/beyondcode/laravel-comments/master.svg?style=flat-square)](https://travis-ci.org/beyondcode/laravel-comments) +[![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/laravel-comments.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/laravel-comments) +[![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/laravel-comments.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-comments) + +Add the ability to associate comments to your Laravel Eloquent models. The comments can be approved and nested. + +```php +$post = Post::find(1); + +$post->comment('This is a comment'); + +$post->commentAsUser($user, 'This is a comment from someone else'); +``` + +## Installation + +You can install the package via composer: + +```bash +composer require beyondcode/laravel-comments +``` + +The package will automatically register itself. + +You can publish the migration with: + +```bash +php artisan vendor:publish --provider=BeyondCode\Comments\CommentsServiceProvider --tag="migrations" +``` + +After the migration has been published you can create the media-table by running the migrations: + +```bash +php artisan migrate +``` + +You can publish the config-file with: + +```bash +php artisan vendor:publish --provider=BeyondCode\Comments\CommentsServiceProvider --tag="config" +``` + +## Usage + +### Registering Models + +To let your models be able to receive comments, add the `HasComments` trait to the model classes. + +``` php +namespace App\Models; + +use Beyondcode\Comments\HasComments; +use Illuminate\Database\Eloquent\Model; + +class Post extends Model +{ + use HasComments; + ... +} +``` + +### Creating Comments + +To create a comment on your commentable models, you can use the `comment` method. It receives the string of the comment that you want to store. + +```php +$post = Post::find(1); + +$comment = $post->comment('This is a comment from a user.'); +``` + +The comment method returns the newly created comment class. + +Sometimes you also might want to create comments on behalf of other users. You can do this using the `commentAsUser` method and pass in your user model that should get associated +with this comment: + +```php +$post = Post::find(1); + +$comment = $post->commentAsUser($yourUser, 'This is a comment from someone else.'); +``` + +### Approving Comments + +By default, all comments that you create are not approved - this is just a boolean flag called `is_approved` that you can use in your views/controllers to filter out comments that you might not yet want to display. + +To approve a single comment, you may use the `approve` method on the Comment model like this: + +```php +$post = Post::find(1); +$comment = $post->comments->first(); + +$comment->approve(); +``` + +### Auto Approve Comments + +If you want to automatically approve a comment for a specific user (and optionally model) you can let your User model implement the following interface and method: + +```php +namespace App\Models; + +use BeyondCode\Comments\Contracts\Commentator; +use Illuminate\Foundation\Auth\User as Authenticatable; + +class User extends Authenticatable implements Commentator +{ + /** + * Check if a comment for a specific model needs to be approved. + * @param mixed $model + * @return bool + */ + public function needsCommentApproval($model): bool + { + return false; + } + +} +``` + +The `needsCommentApproval` method received the model instance that you want to add a comment to and you can either return `true` to mark the comment as **not** approved, or return `false` to mark the comment as **approved**. + +### Retrieving Comments + +The models that use the `HasComments` trait have access to it's comments using the `comments` relation: + +```php + +$post = Post::find(1); + +// Retrieve all comments +$comments = $post->comments; + +// Retrieve only approved comments +$approved = $post->comments()->approved()->get(); + +``` + +### Testing + +``` bash +composer test +``` + +### Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +### Security + +If you discover any security related issues, please email marcel@beyondco.de instead of using the issue tracker. + +## Credits + +- [Marcel Pociot](https://github.com/mpociot) +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e44650c --- /dev/null +++ b/composer.json @@ -0,0 +1,51 @@ +{ + "name": "beyondcode/laravel-comments", + "description": "Add comments to your Laravel application", + "keywords": [ + "beyondcode", + "comments", + "laravel-comments" + ], + "homepage": "https://github.com/beyondcode/laravel-comments", + "license": "MIT", + "authors": [ + { + "name": "Marcel Pociot", + "email": "marcel@beyondco.de", + "homepage": "https://beyondcode.de", + "role": "Developer" + } + ], + "require": { + "php": "^7.1" + }, + "require-dev": { + "orchestra/testbench": "^3.6", + "phpunit/phpunit": "^7.0" + }, + "autoload": { + "psr-4": { + "BeyondCode\\Comments\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "BeyondCode\\Comments\\Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "BeyondCode\\Comments\\CommentsServiceProvider" + ] + } + } +} diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..9c3c85b --- /dev/null +++ b/config/config.php @@ -0,0 +1,18 @@ + \BeyondCode\Comments\Comment::class, + + /* + * The user model that should be used when associating comments with + * commentators. If null, the default user provider from your + * Laravel authentication configuration will be used. + */ + 'user_model' => null, + +]; \ No newline at end of file diff --git a/database/migrations/create_comments_table.php.stub b/database/migrations/create_comments_table.php.stub new file mode 100644 index 0000000..27f2a13 --- /dev/null +++ b/database/migrations/create_comments_table.php.stub @@ -0,0 +1,31 @@ +increments('id'); + $table->morphs('commentable'); + $table->text('comment'); + $table->boolean('is_approved')->default(false); + $table->unsignedInteger('user_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('comments'); + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..1eef57c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + diff --git a/src/Comment.php b/src/Comment.php new file mode 100644 index 0000000..14f1f9c --- /dev/null +++ b/src/Comment.php @@ -0,0 +1,60 @@ + 'boolean' + ]; + + public function scopeApproved($query) + { + return $query->where('is_approved', true); + } + + public function commentable() + { + return $this->morphTo(); + } + + public function commentator() + { + return $this->belongsTo($this->getAuthModelName(), 'user_id'); + } + + public function approve() + { + $this->update([ + 'is_approved' => true, + ]); + + return $this; + } + + protected function getAuthModelName() + { + if (config('comments.user_model')) { + return config('comments.user_model'); + } + + if (!is_null(config('auth.providers.users.model'))) { + return config('auth.providers.users.model'); + } + + throw new Exception('Could not determine the commentator model name.'); + } + +} \ No newline at end of file diff --git a/src/CommentsServiceProvider.php b/src/CommentsServiceProvider.php new file mode 100644 index 0000000..2fd75e3 --- /dev/null +++ b/src/CommentsServiceProvider.php @@ -0,0 +1,35 @@ +app->runningInConsole()) { + $this->publishes([ + __DIR__.'/../config/config.php' => config_path('comments.php'), + ], 'config'); + + + if (! class_exists('CreateCommentsTable')) { + $this->publishes([ + __DIR__.'/../database/migrations/create_comments_table.php.stub' => database_path('migrations/'.date('Y_m_d_His', time()).'_create_comments_table.php'), + ], 'migrations'); + } + } + } + + /** + * Register the application services. + */ + public function register() + { + $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'comments'); + } +} diff --git a/src/Contracts/Commentator.php b/src/Contracts/Commentator.php new file mode 100644 index 0000000..40e963a --- /dev/null +++ b/src/Contracts/Commentator.php @@ -0,0 +1,15 @@ +morphMany(config('comments.comment_class'), 'commentable'); + } + + /** + * Attach a comment to this model. + * + * @param string $comment + * @return \Illuminate\Database\Eloquent\Model + */ + public function comment(string $comment) + { + return $this->commentAsUser(auth()->user(), $comment); + } + + /** + * Attach a comment to this model as a specific user. + * + * @param Model|null $user + * @param string $comment + * @return \Illuminate\Database\Eloquent\Model + */ + public function commentAsUser(?Model $user, string $comment) + { + $commentClass = config('comments.comment_class'); + + $comment = new $commentClass([ + 'comment' => $comment, + 'is_approved' => ($user instanceof Commentator) ? ! $user->needsCommentApproval($this) : false, + 'user_id' => is_null($user) ? null : $user->getKey(), + 'commentable_id' => $this->getKey(), + 'commentable_type' => get_class(), + ]); + + return $this->comments()->save($comment); + } + +} \ No newline at end of file diff --git a/tests/CommentsTest.php b/tests/CommentsTest.php new file mode 100644 index 0000000..e20a05c --- /dev/null +++ b/tests/CommentsTest.php @@ -0,0 +1,148 @@ + 'Some post' + ]); + + $post->comment('this is a comment'); + + $comment = $post->comments()->first(); + + $this->assertFalse($comment->is_approved); + } + + /** @test */ + public function models_can_store_comments() + { + $post = Post::create([ + 'title' => 'Some post' + ]); + + $post->comment('this is a comment'); + $post->comment('this is a different comment'); + + $this->assertCount(2, $post->comments); + + $this->assertSame('this is a comment', $post->comments[0]->comment); + $this->assertSame('this is a different comment', $post->comments[1]->comment); + } + + /** @test */ + public function comments_without_users_have_no_relation() + { + $post = Post::create([ + 'title' => 'Some post' + ]); + + $comment = $post->comment('this is a comment'); + + $this->assertNull($comment->commentator); + $this->assertNull($comment->user_id); + } + + /** @test */ + public function comments_can_be_posted_as_authenticated_users() + { + $user = User::first(); + + auth()->login($user); + + $post = Post::create([ + 'title' => 'Some post' + ]); + + $comment = $post->comment('this is a comment'); + + $this->assertSame($user->toArray(), $comment->commentator->toArray()); + } + + /** @test */ + public function comments_can_be_posted_as_different_users() + { + $user = User::first(); + + $post = Post::create([ + 'title' => 'Some post' + ]); + + $comment = $post->commentAsUser($user, 'this is a comment'); + + $this->assertSame($user->toArray(), $comment->commentator->toArray()); + } + + /** @test */ + public function comments_can_be_approved() + { + $user = User::first(); + + $post = Post::create([ + 'title' => 'Some post' + ]); + + $comment = $post->comment('this is a comment'); + + $this->assertFalse($comment->is_approved); + + $comment->approve(); + + $this->assertTrue($comment->is_approved); + } + + /** @test */ + public function comments_resolve_the_commented_model() + { + $user = User::first(); + + $post = Post::create([ + 'title' => 'Some post' + ]); + + $comment = $post->comment('this is a comment'); + + $this->assertSame($comment->commentable->id, $post->id); + $this->assertSame($comment->commentable->title, $post->title); + } + + /** @test */ + public function users_can_be_auto_approved() + { + $user = ApprovedUser::first(); + + $post = Post::create([ + 'title' => 'Some post' + ]); + + $comment = $post->commentAsUser($user, 'this is a comment'); + + $this->assertTrue($comment->is_approved); + } + + /** @test */ + public function comments_have_an_approved_scope() + { + $user = ApprovedUser::first(); + + $post = Post::create([ + 'title' => 'Some post' + ]); + + $post->comment('this comment is not approved'); + $post->commentAsUser($user, 'this comment is approved'); + + $this->assertCount(2, $post->comments); + $this->assertCount(1, $post->comments()->approved()->get()); + + $this->assertSame('this comment is approved', $post->comments()->approved()->first()->comment); + } +} diff --git a/tests/Models/ApprovedUser.php b/tests/Models/ApprovedUser.php new file mode 100644 index 0000000..53734e0 --- /dev/null +++ b/tests/Models/ApprovedUser.php @@ -0,0 +1,23 @@ +loadLaravelMigrations(['--database' => 'sqlite']); + $this->setUpDatabase(); + $this->createUser(); + } + + protected function getPackageProviders($app) + { + return [ + CommentsServiceProvider::class, + ]; + } + + protected function getEnvironmentSetUp($app) + { + $app['config']->set('auth.providers.users.model', User::class); + $app['config']->set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + $app['config']->set('app.key', 'base64:6Cu/ozj4gPtIjmXjr8EdVnGFNsdRqZfHfVjQkmTlg4Y='); + } + + protected function setUpDatabase() + { + include_once __DIR__ . '/../database/migrations/create_comments_table.php.stub'; + + (new \CreateCommentsTable())->up(); + + $this->app['db']->connection()->getSchemaBuilder()->create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->string('title'); + $table->timestamps(); + }); + } + + protected function createUser() + { + User::forceCreate([ + 'name' => 'User', + 'email' => 'user@email.com', + 'password' => 'test' + ]); + } + +} \ No newline at end of file