Overview

Namespaces

  • Autarky
    • Config
      • Loaders
    • Console
    • Container
      • Exception
      • Factory
      • Proxy
    • Database
    • Errors
    • Events
    • Files
    • Http
    • Logging
    • Providers
    • Routing
      • Events
    • Testing
    • TwigTemplating
      • Extensions
    • Utils

Classes

  • Autarky\Application
  • Autarky\Config\ConfigProvider
  • Autarky\Config\FileStore
  • Autarky\Config\LoaderFactory
  • Autarky\Config\Loaders\CachingYamlFileLoader
  • Autarky\Config\Loaders\PhpFileLoader
  • Autarky\Config\Loaders\YamlFileLoader
  • Autarky\Console\Application
  • Autarky\Console\Command
  • Autarky\Console\RouteDispatchCommand
  • Autarky\Console\RouteListCommand
  • Autarky\Container\Container
  • Autarky\Container\ContainerProvider
  • Autarky\Container\Factory\AbstractArgument
  • Autarky\Container\Factory\ClassArgument
  • Autarky\Container\Factory\Definition
  • Autarky\Container\Factory\Factory
  • Autarky\Container\Factory\ScalarArgument
  • Autarky\Container\Proxy\AbstractProxy
  • Autarky\Container\Proxy\ProxyProvider
  • Autarky\Database\ConnectionManager
  • Autarky\Database\DatabaseProvider
  • Autarky\Errors\ErrorHandlerManager
  • Autarky\Errors\ErrorHandlerProvider
  • Autarky\Errors\StubErrorHandler
  • Autarky\Events\EventDispatcher
  • Autarky\Events\EventDispatcherProvider
  • Autarky\Files\LockingFilesystem
  • Autarky\Files\PathResolver
  • Autarky\Http\CookieMiddleware
  • Autarky\Http\CookieProvider
  • Autarky\Http\CookieQueue
  • Autarky\Http\SessionMiddleware
  • Autarky\Http\SessionProvider
  • Autarky\Logging\ChannelManager
  • Autarky\Logging\DefaultLogConfigurator
  • Autarky\Logging\LoggingErrorHandler
  • Autarky\Logging\LoggingProvider
  • Autarky\Provider
  • Autarky\Providers\AbstractDependantProvider
  • Autarky\Providers\AbstractProvider
  • Autarky\Routing\Configuration
  • Autarky\Routing\Controller
  • Autarky\Routing\DefaultRouteConfigurator
  • Autarky\Routing\Events\AbstractRouteEvent
  • Autarky\Routing\Events\AfterEvent
  • Autarky\Routing\Events\BeforeEvent
  • Autarky\Routing\Events\RouteMatchedEvent
  • Autarky\Routing\Route
  • Autarky\Routing\RoutePathGenerator
  • Autarky\Routing\Router
  • Autarky\Routing\RoutingProvider
  • Autarky\Routing\UrlGenerator
  • Autarky\Testing\TestCase
  • Autarky\Testing\WebTestCase
  • Autarky\TwigTemplating\Extensions\PartialExtension
  • Autarky\TwigTemplating\Extensions\SessionExtension
  • Autarky\TwigTemplating\Extensions\UrlGenerationExtension
  • Autarky\TwigTemplating\Template
  • Autarky\TwigTemplating\TemplateContext
  • Autarky\TwigTemplating\TemplateEvent
  • Autarky\TwigTemplating\TemplatingEngine
  • Autarky\TwigTemplating\TwigEnvironment
  • Autarky\TwigTemplating\TwigTemplate
  • Autarky\TwigTemplating\TwigTemplatingProvider
  • Autarky\Utils\ArrayUtil

Interfaces

  • Autarky\Config\ConfigInterface
  • Autarky\Config\LoaderInterface
  • Autarky\ConfiguratorInterface
  • Autarky\Container\CallableInvokerInterface
  • Autarky\Container\ClassResolverInterface
  • Autarky\Container\ContainerAwareInterface
  • Autarky\Container\ContainerInterface
  • Autarky\Container\Factory\ArgumentInterface
  • Autarky\Container\Factory\FactoryInterface
  • Autarky\Database\ConnectionFactoryInterface
  • Autarky\Errors\ErrorHandlerInterface
  • Autarky\Errors\ErrorHandlerManagerInterface
  • Autarky\Events\EventDispatcherAwareInterface
  • Autarky\Providers\ConsoleProviderInterface
  • Autarky\Providers\DependantProviderInterface
  • Autarky\Providers\ProviderInterface
  • Autarky\Routing\InvokerInterface
  • Autarky\Routing\RoutePathGeneratorInterface
  • Autarky\Routing\RouterInterface

