Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HasStatuses trait and supporting code #26

Merged
merged 5 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 6 additions & 17 deletions database/factories/StatusFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
}
}
29 changes: 29 additions & 0 deletions database/factories/StatusRecordFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Tipoff\Statuses\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Tipoff\Authorization\Models\User;
use Tipoff\Statuses\Models\Status;
use Tipoff\Statuses\Models\StatusRecord;

class StatusRecordFactory extends Factory
{
protected $model = StatusRecord::class;

public function definition()
{
$statusable = User::factory()->create();
$status = randomOrCreate(Status::class);

return [
'status_id' => $status,
'type' => $status->type,
'statusable_type' => get_class($statusable),
'statusable_id' => $statusable->id,
'creator_id' => randomOrCreate(app('user')),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note change to 'type' here. The HasStatuses trait defaults to using the fully qualified model name as the type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chx2 Take a look at the changes in this PR for what you're doing in tipoff/bookings with Status:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pdbreen You prefer using type instead of applies_to for this field?

CC: @sl0wik

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do. Its somewhat tied to potentially allowing a single model to have multiple, different types of statuses. But, even if that's not allowed, I think type is still a more appropriate term for this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Thanks for the explanation

$table->text('note')->nullable();
$table->timestamps();

$table->unique(['name', 'type']);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uniqueness is not name alone - but name for a given type. Its certainly reasonable for two unrelated status types to have overlapping names.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Thanks

});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
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');
$table->string('type'); // Typically, full class name for model using status
$table->foreignIdFor(app('user'), 'creator_id');
$table->timestamps();
$table->timestamp('created_at');
});
}
}
9 changes: 9 additions & 0 deletions src/Exceptions/StatusException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tipoff\Statuses\Exceptions;

interface StatusException
{
}
15 changes: 15 additions & 0 deletions src/Exceptions/UnknownStatusException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tipoff\Statuses\Exceptions;

use Throwable;

class UnknownStatusException extends \InvalidArgumentException implements StatusException
{
public function __construct(string $type, string $name, $code = 0, Throwable $previous = null)
{
parent::__construct("Unknown status value '{$name}' for status type {$type}", $code, $previous);
}
}
77 changes: 76 additions & 1 deletion src/Models/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,87 @@

namespace Tipoff\Statuses\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Tipoff\Support\Models\BaseModel;
use Tipoff\Support\Traits\HasPackageFactory;

/**
* @property int id
* @property string name
* @property string type
* @property string slug
* @property string note
* @property Carbon created_at
* @property Carbon updated_at
*/
class Status extends BaseModel
{
use HasPackageFactory;

protected $casts = [];
protected $casts = [
'id' => 'integer',
];

protected $fillable = [
'type',
'name',
];

public static function publishStatuses(string $type, array $names): Collection
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method would be used in migrations to establish valid statuses for a given type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that. Thanks

{
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;
}
}
44 changes: 44 additions & 0 deletions src/Models/StatusRecord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Tipoff\Statuses\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Tipoff\Authorization\Models\User;
use Tipoff\Support\Models\BaseModel;
use Tipoff\Support\Traits\HasCreator;
use Tipoff\Support\Traits\HasPackageFactory;

/**
* @property int id
* @property Status status
* @property Model statusable
* @property User creator
* @property Carbon created_at
* // Raw Relations
* @property int|null creator_id
*/
class StatusRecord extends BaseModel
{
use HasPackageFactory;
use HasCreator;

const UPDATED_AT = null;

protected $casts = [
'id' => 'integer',
'creator_id' => 'integer',
];

public function status()
{
return $this->belongsTo(Status::class);
}

public function statusable()
{
return $this->morphTo();
}
}
78 changes: 78 additions & 0 deletions src/Traits/HasStatuses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Tipoff\Statuses\Traits;

use Illuminate\Support\Collection;
use Tipoff\Statuses\Exceptions\UnknownStatusException;
use Tipoff\Statuses\Models\Status;
use Tipoff\Statuses\Models\StatusRecord;

/**
* @property Collection statusables
*/
trait HasStatuses
{
// When true, new statuses will be added automatically on first use
// When false, exception occurs if unknown status value is used
protected bool $dynamicStatusCreation = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be pulled if you only want to allow pre-defined statuses to be used. Included only because it was easy to add.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting. Not sure if it is necessary since Nova allows a create resource option from within another model.


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
{
$statusRecord = $this->getStatusHistory($type)->first();

return $statusRecord ? $statusRecord->status : null;
}

public function setStatus(string $name, ?string $type = null): Status
{
$type = $type ?? get_class($this);

$status = $this->getStatusByName($name, $type);

// Check if desired status already set
if ($currentStatus = $this->getStatus($type)) {
if ($currentStatus->id === $status->id) {
return $currentStatus;
}
}
Comment on lines +43 to +48
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drewroberts - want to call this out. If the requested status is the same as the current status, a new record is NOT created. If you always want a record, even if the requested status matches current status, it can be removed and tests updated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a correct implementation for what I want. Thank you!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshtorres Could you make sure this logic is also added to GmbDetails and GmbHours in the the tipoff/location (TIPOFF/locations#44) package and PlaceDetails and PlaceHours in the tipoff/seo package (TIPOFF/seo#22)?


// 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('statusRecords');

return $statusable->status;
}

public function statusRecords()
{
return $this->morphMany(StatusRecord::class, 'statusable');
}

private function getStatusByName(string $name, string $type): Status
{
if ($this->dynamicStatusCreation) {
return Status::createStatus($type, $name);
}

if ($status = Status::findStatus($type, $name)) {
return $status;
}

throw new UnknownStatusException($type, $name);
}
}
15 changes: 0 additions & 15 deletions tests/Support/Providers/NovaPackageServiceProvider.php

This file was deleted.

2 changes: 1 addition & 1 deletion tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
use Spatie\Permission\PermissionServiceProvider;
use Tipoff\Authorization\AuthorizationServiceProvider;
use Tipoff\Statuses\StatusesServiceProvider;
use Tipoff\Statuses\Tests\Support\Providers\NovaPackageServiceProvider;
use Tipoff\Support\SupportServiceProvider;
use Tipoff\TestSupport\BaseTestCase;
use Tipoff\TestSupport\Providers\NovaPackageServiceProvider;

class TestCase extends BaseTestCase
{
Expand Down
Loading