logger?->info("[{$type}] Executing {$actionName}", [ 'action' => $executableClass, 'input' => $input::class, ]); try { if ($executable instanceof Action) { $this->assertInputIsAction($input); return $this->executeWithinTransaction( $executable, $input, $actionName ); } $this->assertInputIsQuery($input); return $this->executeWithoutTransaction( $executable, $input, $actionName ); } catch (\Throwable $e) { $this->logger?->error("[{$type}] Failed {$actionName}: {$e->getMessage()}", [ 'action' => $executableClass, 'input' => $input::class, 'message' => $e->getMessage(), 'exception' => $e, ]); throw $e; } } /** * @template TQuery of Query * @template TQueryInput of QueryInput * * @param TQuery $executable * @param TQueryInput $input */ private function executeWithoutTransaction(Query $executable, QueryInput $input, string $actionName): mixed { $result = $executable($input); $this->logger?->info("[Query] Succeeded {$actionName}"); return $result; } /** * @template TAction of Action * @template TActionInput of ActionInput * * @param TAction $executable * @param TActionInput $input */ private function executeWithinTransaction(Action $executable, ActionInput $input, string $actionName): mixed { return DB::transaction(function () use ($executable, $input, $actionName): mixed { $result = $executable($input); $this->logger?->info("[Action] Succeeded {$actionName}"); return $result; }); } /** * @psalm-assert QueryInput $input */ private function assertInputIsQuery(mixed $input): void { if (! $input instanceof QueryInput) { throw new \InvalidArgumentException('Input must be an instance of QueryInput.'); } } /** * @psalm-assert ActionInput $input */ private function assertInputIsAction(mixed $input): void { if (! $input instanceof ActionInput) { throw new \InvalidArgumentException('Input must be an instance of ActionInput.'); } } }