1414use phpweb \ProjectGlobals ;
1515use ReflectionClass ;
1616use ReflectionException ;
17+ use RuntimeException ;
1718use Stringable ;
1819use Symfony \Component \HttpFoundation \Request ;
20+ use Symfony \Component \HttpFoundation \Response ;
1921use Symfony \Component \Routing \Exception \MethodNotAllowedException ;
2022use Symfony \Component \Routing \Exception \ResourceNotFoundException ;
2123use Symfony \Component \Routing \Matcher \CompiledUrlMatcher ;
2224use Symfony \Component \Routing \Matcher \Dumper \CompiledUrlMatcherDumper ;
2325use Symfony \Component \Routing \RequestContext ;
2426use Symfony \Component \Routing \Route ;
2527use Symfony \Component \Routing \RouteCollection ;
26- use function array_unique ;
2728use function class_exists ;
2829use function explode ;
2930use 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