From fe1caffc0fc152deb00a47b59c6cbf79e08761f5 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 21 Dec 2023 13:19:59 -0500 Subject: [PATCH] Simplify setup of rendering methods by putting them in a wrapper class --- app/appConf/routes.php | 13 +- app/bootstrap.php | 19 +- src/AnimeClient/Command/BaseCommand.php | 13 +- src/AnimeClient/Component/ComponentTrait.php | 1 + src/AnimeClient/Controller.php | 21 ++- .../Controller/AnimeCollection.php | 2 +- src/AnimeClient/RenderHelper.php | 170 ++++++++++++++++++ src/AnimeClient/constants.php | 3 +- src/Ion/Di/Container.php | 58 ++++-- src/Ion/Di/ContainerInterface.php | 8 + src/Ion/View/HtmlView.php | 62 +++++-- src/Ion/View/HttpView.php | 4 +- 12 files changed, 312 insertions(+), 62 deletions(-) create mode 100644 src/AnimeClient/RenderHelper.php diff --git a/app/appConf/routes.php b/app/appConf/routes.php index 6bbf61ba..53fc576b 100644 --- a/app/appConf/routes.php +++ b/app/appConf/routes.php @@ -18,9 +18,8 @@ use const Aviat\AnimeClient\{ ALPHA_SLUG_PATTERN, DEFAULT_CONTROLLER, DEFAULT_CONTROLLER_METHOD, - KITSU_SLUG_PATTERN, - NUM_PATTERN, SLUG_PATTERN, + NUM_PATTERN, }; // ------------------------------------------------------------------------- @@ -60,7 +59,7 @@ $base_routes = [ 'path' => '/anime/details/{id}', 'action' => 'details', 'tokens' => [ - 'id' => KITSU_SLUG_PATTERN, + 'id' => SLUG_PATTERN, ], ], 'anime.delete' => [ @@ -97,7 +96,7 @@ $base_routes = [ 'path' => '/manga/details/{id}', 'action' => 'details', 'tokens' => [ - 'id' => KITSU_SLUG_PATTERN, + 'id' => SLUG_PATTERN, ], ], // --------------------------------------------------------------------- @@ -191,13 +190,13 @@ $base_routes = [ 'character' => [ 'path' => '/character/{slug}', 'tokens' => [ - 'slug' => KITSU_SLUG_PATTERN, + 'slug' => SLUG_PATTERN, ], ], 'person' => [ 'path' => '/people/{slug}', 'tokens' => [ - 'slug' => KITSU_SLUG_PATTERN, + 'slug' => SLUG_PATTERN, ], ], 'default_user_info' => [ @@ -291,7 +290,7 @@ $base_routes = [ 'path' => '/{controller}/edit/{id}/{status}', 'action' => 'edit', 'tokens' => [ - 'id' => KITSU_SLUG_PATTERN, + 'id' => SLUG_PATTERN, 'status' => SLUG_PATTERN, ], ], diff --git a/app/bootstrap.php b/app/bootstrap.php index f568924a..3ceb03e4 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -139,9 +139,6 @@ return static function (array $configArray = []): Container { // Create session Object $container->set('session', static fn () => (new SessionFactory())->newInstance($_COOKIE)); - // Miscellaneous helper methods - $container->set('util', static fn ($container) => new Util($container)); - // Models $container->set('kitsu-model', static function (ContainerInterface $container): Kitsu\Model { $requestBuilder = new Kitsu\RequestBuilder($container); @@ -174,10 +171,6 @@ return static function (array $configArray = []): Container { return $model; }); - $container->set('anime-model', static fn ($container) => new Model\Anime($container)); - $container->set('manga-model', static fn ($container) => new Model\Manga($container)); - $container->set('anime-collection-model', static fn ($container) => new Model\AnimeCollection($container)); - $container->set('manga-collection-model', static fn ($container) => new Model\MangaCollection($container)); $container->set('settings-model', static function ($container) { $model = new Model\Settings($container->get('config')); $model->setContainer($container); @@ -185,14 +178,20 @@ return static function (array $configArray = []): Container { return $model; }); + $container->setSimple('anime-model', Model\Anime::class); + $container->setSimple('manga-model', Model\Manga::class); + $container->setSimple('anime-collection-model', Model\AnimeCollection::class); + // Miscellaneous Classes - $container->set('auth', static fn ($container) => new Kitsu\Auth($container)); - $container->set('url-generator', static fn ($container) => new UrlGenerator($container)); + $container->setSimple('util', Util::class); + $container->setSimple('auth', Kitsu\Auth::class); + $container->setSimple('url-generator', UrlGenerator::class); + $container->setSimple('render-helper', RenderHelper::class); // ------------------------------------------------------------------------- // Dispatcher // ------------------------------------------------------------------------- - $container->set('dispatcher', static fn ($container) => new Dispatcher($container)); + $container->setSimple('dispatcher', Dispatcher::class); return $container; }; diff --git a/src/AnimeClient/Command/BaseCommand.php b/src/AnimeClient/Command/BaseCommand.php index f9fb1527..2c0aa989 100644 --- a/src/AnimeClient/Command/BaseCommand.php +++ b/src/AnimeClient/Command/BaseCommand.php @@ -19,7 +19,8 @@ use Aura\Router\RouterContainer; use Aura\Session\SessionFactory; use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu}; -use Aviat\AnimeClient\{Model, UrlGenerator, Util}; +use Aviat\AnimeClient\ +{Model, RenderHelper, UrlGenerator, Util}; use Aviat\Banker\Teller; use Aviat\Ion\Config; use Aviat\Ion\Di\{Container, ContainerAware, ContainerInterface}; @@ -239,11 +240,11 @@ abstract class BaseCommand extends Command return $model; }); - $container->set('auth', static fn ($container) => new Kitsu\Auth($container)); - - $container->set('url-generator', static fn ($container) => new UrlGenerator($container)); - - $container->set('util', static fn ($container) => new Util($container)); + // Miscellaneous Classes + $container->setSimple('util', Util::class); + $container->setSimple('auth', Kitsu\Auth::class); + $container->setSimple('url-generator', UrlGenerator::class); + $container->setSimple('render-helper', RenderHelper::class); return $container; } diff --git a/src/AnimeClient/Component/ComponentTrait.php b/src/AnimeClient/Component/ComponentTrait.php index d26a0259..a0aa8265 100644 --- a/src/AnimeClient/Component/ComponentTrait.php +++ b/src/AnimeClient/Component/ComponentTrait.php @@ -33,6 +33,7 @@ trait ComponentTrait $helper = $container->get('html-helper'); $baseData = [ + '_' => $container->get('render-helper'), 'auth' => $container->get('auth'), 'escape' => $helper->escape(), 'helper' => $helper, diff --git a/src/AnimeClient/Controller.php b/src/AnimeClient/Controller.php index bbf958af..f973aa64 100644 --- a/src/AnimeClient/Controller.php +++ b/src/AnimeClient/Controller.php @@ -82,6 +82,11 @@ class Controller */ protected array $baseData = []; + /** + * The data bag for rendering + */ + protected RenderHelper $renderHelper; + /** * Controller constructor. * @@ -99,14 +104,22 @@ class Controller $this->auth = $container->get('auth'); $this->cache = $container->get('cache'); $this->config = $container->get('config'); + $this->renderHelper = $container->get('render-helper'); $this->request = $container->get('request'); $this->session = $session->getSegment(SESSION_SEGMENT); $this->url = $auraUrlGenerator; $this->urlGenerator = $urlGenerator; + $helper = $container->get('html-helper'); + $this->baseData = [ + '_' => $this->renderHelper, 'auth' => $container->get('auth'), + 'component' => $container->get('component-helper'), + 'container' => $container, 'config' => $this->config, + 'escape' => $helper->escape(), + 'helper' => $helper, 'menu_name' => '', 'message' => $this->session->getFlash('message'), // Get message box data if it exists 'other_type' => 'manga', @@ -193,11 +206,7 @@ class Controller protected function loadPartial(HtmlView $view, string $template, array $data = []): string { $router = $this->container->get('dispatcher'); - - if (isset($this->baseData)) - { - $data = array_merge($this->baseData, $data); - } + $data = array_merge($this->baseData ?? [], $data); $route = $router->getRoute(); $data['route_path'] = $route !== FALSE ? $route->path : ''; @@ -223,6 +232,8 @@ class Controller "child-src 'self' *.youtube.com polyfill.io", ]; + $data = array_merge($this->baseData ?? [], $data); + $view->addHeader('Content-Security-Policy', implode('; ', $csp)); $view->appendOutput($this->loadPartial($view, 'header', $data)); diff --git a/src/AnimeClient/Controller/AnimeCollection.php b/src/AnimeClient/Controller/AnimeCollection.php index a99e50ea..2cfbc25f 100644 --- a/src/AnimeClient/Controller/AnimeCollection.php +++ b/src/AnimeClient/Controller/AnimeCollection.php @@ -119,7 +119,7 @@ final class AnimeCollection extends BaseController */ #[Route('anime.collection.add.get', '/anime-collection/add')] #[Route('anime.collection.edit.get', '/anime-collection/edit/{id}')] - public function form(?int $id = NULL): void + public function form(?string $id = NULL): void { $this->checkAuth(); diff --git a/src/AnimeClient/RenderHelper.php b/src/AnimeClient/RenderHelper.php new file mode 100644 index 00000000..471512e6 --- /dev/null +++ b/src/AnimeClient/RenderHelper.php @@ -0,0 +1,170 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5.2 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient; + +use Aura\Html; +use Aviat\AnimeClient\API\Kitsu\Auth; +use Aviat\Ion\ConfigInterface; +use Aviat\Ion\Di\ContainerAware; +use Aviat\Ion\Di\ContainerInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * A container for helper functions and data for rendering HTML output + */ +class RenderHelper { + use ContainerAware; + + /** + * The authentication object + */ + public Auth $auth; + + /** + * The global configuration object + */ + public ConfigInterface $config; + + /** + * HTML component helper + */ + public Html\HelperLocator $component; + + /** + * HTML escaper + */ + public Html\Escaper $escape; + + /** + * HTML render helper + */ + public Html\HelperLocator $h; + + /** + * Request object + */ + protected ServerRequestInterface $request; + + /** + * Aura url generator + */ + protected \Aura\Router\Generator $url; + + /** + * Url generation class + */ + private UrlGenerator $urlGenerator; + + /** + * Routes that don't require a second navigation level + */ + private static array $formPages = [ + 'edit', + 'add', + 'update', + 'update_form', + 'login', + 'logout', + 'details', + 'character', + 'me', + ]; + + public function __construct(ContainerInterface $container) { + $this->setContainer($container); + + $this->auth = $container->get('auth'); + $this->component = $container->get('component-helper'); + $this->config = $container->get('config'); + $this->h = $container->get('html-helper'); + $this->escape = $this->h->escape(); + $this->request = $this->container->get('request'); + $this->url = $container->get('aura-router')->getGenerator(); + $this->urlGenerator = $container->get('url-generator'); + } + + /** + * Get the base url for css/js/images + */ + public function assetUrl(string ...$args): string + { + return $this->urlGenerator->assetUrl(...$args); + } + + /** + * Full default path for the list pages + */ + public function defaultUrl(string $type): string + { + return $this->urlGenerator->defaultUrl($type); + } + + /** + * Retrieve the last url segment + */ + public function lastSegment(): string + { + return $this->urlGenerator->lastSegment(); + } + + /** + * Generate a full url from a path + */ + public function urlFromPath(string $path): string + { + return $this->urlGenerator->url($path); + } + + /** + * Generate a url from its name and parameters + */ + public function urlFromRoute(string $name, array $data = []): string + { + return $this->url->generate($name, $data); + } + + /** + * Is the current user authenticated? + */ + public function isAuthenticated(): bool + { + return $this->auth->isAuthenticated(); + } + + /** + * Determine whether to show the sub-menu + */ + public function isViewPage(): bool + { + $url = $this->request->getUri(); + $pageSegments = explode('/', (string) $url); + + $intersect = array_intersect($pageSegments, self::$formPages); + + return empty($intersect); + } + + /** + * Determine whether the page is a page with a form, and + * not suitable for redirection + * + * @throws ContainerException + * @throws NotFoundException + */ + public function isFormPage(): bool + { + return ! $this->isViewPage(); + } +} \ No newline at end of file diff --git a/src/AnimeClient/constants.php b/src/AnimeClient/constants.php index e4bf1e6c..9cbb08f4 100644 --- a/src/AnimeClient/constants.php +++ b/src/AnimeClient/constants.php @@ -31,8 +31,7 @@ const NUM_PATTERN = '[0-9]+'; * Eugh...url slugs can have weird characters * So...if it's not a forward slash, sure it's valid 😅 */ -const KITSU_SLUG_PATTERN = '[^\/]+'; -const SLUG_PATTERN = '[a-zA-Z0-9\- ]+'; +const SLUG_PATTERN = '[^\/]+'; // Why doesn't this already exist? const MILLI_FROM_NANO = 1000 * 1000; diff --git a/src/Ion/Di/Container.php b/src/Ion/Di/Container.php index b75c9468..694692f4 100644 --- a/src/Ion/Di/Container.php +++ b/src/Ion/Di/Container.php @@ -22,16 +22,6 @@ use Psr\Log\LoggerInterface; */ class Container implements ContainerInterface { - /** - * Array of object instances - */ - protected array $instances = []; - - /** - * Map of logger instances - */ - protected array $loggers = []; - /** * Constructor * @@ -41,9 +31,24 @@ class Container implements ContainerInterface /** * Array of container Generator functions */ - protected array $container = [] + protected array $container = [], + + /** + * Array of object instances + */ + protected array $instances = [], + + /** + * Map of logger instances + */ + protected array $loggers = [], + + /** + * Map classes back to container ids, to make automatic + * sub-dependency setup possible + */ + private array $classIdMap = [], ) { - $this->loggers = []; } /** @@ -79,7 +84,7 @@ class Container implements ContainerInterface /** * Get a new instance of the specified item * - * @param string $id - Identifier of the entry to look for. + * @param string $id - Identifier or className of the entry to look for. * @param array|null $args - Optional arguments for the factory callable * @throws ContainerException - Error while retrieving the entry. * @throws NotFoundException - No entry was found for this identifier. @@ -88,6 +93,11 @@ class Container implements ContainerInterface { if ($this->has($id)) { + if (array_key_exists($id, $this->classIdMap)) + { + $id = $this->classIdMap[$id]; + } + // By default, call a factory with the Container $args = \is_array($args) ? $args : [$this]; $obj = ($this->container[$id])(...$args); @@ -112,6 +122,20 @@ class Container implements ContainerInterface return $this; } + /** + * Add a common simple factory to the container + * + * @param string $id + * @param string $className + * @return ContainerInterface + */ + public function setSimple(string $id, string $className): ContainerInterface + { + $this->classIdMap[$className] = $id; + + return $this->set($id, static fn (ContainerInterface $container) => new $className($container)); + } + /** * Set a specific instance in the container for an existing factory * @@ -124,6 +148,12 @@ class Container implements ContainerInterface throw new NotFoundException("Factory '{$id}' does not exist in container. Set that first."); } + $className = get_class($value); + if ( ! array_key_exists((string)$className, $this->classIdMap)) + { + $this->classIdMap[get_class($value)] = $id; + } + $this->instances[$id] = $value; return $this; @@ -137,7 +167,7 @@ class Container implements ContainerInterface */ public function has(string $id): bool { - return array_key_exists($id, $this->container); + return array_key_exists($id, $this->container) || array_key_exists($id, $this->classIdMap); } /** diff --git a/src/Ion/Di/ContainerInterface.php b/src/Ion/Di/ContainerInterface.php index e472a16d..821c6f40 100644 --- a/src/Ion/Di/ContainerInterface.php +++ b/src/Ion/Di/ContainerInterface.php @@ -51,6 +51,14 @@ interface ContainerInterface */ public function set(string $id, callable $value): ContainerInterface; + /** + * Add a common simple factory to the container + * + * @param string $id - The identifier for the factory + * @param string $className - The class name of the factory + */ + public function setSimple(string $id, string $className): ContainerInterface; + /** * Set a specific instance in the container for an existing factory */ diff --git a/src/Ion/View/HtmlView.php b/src/Ion/View/HtmlView.php index e5ab9cb0..50377566 100644 --- a/src/Ion/View/HtmlView.php +++ b/src/Ion/View/HtmlView.php @@ -14,7 +14,7 @@ namespace Aviat\Ion\View; -use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; +use Aviat\Ion\Di\ContainerAware; use Laminas\Diactoros\Response\HtmlResponse; use Throwable; use const EXTR_OVERWRITE; @@ -26,11 +26,21 @@ class HtmlView extends HttpView { use ContainerAware; + /** + * Data to send to every template + */ + protected array $baseData = []; + /** * Response mime type */ protected string $contentType = 'text/html'; + /** + * Whether to 'minify' the html output + */ + protected bool $shouldMinify = false; + /** * Create the Html View */ @@ -42,27 +52,51 @@ class HtmlView extends HttpView $this->response = new HtmlResponse(''); } + /** + * Set data to pass to every template + * + * @param array $data - Keys are variable names + */ + public function setBaseData(array $data): self + { + $this->baseData = $data; + + return $this; + } + + /** + * Should the html be 'minified'? + */ + public function setMinify(bool $shouldMinify): self + { + $this->shouldMinify = $shouldMinify; + + return $this; + } + /** * Render a basic html Template * * @throws Throwable */ - public function renderTemplate(string $path, array $data): string + public function renderTemplate(string $path, array $data = []): string { - $helper = $this->container->get('html-helper'); - $data['component'] = $this->container->get('component-helper'); - $data['helper'] = $helper; - $data['escape'] = $helper->escape(); - $data['container'] = $this->container; + $data = array_merge($this->baseData, $data); - ob_start(); - extract($data, EXTR_OVERWRITE); - include_once $path; - $rawBuffer = ob_get_clean(); - $buffer = ($rawBuffer === FALSE) ? '' : $rawBuffer; + return (function () use ($data, $path) { + ob_start(); + extract($data, EXTR_OVERWRITE); + include_once $path; + $rawBuffer = ob_get_clean(); + $buffer = ($rawBuffer === FALSE) ? '' : $rawBuffer; - // Very basic html minify, that won't affect content between html tags - return preg_replace('/>\s+ <', $buffer) ?? $buffer; + // Very basic html minify, that won't affect content between html tags + if ($this->shouldMinify) + { + $buffer = preg_replace('/>\s+ <', $buffer) ?? $buffer; + } + return $buffer; + })(); } } diff --git a/src/Ion/View/HttpView.php b/src/Ion/View/HttpView.php index 76d561a1..5c3a8483 100644 --- a/src/Ion/View/HttpView.php +++ b/src/Ion/View/HttpView.php @@ -20,9 +20,8 @@ use InvalidArgumentException; use Laminas\Diactoros\Response; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; -use PHPUnit\Framework\Attributes\CodeCoverageIgnore; use Psr\Http\Message\ResponseInterface; -use Stringable; +use \Stringable; /** * Base view class for Http output @@ -172,7 +171,6 @@ class HttpView implements HttpViewInterface, Stringable * @throws DoubleRenderException * @throws InvalidArgumentException */ - #[CodeCoverageIgnore] protected function output(): void { if ($this->hasRendered)