Skip to content

Commit

Permalink
Improved exception handling
Browse files Browse the repository at this point in the history
  • Loading branch information
brendt committed Apr 5, 2024
1 parent c9ba571 commit d6457ef
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 70 deletions.
30 changes: 15 additions & 15 deletions src/ConsoleApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use Tempest\AppConfig;
use Tempest\Application;
use Tempest\Console\Actions\RenderConsoleCommandOverview;
use Tempest\Console\Exceptions\CommandNotFound;
use Tempest\Console\Exceptions\CommandNotFoundException;
use Tempest\Console\Exceptions\ConsoleException;
use Tempest\Console\Exceptions\ConsoleExceptionHandler;
use Tempest\Console\Exceptions\InvalidCommandException;
use Tempest\Container\Container;
use Tempest\Kernel;
use Throwable;
Expand Down Expand Up @@ -64,11 +66,16 @@ public function run(): void
}

$this->handleCommand($commandName);
} catch (ConsoleException $consoleException) {
$consoleException->render($this->container->get(ConsoleOutput::class));
} catch (Throwable $throwable) {
if (! $this->appConfig->enableExceptionHandling) {
if (
! $this->appConfig->enableExceptionHandling
|| $this->appConfig->exceptionHandlers === []
) {
throw $throwable;
}

foreach ($this->appConfig->exceptionHandlers as $exceptionHandler) {
$exceptionHandler->handle($throwable);
}
Expand All @@ -79,13 +86,13 @@ private function handleCommand(string $commandName): void
{
$config = $this->container->get(ConsoleConfig::class);

$consoleCommandConfig = $config->commands[$commandName] ?? null;
$consoleCommand = $config->commands[$commandName] ?? null;

if (! $consoleCommandConfig) {
throw new CommandNotFound($commandName);
if (! $consoleCommand) {
throw new CommandNotFoundException($commandName);
}

$handler = $consoleCommandConfig->handler;
$handler = $consoleCommand->handler;

$params = $this->resolveParameters($handler);

Expand All @@ -94,7 +101,7 @@ private function handleCommand(string $commandName): void
try {
$handler->invoke($commandClass, ...$params);
} catch (ArgumentCountError) {
$this->handleFailingCommand();
throw new InvalidCommandException($commandName, $consoleCommand);
}
}

Expand Down Expand Up @@ -123,11 +130,4 @@ private function resolveParameters(ReflectionMethod $handler): array

return $result;
}

private function handleFailingCommand(): void
{
$output = $this->container->get(ConsoleOutput::class);

$output->error('Something went wrong');
}
}
15 changes: 0 additions & 15 deletions src/Exceptions/CommandNotFound.php

This file was deleted.

21 changes: 21 additions & 0 deletions src/Exceptions/CommandNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Exceptions;

use Tempest\Console\ConsoleOutput;

final class CommandNotFoundException extends ConsoleException
{
public function __construct(
private readonly string $commandName
) {}

public function render(ConsoleOutput $output): void
{
$output->writeln(
sprintf('Command %s not found', $this->commandName),
);
}
}
11 changes: 11 additions & 0 deletions src/Exceptions/ConsoleException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Tempest\Console\Exceptions;

use RuntimeException;
use Tempest\Console\ConsoleOutput;

abstract class ConsoleException extends RuntimeException
{
abstract public function render(ConsoleOutput $output): void;
}
71 changes: 32 additions & 39 deletions src/Exceptions/ConsoleExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,54 @@
namespace Tempest\Console\Exceptions;

use Tempest\Console\Console;
use Tempest\Console\ConsoleOutput;
use Tempest\Console\ConsoleStyle;
use Tempest\ExceptionHandler;
use Tempest\Highlight\Highlighter;
use Tempest\Highlight\Themes\LightTerminalTheme;
use Throwable;

final readonly class ConsoleExceptionHandler implements ExceptionHandler
{
public function __construct(private Console $console)
{
}
public function __construct(
private Console $console,
) {}

public function handle(Throwable $throwable): void
{
$this->console->writeln(ConsoleStyle::BOLD(ConsoleStyle::FG_RED($throwable::class)));

if ($message = $throwable->getMessage()) {
$this->console->writeln(ConsoleStyle::BOLD(ConsoleStyle::FG_RED($message)));
}

$this->console->writeln($throwable->getFile() . ':' . $throwable->getLine());

foreach ($throwable->getTrace() as $line) {
// $this->console->writeln('');

$this->outputLine($line);
// $this->outputPath($line);
}
$this->console
->error($throwable::class)
->when(
expression: $throwable->getMessage(),
callback: fn (ConsoleOutput $output) => $output->writeln($throwable->getMessage()),
)
->writeln();

$this->writeSnippet($throwable);

$this->console
->writeln()
->writeln($throwable->getFile() . ':' . $throwable->getLine())
->writeln();
}

private function outputLine(array $line): void
private function writeSnippet(Throwable $throwable): void
{
$this->console->write(' - ');
match (true) {
isset($line['class']) => $this->outputClassLine($line),
isset($line['function']) => $this->outputFunctionLine($line),
default => $this->outputDefaultLine($line),
};
$this->console->writeln($this->getCodeSample($throwable));
}

private function outputClassLine(array $line): void
private function getCodeSample(Throwable $throwable): string
{
$this->console->write(ConsoleStyle::FG_RED($line['class']));
$this->console->write($line['type']);
$this->console->write(ConsoleStyle::FG_DARK_GREEN($line['function']));
$this->console->writeln('');
}
$highlighter = (new Highlighter(new LightTerminalTheme()))->withGutter();
$code = $highlighter->parse(file_get_contents($throwable->getFile()), 'php');
$lines = explode(PHP_EOL, $code);

private function outputFunctionLine(array $line): void
{
$this->console->write(ConsoleStyle::FG_DARK_GREEN($line['function']));
$this->console->writeln('');
}
$lines[$throwable->getLine() - 1] = $lines[$throwable->getLine() - 1] . ' ' . ConsoleStyle::BG_RED(ConsoleStyle::FG_WHITE(ConsoleStyle::BOLD(' < ')));
$excerptSize = 5;
$start = max(0, $throwable->getLine() - $excerptSize);
$lines = array_slice($lines, $start, $excerptSize * 2);

private function outputDefaultLine(array $line): void
{
$this->console->write($line['file'] . ':' . $line['line']);
$this->console->writeln('');
return implode(PHP_EOL, $lines);
}
}
22 changes: 22 additions & 0 deletions src/Exceptions/InvalidCommandException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Tempest\Console\Exceptions;

use Tempest\Console\Actions\RenderConsoleCommand;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\ConsoleOutput;

final class InvalidCommandException extends ConsoleException
{
public function __construct(
private readonly string $initialCommand,
private readonly ConsoleCommand $consoleCommand,
) {}

public function render(ConsoleOutput $output): void
{
$output->error("Invalid command: {$this->initialCommand}");

(new RenderConsoleCommand($output))($this->consoleCommand);
}
}
3 changes: 2 additions & 1 deletion tempest
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ require_once getcwd() . '/vendor/autoload.php';
ConsoleApplication::boot(
'Tempest Console',
new AppConfig(
getcwd(),
root: getcwd(),
enableExceptionHandling: true,
discoveryLocations: [
new DiscoveryLocation('App\\', __DIR__ . '/app/')
],
Expand Down

0 comments on commit d6457ef

Please sign in to comment.