Skip to content

Commit

Permalink
Merge branch 'release/1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cjmellor committed Jul 5, 2022
2 parents a8d71eb + 003ecab commit 7dc9b72
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 68 deletions.
52 changes: 35 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,6 @@ This is the contents of the published config file:
```php
return [
'approval' => [

/**
* The column name for new data.
*
* Default: 'new_data'
*/
'new_data' => 'new_data',


/**
* The column name for original data
*
* Default: 'original_data
*/
'original_data' => 'original_data',

/**
* The approval polymorphic pivot name
*
Expand All @@ -62,7 +46,7 @@ return [
];
```

The config allows you to change the column names as well as the polymorphic pivot name. The latter should always end with `able` though.
The config allows you to change the polymorphic pivot name. It should end with `able` though.

## Usage

Expand Down Expand Up @@ -107,6 +91,40 @@ If you want to check if the Model data will be bypassed, use the `isApprovalBypa
return $model->isApprovalBypassed();
```

## Scopes

The package comes with some helper methods for the Builder, utilising a custom scope - `ApprovalStateScope`

By default, all queries to the `approvals` table will return all the Models' no matter the state.

There are three methods to help you retrieve the state of an Approval.

```php
<?php

use App\Models\Approval;

Approval::approved()->get();
Approval::rejected()->get();
Approval::pending()->count();
```

You can also set a state for an approval:

```php
<?php

use App\Models\Approval;

Approval::where('id', 1)->approve();
Approval::where('id', 2)->reject();
Approval::where('id', 3)->postpone();
```

In the event you need to reset a state, you can use the `withAnyState` helper.

## Disable Approvals

If you don't want Model data to be approved, you can bypass it with the `withoutApproval` method.