Traits

  • Autarky\Container\ContainerAwareTrait
  • Autarky\Events\EventDispatcherAwareTrait
  • Autarky\Routing\ControllerTrait

Exceptions

  • Autarky\Config\LoadException
  • Autarky\Container\Exception\ContainerException
  • Autarky\Container\Exception\NotInstantiableException
  • Autarky\Container\Exception\ResolvingException
  • Autarky\Container\Exception\ResolvingInternalException
  • Autarky\Container\Exception\UnresolvableArgumentException
  • Autarky\Database\CannotConnectException
  • Autarky\Files\IOException
  • Autarky\Providers\ProviderException
  • Overview
  • Namespace
  • Class
  1: <?php
  2: /**
  3:  * This file is part of the Autarky package.
  4:  *
  5:  * (c) Andreas Lutro <anlutro@gmail.com>
  6:  *
  7:  * For the full copyright and license information, please view the LICENSE
  8:  * file that was distributed with this source code.
  9:  */
 10: 
 11: namespace Autarky\Routing;
 12: 
 13: use Closure;
 14: use Symfony\Component\HttpFoundation\JsonResponse;
 15: use Symfony\Component\HttpFoundation\Request;
 16: use Symfony\Component\HttpFoundation\Response;
 17: use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 18: use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
 19: use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 20: 
 21: use FastRoute\RouteCollector;
 22: use FastRoute\RouteParser;
 23: use FastRoute\Dispatcher\GroupCountBased as Dispatcher;
 24: use FastRoute\DataGenerator\GroupCountBased as DataGenerator;
 25: 
 26: use Autarky\Files\LockingFilesystem;
 27: 
 28: /**
 29:  * FastRoute implementation of the router.
 30:  */
 31: class Router implements RouterInterface
 32: {
 33:     /**
 34:      * @var \Autarky\Routing\InvokerInterface
 35:      */
 36:     protected $invoker;
 37: 
 38:     /**
 39:      * @var EventDispatcherInterface
 40:      */
 41:     protected $eventDispatcher;
 42: 
 43:     /**
 44:      * @var \FastRoute\RouteCollector
 45:      */
 46:     protected $routeCollector;
 47: 
 48:     /**
 49:      * @var \FastRoute\RouteParser
 50:      */
 51:     protected $routeParser;
 52: 
 53:     /**
 54:      * @var mixed
 55:      */
 56:     protected $dispatchData;
 57: 
 58:     /**
 59:      * @var string|null
 60:      */
 61:     protected $cachePath;
 62: 
 63:     /**
 64:      * @var \Autarky\Routing\Route
 65:      */
 66:     protected $currentRoute;
 67: 
 68:     /**
 69:      * The hooks that are currently applied to every route being added.
 70:      *
 71:      * @var array
 72:      */
 73:     protected $currentHooks = [];
 74: 
 75:     /**
 76:      * The URL prefix that is currently applied to every route being added.
 77:      *
 78:      * @var string
 79:      */
 80:     protected $currentPrefix = '';
 81: 
 82:     /**
 83:      * @var array
 84:      */
 85:     protected $hooks = [];
 86: 
 87:     /**
 88:      * @var \SplObjectStorage
 89:      */
 90:     protected $routes;
 91: 
 92:     /**
 93:      * @var array
 94:      */
 95:     protected $namedRoutes = [];
 96: 
 97:     /**
 98:      * @param RouteParser $routeParser
 99:      * @param InvokerInterface $invoker
100:      * @param EventDispatcherInterface|null $eventDispatcher
101:      * @param string|null $cachePath
102:      */
103:     public function __construct(
104:         RouteParser $routeParser,
105:         InvokerInterface $invoker,
106:         EventDispatcherInterface $eventDispatcher = null,
107:         $cachePath = null
108:     ) {
109:         $this->routes = new \SplObjectStorage;
110:         $this->invoker = $invoker;
111:         $this->eventDispatcher = $eventDispatcher;
112: 
113:         if ($cachePath) {
114:             $this->cachePath = $cachePath;
115:             if (file_exists($cachePath)) {
116:                 Route::setRouter($this);
117:                 $this->dispatchData = require $cachePath;
118:                 return;
119:             }
120:         }
121: 
122:         $this->routeCollector = new RouteCollector(
123:             $routeParser, new DataGenerator
124:         );
125:     }
126: 
127:     /**
128:      * @return bool
129:      */
130:     public function isCaching()
131:     {
132:         return $this->dispatchData !== null;
133:     }
134: 
135:     /**
136:      * @return \SplObjectStorage
137:      */
138:     public function getRoutes()
139:     {
140:         return $this->routes;
141:     }
142: 
143:     /**
144:      * {@inheritdoc}
145:      */
146:     public function getCurrentRoute()
147:     {
148:         return $this->currentRoute;
149:     }
150: 
151:     /**
152:      * Add a "before" event listener.
153:      *
154:      * @param  string   $name
155:      * @param  callable $handler
156:      * @param  integer  $priority
157:      *
158:      * @return void
159:      */
160:     public function addBeforeHook($name, $handler, $priority = 0)
161:     {
162:         $this->addEventListener($name, $handler, 'before', $priority);
163:     }
164: 
165:     /**
166:      * Add an "after" event listener.
167:      *
168:      * @param  string   $name
169:      * @param  callable $handler
170:      * @param  integer  $priority
171:      *
172:      * @return void
173:      */
174:     public function addAfterHook($name, $handler, $priority = 0)
175:     {
176:         $this->addEventListener($name, $handler, 'after', $priority);
177:     }
178: 
179:     /**
180:      * Add a global "before" event listener.
181:      *
182:      * @param  callable $handler
183:      * @param  integer  $priority
184:      *
185:      * @return void
186:      */
187:     public function addGlobalBeforeHook($handler, $priority = 0)
188:     {
189:         $this->addEventListener(null, $handler, 'before', $priority);
190:     }
191: 
192:     /**
193:      * Add a global "after" event listener.
194:      *
195:      * @param  callable $handler
196:      * @param  integer  $priority
197:      *
198:      * @return void
199:      */
200:     public function addGlobalAfterHook($handler, $priority = 0)
201:     {
202:         $this->addEventListener(null, $handler, 'after', $priority);
203:     }
204: 
205:     protected function addEventListener($name, $handler, $when, $priority)
206:     {
207:         if ($this->eventDispatcher === null) {
208:             return;
209:         }
210: 
211:         if ($name) {
212:             if (isset($this->hooks[$name])) {
213:                 throw new \LogicException("Hook with name $name already defined");
214:             }
215: 
216:             $this->hooks[$name] = $name;
217:         }
218: 
219:         $name = $name ? "route.$when.$name" : "route.$when";
220:         $this->eventDispatcher->addListener($name, $handler, $priority);
221:     }
222: 
223:     /**
224:      * {@inheritdoc}
225:      */
226:     public function mount(array $routes, $path = '/')
227:     {
228:         if ($this->isCaching()) {
229:             return;
230:         }
231: 
232:         (new Configuration($this, $routes))
233:             ->mount($path);
234:     }
235: 
236:     /**
237:      * {@inheritdoc}
238:      */
239:     public function group(array $flags, Closure $callback)
240:     {
241:         if ($this->isCaching()) {
242:             return;
243:         }
244: 
245:         $oldPrefix = $this->currentPrefix;
246:         $oldHooks = $this->currentHooks;
247: 
248:         foreach (['before', 'after'] as $when) {
249:             if (isset($flags[$when])) {
250:                 foreach ((array) $flags[$when] as $hook) {
251:                     $this->currentHooks[] = [$when, $this->getHook($hook)];
252:                 }
253:             }
254:         }
255: 
256:         if (isset($flags['prefix'])) {
257:             $this->currentPrefix .= '/' . trim($flags['prefix'], '/');
258:         }
259: 
260:         $callback($this);
261: 
262:         $this->currentPrefix = $oldPrefix;
263:         $this->currentHooks = $oldHooks;
264:     }
265: 
266:     protected function getHook($name)
267:     {
268:         if (!isset($this->hooks[$name])) {
269:             throw new \InvalidArgumentException("Hook with name $name is not defined");
270:         }
271: 
272:         return $this->hooks[$name];
273:     }
274: 
275:     /**
276:      * {@inheritdoc}
277:      */
278:     public function addRoute($methods, $path, $controller, $name = null, array $options = [])
279:     {
280:         if ($this->isCaching()) {
281:             return null;
282:         }
283: 
284:         $methods = (array) $methods;
285:         $path = $this->makePath($path);
286: 
287:         $route = $this->createRoute($methods, $path, $controller, $name, $options);
288:         $this->routes->attach($route);
289: 
290:         if ($name) {
291:             $this->addNamedRoute($name, $route);
292:         }
293: 
294:         $this->routeCollector->addRoute($route->getMethods(), $path, $route);
295: 
296:         return $route;
297:     }
298: 
299:     /**
300:      * Add a cached route.
301:      *
302:      * @param Route $route
303:      *
304:      * @internal
305:      */
306:     public function addCachedRoute(Route $route)
307:     {
308:         $this->routes->attach($route);
309:         if ($name = $route->getName()) {
310:             $this->addNamedRoute($name, $route);
311:         }
312:     }
313: 
314:     protected function makePath($path)
315:     {
316:         if ($this->currentPrefix !== null) {
317:             $path = rtrim($this->currentPrefix, '/') .'/'. ltrim($path, '/');
318:         }
319: 
320:         if (substr($path, 0, 1) !== '/') {
321:             $path = '/' . $path;
322:         }
323: 
324:         if ($path == '/') {
325:             return $path;
326:         }
327: 
328:         return rtrim($path, '/');
329:     }
330: 
331:     protected function addNamedRoute($name, Route $route)
332:     {
333:         if (isset($this->namedRoutes[$name])) {
334:             throw new \InvalidArgumentException("Route with name $name already exists");
335:         }
336: 
337:         $this->namedRoutes[$name] = $route;
338:     }
339: 
340:     protected function createRoute($methods, $path, $controller, $name, array $options)
341:     {
342:         $route = new Route($methods, $path, $controller, $name, $options);
343: 
344:         foreach ($this->currentHooks as $hook) {
345:             $route->addHook($hook[0], $hook[1]);
346:         }
347: 
348:         return $route;
349:     }
350: 
351:     /**
352:      * {@inheritdoc}
353:      */
354:     public function getRoute($name)
355:     {
356:         if (!isset($this->namedRoutes[$name])) {
357:             throw new \InvalidArgumentException("Route with name $name not found.");
358:         }
359: 
360:         return $this->namedRoutes[$name];
361:     }
362: 
363:     /**
364:      * {@inheritdoc}
365:      */
366:     public function dispatch(Request $request)
367:     {
368:         $route = $this->getRouteForRequest($request);
369: 
370:         return $this->getResponse($request, $route, $route->getParams());
371:     }
372: 
373:     /**
374:      * Get the Route object corresponding to a given request.
375:      *
376:      * @param  Request $request
377:      *
378:      * @return Route
379:      *
380:      * @throws NotFoundHttpException
381:      * @throws MethodNotAllowedHttpException
382:      */
383:     public function getRouteForRequest(Request $request)
384:     {
385:         $method = $request->getMethod();
386:         $path = $request->getPathInfo() ?: '/';
387: 
388:         $result = $this->getDispatcher()
389:             ->dispatch($method, $path);
390: 
391:         if ($result[0] == \FastRoute\Dispatcher::NOT_FOUND) {
392:             throw new NotFoundHttpException("No route match for path $path");
393:         } else if ($result[0] == \FastRoute\Dispatcher::METHOD_NOT_ALLOWED) {
394:             throw new MethodNotAllowedHttpException($result[1],
395:                 "Method $method not allowed for path $path");
396:         } else if ($result[0] !== \FastRoute\Dispatcher::FOUND) {
397:             throw new \RuntimeException('Unknown result from FastRoute: '.$result[0]);
398:         }
399: 
400:         return $this->matchRoute($result[1], $result[2], $request);
401:     }
402: 
403:     protected function matchRoute(Route $route, array $params, Request $request)
404:     {
405:         $route->setParams($params);
406: 
407:         if ($this->eventDispatcher !== null) {
408:             $event = new Events\RouteMatchedEvent($request, $route);
409:             $this->eventDispatcher->dispatch('route.match', $event);
410:             $route = $event->getRoute();
411:         }
412: 
413:         return $route;
414:     }
415: 
416:     protected function getResponse(Request $request, Route $route, array $params)
417:     {
418:         // convert route params into container params
419:         $params = $this->getContainerParams($route, $params, $request);
420: 
421:         $this->currentRoute = $route;
422: 
423:         if ($this->eventDispatcher !== null) {
424:             $event = new Events\BeforeEvent($request, $route);
425:             $this->eventDispatcher->dispatch("route.before", $event);
426: 
427:             foreach ($route->getBeforeHooks() as $hook) {
428:                 $this->eventDispatcher->dispatch("route.before.$hook", $event);
429:             }
430:         }
431: 
432:         // if the event has been dispatched, check if the event has a response
433:         // that should override the route's response. if the event doesn't have
434:         // a response, check if the event has a controller that should override
435:         // the route's controller
436:         if (isset($event)) {
437:             $response = $event->getResponse();
438:             if (!$response) {
439:                 $callable = $event->getController() ?: $route->getController();
440:             }
441:         } else {
442:             $callable = $route->getController();
443:         }
444: 
445:         // if the event hasn't been dispatched, or the event hasn't had a
446:         // response set onto it, invoke the controller
447:         if (!isset($response) || !$response) {
448:             $constructorArgs = $route->getOption('constructor_params');
449:             $response = $this->invoker->invoke($callable, $params, $constructorArgs);
450:         }
451: 
452:         if (!$response instanceof Response) {
453:             if (is_array($response) || $response instanceof \stdClass) {
454:                 $response = new JsonResponse($response);
455:             } else {
456:                 $response = new Response($response);
457:             }
458:         }
459: 
460:         if ($this->eventDispatcher !== null) {
461:             $event = new Events\AfterEvent($request, $route, $response);
462:             $this->eventDispatcher->dispatch("route.after", $event);
463: 
464:             foreach ($route->getAfterHooks() as $hook) {
465:                 $this->eventDispatcher->dispatch("route.after.$hook", $event);
466:             }
467: 
468:             if ($event->getResponse() !== $response) {
469:                 $response = $event->getResponse();
470:             }
471:         }
472: 
473:         $this->currentRoute = null;
474: 
475:         return $response;
476:     }
477: 
478:     protected function getContainerParams(Route $route, array $routeParams, Request $request)
479:     {
480:         $params = [];
481: 
482:         if ($extraParams = $route->getOption('params')) {
483:             $params = $extraParams;
484:         }
485: 
486:         // the container expects a dollar sign in front of non-class function
487:         // arguments
488:         foreach ($routeParams as $key => $value) {
489:             $params["\$$key"] = $value;
490:         }
491: 
492:         // this allows controllers to type-hint against the Request class to get
493:         // access to it directly
494:         $params['Symfony\Component\HttpFoundation\Request'] = $request;
495: 
496:         return $params;
497:     }
498: 
499:     protected function getDispatcher()
500:     {
501:         if ($this->dispatchData !== null) {
502:             $dispatchData = $this->dispatchData;
503:         } else if ($this->routeCollector !== null) {
504:             $dispatchData = $this->generateDispatchData();
505:         } else {
506:             throw new \RuntimeException('No dispatch data or route collector set');
507:         }
508: 
509:         return new Dispatcher($dispatchData);
510:     }
511: 
512:     protected function generateDispatchData()
513:     {
514:         $data = $this->routeCollector->getData();
515: 
516:         if ($this->cachePath !== null) {
517:             $filesys = new LockingFilesystem;
518:             $php = '<?php return '.var_export($data, true).";\n";
519:             $filesys->write($this->cachePath, $php);
520:         }
521: 
522:         return $data;
523:     }
524: }
525: 
Autarky Framework API documentation generated by ApiGen