From 03309dfd8a2290352535db69657da6a929b1fd81 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 27 Jul 2015 12:34:17 -0700 Subject: [PATCH] Getting recusrive cloning working with functional tests --- .gitignore | 3 +- composer.json | 42 ++++++++++++----- gulpfile.js | 24 ++++++++++ package.json | 9 ++++ phpunit.xml | 18 +++++++ src/AttachmentAdapter.php | 5 ++ src/Cloneable.php | 41 ++++++++++++++++ src/Cloner.php | 95 +++++++++++++++++++++++++++++++++++++ stubs/Article.php | 19 ++++++++ stubs/Author.php | 14 ++++++ stubs/Photo.php | 14 ++++++ stubs/README.md | 3 ++ tests/ClonerTest.php | 98 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 src/AttachmentAdapter.php create mode 100644 src/Cloneable.php create mode 100644 src/Cloner.php create mode 100644 stubs/Article.php create mode 100644 stubs/Author.php create mode 100644 stubs/Photo.php create mode 100644 stubs/README.md create mode 100644 tests/ClonerTest.php diff --git a/.gitignore b/.gitignore index a9a42fd..18c3658 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor/ composer.lock -.DS_Store \ No newline at end of file +.DS_Store +node_modules diff --git a/composer.json b/composer.json index a7f5486..5c2e3d4 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,32 @@ { - "name": "bkwld/cloner", - "description": "A trait for Laravel Eloquent models that allows for recursive (via relationships) cloning of a model, including files (via BKWLD/upchuck)", - "require-dev": { - "illuminate/database": "^4.0" - }, - "license": "MIT", - "authors": [ - { - "name": "Robert Reinhard", - "email": "info@bkwld.com" - } - ] + "name": "bkwld/cloner", + "description": "A trait for Laravel Eloquent models that allows for recursive (via relationships) cloning of a model, including files (via BKWLD/upchuck)", + "require": { + "php": ">=5.4.0", + "illuminate/support": "~4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "illuminate/database": "~4.0" + }, + "suggest": { + "bkwld/upchuck": "Required for replicating of files." + }, + "autoload": { + "psr-4": { + "Bkwld\\Cloner\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Bkwld\\Cloner\\Stubs\\": "stubs/" + } + }, + "license": "MIT", + "authors": [ + { + "name": "Robert Reinhard", + "email": "info@bkwld.com" + } + ] } diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..7a08a2d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,24 @@ +var gulp = require('gulp') + , phpunit = require('gulp-phpunit') +; + +// PHPunit +gulp.task('phpunit', function() { + var options = { + debug: false + , clear: true + }; + gulp + .src('phpunit.xml') + .pipe(phpunit('./vendor/phpunit/phpunit/phpunit', options)) + .on('error', function(){}) + ; +}); + +// Watch files +gulp.task('watch', function () { + gulp.watch('**/*.php', ['phpunit']); +}); + +// Default task +gulp.task('default', ['watch']); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..617fb0b --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "Cloner", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "gulp": "^3.9.0", + "gulp-phpunit": "^0.8.1" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e89ac6d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + \ No newline at end of file diff --git a/src/AttachmentAdapter.php b/src/AttachmentAdapter.php new file mode 100644 index 0000000..18a110a --- /dev/null +++ b/src/AttachmentAdapter.php @@ -0,0 +1,5 @@ +clone_except_attributes)) return []; + return $this->clone_except_attributes; + } + + /** + * Return the list of relations on this model that should be cloned + * + * @return array + */ + public function getCloneableRelations() { + if (!isset($this->cloneable_relations)) return []; + return $this->cloneable_relations; + } + + /** + * A no-op callback that gets fired when a model is cloning but before it gets + * committed to the database + * + * @return void + */ + public function onCloning() {} + + /** + * A no-op callback that gets fired when a model is cloned and saved to the + * database + * + * @return void + */ + public function onCloned() {} + +} diff --git a/src/Cloner.php b/src/Cloner.php new file mode 100644 index 0000000..7c1d206 --- /dev/null +++ b/src/Cloner.php @@ -0,0 +1,95 @@ +attachment_adapter = $attachment_adapter; + } + + /** + * Clone a model instance and all of it's files and relations + * + * @param Illuminate\Database\Eloquent\Model $model + * @param Illuminate\Database\Eloquent\Relations\Relation $relation + * @return Illuminate\Database\Eloquent\Model The new model instance + */ + public function duplicate($model, $relation = null) { + + // Duplicate the model + $clone = $model->replicate($model->getCloneExemptAttributes()); + $clone->onCloning(); + if ($relation) $relation->save($clone); + else $clone->save(); + $clone->onCloned(); + + // Loop though all of it's cloneable relationshsips and duplicate the + // relationship + foreach($model->getCloneableRelations() as $relation_name) { + $this->duplicateRelation($model, $relation_name, $clone); + } + + // Return the duplicated model + return $clone; + + } + + /** + * Duplicate relationships to the clone + * + * @param Illuminate\Database\Eloquent\Model $model + * @param string $relation_name + * @param Illuminate\Database\Eloquent\Model $clone + * @return void + */ + public function duplicateRelation($model, $relation_name, $clone) { + $relation = call_user_func([$model, $relation_name]); + if (is_a($relation, 'Illuminate\Database\Eloquent\Relations\BelongsToMany')) { + $this->duplicatePivotedRelation($relation, $relation_name, $clone); + } else $this->duplicateDirectRelation($relation, $relation_name, $clone); + } + + /** + * Duplicate a many-to-many style relation where we are just attaching the + * relation to the dupe + * + * @param Illuminate\Database\Eloquent\Relations\Relation $relation + * @param string $relation_name + * @param Illuminate\Database\Eloquent\Model $clone + * @return void + */ + public function duplicatePivotedRelation($relation, $relation_name, $clone) { + $relation->get()->each(function($foreign) use ($clone, $relation_name) { + $clone->$relation_name()->attach($foreign); + }); + } + + /** + * Duplicate a one-to-many style relation where the foreign model is ALSO + * cloned and then associated + * + * @param Illuminate\Database\Eloquent\Relations\Relation $relation + * @param string $relation_name + * @param Illuminate\Database\Eloquent\Model $clone + * @return void + */ + public function duplicateDirectRelation($relation, $relation_name, $clone) { + $relation->get()->each(function($foreign) use ($clone, $relation_name) { + $this->duplicate($foreign, $clone->$relation_name()); + }); + } +} diff --git a/stubs/Article.php b/stubs/Article.php new file mode 100644 index 0000000..660a6af --- /dev/null +++ b/stubs/Article.php @@ -0,0 +1,19 @@ +hasMany('Bkwld\Cloner\Stubs\Photo'); + } + + public function authors() { + return $this->belongsToMany('Bkwld\Cloner\Stubs\Author'); + } +} \ No newline at end of file diff --git a/stubs/Author.php b/stubs/Author.php new file mode 100644 index 0000000..1d4202d --- /dev/null +++ b/stubs/Author.php @@ -0,0 +1,14 @@ +belongsToMany('Bkwld\Cloner\Stubs\Article'); + } +} \ No newline at end of file diff --git a/stubs/Photo.php b/stubs/Photo.php new file mode 100644 index 0000000..dd31ea0 --- /dev/null +++ b/stubs/Photo.php @@ -0,0 +1,14 @@ +belongsTo('Bkwld\Cloner\Stubs\Article'); + } +} \ No newline at end of file diff --git a/stubs/README.md b/stubs/README.md new file mode 100644 index 0000000..06e71c9 --- /dev/null +++ b/stubs/README.md @@ -0,0 +1,3 @@ +Models to run automated tests against. + +I went this approach so that I could test the trait. \ No newline at end of file diff --git a/tests/ClonerTest.php b/tests/ClonerTest.php new file mode 100644 index 0000000..7c3dd07 --- /dev/null +++ b/tests/ClonerTest.php @@ -0,0 +1,98 @@ +setUpDatabase(); + $this->migrateTables(); + $this->seed(); + } + + // https://github.com/laracasts/TestDummy/blob/master/tests/FactoryTest.php#L18 + protected function setUpDatabase() { + $db = new DB; + + $db->addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:' + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + } + + // https://github.com/laracasts/TestDummy/blob/master/tests/FactoryTest.php#L31 + protected function migrateTables() { + DB::schema()->create('articles', function ($table) { + $table->increments('id'); + $table->string('title'); + $table->timestamps(); + }); + + DB::schema()->create('authors', function ($table) { + $table->increments('id'); + $table->string('name'); + $table->timestamps(); + }); + + DB::schema()->create('article_author', function ($table) { + $table->increments('id'); + $table->integer('article_id')->unsigned(); + $table->integer('author_id')->unsigned(); + }); + + DB::schema()->create('photos', function ($table) { + $table->increments('id'); + $table->integer('article_id')->unsigned(); + $table->string('image'); + $table->timestamps(); + }); + } + + protected function seed() { + Article::unguard(); + $this->article = Article::create([ + 'title' => 'Test', + ]); + + Author::unguard(); + $this->article->authors()->attach(Author::create([ + 'name' => 'Steve', + ])); + + Photo::unguard(); + $this->article->photos()->save(new Photo([ + 'image' => '/test.jpg', + ])); + } + + public function testDuplicate() { + $cloner = new Cloner; + $clone = $cloner->duplicate($this->article); + + // Test that the new article was created + $this->assertTrue($clone->exists); + $this->assertEquals(2, $clone->id); + $this->assertEquals('Test', $clone->title); + + // Test that new author relationship was formed + $this->assertEquals(1, $clone->authors()->count()); + $this->assertEquals('Steve', $clone->authors()->first()->name); + $this->assertEquals(2, DB::table('article_author')->count()); + + // Test that the duplicate photo was formed + $this->assertEquals(1, $clone->photos()->count()); + // $this->assertNotEquals('/test.jpg', $clone->photos()->first()->image); + + + } + +} \ No newline at end of file