From 5b1c82e59d63ae49157097a8b94c8f82c8f0e542 Mon Sep 17 00:00:00 2001 From: Patrick Breen Date: Wed, 10 Mar 2021 09:38:45 -0500 Subject: [PATCH 1/5] Add HasStatuses trait and supporting code --- database/factories/StatusFactory.php | 23 ++--- database/factories/StatusableFactory.php | 28 ++++++ ...020_05_08_100000_create_statuses_table.php | 6 +- src/Exceptions/StatusException.php | 9 ++ src/Exceptions/UnknownStatusException.php | 15 +++ src/Models/Status.php | 79 ++++++++++++++- src/Models/Statusable.php | 43 +++++++++ src/Traits/HasStatuses.php | 65 +++++++++++++ .../Providers/NovaPackageServiceProvider.php | 15 --- tests/TestCase.php | 2 +- tests/Unit/Models/StatusTest.php | 83 ++++++++++++++++ tests/Unit/Models/StatusableTest.php | 29 ++++++ tests/Unit/Traits/HasStatusesTest.php | 95 +++++++++++++++++++ 13 files changed, 456 insertions(+), 36 deletions(-) create mode 100644 database/factories/StatusableFactory.php create mode 100644 src/Exceptions/StatusException.php create mode 100644 src/Exceptions/UnknownStatusException.php create mode 100644 src/Models/Statusable.php create mode 100644 src/Traits/HasStatuses.php delete mode 100644 tests/Support/Providers/NovaPackageServiceProvider.php create mode 100644 tests/Unit/Models/StatusTest.php create mode 100644 tests/Unit/Models/StatusableTest.php create mode 100644 tests/Unit/Traits/HasStatusesTest.php diff --git a/database/factories/StatusFactory.php b/database/factories/StatusFactory.php index 5b72da2..e740385 100644 --- a/database/factories/StatusFactory.php +++ b/database/factories/StatusFactory.php @@ -6,32 +6,21 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; -use Tipoff\Forms\Models\Status; +use Tipoff\Statuses\Models\Status; class StatusFactory extends Factory { - /** - * The name of the factory's corresponding model. - * - * @var string - */ protected $model = Status::class; - /** - * Define the model's default state. - * - * @return array - */ public function definition() { - $word1 = $this->faker->unique->word; - $word2 = $this->faker->word; + $type = $this->faker->randomElement(['order', 'slot', 'game', 'invoice', 'payment']); + $words = $this->faker->unique()->words(2, true); return [ - 'slug' => Str::slug(rand(1, 1000).'-'.$word1.'-'.$word2), - 'name' => $word1.' '.$word2, - 'applies_to' => $this->faker->randomElement(['order', 'slot', 'game', 'invoice', 'payment']), - 'note' => $this->faker->sentences(1, true), + 'name' => $words, + 'type' => $type, + 'note' => $this->faker->optional()->sentences(1, true), ]; } } diff --git a/database/factories/StatusableFactory.php b/database/factories/StatusableFactory.php new file mode 100644 index 0000000..3221aa3 --- /dev/null +++ b/database/factories/StatusableFactory.php @@ -0,0 +1,28 @@ +create(); + + return [ + 'status_id' => randomOrCreate(Status::class), + 'statusable_type' => get_class($statusable), + 'statusable_id' => $statusable->id, + 'creator_id' => randomOrCreate(app('user')), + ]; + } +} diff --git a/database/migrations/2020_05_08_100000_create_statuses_table.php b/database/migrations/2020_05_08_100000_create_statuses_table.php index 8a2e2a6..5c4de31 100644 --- a/database/migrations/2020_05_08_100000_create_statuses_table.php +++ b/database/migrations/2020_05_08_100000_create_statuses_table.php @@ -13,10 +13,12 @@ public function up() Schema::create('statuses', function (Blueprint $table) { $table->id(); $table->string('slug')->unique()->index(); - $table->string('name')->unique(); - $table->string('applies_to')->default('order'); // Values include 'order', 'slot', 'game', 'invoice', 'payment' + $table->string('name'); + $table->string('type'); // Typically, full class name for model using status $table->text('note')->nullable(); $table->timestamps(); + + $table->unique(['name', 'type']); }); } } diff --git a/src/Exceptions/StatusException.php b/src/Exceptions/StatusException.php new file mode 100644 index 0000000..6e57834 --- /dev/null +++ b/src/Exceptions/StatusException.php @@ -0,0 +1,9 @@ + 'integer', + ]; + + protected $fillable = [ + 'type', + 'name', + ]; + + public static function publishStatuses(string $type, array $names): Collection + { + return collect($names) + ->map(function (string $name) use ($type) { + return static::createStatus($type, $name); + }); + } + + public static function createStatus(string $type, string $name, ?string $note = null): self + { + /** @var Status $status */ + $status = static::query()->firstOrNew([ + 'type' => $type, + 'name' => $name + ]); + + if ($note) { + $status->note = $note; + } + + $status->save(); + + return $status; + } + + public static function findStatus(string $type, string $name): ?self + { + /** @var Status $status */ + $status = static::query() + ->byType($type) + ->where('name', '=', $name) + ->first(); + + return $status; + } + + protected static function boot() + { + parent::boot(); + + static::creating(function (Status $status) { + $status->slug = $status->slug ?: Str::slug("{$status->type}-{$status->name}"); + }); + } + + public function scopeByType(Builder $query, string $type): Builder + { + return $query->where('type', '=', $type); + } + + public function __toString() + { + return $this->name; + } } diff --git a/src/Models/Statusable.php b/src/Models/Statusable.php new file mode 100644 index 0000000..4d080b3 --- /dev/null +++ b/src/Models/Statusable.php @@ -0,0 +1,43 @@ + 'integer', + 'creator_id' => 'integer', + ]; + + public function status() + { + return $this->belongsTo(Status::class); + } + + public function statusable() + { + return $this->morphTo(); + } +} diff --git a/src/Traits/HasStatuses.php b/src/Traits/HasStatuses.php new file mode 100644 index 0000000..b370ca5 --- /dev/null +++ b/src/Traits/HasStatuses.php @@ -0,0 +1,65 @@ +statusable; + + if ($name) { + $status = $this->getStatusByName($name); + + if (!$statusable) { + $statusable = new Statusable(); + $statusable->statusable()->associate($this); + } + + $statusable->status()->associate($status)->save(); + $this->load('statusable'); + } + + return $statusable ? $statusable->status : null; + } + + public function statusable() + { + return $this->morphOne(Statusable::class, 'statusable'); + } + + private function getStatusByName(string $name): Status + { + if ($this->dynamicStatusCreation) { + return Status::createStatus($this->statusType(), $name); + } + + if ($status = Status::findStatus($this->statusType(), $name)) { + return $status; + } + + throw new UnknownStatusException($this->statusType(), $name); + } +} diff --git a/tests/Support/Providers/NovaPackageServiceProvider.php b/tests/Support/Providers/NovaPackageServiceProvider.php deleted file mode 100644 index 1bf088f..0000000 --- a/tests/Support/Providers/NovaPackageServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -create(); + $this->assertNotNull($model); + } + + /** @test */ + public function create_status() + { + $status = Status::createStatus('type', 'name', 'my note'); + $this->assertEquals('type-name', $status->slug); + $this->assertEquals('type', $status->type); + $this->assertEquals('name', $status->name); + $this->assertEquals('my note', $status->note); + $this->assertEquals('name', (string) $status); + + // Add same status again + $status = Status::createStatus('type', 'name'); + $this->assertEquals('type-name', $status->slug); + $this->assertEquals('my note', $status->note); + } + + /** @test */ + public function find_status() + { + $status = Status::findStatus('type', 'name'); + $this->assertNull($status); + + Status::createStatus('type', 'name'); + + $status = Status::findStatus('type', 'name'); + $this->assertEquals('type-name', $status->slug); + $this->assertEquals('type', $status->type); + $this->assertEquals('name', $status->name); + $this->assertEquals('name', (string) $status); + + $status = Status::findStatus('other-type', 'name'); + $this->assertNull($status); + } + + /** @test */ + public function publish_statuses() + { + $statuses = Status::publishStatuses('type', ['a', 'b', 'c']); + $this->assertCount(3, $statuses); + + $status = Status::findStatus('type', 'a'); + $this->assertNotNull($status); + + $status = Status::findStatus('type', 'b'); + $this->assertNotNull($status); + + $status = Status::findStatus('type', 'c'); + $this->assertNotNull($status); + } + + /** @test */ + public function scope_by_type() + { + $statuses = Status::query()->byType('type')->get(); + $this->assertCount(0, $statuses); + + Status::publishStatuses('type', ['a', 'b', 'c']); + + $statuses = Status::query()->byType('type')->get(); + $this->assertCount(3, $statuses); + } +} diff --git a/tests/Unit/Models/StatusableTest.php b/tests/Unit/Models/StatusableTest.php new file mode 100644 index 0000000..0c6e34c --- /dev/null +++ b/tests/Unit/Models/StatusableTest.php @@ -0,0 +1,29 @@ +create(); + $this->assertNotNull($model); + + $status = $model->status; + $this->assertInstanceOf(Status::class, $status); + + $statusable = $model->statusable; + $this->assertInstanceOf(Model::class, $statusable); + } +} diff --git a/tests/Unit/Traits/HasStatusesTest.php b/tests/Unit/Traits/HasStatusesTest.php new file mode 100644 index 0000000..2c179fb --- /dev/null +++ b/tests/Unit/Traits/HasStatusesTest.php @@ -0,0 +1,95 @@ +save(); + + $status = $model->status(); + $this->assertNull($status); + } + + /** @test */ + public function set_valid_status() + { + TestModel::createTable(); + + $status = Status::createStatus(TestModel::class, 'test status'); + + $model = new TestModel(); + $model->save(); + + $this->actingAs(User::factory()->create()); + $model->status('test status'); + + $foundStatus = $model->status(); + $this->assertNotNull($foundStatus); + $this->assertEquals($status->id, $foundStatus->id); + } + + /** @test */ + public function set_unknown_status() + { + TestModel::createTable(); + + $model = new TestModel(); + $model->save(); + + $this->expectException(UnknownStatusException::class); + $this->expectExceptionMessage("Unknown status value 'test status' for status type Tipoff\Statuses\Tests\Unit\Traits\TestModel"); + + $this->actingAs(User::factory()->create()); + $model->status('test status'); + } + + /** @test */ + public function set_unknown_status_dynamic_creation() + { + TestModel::createTable(); + + $model = new TestModel(); + $model->save(); + $model->setDynamicStaticCreation(true); + + $this->actingAs(User::factory()->create()); + $status = $model->status('test status'); + + $this->assertNotNull($status); + $this->assertEquals('test status', (string) $status); + } +} + +class TestModel extends BaseModel +{ + use TestModelStub; + use HasStatuses; + + public function setDynamicStaticCreation(bool $isDynamic): self + { + $this->dynamicStatusCreation = $isDynamic; + + return $this; + } +} From baef1acbd957cc920853074eee6545cb3616c19d Mon Sep 17 00:00:00 2001 From: pdbreen Date: Wed, 10 Mar 2021 14:39:58 +0000 Subject: [PATCH 2/5] Fix styling --- src/Models/Status.php | 4 +--- src/Traits/HasStatuses.php | 3 +-- tests/Unit/Traits/HasStatusesTest.php | 2 -- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Models/Status.php b/src/Models/Status.php index a7270de..afecc7e 100644 --- a/src/Models/Status.php +++ b/src/Models/Status.php @@ -4,12 +4,10 @@ namespace Tipoff\Statuses\Models; -use Assert\Assert; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Tipoff\Discounts\Models\Discount; use Tipoff\Support\Models\BaseModel; use Tipoff\Support\Traits\HasPackageFactory; @@ -48,7 +46,7 @@ public static function createStatus(string $type, string $name, ?string $note = /** @var Status $status */ $status = static::query()->firstOrNew([ 'type' => $type, - 'name' => $name + 'name' => $name, ]); if ($note) { diff --git a/src/Traits/HasStatuses.php b/src/Traits/HasStatuses.php index b370ca5..0f736e1 100644 --- a/src/Traits/HasStatuses.php +++ b/src/Traits/HasStatuses.php @@ -4,7 +4,6 @@ namespace Tipoff\Statuses\Traits; - use Tipoff\Statuses\Exceptions\UnknownStatusException; use Tipoff\Statuses\Models\Status; use Tipoff\Statuses\Models\Statusable; @@ -33,7 +32,7 @@ public function status(?string $name = null): ?Status if ($name) { $status = $this->getStatusByName($name); - if (!$statusable) { + if (! $statusable) { $statusable = new Statusable(); $statusable->statusable()->associate($this); } diff --git a/tests/Unit/Traits/HasStatusesTest.php b/tests/Unit/Traits/HasStatusesTest.php index 2c179fb..e9859e2 100644 --- a/tests/Unit/Traits/HasStatusesTest.php +++ b/tests/Unit/Traits/HasStatusesTest.php @@ -4,12 +4,10 @@ namespace Tipoff\Statuses\Tests\Unit\Traits; -use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tipoff\Authorization\Models\User; use Tipoff\Statuses\Exceptions\UnknownStatusException; use Tipoff\Statuses\Models\Status; -use Tipoff\Statuses\Models\Statusable; use Tipoff\Statuses\Tests\TestCase; use Tipoff\Statuses\Traits\HasStatuses; use Tipoff\Support\Models\BaseModel; From 641f2de2902308ffee31f0ccbd53efa210de84e0 Mon Sep 17 00:00:00 2001 From: Patrick Breen Date: Wed, 10 Mar 2021 11:38:50 -0500 Subject: [PATCH 3/5] Add support for multiple types of status per model --- database/factories/StatusableFactory.php | 4 +- ..._05_08_110000_create_statusables_table.php | 1 + src/Traits/HasStatuses.php | 53 ++++++++++--------- tests/Unit/Traits/HasStatusesTest.php | 43 +++++++++++++-- 4 files changed, 71 insertions(+), 30 deletions(-) diff --git a/database/factories/StatusableFactory.php b/database/factories/StatusableFactory.php index 3221aa3..0839769 100644 --- a/database/factories/StatusableFactory.php +++ b/database/factories/StatusableFactory.php @@ -17,9 +17,11 @@ class StatusableFactory extends Factory public function definition() { $statusable = User::factory()->create(); + $status = randomOrCreate(Status::class); return [ - 'status_id' => randomOrCreate(Status::class), + 'status_id' => $status, + 'type' => $status->type, 'statusable_type' => get_class($statusable), 'statusable_id' => $statusable->id, 'creator_id' => randomOrCreate(app('user')), diff --git a/database/migrations/2020_05_08_110000_create_statusables_table.php b/database/migrations/2020_05_08_110000_create_statusables_table.php index a2565bd..ce147e1 100644 --- a/database/migrations/2020_05_08_110000_create_statusables_table.php +++ b/database/migrations/2020_05_08_110000_create_statusables_table.php @@ -14,6 +14,7 @@ public function up() $table->id(); $table->foreignIdFor(app('status'))->index(); $table->morphs('statusable'); + $table->string('type'); // Typically, full class name for model using status $table->foreignIdFor(app('user'), 'creator_id'); $table->timestamps(); }); diff --git a/src/Traits/HasStatuses.php b/src/Traits/HasStatuses.php index 0f736e1..51fb27e 100644 --- a/src/Traits/HasStatuses.php +++ b/src/Traits/HasStatuses.php @@ -4,12 +4,13 @@ namespace Tipoff\Statuses\Traits; +use Illuminate\Support\Collection; use Tipoff\Statuses\Exceptions\UnknownStatusException; use Tipoff\Statuses\Models\Status; use Tipoff\Statuses\Models\Statusable; /** - * @property Statusable statusable + * @property Collection statusables */ trait HasStatuses { @@ -17,48 +18,52 @@ trait HasStatuses // When false, exception occurs if unknown status value is used protected bool $dynamicStatusCreation = false; - // Assumes models can only have a single type of status - public function statusType(): string + public function getStatus(?string $type = null): ?Status { - // Default to using full class name as type - return get_class($this); + $statusable = $this->getStatusableByType($type ?? get_class($this)); + + return $statusable ? $statusable->status : null; } - public function status(?string $name = null): ?Status + public function setStatus(string $name, ?string $type = null): ?Status { - /** @var Statusable|null $statusable */ - $statusable = $this->statusable; + $type = $type ?? get_class($this); + $statusable = $this->getStatusableByType($type); - if ($name) { - $status = $this->getStatusByName($name); + $status = $this->getStatusByName($name, $type); - if (! $statusable) { - $statusable = new Statusable(); - $statusable->statusable()->associate($this); - } - - $statusable->status()->associate($status)->save(); - $this->load('statusable'); + if (! $statusable) { + $statusable = new Statusable(); + $statusable->type = $type; + $statusable->statusable()->associate($this); } - return $statusable ? $statusable->status : null; + $statusable->status()->associate($status)->save(); + $this->load('statusables'); + + return $statusable->status; + } + + public function statusables() + { + return $this->morphMany(Statusable::class, 'statusable'); } - public function statusable() + private function getStatusableByType(string $type): ?Statusable { - return $this->morphOne(Statusable::class, 'statusable'); + return $this->statusables()->where('type', '=', $type)->first(); } - private function getStatusByName(string $name): Status + private function getStatusByName(string $name, string $type): Status { if ($this->dynamicStatusCreation) { - return Status::createStatus($this->statusType(), $name); + return Status::createStatus($type, $name); } - if ($status = Status::findStatus($this->statusType(), $name)) { + if ($status = Status::findStatus($type, $name)) { return $status; } - throw new UnknownStatusException($this->statusType(), $name); + throw new UnknownStatusException($type, $name); } } diff --git a/tests/Unit/Traits/HasStatusesTest.php b/tests/Unit/Traits/HasStatusesTest.php index e9859e2..1ef022a 100644 --- a/tests/Unit/Traits/HasStatusesTest.php +++ b/tests/Unit/Traits/HasStatusesTest.php @@ -25,7 +25,7 @@ public function no_status_set() $model = new TestModel(); $model->save(); - $status = $model->status(); + $status = $model->getStatus(); $this->assertNull($status); } @@ -40,13 +40,46 @@ public function set_valid_status() $model->save(); $this->actingAs(User::factory()->create()); - $model->status('test status'); + $model->setStatus('test status'); - $foundStatus = $model->status(); + $foundStatus = $model->getStatus(); $this->assertNotNull($foundStatus); $this->assertEquals($status->id, $foundStatus->id); } + /** @test */ + public function set_valid_status_by_type() + { + TestModel::createTable(); + + Status::publishStatuses('type1', ['statusA', 'statusB']); + Status::publishStatuses('type2', ['status1', 'status2']); + + $model = new TestModel(); + $model->save(); + + $this->actingAs(User::factory()->create()); + $model->setStatus('statusA', 'type1'); + $model->setStatus('status1', 'type2'); + + $foundStatus = $model->getStatus(); + $this->assertNull($foundStatus); + + $foundStatus = $model->getStatus('type1'); + $this->assertNotNull($foundStatus); + $this->assertEquals('statusA', (string) $foundStatus); + + $foundStatus = $model->getStatus('type2'); + $this->assertNotNull($foundStatus); + $this->assertEquals('status1', (string) $foundStatus); + + $model->setStatus('statusB', 'type1'); + $model->setStatus('status2', 'type2'); + + $this->assertEquals('statusB', (string) $model->getStatus('type1')); + $this->assertEquals('status2', (string) $model->getStatus('type2')); + } + /** @test */ public function set_unknown_status() { @@ -59,7 +92,7 @@ public function set_unknown_status() $this->expectExceptionMessage("Unknown status value 'test status' for status type Tipoff\Statuses\Tests\Unit\Traits\TestModel"); $this->actingAs(User::factory()->create()); - $model->status('test status'); + $model->setStatus('test status'); } /** @test */ @@ -72,7 +105,7 @@ public function set_unknown_status_dynamic_creation() $model->setDynamicStaticCreation(true); $this->actingAs(User::factory()->create()); - $status = $model->status('test status'); + $status = $model->setStatus('test status'); $this->assertNotNull($status); $this->assertEquals('test status', (string) $status); From cc0eb731dd08d64e7e22ce70754e32bf2f6913b1 Mon Sep 17 00:00:00 2001 From: Drew Roberts Date: Wed, 10 Mar 2021 11:44:38 -0500 Subject: [PATCH 4/5] Remove updated_at timestamp from statusables --- .../migrations/2020_05_08_110000_create_statusables_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2020_05_08_110000_create_statusables_table.php b/database/migrations/2020_05_08_110000_create_statusables_table.php index ce147e1..a5cb3c5 100644 --- a/database/migrations/2020_05_08_110000_create_statusables_table.php +++ b/database/migrations/2020_05_08_110000_create_statusables_table.php @@ -16,7 +16,7 @@ public function up() $table->morphs('statusable'); $table->string('type'); // Typically, full class name for model using status $table->foreignIdFor(app('user'), 'creator_id'); - $table->timestamps(); + $table->timestamp('created_at'); }); } } From 05875338fb4097239b43c3fe9be901d45fbab2f0 Mon Sep 17 00:00:00 2001 From: Patrick Breen Date: Wed, 10 Mar 2021 12:07:24 -0500 Subject: [PATCH 5/5] Change Statusable to StatusRecord and allow for history in table --- ...bleFactory.php => StatusRecordFactory.php} | 7 ++- ...08_110000_create_status_records_table.php} | 4 +- .../{Statusable.php => StatusRecord.php} | 5 ++- src/Traits/HasStatuses.php | 43 +++++++++++-------- ...tatusableTest.php => StatusRecordTest.php} | 6 +-- tests/Unit/Traits/HasStatusesTest.php | 40 +++++++++++++++++ 6 files changed, 77 insertions(+), 28 deletions(-) rename database/factories/{StatusableFactory.php => StatusRecordFactory.php} (80%) rename database/migrations/{2020_05_08_110000_create_statusables_table.php => 2020_05_08_110000_create_status_records_table.php} (81%) rename src/Models/{Statusable.php => StatusRecord.php} (92%) rename tests/Unit/Models/{StatusableTest.php => StatusRecordTest.php} (81%) diff --git a/database/factories/StatusableFactory.php b/database/factories/StatusRecordFactory.php similarity index 80% rename from database/factories/StatusableFactory.php rename to database/factories/StatusRecordFactory.php index 0839769..3209e3a 100644 --- a/database/factories/StatusableFactory.php +++ b/database/factories/StatusRecordFactory.php @@ -5,14 +5,13 @@ namespace Tipoff\Statuses\Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Str; use Tipoff\Authorization\Models\User; use Tipoff\Statuses\Models\Status; -use Tipoff\Statuses\Models\Statusable; +use Tipoff\Statuses\Models\StatusRecord; -class StatusableFactory extends Factory +class StatusRecordFactory extends Factory { - protected $model = Statusable::class; + protected $model = StatusRecord::class; public function definition() { diff --git a/database/migrations/2020_05_08_110000_create_statusables_table.php b/database/migrations/2020_05_08_110000_create_status_records_table.php similarity index 81% rename from database/migrations/2020_05_08_110000_create_statusables_table.php rename to database/migrations/2020_05_08_110000_create_status_records_table.php index a5cb3c5..ca6050b 100644 --- a/database/migrations/2020_05_08_110000_create_statusables_table.php +++ b/database/migrations/2020_05_08_110000_create_status_records_table.php @@ -6,11 +6,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateStatusablesTable extends Migration +class CreateStatusRecordsTable extends Migration { public function up() { - Schema::create('statusables', function (Blueprint $table) { + Schema::create('status_records', function (Blueprint $table) { $table->id(); $table->foreignIdFor(app('status'))->index(); $table->morphs('statusable'); diff --git a/src/Models/Statusable.php b/src/Models/StatusRecord.php similarity index 92% rename from src/Models/Statusable.php rename to src/Models/StatusRecord.php index 4d080b3..a08f116 100644 --- a/src/Models/Statusable.php +++ b/src/Models/StatusRecord.php @@ -17,15 +17,16 @@ * @property Model statusable * @property User creator * @property Carbon created_at - * @property Carbon updated_at * // Raw Relations * @property int|null creator_id */ -class Statusable extends BaseModel +class StatusRecord extends BaseModel { use HasPackageFactory; use HasCreator; + const UPDATED_AT = null; + protected $casts = [ 'id' => 'integer', 'creator_id' => 'integer', diff --git a/src/Traits/HasStatuses.php b/src/Traits/HasStatuses.php index 51fb27e..2b2908c 100644 --- a/src/Traits/HasStatuses.php +++ b/src/Traits/HasStatuses.php @@ -7,7 +7,7 @@ use Illuminate\Support\Collection; use Tipoff\Statuses\Exceptions\UnknownStatusException; use Tipoff\Statuses\Models\Status; -use Tipoff\Statuses\Models\Statusable; +use Tipoff\Statuses\Models\StatusRecord; /** * @property Collection statusables @@ -18,40 +18,49 @@ trait HasStatuses // When false, exception occurs if unknown status value is used protected bool $dynamicStatusCreation = false; + public function getStatusHistory(?string $type = null): Collection + { + return $this->statusRecords() + ->where('type', '=', $type ?? get_class($this)) + ->orderBy('created_at', 'desc') + ->orderBy('id', 'desc') + ->get(); + } + public function getStatus(?string $type = null): ?Status { - $statusable = $this->getStatusableByType($type ?? get_class($this)); + $statusRecord = $this->getStatusHistory($type)->first(); - return $statusable ? $statusable->status : null; + return $statusRecord ? $statusRecord->status : null; } - public function setStatus(string $name, ?string $type = null): ?Status + public function setStatus(string $name, ?string $type = null): Status { $type = $type ?? get_class($this); - $statusable = $this->getStatusableByType($type); $status = $this->getStatusByName($name, $type); - if (! $statusable) { - $statusable = new Statusable(); - $statusable->type = $type; - $statusable->statusable()->associate($this); + // Check if desired status already set + if ($currentStatus = $this->getStatus($type)) { + if ($currentStatus->id === $status->id) { + return $currentStatus; + } } + // Set status always creates new record, leaving full history behind + $statusable = new StatusRecord(); + $statusable->type = $type; + $statusable->statusable()->associate($this); + $statusable->status()->associate($status)->save(); - $this->load('statusables'); + $this->load('statusRecords'); return $statusable->status; } - public function statusables() - { - return $this->morphMany(Statusable::class, 'statusable'); - } - - private function getStatusableByType(string $type): ?Statusable + public function statusRecords() { - return $this->statusables()->where('type', '=', $type)->first(); + return $this->morphMany(StatusRecord::class, 'statusable'); } private function getStatusByName(string $name, string $type): Status diff --git a/tests/Unit/Models/StatusableTest.php b/tests/Unit/Models/StatusRecordTest.php similarity index 81% rename from tests/Unit/Models/StatusableTest.php rename to tests/Unit/Models/StatusRecordTest.php index 0c6e34c..6ce86f2 100644 --- a/tests/Unit/Models/StatusableTest.php +++ b/tests/Unit/Models/StatusRecordTest.php @@ -7,17 +7,17 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tipoff\Statuses\Models\Status; -use Tipoff\Statuses\Models\Statusable; +use Tipoff\Statuses\Models\StatusRecord; use Tipoff\Statuses\Tests\TestCase; -class StatusableTest extends TestCase +class StatusRecordTest extends TestCase { use DatabaseTransactions; /** @test */ public function create() { - $model = Statusable::factory()->create(); + $model = StatusRecord::factory()->create(); $this->assertNotNull($model); $status = $model->status; diff --git a/tests/Unit/Traits/HasStatusesTest.php b/tests/Unit/Traits/HasStatusesTest.php index 1ef022a..9acd487 100644 --- a/tests/Unit/Traits/HasStatusesTest.php +++ b/tests/Unit/Traits/HasStatusesTest.php @@ -8,6 +8,7 @@ use Tipoff\Authorization\Models\User; use Tipoff\Statuses\Exceptions\UnknownStatusException; use Tipoff\Statuses\Models\Status; +use Tipoff\Statuses\Models\StatusRecord; use Tipoff\Statuses\Tests\TestCase; use Tipoff\Statuses\Traits\HasStatuses; use Tipoff\Support\Models\BaseModel; @@ -80,6 +81,45 @@ public function set_valid_status_by_type() $this->assertEquals('status2', (string) $model->getStatus('type2')); } + public function get_status_history() + { + TestModel::createTable(); + + Status::publishStatuses('type1', ['A', 'B', 'C', 'D']); + Status::publishStatuses('type2', ['1', '2', '3', '4']); + + $model = new TestModel(); + $model->save(); + + $this->actingAs(User::factory()->create()); + $model->setStatus('A', 'type1'); + $model->setStatus('D', 'type1'); + $model->setStatus('C', 'type1'); + $model->setStatus('A', 'type1'); + $model->setStatus('B', 'type1'); + $history = $model->getStatusHistory('type1') + ->map(function (StatusRecord $statusRecord) { + return (string) $statusRecord->status; + }) + ->toArray(); + $this->assertEquals(['B','A','C','D','A'], $history); + + $model->setStatus('3', 'type2'); + $model->setStatus('3', 'type2'); + $model->setStatus('2', 'type2'); + $model->setStatus('2', 'type2'); + $model->setStatus('3', 'type2'); + $model->setStatus('3', 'type2'); + $model->setStatus('2', 'type2'); + $model->setStatus('2', 'type2'); + $history = $model->getStatusHistory('type2') + ->map(function (StatusRecord $statusRecord) { + return (string) $statusRecord->status; + }) + ->toArray(); + $this->assertEquals(['2', '3', '2', '3'], $history); + } + /** @test */ public function set_unknown_status() {