diff --git a/src/Console/Command/TaskCommand.php b/src/Console/Command/TaskCommand.php index 245dd025..54d415a6 100644 --- a/src/Console/Command/TaskCommand.php +++ b/src/Console/Command/TaskCommand.php @@ -10,6 +10,7 @@ use Castor\Event\AfterExecuteTaskEvent; use Castor\Event\BeforeExecuteTaskEvent; use Castor\EventDispatcher; +use Castor\Exception\FunctionConfigurationException; use Castor\ExpressionLanguage; use Castor\SluggerHelper; use Symfony\Component\Console\Command\Command; @@ -107,6 +108,10 @@ protected function configure(): void $taskArgumentAttribute->suggestedValues, ); } elseif ($taskArgumentAttribute instanceof AsOption) { + if ('verbose' === $name) { + throw new FunctionConfigurationException('You cannot re-define a "verbose" option. But you can use "output()->isVerbose()" in your code instead.', $this->function); + } + $mode = $taskArgumentAttribute->mode; $defaultValue = $parameter->isOptional() ? $parameter->getDefaultValue() : null; @@ -130,7 +135,7 @@ protected function configure(): void ); } } catch (LogicException $e) { - throw new \LogicException(sprintf('The argument "%s" for task "%s" cannot be configured: "%s".', $parameter->getName(), $this->getName(), $e->getMessage())); + throw new FunctionConfigurationException(sprintf('The argument "%s" cannot be configured: "%s".', $parameter->getName(), $e->getMessage()), $this->function, $e); } } } diff --git a/src/Context.php b/src/Context.php index f76bc1f9..1b937a6a 100644 --- a/src/Context.php +++ b/src/Context.php @@ -250,11 +250,11 @@ public function offsetGet(mixed $offset): mixed public function offsetSet(mixed $offset, mixed $value): void { - throw new \LogicException('Context is immutable'); + throw new \LogicException('Context is immutable.'); } public function offsetUnset(mixed $offset): void { - throw new \LogicException('Context is immutable'); + throw new \LogicException('Context is immutable.'); } } diff --git a/src/ContextRegistry.php b/src/ContextRegistry.php index faa79ca7..a9aa48c4 100644 --- a/src/ContextRegistry.php +++ b/src/ContextRegistry.php @@ -2,6 +2,8 @@ namespace Castor; +use Castor\Exception\FunctionConfigurationException; + /** @internal */ class ContextRegistry { @@ -15,14 +17,18 @@ public function addDescriptor(ContextDescriptor $descriptor): void { $name = $descriptor->contextAttribute->name; if (\array_key_exists($name, $this->descriptors)) { - throw new \RuntimeException(sprintf('You cannot defined two context with the same name "%s". There is one defined in "%s" and another in "%s".', $name, $this->describeFunction($this->descriptors[$name]->function), $this->describeFunction($descriptor->function))); + $alreadyDefined = $this->descriptors[$name]->function; + + throw new FunctionConfigurationException(sprintf('You cannot define two contexts with the same name "%s". There is one already defined in "%s:%d".', $name, PathHelper::makeRelative((string) $alreadyDefined->getFileName()), $alreadyDefined->getStartLine()), $descriptor->function); } $this->descriptors[$name] = $descriptor; if ($descriptor->contextAttribute->default) { if ($this->defaultName) { - throw new \RuntimeException(sprintf('You cannot set multiple "default: true" context. There is one defined in "%s" and another in "%s".', $this->defaultName, $this->describeFunction($descriptor->function))); + $alreadyDefined = $this->descriptors[$this->defaultName]->function; + + throw new FunctionConfigurationException(sprintf('You cannot set multiple "default: true" context. There is one already defined in "%s:%d".', PathHelper::makeRelative((string) $alreadyDefined->getFileName()), $alreadyDefined->getStartLine()), $descriptor->function); } $this->defaultName = $name; } @@ -73,7 +79,7 @@ public function get(?string $name = null): Context $context = $this->descriptors[$name]->function->invoke(); if (!$context instanceof Context) { - throw new \LogicException(sprintf('The context generator "%s", defined at "%s:%s" must return an instance of "%s", "%s" returned', $name, $this->descriptors[$name]->function->getFileName(), $this->descriptors[$name]->function->getStartLine(), Context::class, get_debug_type($context))); + throw new FunctionConfigurationException(sprintf('The context generator must return an instance of "%s", "%s" returned.', Context::class, get_debug_type($context)), $this->descriptors[$name]->function); } return $context; @@ -130,17 +136,4 @@ public function getNames(): array return $names; } - - private function describeFunction(\ReflectionFunction $function): string - { - $name = $function->getName(); - $shortFilename = str_replace(PathHelper::getRoot() . '/', '', (string) $function->getFileName()); - $location = sprintf('%s:%d', $shortFilename, $function->getStartLine()); - - if (str_contains($name, '{closure}')) { - return $location; - } - - return sprintf('%s@%s', $name, $location); - } } diff --git a/src/Exception/FunctionConfigurationException.php b/src/Exception/FunctionConfigurationException.php new file mode 100644 index 00000000..1757eb0e --- /dev/null +++ b/src/Exception/FunctionConfigurationException.php @@ -0,0 +1,24 @@ +getName(), + $message, + PathHelper::makeRelative((string) $function->getFileName()), + $function->getStartLine(), + ); + + parent::__construct($message, previous: $e); + } +} diff --git a/src/FunctionFinder.php b/src/FunctionFinder.php index 037a4cab..f48ddc3e 100644 --- a/src/FunctionFinder.php +++ b/src/FunctionFinder.php @@ -7,14 +7,13 @@ use Castor\Attribute\AsListener; use Castor\Attribute\AsSymfonyTask; use Castor\Attribute\AsTask; +use Castor\Exception\FunctionConfigurationException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Process; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; -use function Symfony\Component\String\u; - /** @internal */ class FunctionFinder { @@ -49,8 +48,6 @@ public function findFunctions(string $path): iterable * @param iterable<\SplFileInfo> $files * * @return iterable - * - * @throws \ReflectionException */ private function doFindFunctions(iterable $files): iterable { @@ -96,8 +93,8 @@ private function resolveTasks(\ReflectionFunction $reflectionFunction): iterable try { $taskAttribute = $attributes[0]->newInstance(); - } catch (\Throwable $th) { - throw new \InvalidArgumentException(sprintf('Could not instantiate the attribute "%s" on function "%s".', AsTask::class, $reflectionFunction->getName()), 0, $th); + } catch (\Throwable $e) { + throw new FunctionConfigurationException(sprintf('Could not instantiate the attribute "%s".', AsTask::class), $reflectionFunction, $e); } if ('' === $taskAttribute->name) { @@ -112,7 +109,7 @@ private function resolveTasks(\ReflectionFunction $reflectionFunction): iterable foreach ($taskAttribute->onSignals as $signal => $callable) { if (!\is_callable($callable)) { - throw new \InvalidArgumentException(sprintf('The callable for signal "%s" is not callable on function "%s".', $signal, $reflectionFunction->getName())); + throw new FunctionConfigurationException(sprintf('The callable for signal "%s" is not callable.', $signal), $reflectionFunction); } } @@ -155,7 +152,7 @@ private function resolveSymfonyTask(\ReflectionClass $reflectionClass): iterable } if (!$taskAttribute->name) { - throw new \RuntimeException('The task command must have a name.'); + throw new FunctionConfigurationException('The task command must have a name.', $reflectionClass); } $definition = null; @@ -167,7 +164,7 @@ private function resolveSymfonyTask(\ReflectionClass $reflectionClass): iterable break; } if ($definition['name'] !== $taskAttribute->originalName) { - throw new \RuntimeException(sprintf('Could not find a command named "%s" in the Symfony application', $taskAttribute->name)); + throw new FunctionConfigurationException(sprintf('Could not find a command named "%s" in the Symfony application', $taskAttribute->name), $reflectionClass); } yield new SymfonyTaskDescriptor($taskAttribute, $reflectionClass, $definition); @@ -209,16 +206,16 @@ private function resolveContextGenerators(\ReflectionFunction $reflectionFunctio $contextAttribute = $attributes[0]->newInstance(); if ($reflectionFunction->getParameters()) { - throw new \InvalidArgumentException(sprintf('The contexts generator "%s()" must not have arguments.', $reflectionFunction->getName())); + throw new FunctionConfigurationException('The contexts generator must not have arguments.', $reflectionFunction); } $generators = []; foreach ($reflectionFunction->invoke() as $name => $generator) { if (!$generator instanceof \Closure) { - throw new \InvalidArgumentException(sprintf('The context generator "%s" is not callable in function "%s()".', $name, $reflectionFunction->getName())); + throw new FunctionConfigurationException(sprintf('The context generator "%s" is not callable.', $name), $reflectionFunction); } $r = new \ReflectionFunction($generator); if ($r->getParameters()) { - throw new \InvalidArgumentException(sprintf('The context generator "%s()::%s" must not have arguments.', $reflectionFunction->getName(), $name)); + throw new FunctionConfigurationException(sprintf('The context generator "%s" must not have arguments.', $name), $reflectionFunction); } $generators[$name] = $generator; } @@ -237,10 +234,6 @@ private function resolveListeners(\ReflectionFunction $reflectionFunction): iter /** @var AsListener $listenerAttribute */ $listenerAttribute = $attribute->newInstance(); - if (u($listenerAttribute->event)->endsWith('::class') && !class_exists($listenerAttribute->event)) { - throw new \InvalidArgumentException(sprintf('The event "%s" does not exist.', $listenerAttribute->event)); - } - yield new ListenerDescriptor($listenerAttribute, $reflectionFunction); } } diff --git a/src/HasherHelper.php b/src/HasherHelper.php index 28cc0aa4..d70564b9 100644 --- a/src/HasherHelper.php +++ b/src/HasherHelper.php @@ -39,11 +39,11 @@ public function writeFile(string $path, FileHashStrategy $strategy = FileHashStr } if (!is_file($path)) { - throw new \InvalidArgumentException(sprintf('The path "%s" is not a file', $path)); + throw new \InvalidArgumentException(sprintf('The path "%s" is not a file.', $path)); } if (!is_readable($path)) { - throw new \InvalidArgumentException(sprintf('The file "%s" is not readable', $path)); + throw new \InvalidArgumentException(sprintf('The file "%s" is not readable.', $path)); } $this->logger->debug('Hashing file "{path}" with strategy "{strategy}".', [ @@ -97,7 +97,7 @@ public function writeGlob(string $pattern, FileHashStrategy $strategy = FileHash $files = glob($pattern); if (false === $files) { - throw new \InvalidArgumentException(sprintf('The pattern "%s" is invalid', $pattern)); + throw new \InvalidArgumentException(sprintf('The pattern "%s" is invalid.', $pattern)); } foreach ($files as $file) { diff --git a/src/PathHelper.php b/src/PathHelper.php index 1908c7d0..5e255dc3 100644 --- a/src/PathHelper.php +++ b/src/PathHelper.php @@ -41,4 +41,13 @@ public static function realpath(string $path): string return $realpath; } + + public static function makeRelative(string $path): string + { + if (!Path::isAbsolute($path)) { + throw new \RuntimeException(sprintf('Path "%s" is not absolute.', $path)); + } + + return Path::makeRelative($path, self::getRoot()); + } }