From 7d47e86cfae026b8cf2eec55318d98925d58d75c Mon Sep 17 00:00:00 2001 From: Douglas Silva Date: Sat, 1 May 2021 19:20:42 -0300 Subject: [PATCH] Added a PHP 8 Attribute route parser --- README.md | 21 ++++- src/Fragments/Bundle/Attribute/Route.php | 39 ++++++++ src/Fragments/Component/Html/Templating.php | 5 +- .../Component/Routing/Model/Route.php | 16 +--- .../Routing/Parser/AttributeParser.php | 92 +++++++++++++++++++ .../Component/Routing/Parser/XMLParser.php | 4 +- src/Fragments/Component/Routing/Router.php | 15 ++- 7 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 src/Fragments/Bundle/Attribute/Route.php create mode 100644 src/Fragments/Component/Routing/Parser/AttributeParser.php diff --git a/README.md b/README.md index 3f16215..ac1ad37 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Fragments aims to be a small PHP framework for web applications. Keep in mind th It has its own router component and is heavily inspired by [Symfony](https://symfony.com/). ## Requirements -- PHP 8 +- PHP 8 or newer - [Composer](https://getcomposer.org/) - PHP XML extension. This package is called `php-xml` on Ubuntu. @@ -26,7 +26,24 @@ username = example password = example ``` -4. Add routes in the file `/config/routes.xml` and start building your first controller at `/src/Controller/`. You can also try our [Fragments application](https://github.com/o-alquimista/fragments-app) to get an idea of how things work. +4. Create your first controller at `/src/Controller/`. +```php +namespace App\Controller; + +use Fragments\Bundle\Controller\AbstractController; +use Fragments\Bundle\Attribute\Route; +use Fragments\Component\Http\Response; + +class MyController extends AbstractController +{ + #[Route("/", name: "main_page", methods: ["GET"])] + public function mainPage(): Response + { + // Render a template + return $this->render('main/main_page.php'); + } +} +``` ## License Copyright 2019-2021 Douglas Silva (0x9fd287d56ec107ac) diff --git a/src/Fragments/Bundle/Attribute/Route.php b/src/Fragments/Bundle/Attribute/Route.php new file mode 100644 index 0000000..5e339ce --- /dev/null +++ b/src/Fragments/Bundle/Attribute/Route.php @@ -0,0 +1,39 @@ +. + */ + +namespace Fragments\Bundle\Attribute; + +#[\Attribute] +class Route +{ + public string $name; + + public string $path; + + public array $methods = []; + + public function __construct(string $path, string $name, array $methods = []) + { + $this->name = $name; + $this->path = $path; + $this->methods = $methods; + } +} \ No newline at end of file diff --git a/src/Fragments/Component/Html/Templating.php b/src/Fragments/Component/Html/Templating.php index aa3003e..cd16088 100644 --- a/src/Fragments/Component/Html/Templating.php +++ b/src/Fragments/Component/Html/Templating.php @@ -47,7 +47,10 @@ public function render(string $template, array $variables = []): Response include "../templates/{$template}"; - return new Response(ob_get_clean()); + $contents = ob_get_contents(); + ob_end_clean(); + + return new Response($contents); } /** diff --git a/src/Fragments/Component/Routing/Model/Route.php b/src/Fragments/Component/Routing/Model/Route.php index 71d7374..5ae0df5 100644 --- a/src/Fragments/Component/Routing/Model/Route.php +++ b/src/Fragments/Component/Routing/Model/Route.php @@ -46,24 +46,10 @@ class Route /** * The request methods supported. */ - private $methods = []; + public array $methods = []; /** * Route parameters injected by the router. */ public array $parameters = []; - - public function getMethods(): array - { - return $this->methods; - } - - public function setMethods(string $methods): self - { - // Store them in an array - $methods = explode('|', $methods); - $this->methods = $methods; - - return $this; - } } diff --git a/src/Fragments/Component/Routing/Parser/AttributeParser.php b/src/Fragments/Component/Routing/Parser/AttributeParser.php new file mode 100644 index 0000000..8023050 --- /dev/null +++ b/src/Fragments/Component/Routing/Parser/AttributeParser.php @@ -0,0 +1,92 @@ +. + */ + +namespace Fragments\Component\Routing\Parser; + +use Fragments\Component\Routing\Model\Route; +use Fragments\Bundle\Attribute\Route as RouteAttribute; + +class AttributeParser implements ParserInterface +{ + public function getRoutes(): array + { + $routes = []; + $controllers = $this->getControllers(); + + foreach ($controllers as $controller) { + $class = new \ReflectionClass($controller); + + foreach ($class->getMethods() as $method) { + foreach ($method->getAttributes(RouteAttribute::class) as $attribute) { + $routes[] = $this->getRoute($attribute->newInstance(), $class->getName(), $method->getName()); + } + } + } + + return $routes; + } + + private function getRoute(RouteAttribute $routeAttribute, string $controller, string $action): Route + { + $route = new Route; + $route->id = $routeAttribute->name; + $route->path = $routeAttribute->path; + $route->controller = $controller; + $route->action = $action; + $route->methods = $routeAttribute->methods; + + return $route; + } + + /** + * Include all files from the Controller directory + */ + private function includeControllers() + { + $files = new \FilesystemIterator('../src/Controller/'); + + ob_start(); + + foreach ($files as $file) { + require_once $file->getRealPath(); + } + + ob_end_clean(); + } + + /** + * Get all controller classes from the list of declared classes. + */ + private function getControllers(): array + { + $this->includeControllers(); + + $controllers = []; + + foreach (get_declared_classes() as $fqcn) { + if (str_starts_with($fqcn, "App\Controller")) { + $controllers[] = $fqcn; + } + } + + return $controllers; + } +} \ No newline at end of file diff --git a/src/Fragments/Component/Routing/Parser/XMLParser.php b/src/Fragments/Component/Routing/Parser/XMLParser.php index 65bb46f..2a99748 100644 --- a/src/Fragments/Component/Routing/Parser/XMLParser.php +++ b/src/Fragments/Component/Routing/Parser/XMLParser.php @@ -40,7 +40,9 @@ public function getRoutes(): array $route->path = $entry->path; $route->controller = $entry->controller; $route->action = $entry->action; - $route->setMethods($entry->methods); + + $methods = explode('|', $entry->methods); + $route->methods = $methods; $routes[] = $route; } diff --git a/src/Fragments/Component/Routing/Router.php b/src/Fragments/Component/Routing/Router.php index ce5bbd5..3477ca0 100644 --- a/src/Fragments/Component/Routing/Router.php +++ b/src/Fragments/Component/Routing/Router.php @@ -22,6 +22,8 @@ namespace Fragments\Component\Routing; use Fragments\Component\Routing\Model\Route; +use Fragments\Component\Routing\Parser\ParserInterface; +use Fragments\Component\Routing\Parser\AttributeParser; use Fragments\Component\Routing\Parser\XMLParser; use Fragments\Component\Http\Request; use Fragments\Component\Http\Exception\HttpException; @@ -32,7 +34,16 @@ class Router public function __construct() { - $this->parser = new XMLParser; + $this->parser = $this->getParser(); + } + + private function getParser(): ParserInterface + { + if (file_exists('../config/routes.xml')) { + return new XMLParser(); + } + + return new AttributeParser(); } public function getRouteFromRequest(Request $request): Route @@ -82,7 +93,7 @@ public function getRouteFromRequest(Request $request): Route } } - if (false === in_array(needle: $request->server['REQUEST_METHOD'], haystack: $route->getMethods())) { + if (false === in_array(needle: $request->server['REQUEST_METHOD'], haystack: $route->methods)) { throw new HttpException(statusCode: 405, message: 'Method not allowed.'); }