diff --git a/.gitignore b/.gitignore index e75f91b..e4a8caf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ -composer.phar /vendor/ - -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +/logs/ +composer.phar composer.lock +clover.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fc0ef09 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php +php: + - "7.3" + - "7.2" + - "7.1" +# env: +# - PHPUNIT_VERSION=^7 TESTBENCH_VERSION=3.* +# matrix: +# include: +# - php: 7.0 +# env: PHPUNIT_VERSION=^6 TESTBENCH_VERSION=3.5.* +# before_install: +# - composer require --dev "phpunit/phpunit:${PHPUNIT_VERSION}" +# - composer require --dev "orchestra/testbench:${TESTBENCH_VERSION}" +install: + - composer install -n +script: + - composer test:dist +after_success: + - travis_retry composer test:coverage diff --git a/README.md b/README.md index dcc0c02..23722ff 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ +

+Build Status +Coverage Status +Total Downloads +Latest Stable Version +License +

+ # History + Eloquent model history tracking for Laravel ## Installation @@ -121,7 +130,6 @@ $model->histories()->orderBy('performed_at', 'desc')->take(10) $model->histories()->where('user_id', 10010) ``` - ### History ```php @@ -201,3 +209,35 @@ This will translate your model history into ### Filters You may set whitelist and blacklist in config file. Please follow the description guide in the published config file. + +### Known issues + +1. When updating a model, if its model label(attributes returned from `getModelLabel`) has been modified, the history message will use its new attributes, which might not be what you expect. + +```php +class Article extends Model +{ + use HasHistories; + + public function getModelLabel() + { + return $this->title; + } +} +// original title is 'my title' +// modify title +$article->title = 'new title'; +$article->save(); +// the updating history message +// expect: Updating Article my title +// actual: Updating Article new title +``` + +A workaround + +```php +public function getModelLabel() +{ + return $this->getOriginal('title', $this->title); +} +``` diff --git a/composer.json b/composer.json index 0764415..eaf6f51 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "homepage": "https://github.com/seancheung/history", "require": { "php": ">=5.6.4", - "illuminate/support": "^5.3" + "illuminate/support": "^5.3" }, "license": "MIT", "authors": [ @@ -18,5 +18,21 @@ "psr-4": { "Panoscape\\History\\": "src/" } + }, + "scripts": { + "test": "phpunit", + "test:dist": "phpunit --coverage-clover clover.xml", + "test:coverage": "php-coveralls -v -x clover.xml -o ./logs --exclude-no-stmt" + }, + "autoload-dev": { + "psr-4": { + "Panoscape\\History\\Tests\\": "tests/" + } + }, + "require-dev": { + "phpunit/phpunit": "~7.4", + "orchestra/testbench": "~3.7", + "mockery/mockery": "^1.2", + "php-coveralls/php-coveralls": "^2.1" } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ae4f23d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + tests/ + + + + + src/ + + + + + + + \ No newline at end of file diff --git a/src/History.php b/src/History.php index e9f4262..271ee9a 100644 --- a/src/History.php +++ b/src/History.php @@ -56,11 +56,11 @@ public function __construct(array $attributes = []) } /** - * Get the user of this record + * Get the user who performed this record */ public function user() { - return $this->morphTo(); + return $this->hasUser()? $this->morphTo()->first(): null; } /** @@ -70,7 +70,7 @@ public function user() */ public function hasUser() { - return !empty($this->agent_type) && !empty($this->agent_id); + return !empty($this->user_type) && !empty($this->user_id); } /** @@ -78,6 +78,6 @@ public function hasUser() */ public function model() { - return $this->morphTo(); + return $this->morphTo()->first(); } } \ No newline at end of file diff --git a/src/HistoryObserver.php b/src/HistoryObserver.php index 7cfc703..c97e4fc 100644 --- a/src/HistoryObserver.php +++ b/src/HistoryObserver.php @@ -36,6 +36,11 @@ public function updating($model) * Gets the model's altered values and tracks what had changed */ $changes = $model->getDirty(); + /** + * Bypass restoring event + */ + if(array_key_exists('deleted_at', $changes)) return; + $changed = []; foreach ($changes as $key => $value) { if(static::isIgnored($model, $key)) continue; @@ -111,7 +116,7 @@ public static function isIgnored($model, $key) { $blacklist = config('history.attributes_blacklist'); $name = get_class($model); - $array = $blacklist[$name]; + $array = isset($blacklist[$name])? $blacklist[$name]: null; return !empty($array) && in_array($key, $array); } diff --git a/tests/Article.php b/tests/Article.php new file mode 100644 index 0000000..054ea21 --- /dev/null +++ b/tests/Article.php @@ -0,0 +1,20 @@ +getOriginal('title', $this->title); + } +} \ No newline at end of file diff --git a/tests/TestCaseTest.php b/tests/TestCaseTest.php new file mode 100644 index 0000000..472fa78 --- /dev/null +++ b/tests/TestCaseTest.php @@ -0,0 +1,181 @@ + History::class + ]; + } + + protected function getEnvironmentSetUp($app) + { + $app['config']->set('app.debug', true); + $app['config']->set('database.default', 'testing'); + $app['config']->set('history.console_enabled', true); + $app['config']->set('history.test_enabled', true); + $app['config']->set('history.attributes_blacklist', [ + User::class => [ + 'password' + ] + ]); + + $app['router']->post('articles', function(Request $request) { + return Article::create(['title' => $request->title]); + }); + $app['router']->put('articles/{id}', function(Request $request, $id) { + $model = Article::find($id); + $model->title = $request->title; + $model->save(); + return $model; + }); + $app['router']->delete('articles/{id}', function($id) { + Article::destroy($id); + }); + $app['router']->post('articles/{id}/restore', function($id) { + Article::withTrashed()->find($id)->restore(); + }); + $app['router']->get('articles/{id}', function($id) { + $model = Article::find($id); + if(!is_null($model)) { + event(new ModelChanged($model, 'Query Article ' . $model->title, $model->pluck('id')->toArray())); + } + return $model; + }); + } + + public function setUp() + { + parent::setUp(); + $this->setUpDatabase(); + } + + protected function setUpDatabase() + { + $builder = $this->app['db']->connection()->getSchemaBuilder(); + + $builder->create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('password'); + }); + + $builder->create('articles', function (Blueprint $table) { + $table->increments('id'); + $table->string('title'); + $table->softDeletes(); + }); + + User::create(['name' => 'Esther', 'password' => '6ecd6a17b723']); + + $this->loadMigrationsFrom(realpath(__DIR__.'/../src/migrations')); + } + + public function testHistory() + { + $content = ['title' => 'enim officiis omnis']; + $this->json('POST', '/articles', $content)->assertJson($content); + $history = History::first(); + $article = Article::first(); + $this->assertNotNull($history); + $this->assertEquals(Article::class, $history->model_type); + $this->assertEquals($article->id, $history->model_id); + $this->assertEquals('Created Article ' . $content['title'], $history->message); + $this->assertTrue($history->performed_at instanceof \Illuminate\Support\Carbon); + $history->delete(); + + $data = ['title' => 'eligendi fugiat culpa']; + $this->json('PUT', '/articles/' . $article->id, $data)->assertJson($data); + $history = History::first(); + $this->assertNotNull($history); + $this->assertEquals($article->id, $history->model_id); + $this->assertEquals('Updating Article ' . $content['title'], $history->message); + $this->assertEquals([['key' => 'title', 'old' => 'enim officiis omnis', 'new' => 'eligendi fugiat culpa']], $history->meta); + $history->delete(); + $article->refresh(); + + $this->json('DELETE', '/articles/' . $article->id); + $history = History::first(); + $this->assertNotNull($history); + $this->assertEquals($article->id, $history->model_id); + $this->assertEquals('Deleting Article ' . $article->title, $history->message); + $history->delete(); + + $this->json('POST', '/articles/' . $article->id . '/restore'); + $history = History::first(); + $this->assertNotNull($history); + $this->assertEquals($article->id, $history->model_id); + $this->assertEquals('Restored Article ' . $article->title, $history->message); + } + + public function testAuthed() + { + $user = User::first(); + $this->assertNotNull($user); + + $content = ['title' => 'voluptas ut rem']; + $this->actingAs($user)->json('POST', '/articles', $content)->assertJson($content); + + $article = Article::first(); + $this->assertNotNull($article); + $histories = $article->histories; + $this->assertNotNull($histories); + $this->assertEquals(1, count($histories)); + $history = $histories[0]; + $this->assertTrue($history->hasUser()); + $this->assertNotNull($history->user()); + $this->assertEquals($user->toJson(), $history->user()->toJson()); + $this->assertEquals($article->makeHidden('histories')->toJson(), $history->model()->toJson()); + + $operations = $user->operations; + $this->assertNotNull($operations); + $this->assertEquals(1, count($operations)); + $operation = $operations[0]; + $this->assertEquals($history->toJson(), $operation->toJson()); + } + + public function testAnonymous() + { + $content = ['title' => 'quae et est']; + $this->json('POST', '/articles', $content)->assertJson($content); + + $article = Article::first(); + $this->assertNotNull($article); + $histories = $article->histories; + $this->assertNotNull($histories); + $this->assertEquals(1, count($histories)); + $history = $histories[0]; + $this->assertNotTrue($history->hasUser()); + $this->assertNull($history->user()); + } + + public function testCustomEvent() + { + Article::create(['title' => 'maxime fugit saepe']); + $article = Article::first(); + $this->assertNotNull($article); + $this->json('GET', '/articles/' . $article->id); + $history = History::skip(1)->first(); + $this->assertNotNull($history); + $this->assertEquals($article->id, $history->model_id); + $this->assertEquals('Query Article ' . $article->title, $history->message); + $this->assertEquals([$article->id], $history->meta); + } +} \ No newline at end of file diff --git a/tests/User.php b/tests/User.php new file mode 100644 index 0000000..3f076a2 --- /dev/null +++ b/tests/User.php @@ -0,0 +1,15 @@ +