Skip to content

Commit ff68f63

Browse files
committed
Improve build step error reporting by allowing use of reflection in $target of exception
1 parent c7f1268 commit ff68f63

3 files changed

Lines changed: 75 additions & 17 deletions

File tree

src/Framework/Build/BuildStepFailureException.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace phpweb\Framework\Build;
44

55
use Exception;
6+
use Reflector;
67

78
class BuildStepFailureException extends Exception
89
{
9-
10+
public function __construct(string $message, public readonly Reflector $target)
11+
{
12+
parent::__construct($message);
13+
}
1014
}

src/Framework/Build/BuildTools.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
use RecursiveDirectoryIterator;
1414
use RecursiveIteratorIterator;
1515
use ReflectionClass;
16+
use ReflectionFunction;
17+
use ReflectionMethod;
1618
use SplFileInfo;
1719
use Throwable;
1820
use function count;
1921
use function fwrite;
2022
use function is_array;
23+
use function sprintf;
2124
use function time;
2225
use function token_get_all;
2326
use function trim;
@@ -84,7 +87,28 @@ public static function executeBuild(?LoggerInterface $logger = null): int
8487
/* this allows our cache to kick in */
8588
BuildTools::updateLastModifiedTime(time());
8689
} catch (BuildStepFailureException $e) {
87-
fwrite(STDERR, 'Error building ' . $classId . ': ' . $e->getMessage());
90+
$logger->critical("Build step '$classId' failed with an exception! " . $e->getMessage());
91+
92+
fwrite(STDERR, "--------------------------\n");
93+
fwrite(STDERR, "Step: $classId\n");
94+
fwrite(STDERR, "Error: {$e->getMessage()}\n");
95+
96+
$target = $e->target;
97+
if ($target instanceof ReflectionMethod) {
98+
fwrite(
99+
STDERR,
100+
sprintf("Target: %s%s%s\n",
101+
$target->getDeclaringClass()->getName(),
102+
($target->isStatic() ? '::' : '->'),
103+
$target->getName(),
104+
),
105+
);
106+
} elseif ($target instanceof ReflectionClass) {
107+
fwrite(STDERR, "Target: {$target->getName()}\n");
108+
} elseif ($target instanceof ReflectionFunction) {
109+
fwrite(STDERR, "Target: {$target->getName()}\n");
110+
}
111+
88112
exit(1);
89113
} catch (Throwable $e) {
90114
fwrite(STDERR, 'Unhandled exception building ' . $classId . ': ' . $e->getMessage() . "\n");

src/Framework/Routing/Routing.php

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@
1414
use phpweb\ProjectGlobals;
1515
use ReflectionClass;
1616
use ReflectionException;
17+
use RuntimeException;
1718
use Stringable;
1819
use Symfony\Component\HttpFoundation\Request;
20+
use Symfony\Component\HttpFoundation\Response;
1921
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
2022
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
2123
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
2224
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
2325
use Symfony\Component\Routing\RequestContext;
2426
use Symfony\Component\Routing\Route;
2527
use Symfony\Component\Routing\RouteCollection;
26-
use function array_unique;
2728
use function class_exists;
2829
use function explode;
2930
use function http_build_query;
@@ -55,12 +56,12 @@ class Routing implements BuildStep
5556
/**
5657
* Returns an array of the resolved route information, otherwise throws
5758
*
58-
* @throws ResourceNotFoundException
59-
* @throws MethodNotAllowedException
60-
*
6159
* @return null|array{
6260
* _controller: array{class-string, string}
6361
* }
62+
* @throws MethodNotAllowedException
63+
*
64+
* @throws ResourceNotFoundException
6465
*/
6566
public function match(Request $request): ?array
6667
{
@@ -72,7 +73,7 @@ public function match(Request $request): ?array
7273
$requestContext,
7374
);
7475

75-
/** @phpstan-ignore return.type */
76+
/** @phpstan-ignore return.type */
7677
return $matcher->matchRequest($request);
7778
}
7879

