style updates

This commit is contained in:
Mark Gregory
2026-07-24 14:59:44 +01:00
parent 24ab771cd3
commit dd23205d0b
482 changed files with 7173 additions and 1435 deletions
+17 -2
View File
@@ -1,4 +1,19 @@
<?php
namespace Modules\Workflow\Concerns;
use Illuminate\Database\Eloquent\Relations\MorphOne; use Modules\Workflow\Models\WorkflowInstance;
trait HasWorkflow { public function workflowInstance(): MorphOne { return $this->morphOne(WorkflowInstance::class,'subject'); } public function workflowLabel(): string { return (string) ($this->title ?? $this->name ?? class_basename($this).' #'.$this->getKey()); } }
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Modules\Workflow\Models\WorkflowInstance;
trait HasWorkflow
{
public function workflowInstance(): MorphOne
{
return $this->morphOne(WorkflowInstance::class, 'subject');
}
public function workflowLabel(): string
{
return (string) ($this->title ?? $this->name ?? class_basename($this).' #'.$this->getKey());
}
}
+1
View File
@@ -1,2 +1,3 @@
<?php
return ['name' => 'Workflow', 'default_due_days' => 3];
+9 -1
View File
@@ -1,4 +1,12 @@
<?php
namespace Modules\Workflow\Contracts;
use Illuminate\Database\Eloquent\Relations\MorphOne;
interface Workflowable { public function workflowInstance(): MorphOne; public function workflowLabel(): string; }
interface Workflowable
{
public function workflowInstance(): MorphOne;
public function workflowLabel(): string;
}
@@ -1,14 +1,81 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('workflow_definitions', function(Blueprint $t){ $t->id(); $t->string('name'); $t->string('slug')->unique(); $t->text('description')->nullable(); $t->string('subject_type')->nullable()->index(); $t->boolean('is_active')->default(true); $t->timestamps(); });
Schema::create('workflow_stages', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); $t->string('name'); $t->string('slug'); $t->unsignedInteger('position')->default(0); $t->string('required_permission')->nullable(); $t->boolean('is_initial')->default(false); $t->boolean('is_terminal')->default(false); $t->boolean('allows_rejection')->default(true); $t->unsignedInteger('due_days')->nullable(); $t->timestamps(); $t->unique(['workflow_definition_id','slug']); });
Schema::create('workflow_transitions', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); $t->foreignId('from_stage_id')->constrained('workflow_stages')->cascadeOnDelete(); $t->foreignId('to_stage_id')->constrained('workflow_stages')->cascadeOnDelete(); $t->string('name'); $t->string('action'); $t->string('required_permission')->nullable(); $t->boolean('requires_comment')->default(false); $t->timestamps(); });
Schema::create('workflow_instances', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); $t->foreignId('workflow_stage_id')->constrained('workflow_stages'); $t->string('subject_type'); $t->unsignedBigInteger('subject_id'); $t->string('status')->default('active'); $t->foreignId('started_by')->nullable()->constrained('users')->nullOnDelete(); $t->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete(); $t->timestamp('due_at')->nullable(); $t->timestamp('completed_at')->nullable(); $t->timestamps(); $t->unique(['subject_type','subject_id']); $t->index(['status','workflow_stage_id']); });
Schema::create('workflow_history', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_instance_id')->constrained()->cascadeOnDelete(); $t->foreignId('from_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete(); $t->foreignId('to_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete(); $t->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); $t->string('action'); $t->text('comment')->nullable(); $t->json('metadata')->nullable(); $t->timestamps(); });
}
public function down(): void { Schema::dropIfExists('workflow_history'); Schema::dropIfExists('workflow_instances'); Schema::dropIfExists('workflow_transitions'); Schema::dropIfExists('workflow_stages'); Schema::dropIfExists('workflow_definitions'); }
return new class extends Migration
{
public function up(): void
{
Schema::create('workflow_definitions', function (Blueprint $t) {
$t->id();
$t->string('name');
$t->string('slug')->unique();
$t->text('description')->nullable();
$t->string('subject_type')->nullable()->index();
$t->boolean('is_active')->default(true);
$t->timestamps();
});
Schema::create('workflow_stages', function (Blueprint $t) {
$t->id();
$t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete();
$t->string('name');
$t->string('slug');
$t->unsignedInteger('position')->default(0);
$t->string('required_permission')->nullable();
$t->boolean('is_initial')->default(false);
$t->boolean('is_terminal')->default(false);
$t->boolean('allows_rejection')->default(true);
$t->unsignedInteger('due_days')->nullable();
$t->timestamps();
$t->unique(['workflow_definition_id', 'slug']);
});
Schema::create('workflow_transitions', function (Blueprint $t) {
$t->id();
$t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete();
$t->foreignId('from_stage_id')->constrained('workflow_stages')->cascadeOnDelete();
$t->foreignId('to_stage_id')->constrained('workflow_stages')->cascadeOnDelete();
$t->string('name');
$t->string('action');
$t->string('required_permission')->nullable();
$t->boolean('requires_comment')->default(false);
$t->timestamps();
});
Schema::create('workflow_instances', function (Blueprint $t) {
$t->id();
$t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete();
$t->foreignId('workflow_stage_id')->constrained('workflow_stages');
$t->string('subject_type');
$t->unsignedBigInteger('subject_id');
$t->string('status')->default('active');
$t->foreignId('started_by')->nullable()->constrained('users')->nullOnDelete();
$t->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
$t->timestamp('due_at')->nullable();
$t->timestamp('completed_at')->nullable();
$t->timestamps();
$t->unique(['subject_type', 'subject_id']);
$t->index(['status', 'workflow_stage_id']);
});
Schema::create('workflow_history', function (Blueprint $t) {
$t->id();
$t->foreignId('workflow_instance_id')->constrained()->cascadeOnDelete();
$t->foreignId('from_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete();
$t->foreignId('to_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete();
$t->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$t->string('action');
$t->text('comment')->nullable();
$t->json('metadata')->nullable();
$t->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('workflow_history');
Schema::dropIfExists('workflow_instances');
Schema::dropIfExists('workflow_transitions');
Schema::dropIfExists('workflow_stages');
Schema::dropIfExists('workflow_definitions');
}
};
@@ -1,8 +1,44 @@
<?php
namespace Modules\Workflow\Http\Controllers;
use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; use Illuminate\View\View; use Modules\Workflow\Models\WorkflowDefinition; use Modules\Workflow\Models\WorkflowInstance; use Modules\Workflow\Services\WorkflowManager;
final class WorkflowController extends Controller {
public function index(): View { abort_unless(auth()->user()->can('workflow.view'),403); return view('workflow::admin.index',['definitions'=>WorkflowDefinition::with('stages')->latest()->get(),'queue'=>WorkflowInstance::with(['definition','stage','subject'])->where('status','active')->latest()->paginate(20),'completed'=>WorkflowInstance::where('status','completed')->count()]); }
public function store(Request $request): RedirectResponse { abort_unless($request->user()->can('workflow.manage'),403); $data=$request->validate(['name'=>'required|string|max:120','description'=>'nullable|string','subject_type'=>'nullable|string|max:255']); $definition=WorkflowDefinition::create($data+['slug'=>Str::slug($data['name']),'is_active'=>true]); $draft=$definition->stages()->create(['name'=>'Draft','slug'=>'draft','position'=>0,'is_initial'=>true,'due_days'=>null]); $review=$definition->stages()->create(['name'=>'Review','slug'=>'review','position'=>10,'required_permission'=>'workflow.review','due_days'=>3]); $published=$definition->stages()->create(['name'=>'Approved','slug'=>'approved','position'=>20,'is_terminal'=>true,'allows_rejection'=>false]); $definition->transitions()->createMany([['from_stage_id'=>$draft->id,'to_stage_id'=>$review->id,'name'=>'Submit for review','action'=>'submit'],['from_stage_id'=>$review->id,'to_stage_id'=>$published->id,'name'=>'Approve','action'=>'approve','required_permission'=>'workflow.review'],['from_stage_id'=>$review->id,'to_stage_id'=>$draft->id,'name'=>'Request changes','action'=>'reject','required_permission'=>'workflow.review','requires_comment'=>true]]); return back()->with('success','Workflow created.'); }
public function transition(Request $request, WorkflowInstance $instance, WorkflowManager $manager): RedirectResponse { abort_unless($request->user()->can('workflow.review'),403); $data=$request->validate(['action'=>'required|string','comment'=>'nullable|string|max:2000']); $manager->transition($instance,$data['action'],$request->user(),$data['comment']??null); return back()->with('success','Workflow updated.'); }
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Modules\Workflow\Models\WorkflowDefinition;
use Modules\Workflow\Models\WorkflowInstance;
use Modules\Workflow\Services\WorkflowManager;
final class WorkflowController extends Controller
{
public function index(): View
{
abort_unless(auth()->user()->can('workflow.view'), 403);
return view('workflow::admin.index', ['definitions' => WorkflowDefinition::with('stages')->latest()->get(), 'queue' => WorkflowInstance::with(['definition', 'stage', 'subject'])->where('status', 'active')->latest()->paginate(20), 'completed' => WorkflowInstance::where('status', 'completed')->count()]);
}
public function store(Request $request): RedirectResponse
{
abort_unless($request->user()->can('workflow.manage'), 403);
$data = $request->validate(['name' => 'required|string|max:120', 'description' => 'nullable|string', 'subject_type' => 'nullable|string|max:255']);
$definition = WorkflowDefinition::create($data + ['slug' => Str::slug($data['name']), 'is_active' => true]);
$draft = $definition->stages()->create(['name' => 'Draft', 'slug' => 'draft', 'position' => 0, 'is_initial' => true, 'due_days' => null]);
$review = $definition->stages()->create(['name' => 'Review', 'slug' => 'review', 'position' => 10, 'required_permission' => 'workflow.review', 'due_days' => 3]);
$published = $definition->stages()->create(['name' => 'Approved', 'slug' => 'approved', 'position' => 20, 'is_terminal' => true, 'allows_rejection' => false]);
$definition->transitions()->createMany([['from_stage_id' => $draft->id, 'to_stage_id' => $review->id, 'name' => 'Submit for review', 'action' => 'submit'], ['from_stage_id' => $review->id, 'to_stage_id' => $published->id, 'name' => 'Approve', 'action' => 'approve', 'required_permission' => 'workflow.review'], ['from_stage_id' => $review->id, 'to_stage_id' => $draft->id, 'name' => 'Request changes', 'action' => 'reject', 'required_permission' => 'workflow.review', 'requires_comment' => true]]);
return back()->with('success', 'Workflow created.');
}
public function transition(Request $request, WorkflowInstance $instance, WorkflowManager $manager): RedirectResponse
{
abort_unless($request->user()->can('workflow.review'), 403);
$data = $request->validate(['action' => 'required|string', 'comment' => 'nullable|string|max:2000']);
$manager->transition($instance, $data['action'], $request->user(), $data['comment'] ?? null);
return back()->with('success', 'Workflow updated.');
}
}
+24 -2
View File
@@ -1,4 +1,26 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany;
final class WorkflowDefinition extends Model { protected $fillable=['name','slug','description','subject_type','is_active']; protected function casts(): array { return ['is_active'=>'boolean']; } public function stages(): HasMany { return $this->hasMany(WorkflowStage::class)->orderBy('position'); } public function transitions(): HasMany { return $this->hasMany(WorkflowTransition::class); } }
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class WorkflowDefinition extends Model
{
protected $fillable = ['name', 'slug', 'description', 'subject_type', 'is_active'];
protected function casts(): array
{
return ['is_active' => 'boolean'];
}
public function stages(): HasMany
{
return $this->hasMany(WorkflowStage::class)->orderBy('position');
}
public function transitions(): HasMany
{
return $this->hasMany(WorkflowTransition::class);
}
}
+14 -1
View File
@@ -1,4 +1,17 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model;
final class WorkflowHistory extends Model { protected $table='workflow_history'; protected $fillable=['workflow_instance_id','from_stage_id','to_stage_id','user_id','action','comment','metadata']; protected function casts(): array { return ['metadata'=>'array']; } }
final class WorkflowHistory extends Model
{
protected $table = 'workflow_history';
protected $fillable = ['workflow_instance_id', 'from_stage_id', 'to_stage_id', 'user_id', 'action', 'comment', 'metadata'];
protected function casts(): array
{
return ['metadata' => 'array'];
}
}
+36 -2
View File
@@ -1,4 +1,38 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo;
final class WorkflowInstance extends Model { protected $fillable=['workflow_definition_id','workflow_stage_id','subject_type','subject_id','status','started_by','assigned_to','due_at','completed_at']; protected function casts(): array { return ['due_at'=>'datetime','completed_at'=>'datetime']; } public function definition(): BelongsTo { return $this->belongsTo(WorkflowDefinition::class,'workflow_definition_id'); } public function stage(): BelongsTo { return $this->belongsTo(WorkflowStage::class,'workflow_stage_id'); } public function subject(): MorphTo { return $this->morphTo(); } public function history(): HasMany { return $this->hasMany(WorkflowHistory::class)->latest(); } }
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
final class WorkflowInstance extends Model
{
protected $fillable = ['workflow_definition_id', 'workflow_stage_id', 'subject_type', 'subject_id', 'status', 'started_by', 'assigned_to', 'due_at', 'completed_at'];
protected function casts(): array
{
return ['due_at' => 'datetime', 'completed_at' => 'datetime'];
}
public function definition(): BelongsTo
{
return $this->belongsTo(WorkflowDefinition::class, 'workflow_definition_id');
}
public function stage(): BelongsTo
{
return $this->belongsTo(WorkflowStage::class, 'workflow_stage_id');
}
public function subject(): MorphTo
{
return $this->morphTo();
}
public function history(): HasMany
{
return $this->hasMany(WorkflowHistory::class)->latest();
}
}
+19 -2
View File
@@ -1,4 +1,21 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class WorkflowStage extends Model { protected $fillable=['workflow_definition_id','name','slug','position','required_permission','is_initial','is_terminal','allows_rejection','due_days']; protected function casts(): array { return ['is_initial'=>'boolean','is_terminal'=>'boolean','allows_rejection'=>'boolean']; } public function definition(): BelongsTo { return $this->belongsTo(WorkflowDefinition::class,'workflow_definition_id'); } }
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class WorkflowStage extends Model
{
protected $fillable = ['workflow_definition_id', 'name', 'slug', 'position', 'required_permission', 'is_initial', 'is_terminal', 'allows_rejection', 'due_days'];
protected function casts(): array
{
return ['is_initial' => 'boolean', 'is_terminal' => 'boolean', 'allows_rejection' => 'boolean'];
}
public function definition(): BelongsTo
{
return $this->belongsTo(WorkflowDefinition::class, 'workflow_definition_id');
}
}
+12 -1
View File
@@ -1,4 +1,15 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model;
final class WorkflowTransition extends Model { protected $fillable=['workflow_definition_id','from_stage_id','to_stage_id','name','action','required_permission','requires_comment']; protected function casts(): array { return ['requires_comment'=>'boolean']; } }
final class WorkflowTransition extends Model
{
protected $fillable = ['workflow_definition_id', 'from_stage_id', 'to_stage_id', 'name', 'action', 'required_permission', 'requires_comment'];
protected function casts(): array
{
return ['requires_comment' => 'boolean'];
}
}
@@ -1,5 +1,15 @@
<?php
namespace Modules\Workflow\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider { public function boot(): void { parent::boot(); Route::middleware('web')->group(module_path('Workflow', 'Routes/web.php')); } }
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
Route::middleware('web')->group(module_path('Workflow', 'Routes/web.php'));
}
}
@@ -1,8 +1,22 @@
<?php
namespace Modules\Workflow\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Workflow\Services\WorkflowManager;
final class WorkflowServiceProvider extends ServiceProvider {
public function register(): void { $this->mergeConfigFrom(module_path('Workflow','Config/config.php'),'workflow'); $this->app->singleton(WorkflowManager::class); $this->app->register(RouteServiceProvider::class); }
public function boot(): void { $this->loadMigrationsFrom(module_path('Workflow','Database/Migrations')); $this->loadViewsFrom(module_path('Workflow','Resources/views'),'workflow'); }
final class WorkflowServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(module_path('Workflow', 'Config/config.php'), 'workflow');
$this->app->singleton(WorkflowManager::class);
$this->app->register(RouteServiceProvider::class);
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('Workflow', 'Database/Migrations'));
$this->loadViewsFrom(module_path('Workflow', 'Resources/views'), 'workflow');
}
}
+9 -2
View File
@@ -1,3 +1,10 @@
<?php
use Illuminate\Support\Facades\Route; use Modules\Workflow\Http\Controllers\WorkflowController;
Route::middleware(['auth','verified'])->prefix('administration/workflows')->name('administration.workflows.')->group(function(){ Route::get('/',[WorkflowController::class,'index'])->name('index'); Route::post('/',[WorkflowController::class,'store'])->name('store'); Route::post('/{instance}/transition',[WorkflowController::class,'transition'])->name('transition'); });
use Illuminate\Support\Facades\Route;
use Modules\Workflow\Http\Controllers\WorkflowController;
Route::middleware(['auth', 'verified'])->prefix('administration/workflows')->name('administration.workflows.')->group(function () {
Route::get('/', [WorkflowController::class, 'index'])->name('index');
Route::post('/', [WorkflowController::class, 'store'])->name('store');
Route::post('/{instance}/transition', [WorkflowController::class, 'transition'])->name('transition');
});
+43 -4
View File
@@ -1,7 +1,46 @@
<?php
namespace Modules\Workflow\Services;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; use Modules\Workflow\Models\WorkflowDefinition; use Modules\Workflow\Models\WorkflowHistory; use Modules\Workflow\Models\WorkflowInstance; use Modules\Workflow\Models\WorkflowTransition;
final class WorkflowManager {
public function start(Model $subject, WorkflowDefinition $definition, ?Authenticatable $user=null): WorkflowInstance { return DB::transaction(function() use($subject,$definition,$user){ $stage=$definition->stages()->where('is_initial',true)->first() ?? $definition->stages()->firstOrFail(); $instance=WorkflowInstance::updateOrCreate(['subject_type'=>$subject->getMorphClass(),'subject_id'=>$subject->getKey()],['workflow_definition_id'=>$definition->getKey(),'workflow_stage_id'=>$stage->getKey(),'status'=>$stage->is_terminal?'completed':'active','started_by'=>$user?->getAuthIdentifier(),'due_at'=>$stage->due_days?now()->addDays($stage->due_days):null,'completed_at'=>$stage->is_terminal?now():null]); WorkflowHistory::create(['workflow_instance_id'=>$instance->id,'to_stage_id'=>$stage->id,'user_id'=>$user?->getAuthIdentifier(),'action'=>'started']); return $instance->fresh(['definition','stage']); }); }
public function transition(WorkflowInstance $instance, string $action, ?Authenticatable $user=null, ?string $comment=null): WorkflowInstance { return DB::transaction(function() use($instance,$action,$user,$comment){ $transition=WorkflowTransition::where('workflow_definition_id',$instance->workflow_definition_id)->where('from_stage_id',$instance->workflow_stage_id)->where('action',$action)->first(); if(!$transition) throw ValidationException::withMessages(['action'=>'This transition is not available.']); if($transition->requires_comment && blank($comment)) throw ValidationException::withMessages(['comment'=>'A comment is required.']); if($transition->required_permission && (!$user || !method_exists($user,'can') || !$user->can($transition->required_permission))) abort(403); $target=\Modules\Workflow\Models\WorkflowStage::findOrFail($transition->to_stage_id); $from=$instance->workflow_stage_id; $instance->update(['workflow_stage_id'=>$target->id,'status'=>$target->is_terminal?'completed':'active','due_at'=>$target->due_days?now()->addDays($target->due_days):null,'completed_at'=>$target->is_terminal?now():null]); WorkflowHistory::create(['workflow_instance_id'=>$instance->id,'from_stage_id'=>$from,'to_stage_id'=>$target->id,'user_id'=>$user?->getAuthIdentifier(),'action'=>$action,'comment'=>$comment]); return $instance->fresh(['definition','stage','subject']); }); }
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Modules\Workflow\Models\WorkflowDefinition;
use Modules\Workflow\Models\WorkflowHistory;
use Modules\Workflow\Models\WorkflowInstance;
use Modules\Workflow\Models\WorkflowStage;
use Modules\Workflow\Models\WorkflowTransition;
final class WorkflowManager
{
public function start(Model $subject, WorkflowDefinition $definition, ?Authenticatable $user = null): WorkflowInstance
{
return DB::transaction(function () use ($subject, $definition, $user) {
$stage = $definition->stages()->where('is_initial', true)->first() ?? $definition->stages()->firstOrFail();
$instance = WorkflowInstance::updateOrCreate(['subject_type' => $subject->getMorphClass(), 'subject_id' => $subject->getKey()], ['workflow_definition_id' => $definition->getKey(), 'workflow_stage_id' => $stage->getKey(), 'status' => $stage->is_terminal ? 'completed' : 'active', 'started_by' => $user?->getAuthIdentifier(), 'due_at' => $stage->due_days ? now()->addDays($stage->due_days) : null, 'completed_at' => $stage->is_terminal ? now() : null]);
WorkflowHistory::create(['workflow_instance_id' => $instance->id, 'to_stage_id' => $stage->id, 'user_id' => $user?->getAuthIdentifier(), 'action' => 'started']);
return $instance->fresh(['definition', 'stage']);
});
}
public function transition(WorkflowInstance $instance, string $action, ?Authenticatable $user = null, ?string $comment = null): WorkflowInstance
{
return DB::transaction(function () use ($instance, $action, $user, $comment) {
$transition = WorkflowTransition::where('workflow_definition_id', $instance->workflow_definition_id)->where('from_stage_id', $instance->workflow_stage_id)->where('action', $action)->first();
if (! $transition) {
throw ValidationException::withMessages(['action' => 'This transition is not available.']);
} if ($transition->requires_comment && blank($comment)) {
throw ValidationException::withMessages(['comment' => 'A comment is required.']);
} if ($transition->required_permission && (! $user || ! method_exists($user, 'can') || ! $user->can($transition->required_permission))) {
abort(403);
} $target = WorkflowStage::findOrFail($transition->to_stage_id);
$from = $instance->workflow_stage_id;
$instance->update(['workflow_stage_id' => $target->id, 'status' => $target->is_terminal ? 'completed' : 'active', 'due_at' => $target->due_days ? now()->addDays($target->due_days) : null, 'completed_at' => $target->is_terminal ? now() : null]);
WorkflowHistory::create(['workflow_instance_id' => $instance->id, 'from_stage_id' => $from, 'to_stage_id' => $target->id, 'user_id' => $user?->getAuthIdentifier(), 'action' => $action, 'comment' => $comment]);
return $instance->fresh(['definition', 'stage', 'subject']);
});
}
}
@@ -1,4 +1,14 @@
<?php
namespace Modules\Workflow\Tests\Feature;
use Modules\Workflow\Providers\WorkflowServiceProvider;
use Tests\TestCase;
final class ModuleBootTest extends TestCase { public function test_workflow_module_is_registered(): void { $this->assertTrue(app()->providerIsLoaded(\Modules\Workflow\Providers\WorkflowServiceProvider::class)); } }
final class ModuleBootTest extends TestCase
{
public function test_workflow_module_is_registered(): void
{
$this->assertTrue(app()->providerIsLoaded(WorkflowServiceProvider::class));
}
}