style updates
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'queue' => env('AUTOMATION_QUEUE', 'default'),
|
||||
'max_steps' => (int) env('AUTOMATION_MAX_STEPS', 50),
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Automation\Models\Automation;
|
||||
use Modules\Automation\Services\AutomationRunner;
|
||||
|
||||
final class DispatchScheduledAutomations extends Command
|
||||
{
|
||||
protected $signature='automation:dispatch-scheduled';
|
||||
protected $description='Dispatch active automations whose next run time is due.';
|
||||
protected $signature = 'automation:dispatch-scheduled';
|
||||
|
||||
protected $description = 'Dispatch active automations whose next run time is due.';
|
||||
|
||||
public function handle(AutomationRunner $runner): int
|
||||
{
|
||||
$count=0; Automation::query()->where('status','active')->where('trigger_type','schedule')->whereNotNull('next_run_at')->where('next_run_at','<=',now())->chunkById(100,function($items)use($runner,&$count){foreach($items as $automation){$runner->dispatch($automation,['scheduled_at'=>now()->toIso8601String()],'schedule');$interval=max(1,(int)($automation->trigger_config['interval_minutes']??60));$automation->update(['next_run_at'=>now()->addMinutes($interval)]);$count++;}});
|
||||
$this->info("Dispatched {$count} scheduled automation(s)."); return self::SUCCESS;
|
||||
$count = 0;
|
||||
Automation::query()->where('status', 'active')->where('trigger_type', 'schedule')->whereNotNull('next_run_at')->where('next_run_at', '<=', now())->chunkById(100, function ($items) use ($runner, &$count) {
|
||||
foreach ($items as $automation) {
|
||||
$runner->dispatch($automation, ['scheduled_at' => now()->toIso8601String()], 'schedule');
|
||||
$interval = max(1, (int) ($automation->trigger_config['interval_minutes'] ?? 60));
|
||||
$automation->update(['next_run_at' => now()->addMinutes($interval)]);
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
$this->info("Dispatched {$count} scheduled automation(s).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Contracts;
|
||||
|
||||
use Modules\Automation\Models\AutomationStep;
|
||||
|
||||
interface ActionRegistry
|
||||
{
|
||||
public function all(): array;
|
||||
|
||||
public function execute(AutomationStep $step, array $payload): array;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Contracts;
|
||||
|
||||
interface TriggerRegistry
|
||||
{
|
||||
public function all(): array;
|
||||
|
||||
public function has(string $type): bool;
|
||||
}
|
||||
|
||||
+44
-18
@@ -1,41 +1,67 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration {
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('automations', function (Blueprint $table) {
|
||||
$table->id(); $table->string('name'); $table->text('description')->nullable();
|
||||
$table->string('trigger_type')->index(); $table->json('trigger_config')->nullable();
|
||||
$table->string('status', 20)->default('draft')->index(); $table->boolean('stop_on_failure')->default(true);
|
||||
$table->unsignedInteger('run_count')->default(0); $table->timestamp('last_ran_at')->nullable();
|
||||
$table->timestamp('next_run_at')->nullable()->index(); $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->string('trigger_type')->index();
|
||||
$table->json('trigger_config')->nullable();
|
||||
$table->string('status', 20)->default('draft')->index();
|
||||
$table->boolean('stop_on_failure')->default(true);
|
||||
$table->unsignedInteger('run_count')->default(0);
|
||||
$table->timestamp('last_ran_at')->nullable();
|
||||
$table->timestamp('next_run_at')->nullable()->index();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('automation_steps', function (Blueprint $table) {
|
||||
$table->id(); $table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete();
|
||||
$table->unsignedSmallInteger('position')->default(1); $table->string('action_type');
|
||||
$table->string('name')->nullable(); $table->json('configuration')->nullable(); $table->boolean('is_enabled')->default(true);
|
||||
$table->timestamps(); $table->unique(['automation_id','position']);
|
||||
$table->id();
|
||||
$table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete();
|
||||
$table->unsignedSmallInteger('position')->default(1);
|
||||
$table->string('action_type');
|
||||
$table->string('name')->nullable();
|
||||
$table->json('configuration')->nullable();
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
$table->timestamps();
|
||||
$table->unique(['automation_id', 'position']);
|
||||
});
|
||||
Schema::create('automation_runs', function (Blueprint $table) {
|
||||
$table->id(); $table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete();
|
||||
$table->uuid('execution_id')->unique(); $table->string('trigger_type'); $table->json('payload')->nullable();
|
||||
$table->string('status', 20)->default('queued')->index(); $table->unsignedSmallInteger('current_step')->default(0);
|
||||
$table->text('error')->nullable(); $table->timestamp('started_at')->nullable(); $table->timestamp('finished_at')->nullable();
|
||||
$table->id();
|
||||
$table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete();
|
||||
$table->uuid('execution_id')->unique();
|
||||
$table->string('trigger_type');
|
||||
$table->json('payload')->nullable();
|
||||
$table->string('status', 20)->default('queued')->index();
|
||||
$table->unsignedSmallInteger('current_step')->default(0);
|
||||
$table->text('error')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('finished_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('automation_run_logs', function (Blueprint $table) {
|
||||
$table->id(); $table->foreignId('automation_run_id')->constrained('automation_runs')->cascadeOnDelete();
|
||||
$table->id();
|
||||
$table->foreignId('automation_run_id')->constrained('automation_runs')->cascadeOnDelete();
|
||||
$table->foreignId('automation_step_id')->nullable()->constrained('automation_steps')->nullOnDelete();
|
||||
$table->string('level', 20)->default('info'); $table->string('message'); $table->json('context')->nullable();
|
||||
$table->string('level', 20)->default('info');
|
||||
$table->string('message');
|
||||
$table->json('context')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('automation_run_logs'); Schema::dropIfExists('automation_runs');
|
||||
Schema::dropIfExists('automation_steps'); Schema::dropIfExists('automations');
|
||||
Schema::dropIfExists('automation_run_logs');
|
||||
Schema::dropIfExists('automation_runs');
|
||||
Schema::dropIfExists('automation_steps');
|
||||
Schema::dropIfExists('automations');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Automation\Models\Automation;
|
||||
use Modules\Automation\Services\AutomationRunner;
|
||||
|
||||
final class AutomationApiController extends Controller
|
||||
{
|
||||
public function trigger(Request $request,Automation $automation,AutomationRunner $runner): JsonResponse
|
||||
public function trigger(Request $request, Automation $automation, AutomationRunner $runner): JsonResponse
|
||||
{
|
||||
abort_unless($automation->trigger_type==='webhook',404); $secret=(string)($automation->trigger_config['secret']??'');
|
||||
if($secret!=='') abort_unless(hash_equals($secret,(string)$request->header('X-Automation-Secret')),401,'Invalid automation secret.');
|
||||
$run=$runner->dispatch($automation,$request->all(),'webhook'); return response()->json(['data'=>['execution_id'=>$run->execution_id,'status'=>$run->status]],202);
|
||||
abort_unless($automation->trigger_type === 'webhook', 404);
|
||||
$secret = (string) ($automation->trigger_config['secret'] ?? '');
|
||||
if ($secret !== '') {
|
||||
abort_unless(hash_equals($secret, (string) $request->header('X-Automation-Secret')), 401, 'Invalid automation secret.');
|
||||
}
|
||||
$run = $runner->dispatch($automation, $request->all(), 'webhook');
|
||||
|
||||
return response()->json(['data' => ['execution_id' => $run->execution_id, 'status' => $run->status]], 202);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -9,27 +11,53 @@ use Modules\Automation\Contracts\TriggerRegistry;
|
||||
use Modules\Automation\Models\Automation;
|
||||
use Modules\Automation\Models\AutomationRun;
|
||||
use Modules\Automation\Services\AutomationRunner;
|
||||
|
||||
final class AutomationController extends Controller
|
||||
{
|
||||
public function index(TriggerRegistry $triggers,ActionRegistry $actions): View
|
||||
public function index(TriggerRegistry $triggers, ActionRegistry $actions): View
|
||||
{
|
||||
return view('automation::admin.index',['automations'=>Automation::withCount(['steps','runs'])->latest()->paginate(20),'recentRuns'=>AutomationRun::with('automation')->latest()->limit(15)->get(),'triggers'=>$triggers->all(),'actions'=>$actions->all()]);
|
||||
return view('automation::admin.index', ['automations' => Automation::withCount(['steps', 'runs'])->latest()->paginate(20), 'recentRuns' => AutomationRun::with('automation')->latest()->limit(15)->get(), 'triggers' => $triggers->all(), 'actions' => $actions->all()]);
|
||||
}
|
||||
public function store(Request $request,TriggerRegistry $triggers): RedirectResponse
|
||||
|
||||
public function store(Request $request, TriggerRegistry $triggers): RedirectResponse
|
||||
{
|
||||
$data=$request->validate(['name'=>['required','string','max:255'],'description'=>['nullable','string'],'trigger_type'=>['required','string'],'status'=>['required','in:draft,active,paused'],'next_run_at'=>['nullable','date'],'trigger_config'=>['nullable','json']]);
|
||||
abort_unless($triggers->has($data['trigger_type']),422,'Unknown trigger type.');
|
||||
$data['trigger_config']=filled($data['trigger_config']??null)?json_decode($data['trigger_config'],true,512,JSON_THROW_ON_ERROR):[]; $data['stop_on_failure']=$request->boolean('stop_on_failure'); $data['created_by']=$request->user()->id;
|
||||
Automation::create($data); return back()->with('status','Automation created.');
|
||||
$data = $request->validate(['name' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'trigger_type' => ['required', 'string'], 'status' => ['required', 'in:draft,active,paused'], 'next_run_at' => ['nullable', 'date'], 'trigger_config' => ['nullable', 'json']]);
|
||||
abort_unless($triggers->has($data['trigger_type']), 422, 'Unknown trigger type.');
|
||||
$data['trigger_config'] = filled($data['trigger_config'] ?? null) ? json_decode($data['trigger_config'], true, 512, JSON_THROW_ON_ERROR) : [];
|
||||
$data['stop_on_failure'] = $request->boolean('stop_on_failure');
|
||||
$data['created_by'] = $request->user()->id;
|
||||
Automation::create($data);
|
||||
|
||||
return back()->with('status', 'Automation created.');
|
||||
}
|
||||
public function addStep(Request $request,Automation $automation,ActionRegistry $actions): RedirectResponse
|
||||
|
||||
public function addStep(Request $request, Automation $automation, ActionRegistry $actions): RedirectResponse
|
||||
{
|
||||
$data=$request->validate(['action_type'=>['required','string'],'name'=>['nullable','string','max:255'],'configuration'=>['nullable','json']]);
|
||||
abort_unless(array_key_exists($data['action_type'],$actions->all()),422,'Unknown action type.');
|
||||
$automation->steps()->create(['position'=>(int)$automation->steps()->max('position')+1,'action_type'=>$data['action_type'],'name'=>$data['name']??null,'configuration'=>filled($data['configuration']??null)?json_decode($data['configuration'],true,512,JSON_THROW_ON_ERROR):[],'is_enabled'=>true]);
|
||||
return back()->with('status','Automation step added.');
|
||||
$data = $request->validate(['action_type' => ['required', 'string'], 'name' => ['nullable', 'string', 'max:255'], 'configuration' => ['nullable', 'json']]);
|
||||
abort_unless(array_key_exists($data['action_type'], $actions->all()), 422, 'Unknown action type.');
|
||||
$automation->steps()->create(['position' => (int) $automation->steps()->max('position') + 1, 'action_type' => $data['action_type'], 'name' => $data['name'] ?? null, 'configuration' => filled($data['configuration'] ?? null) ? json_decode($data['configuration'], true, 512, JSON_THROW_ON_ERROR) : [], 'is_enabled' => true]);
|
||||
|
||||
return back()->with('status', 'Automation step added.');
|
||||
}
|
||||
|
||||
public function run(Automation $automation, AutomationRunner $runner): RedirectResponse
|
||||
{
|
||||
$run = $runner->dispatch($automation, ['initiated_by' => request()->user()->id], 'manual');
|
||||
|
||||
return back()->with('status', "Automation queued as {$run->execution_id}.");
|
||||
}
|
||||
|
||||
public function toggle(Automation $automation): RedirectResponse
|
||||
{
|
||||
$automation->update(['status' => $automation->status === 'active' ? 'paused' : 'active']);
|
||||
|
||||
return back()->with('status', 'Automation status updated.');
|
||||
}
|
||||
|
||||
public function destroy(Automation $automation): RedirectResponse
|
||||
{
|
||||
$automation->delete();
|
||||
|
||||
return back()->with('status','Automation deleted.');
|
||||
}
|
||||
public function run(Automation $automation,AutomationRunner $runner): RedirectResponse { $run=$runner->dispatch($automation,['initiated_by'=>request()->user()->id],'manual'); return back()->with('status',"Automation queued as {$run->execution_id}."); }
|
||||
public function toggle(Automation $automation): RedirectResponse { $automation->update(['status'=>$automation->status==='active'?'paused':'active']); return back()->with('status','Automation status updated.'); }
|
||||
public function destroy(Automation $automation): RedirectResponse { $automation->delete(); return back()->with('status','Automation deleted.'); }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -7,22 +9,38 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Modules\Automation\Contracts\ActionRegistry;
|
||||
use Modules\Automation\Models\AutomationRun;
|
||||
|
||||
final class ExecuteAutomation implements ShouldQueue
|
||||
{
|
||||
use Dispatchable,InteractsWithQueue,Queueable,SerializesModels;
|
||||
|
||||
public function __construct(public readonly int $runId) {}
|
||||
|
||||
public function handle(ActionRegistry $actions): void
|
||||
{
|
||||
$run=AutomationRun::with('automation.steps')->findOrFail($this->runId); $automation=$run->automation;
|
||||
$run->update(['status'=>'running','started_at'=>now()]);
|
||||
$run = AutomationRun::with('automation.steps')->findOrFail($this->runId);
|
||||
$automation = $run->automation;
|
||||
$run->update(['status' => 'running', 'started_at' => now()]);
|
||||
try {
|
||||
foreach($automation->steps->where('is_enabled',true) as $step) {
|
||||
$run->update(['current_step'=>$step->position]);
|
||||
try { $result=$actions->execute($step,$run->payload??[]); $run->logs()->create(['automation_step_id'=>$step->id,'level'=>'info','message'=>$step->name?:$step->action_type.' completed','context'=>$result,'created_at'=>now()]); }
|
||||
catch(\Throwable $e) { $run->logs()->create(['automation_step_id'=>$step->id,'level'=>'error','message'=>$e->getMessage(),'created_at'=>now()]); if($automation->stop_on_failure) throw $e; }
|
||||
foreach ($automation->steps->where('is_enabled', true) as $step) {
|
||||
$run->update(['current_step' => $step->position]);
|
||||
try {
|
||||
$result = $actions->execute($step, $run->payload ?? []);
|
||||
$run->logs()->create(['automation_step_id' => $step->id, 'level' => 'info', 'message' => $step->name ?: $step->action_type.' completed', 'context' => $result, 'created_at' => now()]);
|
||||
} catch (\Throwable $e) {
|
||||
$run->logs()->create(['automation_step_id' => $step->id, 'level' => 'error', 'message' => $e->getMessage(), 'created_at' => now()]);
|
||||
if ($automation->stop_on_failure) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
$run->update(['status'=>'completed','finished_at'=>now()]);
|
||||
} catch(\Throwable $e) { $run->update(['status'=>'failed','error'=>$e->getMessage(),'finished_at'=>now()]); throw $e; }
|
||||
finally { $automation->increment('run_count'); $automation->update(['last_ran_at'=>now()]); }
|
||||
$run->update(['status' => 'completed', 'finished_at' => now()]);
|
||||
} catch (\Throwable $e) {
|
||||
$run->update(['status' => 'failed', 'error' => $e->getMessage(), 'finished_at' => now()]);
|
||||
throw $e;
|
||||
} finally {
|
||||
$automation->increment('run_count');
|
||||
$automation->update(['last_ran_at' => now()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
final class Automation extends Model
|
||||
{
|
||||
protected $fillable=['name','description','trigger_type','trigger_config','status','stop_on_failure','run_count','last_ran_at','next_run_at','created_by'];
|
||||
protected $casts=['trigger_config'=>'array','stop_on_failure'=>'boolean','last_ran_at'=>'datetime','next_run_at'=>'datetime'];
|
||||
public function steps(): HasMany { return $this->hasMany(AutomationStep::class)->orderBy('position'); }
|
||||
public function runs(): HasMany { return $this->hasMany(AutomationRun::class); }
|
||||
public function isActive(): bool { return $this->status === 'active'; }
|
||||
protected $fillable = ['name', 'description', 'trigger_type', 'trigger_config', 'status', 'stop_on_failure', 'run_count', 'last_ran_at', 'next_run_at', 'created_by'];
|
||||
|
||||
protected $casts = ['trigger_config' => 'array', 'stop_on_failure' => 'boolean', 'last_ran_at' => 'datetime', 'next_run_at' => 'datetime'];
|
||||
|
||||
public function steps(): HasMany
|
||||
{
|
||||
return $this->hasMany(AutomationStep::class)->orderBy('position');
|
||||
}
|
||||
|
||||
public function runs(): HasMany
|
||||
{
|
||||
return $this->hasMany(AutomationRun::class);
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 'active';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
final class AutomationRun extends Model
|
||||
{
|
||||
protected $fillable=['automation_id','execution_id','trigger_type','payload','status','current_step','error','started_at','finished_at'];
|
||||
protected $casts=['payload'=>'array','started_at'=>'datetime','finished_at'=>'datetime'];
|
||||
public function automation(): BelongsTo { return $this->belongsTo(Automation::class); }
|
||||
public function logs(): HasMany { return $this->hasMany(AutomationRunLog::class); }
|
||||
protected $fillable = ['automation_id', 'execution_id', 'trigger_type', 'payload', 'status', 'current_step', 'error', 'started_at', 'finished_at'];
|
||||
|
||||
protected $casts = ['payload' => 'array', 'started_at' => 'datetime', 'finished_at' => 'datetime'];
|
||||
|
||||
public function automation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Automation::class);
|
||||
}
|
||||
|
||||
public function logs(): HasMany
|
||||
{
|
||||
return $this->hasMany(AutomationRunLog::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
final class AutomationRunLog extends Model
|
||||
{
|
||||
public $timestamps=false;
|
||||
protected $fillable=['automation_run_id','automation_step_id','level','message','context','created_at'];
|
||||
protected $casts=['context'=>'array','created_at'=>'datetime'];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['automation_run_id', 'automation_step_id', 'level', 'message', 'context', 'created_at'];
|
||||
|
||||
protected $casts = ['context' => 'array', 'created_at' => 'datetime'];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class AutomationStep extends Model
|
||||
{
|
||||
protected $fillable=['automation_id','position','action_type','name','configuration','is_enabled'];
|
||||
protected $casts=['configuration'=>'array','is_enabled'=>'boolean'];
|
||||
public function automation(): BelongsTo { return $this->belongsTo(Automation::class); }
|
||||
protected $fillable = ['automation_id', 'position', 'action_type', 'name', 'configuration', 'is_enabled'];
|
||||
|
||||
protected $casts = ['configuration' => 'array', 'is_enabled' => 'boolean'];
|
||||
|
||||
public function automation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Automation::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Automation\Console\DispatchScheduledAutomations;
|
||||
use Modules\Automation\Contracts\ActionRegistry;
|
||||
use Modules\Automation\Contracts\TriggerRegistry;
|
||||
use Modules\Automation\Services\AutomationActionRegistry;
|
||||
use Modules\Automation\Services\AutomationTriggerRegistry;
|
||||
|
||||
final class AutomationServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
@@ -15,10 +18,13 @@ final class AutomationServiceProvider extends ServiceProvider
|
||||
$this->app->singleton(ActionRegistry::class, AutomationActionRegistry::class);
|
||||
$this->app->singleton(TriggerRegistry::class, AutomationTriggerRegistry::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Automation', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Automation', 'Resources/views'), 'automation');
|
||||
if ($this->app->runningInConsole()) $this->commands([DispatchScheduledAutomations::class]);
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([DispatchScheduledAutomations::class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Automation\Http\Controllers\AutomationApiController;
|
||||
Route::post('/automations/{automation}/trigger',[AutomationApiController::class,'trigger'])->middleware('throttle:60,1')->name('api.automations.trigger');
|
||||
|
||||
Route::post('/automations/{automation}/trigger', [AutomationApiController::class, 'trigger'])->middleware('throttle:60,1')->name('api.automations.trigger');
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Automation\Http\Controllers\AutomationController;
|
||||
Route::prefix('administration/automation')->name('administration.automation.')->middleware(['auth','permission:automation.view'])->group(function(){
|
||||
Route::get('/',[AutomationController::class,'index'])->name('index');
|
||||
Route::post('/',[AutomationController::class,'store'])->middleware('permission:automation.manage')->name('store');
|
||||
Route::post('/{automation}/steps',[AutomationController::class,'addStep'])->middleware('permission:automation.manage')->name('steps.store');
|
||||
Route::post('/{automation}/run',[AutomationController::class,'run'])->middleware('permission:automation.execute')->name('run');
|
||||
Route::patch('/{automation}/toggle',[AutomationController::class,'toggle'])->middleware('permission:automation.manage')->name('toggle');
|
||||
Route::delete('/{automation}',[AutomationController::class,'destroy'])->middleware('permission:automation.manage')->name('destroy');
|
||||
|
||||
Route::prefix('administration/automation')->name('administration.automation.')->middleware(['auth', 'permission:automation.view'])->group(function () {
|
||||
Route::get('/', [AutomationController::class, 'index'])->name('index');
|
||||
Route::post('/', [AutomationController::class, 'store'])->middleware('permission:automation.manage')->name('store');
|
||||
Route::post('/{automation}/steps', [AutomationController::class, 'addStep'])->middleware('permission:automation.manage')->name('steps.store');
|
||||
Route::post('/{automation}/run', [AutomationController::class, 'run'])->middleware('permission:automation.execute')->name('run');
|
||||
Route::patch('/{automation}/toggle', [AutomationController::class, 'toggle'])->middleware('permission:automation.manage')->name('toggle');
|
||||
Route::delete('/{automation}', [AutomationController::class, 'destroy'])->middleware('permission:automation.manage')->name('destroy');
|
||||
});
|
||||
|
||||
@@ -1,50 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Modules\Automation\Contracts\ActionRegistry;
|
||||
use Modules\Automation\Models\AutomationStep;
|
||||
|
||||
final class AutomationActionRegistry implements ActionRegistry
|
||||
{
|
||||
public function all(): array
|
||||
{
|
||||
return [
|
||||
'log'=>['label'=>'Write log','fields'=>['message']],
|
||||
'webhook'=>['label'=>'Send webhook','fields'=>['url','method']],
|
||||
'email'=>['label'=>'Send email','fields'=>['to','subject','body']],
|
||||
'delay'=>['label'=>'Delay','fields'=>['seconds']],
|
||||
'log' => ['label' => 'Write log', 'fields' => ['message']],
|
||||
'webhook' => ['label' => 'Send webhook', 'fields' => ['url', 'method']],
|
||||
'email' => ['label' => 'Send email', 'fields' => ['to', 'subject', 'body']],
|
||||
'delay' => ['label' => 'Delay', 'fields' => ['seconds']],
|
||||
];
|
||||
}
|
||||
|
||||
public function execute(AutomationStep $step, array $payload): array
|
||||
{
|
||||
$config=$step->configuration ?? [];
|
||||
return match($step->action_type) {
|
||||
'log'=>$this->log($config,$payload),
|
||||
'webhook'=>$this->webhook($config,$payload),
|
||||
'email'=>$this->email($config,$payload),
|
||||
'delay'=>$this->delay($config),
|
||||
default=>throw new \InvalidArgumentException("Unknown automation action [{$step->action_type}]."),
|
||||
$config = $step->configuration ?? [];
|
||||
|
||||
return match ($step->action_type) {
|
||||
'log' => $this->log($config, $payload),
|
||||
'webhook' => $this->webhook($config, $payload),
|
||||
'email' => $this->email($config, $payload),
|
||||
'delay' => $this->delay($config),
|
||||
default => throw new \InvalidArgumentException("Unknown automation action [{$step->action_type}]."),
|
||||
};
|
||||
}
|
||||
private function log(array $config,array $payload): array
|
||||
|
||||
private function log(array $config, array $payload): array
|
||||
{
|
||||
$message=(string)($config['message']??'Automation executed'); Log::info($message,$payload); return ['message'=>$message];
|
||||
$message = (string) ($config['message'] ?? 'Automation executed');
|
||||
Log::info($message, $payload);
|
||||
|
||||
return ['message' => $message];
|
||||
}
|
||||
private function webhook(array $config,array $payload): array
|
||||
|
||||
private function webhook(array $config, array $payload): array
|
||||
{
|
||||
$url=(string)($config['url']??''); if ($url==='') throw new \InvalidArgumentException('Webhook URL is required.');
|
||||
$method=strtolower((string)($config['method']??'post')); $response=Http::timeout(config('automation.webhook_timeout',10))->send($method,$url,['json'=>$payload]);
|
||||
$response->throw(); return ['status'=>$response->status()];
|
||||
$url = (string) ($config['url'] ?? '');
|
||||
if ($url === '') {
|
||||
throw new \InvalidArgumentException('Webhook URL is required.');
|
||||
}
|
||||
$method = strtolower((string) ($config['method'] ?? 'post'));
|
||||
$response = Http::timeout(config('automation.webhook_timeout', 10))->send($method, $url, ['json' => $payload]);
|
||||
$response->throw();
|
||||
|
||||
return ['status' => $response->status()];
|
||||
}
|
||||
private function email(array $config,array $payload): array
|
||||
|
||||
private function email(array $config, array $payload): array
|
||||
{
|
||||
$to=(string)($config['to']??''); if ($to==='') throw new \InvalidArgumentException('Email recipient is required.');
|
||||
$subject=(string)($config['subject']??'Automation notification'); $body=(string)($config['body']??json_encode($payload,JSON_PRETTY_PRINT));
|
||||
Mail::raw($body,fn($message)=>$message->to($to)->subject($subject)); return ['to'=>$to];
|
||||
$to = (string) ($config['to'] ?? '');
|
||||
if ($to === '') {
|
||||
throw new \InvalidArgumentException('Email recipient is required.');
|
||||
}
|
||||
$subject = (string) ($config['subject'] ?? 'Automation notification');
|
||||
$body = (string) ($config['body'] ?? json_encode($payload, JSON_PRETTY_PRINT));
|
||||
Mail::raw($body, fn ($message) => $message->to($to)->subject($subject));
|
||||
|
||||
return ['to' => $to];
|
||||
}
|
||||
|
||||
private function delay(array $config): array
|
||||
{
|
||||
$seconds=max(0,min(30,(int)($config['seconds']??1))); if ($seconds>0) sleep($seconds); return ['seconds'=>$seconds];
|
||||
$seconds = max(0, min(30, (int) ($config['seconds'] ?? 1)));
|
||||
if ($seconds > 0) {
|
||||
sleep($seconds);
|
||||
}
|
||||
|
||||
return ['seconds' => $seconds];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Services;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Automation\Jobs\ExecuteAutomation;
|
||||
use Modules\Automation\Models\Automation;
|
||||
use Modules\Automation\Models\AutomationRun;
|
||||
|
||||
final class AutomationRunner
|
||||
{
|
||||
public function dispatch(Automation $automation,array $payload=[],?string $triggerType=null): AutomationRun
|
||||
public function dispatch(Automation $automation, array $payload = [], ?string $triggerType = null): AutomationRun
|
||||
{
|
||||
abort_unless($automation->isActive(),422,'Automation is not active.');
|
||||
$run=$automation->runs()->create(['execution_id'=>(string)Str::uuid(),'trigger_type'=>$triggerType??$automation->trigger_type,'payload'=>$payload,'status'=>'queued']);
|
||||
ExecuteAutomation::dispatch($run->id)->onQueue(config('automation.queue','default'));
|
||||
abort_unless($automation->isActive(), 422, 'Automation is not active.');
|
||||
$run = $automation->runs()->create(['execution_id' => (string) Str::uuid(), 'trigger_type' => $triggerType ?? $automation->trigger_type, 'payload' => $payload, 'status' => 'queued']);
|
||||
ExecuteAutomation::dispatch($run->id)->onQueue(config('automation.queue', 'default'));
|
||||
|
||||
return $run;
|
||||
}
|
||||
public function dispatchMatching(string $triggerType,array $payload=[],array $constraints=[]): int
|
||||
|
||||
public function dispatchMatching(string $triggerType, array $payload = [], array $constraints = []): int
|
||||
{
|
||||
$query=Automation::query()->where('status','active')->where('trigger_type',$triggerType);
|
||||
$count=0; foreach($query->get() as $automation) { if($this->matches($automation,$constraints)){ $this->dispatch($automation,$payload,$triggerType); $count++; } }
|
||||
$query = Automation::query()->where('status', 'active')->where('trigger_type', $triggerType);
|
||||
$count = 0;
|
||||
foreach ($query->get() as $automation) {
|
||||
if ($this->matches($automation, $constraints)) {
|
||||
$this->dispatch($automation, $payload, $triggerType);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
private function matches(Automation $automation,array $constraints): bool
|
||||
|
||||
private function matches(Automation $automation, array $constraints): bool
|
||||
{
|
||||
$config=$automation->trigger_config??[];
|
||||
foreach($constraints as $key=>$value) if(isset($config[$key]) && (string)$config[$key] !== (string)$value) return false;
|
||||
$config = $automation->trigger_config ?? [];
|
||||
foreach ($constraints as $key => $value) {
|
||||
if (isset($config[$key]) && (string) $config[$key] !== (string) $value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Services;
|
||||
|
||||
use Modules\Automation\Contracts\TriggerRegistry;
|
||||
|
||||
final class AutomationTriggerRegistry implements TriggerRegistry
|
||||
{
|
||||
public function all(): array
|
||||
{
|
||||
return [
|
||||
'manual'=>['label'=>'Manual','description'=>'Run from the administration area or API.'],
|
||||
'schedule'=>['label'=>'Schedule','description'=>'Run when next_run_at is due.'],
|
||||
'webhook'=>['label'=>'Incoming webhook','description'=>'Run from a signed API request.'],
|
||||
'domain_event'=>['label'=>'Domain event','description'=>'Run when an application event is dispatched.'],
|
||||
'manual' => ['label' => 'Manual', 'description' => 'Run from the administration area or API.'],
|
||||
'schedule' => ['label' => 'Schedule', 'description' => 'Run when next_run_at is due.'],
|
||||
'webhook' => ['label' => 'Incoming webhook', 'description' => 'Run from a signed API request.'],
|
||||
'domain_event' => ['label' => 'Domain event', 'description' => 'Run when an application event is dispatched.'],
|
||||
];
|
||||
}
|
||||
public function has(string $type): bool { return array_key_exists($type,$this->all()); }
|
||||
|
||||
public function has(string $type): bool
|
||||
{
|
||||
return array_key_exists($type, $this->all());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Automation\Tests\Feature;
|
||||
|
||||
use Modules\Automation\Contracts\ActionRegistry;
|
||||
use Modules\Automation\Providers\AutomationServiceProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_automation_module_is_registered(): void
|
||||
{
|
||||
$this->assertTrue(class_exists(\Modules\Automation\Providers\AutomationServiceProvider::class));
|
||||
$this->assertTrue(app()->bound(\Modules\Automation\Contracts\ActionRegistry::class));
|
||||
$this->assertTrue(class_exists(AutomationServiceProvider::class));
|
||||
$this->assertTrue(app()->bound(ActionRegistry::class));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user