```php
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"illuminate/contracts": "^9.0"
},
"require-dev": {
"laravel/pint": "^0.1.7",
"nunomaduro/collision": "^6.0",
"orchestra/testbench": "^7.0",
"pestphp/pest": "^1.21",
Expand Down
16 changes: 0 additions & 16 deletions config/approval.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,6 @@

return [
'approval' => [

/**
* The column name for new data.
*
* Default: 'new_data'
*/
'new_data' => 'new_data',


/**
* The column name for original data
*
* Default: 'original_data
*/
'original_data' => 'original_data',

/**
* The approval polymorphic pivot name
*
Expand Down
15 changes: 7 additions & 8 deletions database/migrations/2022_02_12_195950_create_approvals_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
return new class() extends Migration {
public function up()
{
Schema::create('approvals', function (Blueprint $table) {
Schema::create(table: 'approvals', callback: function (Blueprint $table) {
$table->id();
$table->morphs(config(key: 'approval.approval.approval_pivot'));
$table->enum('state', [0, 1, 2])->default(0)->comment('0:PENDING, 1:APPROVED, 2:REJECTED');
$table->json(config(key: 'approval.approval.new_data'))->nullable();
$table->json(config(key: 'approval.approval.original_data'))->nullable();
$table->nullableMorphs(config(key: 'approval.approval.approval_pivot'));
$table->enum('state', ['pending', 'approved', 'rejected'])->default('pending');
$table->json('new_data')->nullable();
$table->json('original_data')->nullable();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('approvals');
Schema::dropIfExists(table: 'approvals');
}
};
65 changes: 50 additions & 15 deletions src/Concerns/MustBeApproved.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,83 @@

namespace Cjmellor\Approval\Concerns;

use Cjmellor\Approval\Enums\ApprovalStatus;
use Cjmellor\Approval\Models\Approval;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Collection;

trait MustBeApproved
{
protected bool $bypassApproval = false;

public static function bootMustBeApproved(): void
{
static::saved(callback: function ($model) {
if (! $model->wasChanged()) {
return;
}
static::creating(callback: fn ($model) => static::insertApprovalRequest($model));
static::updating(callback: fn ($model) => static::insertApprovalRequest($model));
}

if ($model->isApprovalBypassed()) {
return;
/**
* Create an Approval request before committing to the database.
*/
protected static function insertApprovalRequest($model)
{
if (! $model->isApprovalBypassed()) {
/**
* Create the new Approval model
*/
if (self::approvalModelExists($model)) {
return false;
}

$model->approvals()->create([
(string)config(key: 'approval.approval.new_data') => $model->when($model->wasChanged(), fn () => $model->getChanges()),
(string)config(key: 'approval.approval.original_data') => $model->when($model->wasChanged(), fn () => $model->getOriginalMatchingChanges()),
'new_data' => $model->getDirty(),
'original_data' => $model->getOriginalMatchingChanges(),
]);
});

return false;
}
}

public function isApprovalBypassed(): bool
/**
* Check if the Approval model been created already exists with a 'pending' state
*/
protected static function approvalModelExists($model): bool
{
return $this->bypassApproval;
return Approval::where([
['state', '=', ApprovalStatus::Pending],
['new_data', '=', json_encode($model->getDirty())],
['original_data', '=', json_encode($model->getOriginalMatchingChanges())],
])->exists();
}

/**
* The polymorphic relationship for the Approval model.
*/
public function approvals(): MorphMany
{
return $this->morphMany(related: Approval::class, name: config(key: 'approval.approval.approval_pivot'));
return $this->morphMany(related: Approval::class, name: 'approvalable');
}

protected function getOriginalMatchingChanges(): Collection
/**
* Gets the original model data and only gets the keys that match the dirty attributes.
*/
protected function getOriginalMatchingChanges(): array
{
return collect($this->getOriginal())
->only(collect($this->getChanges())->keys());
->only(collect($this->getDirty())->keys())
->toArray();
}

/**
* Check is the approval can be bypassed.
*/
public function isApprovalBypassed(): bool
{
return $this->bypassApproval;
}

/**
* Approval is ignored and persisted to the database.
*/
public function withoutApproval(): static
{
$this->bypassApproval = true;
Expand Down
8 changes: 4 additions & 4 deletions src/Enums/ApprovalStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace Cjmellor\Approval\Enums;

enum ApprovalStatus: int
enum ApprovalStatus: string
{
case Pending = 0;
case Approved = 1;
case Rejected = 2;
case Pending = 'pending';
case Approved = 'approved';
case Rejected = 'rejected';
}
10 changes: 8 additions & 2 deletions src/Models/Approval.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Cjmellor\Approval\Models;

use Cjmellor\Approval\Enums\ApprovalStatus;
use Cjmellor\Approval\Scopes\ApprovalStateScope;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
Expand All @@ -12,11 +13,16 @@ class Approval extends Model
protected $guarded = [];

protected $casts = [
"{config('approval.approval.new_data')}" => AsArrayObject::class,
"{config('approval.approval.original_data')}" => AsArrayObject::class,
'new_data' => AsArrayObject::class,
'original_data' => AsArrayObject::class,
'state' => ApprovalStatus::class,
];

public static function booted()
{
static::addGlobalScope(new ApprovalStateScope());
}

public function approvalable(): MorphTo
{
return $this->morphTo();
Expand Down
117 changes: 117 additions & 0 deletions src/Scopes/ApprovalStateScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Cjmellor\Approval\Scopes;

use Cjmellor\Approval\Enums\ApprovalStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ApprovalStateScope implements Scope
{
/**
* Add extra extensions.
*/
protected array $extensions = [
// Model with no state
'WithAnyState',
// Get Models with state
'Approved',
'Pending',
'Rejected',
// Set Models with state
'Approve',
'Postpone',
'Reject',
];

/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model)
{
$builder->withAnyState();
}

/**
* Extend the query builder with the needed functions.
*/
public function extend(Builder $builder)
{
foreach ($this->extensions as $extension) {
$this->{"add{$extension}"}($builder);
}
}

/**
* Return all query results with no state.
*/
protected function addWithAnyState(Builder $builder)
{
$builder->macro('withAnyState', fn (Builder $builder) => $builder->withoutGlobalScope($this));
}

/**
* Return only Approval states that are set to 'approved'.
*/
protected function addApproved(Builder $builder)
{
$builder->macro('approved', fn (Builder $builder) => $builder
->withAnyState()
->where(column: 'state', operator: ApprovalStatus::Approved));
}

/**
* Return only Approval states that are set to 'pending'.
*/
protected function addPending(Builder $builder)
{
$builder->macro('pending', fn (Builder $builder) => $builder
->withAnyState()
->where(column: 'state', operator: ApprovalStatus::Pending));
}

/**
* Return only Approval states that are set to 'rejected'.
*/
protected function addRejected(Builder $builder)
{
$builder->macro('rejected', fn (Builder $builder) => $builder
->withAnyState()
->where(column: 'state', operator: ApprovalStatus::Rejected));
}

/**
* Set state as 'approved'.
*/
protected function addApprove(Builder $builder)
{
$builder->macro('approve', fn (Builder $builder) => $this->updateApprovalState($builder, state: ApprovalStatus::Approved));
}

/**
* Set state as 'pending' (default).
*/
protected function addPostpone(Builder $builder)
{
$builder->macro('postpone', fn (Builder $builder) => $this->updateApprovalState($builder, state: ApprovalStatus::Pending));
}

/**
* Set state as 'rejected'
*/
protected function addReject(Builder $builder)
{
$builder->macro('reject', fn (Builder $builder) => $this->updateApprovalState($builder, state: ApprovalStatus::Rejected));
}

/**
* A helper method for updating the approvals state.
*/
protected function updateApprovalState(Builder $builder, $state): int
{
return $builder->update([
'state' => $state,
]);
}
}
Loading

0 comments on commit 7dc9b72

Please sign in to comment.