@@ -86,12 +87,33 @@ public static function build(BuildContext $context): void
8687
continue;
8788
}
8889

90+
if (!$class->isInstantiable()) {
91+
throw new BuildStepFailureException(
92+
message: 'Controller #[Route]s cannot be placed on non-instantiable classes',
93+
target: $class,
94+
);
95+
}
96+
8997
foreach ($class->getMethods() as $method) {
9098
$methodAttr = ReflectionHelpers::getAttribute($method, RouteAttribute::class);
9199
if (!$methodAttr) {
92100
continue;
93101
}
94102

103+
/* type checking */
104+
$type = (string)$method->getReturnType();
105+
if ($type !== Response::class && !is_subclass_of($type, Response::class)) {
106+
throw new BuildStepFailureException('Controller routes must return a Response', target: $method);
107+
}
108+
109+
$methods = $methodAttr->methods ?: $classAttr->methods;
110+
if (!$methods) {
111+
throw new BuildStepFailureException(
112+
message: 'Controller #[Route] must define at least one method at either class or method level',
113+
target: $method,
114+
);
115+
}
116+
95117
/* titles get concatenated */
96118
$path = ($classAttr->path ?? '');
97119
if (is_string($methodAttr->path)) {
@@ -101,20 +123,27 @@ public static function build(BuildContext $context): void
101123
/* to make our lives easier, we can replace the substitution tokens in the paths with
102124
* symfony requires */
103125
$replacements = $requirements = [];
104-
preg_match_all(self::PATTERN_MATCH, $path, $matches, PREG_SET_ORDER);
105-
foreach ($matches as $match) {
106-
[$full, $inner] = $match;
107-
108-
[$name, $pattern] = self::parsePlaceholder($inner);
109-
$requirements[$name] = $pattern;
110-
$replacements[$full] = '{' . $name . '}';
126+
try {
127+
preg_match_all(self::PATTERN_MATCH, $path, $matches, PREG_SET_ORDER);
128+
foreach ($matches as $match) {
129+
[$full, $inner] = $match;
130+
[$name, $pattern] = self::parsePlaceholder($inner);
131+
$requirements[$name] = $pattern;
132+
$replacements[$full] = '{' . $name . '}';
133+
}
134+
} catch (\RuntimeException $e) {
135+
throw new BuildStepFailureException(
136+
message: "Controller #[Route] parsing error for '$path'; {$e->getMessage()}",
137+
target: $method,
138+
);
111139
}
112140

113141
$normalizedPath = str_replace(array_keys($replacements), $replacements, $path);
114-
$methods = array_values(array_unique($methodAttr->methods ?: $classAttr->methods ?: ['GET']));
115142
$methodString = implode('|', $methods);
116143

117-
$context->logger->debug("Routing ($methodString) '$normalizedPath' to {$class->getName()}->{$method->getName()}");
144+
$context->logger->debug(
145+
"Routing ($methodString) '$normalizedPath' to {$class->getName()}->{$method->getName()}",
146+
);
118147

119148
$routes->add(
120149
$normalizedPath,
@@ -212,14 +241,15 @@ public static function resolve(array|string $controller, array $params = [], arr
212241

213242
/**
214243
* @return array{string,string}
244+
* @throws \RuntimeException
215245
*/
216246
private static function parsePlaceholder(string $input): array
217247
{
218248
$withoutCurley = substr($input, 1, strlen($input) - 2);
219249
[$name, $type] = explode(':', $withoutCurley);
220250

221251
$matchPattern = self::PATTERNS[$type]
222-
?? throw new BuildStepFailureException("Unknown routing pattern '$type' for placeholder '$name'");
252+
?? throw new RuntimeException("Unknown routing pattern '$type' for placeholder '$name'");
223253

224254
return [$name, $matchPattern];
225255
}

0 commit comments

Comments
 (0)