Simplify setup of rendering methods by putting them in a wrapper class

This commit is contained in:
Timothy Warren 2023-12-21 13:19:59 -05:00
parent 8e7b2a04fd
commit fe1caffc0f
12 changed files with 312 additions and 62 deletions

View File

@ -18,9 +18,8 @@ use const Aviat\AnimeClient\{
ALPHA_SLUG_PATTERN, ALPHA_SLUG_PATTERN,
DEFAULT_CONTROLLER, DEFAULT_CONTROLLER,
DEFAULT_CONTROLLER_METHOD, DEFAULT_CONTROLLER_METHOD,
KITSU_SLUG_PATTERN,
NUM_PATTERN,
SLUG_PATTERN, SLUG_PATTERN,
NUM_PATTERN,
}; };
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -60,7 +59,7 @@ $base_routes = [
'path' => '/anime/details/{id}', 'path' => '/anime/details/{id}',
'action' => 'details', 'action' => 'details',
'tokens' => [ 'tokens' => [
'id' => KITSU_SLUG_PATTERN, 'id' => SLUG_PATTERN,
], ],
], ],
'anime.delete' => [ 'anime.delete' => [
@ -97,7 +96,7 @@ $base_routes = [
'path' => '/manga/details/{id}', 'path' => '/manga/details/{id}',
'action' => 'details', 'action' => 'details',
'tokens' => [ 'tokens' => [
'id' => KITSU_SLUG_PATTERN, 'id' => SLUG_PATTERN,
], ],
], ],
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
@ -191,13 +190,13 @@ $base_routes = [
'character' => [ 'character' => [
'path' => '/character/{slug}', 'path' => '/character/{slug}',
'tokens' => [ 'tokens' => [
'slug' => KITSU_SLUG_PATTERN, 'slug' => SLUG_PATTERN,
], ],
], ],
'person' => [ 'person' => [
'path' => '/people/{slug}', 'path' => '/people/{slug}',
'tokens' => [ 'tokens' => [
'slug' => KITSU_SLUG_PATTERN, 'slug' => SLUG_PATTERN,
], ],
], ],
'default_user_info' => [ 'default_user_info' => [
@ -291,7 +290,7 @@ $base_routes = [
'path' => '/{controller}/edit/{id}/{status}', 'path' => '/{controller}/edit/{id}/{status}',
'action' => 'edit', 'action' => 'edit',
'tokens' => [ 'tokens' => [
'id' => KITSU_SLUG_PATTERN, 'id' => SLUG_PATTERN,
'status' => SLUG_PATTERN, 'status' => SLUG_PATTERN,
], ],
], ],

View File

@ -139,9 +139,6 @@ return static function (array $configArray = []): Container {
// Create session Object // Create session Object
$container->set('session', static fn () => (new SessionFactory())->newInstance($_COOKIE)); $container->set('session', static fn () => (new SessionFactory())->newInstance($_COOKIE));
// Miscellaneous helper methods
$container->set('util', static fn ($container) => new Util($container));
// Models // Models
$container->set('kitsu-model', static function (ContainerInterface $container): Kitsu\Model { $container->set('kitsu-model', static function (ContainerInterface $container): Kitsu\Model {
$requestBuilder = new Kitsu\RequestBuilder($container); $requestBuilder = new Kitsu\RequestBuilder($container);
@ -174,10 +171,6 @@ return static function (array $configArray = []): Container {
return $model; 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) { $container->set('settings-model', static function ($container) {
$model = new Model\Settings($container->get('config')); $model = new Model\Settings($container->get('config'));
$model->setContainer($container); $model->setContainer($container);
@ -185,14 +178,20 @@ return static function (array $configArray = []): Container {
return $model; 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 // Miscellaneous Classes
$container->set('auth', static fn ($container) => new Kitsu\Auth($container)); $container->setSimple('util', Util::class);
$container->set('url-generator', static fn ($container) => new UrlGenerator($container)); $container->setSimple('auth', Kitsu\Auth::class);
$container->setSimple('url-generator', UrlGenerator::class);
$container->setSimple('render-helper', RenderHelper::class);
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Dispatcher // Dispatcher
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
$container->set('dispatcher', static fn ($container) => new Dispatcher($container)); $container->setSimple('dispatcher', Dispatcher::class);
return $container; return $container;
}; };

View File

@ -19,7 +19,8 @@ use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu}; 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\Banker\Teller;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{Container, ContainerAware, ContainerInterface};
@ -239,11 +240,11 @@ abstract class BaseCommand extends Command
return $model; return $model;
}); });
$container->set('auth', static fn ($container) => new Kitsu\Auth($container)); // Miscellaneous Classes
$container->setSimple('util', Util::class);
$container->set('url-generator', static fn ($container) => new UrlGenerator($container)); $container->setSimple('auth', Kitsu\Auth::class);
$container->setSimple('url-generator', UrlGenerator::class);
$container->set('util', static fn ($container) => new Util($container)); $container->setSimple('render-helper', RenderHelper::class);
return $container; return $container;
} }

View File

@ -33,6 +33,7 @@ trait ComponentTrait
$helper = $container->get('html-helper'); $helper = $container->get('html-helper');
$baseData = [ $baseData = [
'_' => $container->get('render-helper'),
'auth' => $container->get('auth'), 'auth' => $container->get('auth'),
'escape' => $helper->escape(), 'escape' => $helper->escape(),
'helper' => $helper, 'helper' => $helper,

View File

@ -82,6 +82,11 @@ class Controller
*/ */
protected array $baseData = []; protected array $baseData = [];
/**
* The data bag for rendering
*/
protected RenderHelper $renderHelper;
/** /**
* Controller constructor. * Controller constructor.
* *
@ -99,14 +104,22 @@ class Controller
$this->auth = $container->get('auth'); $this->auth = $container->get('auth');
$this->cache = $container->get('cache'); $this->cache = $container->get('cache');
$this->config = $container->get('config'); $this->config = $container->get('config');
$this->renderHelper = $container->get('render-helper');
$this->request = $container->get('request'); $this->request = $container->get('request');
$this->session = $session->getSegment(SESSION_SEGMENT); $this->session = $session->getSegment(SESSION_SEGMENT);
$this->url = $auraUrlGenerator; $this->url = $auraUrlGenerator;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$helper = $container->get('html-helper');
$this->baseData = [ $this->baseData = [
'_' => $this->renderHelper,
'auth' => $container->get('auth'), 'auth' => $container->get('auth'),
'component' => $container->get('component-helper'),
'container' => $container,
'config' => $this->config, 'config' => $this->config,
'escape' => $helper->escape(),
'helper' => $helper,
'menu_name' => '', 'menu_name' => '',
'message' => $this->session->getFlash('message'), // Get message box data if it exists 'message' => $this->session->getFlash('message'), // Get message box data if it exists
'other_type' => 'manga', 'other_type' => 'manga',
@ -193,11 +206,7 @@ class Controller
protected function loadPartial(HtmlView $view, string $template, array $data = []): string protected function loadPartial(HtmlView $view, string $template, array $data = []): string
{ {
$router = $this->container->get('dispatcher'); $router = $this->container->get('dispatcher');
$data = array_merge($this->baseData ?? [], $data);
if (isset($this->baseData))
{
$data = array_merge($this->baseData, $data);
}
$route = $router->getRoute(); $route = $router->getRoute();
$data['route_path'] = $route !== FALSE ? $route->path : ''; $data['route_path'] = $route !== FALSE ? $route->path : '';
@ -223,6 +232,8 @@ class Controller
"child-src 'self' *.youtube.com polyfill.io", "child-src 'self' *.youtube.com polyfill.io",
]; ];
$data = array_merge($this->baseData ?? [], $data);
$view->addHeader('Content-Security-Policy', implode('; ', $csp)); $view->addHeader('Content-Security-Policy', implode('; ', $csp));
$view->appendOutput($this->loadPartial($view, 'header', $data)); $view->appendOutput($this->loadPartial($view, 'header', $data));

View File

@ -119,7 +119,7 @@ final class AnimeCollection extends BaseController
*/ */
#[Route('anime.collection.add.get', '/anime-collection/add')] #[Route('anime.collection.add.get', '/anime-collection/add')]
#[Route('anime.collection.edit.get', '/anime-collection/edit/{id}')] #[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(); $this->checkAuth();

View File

@ -0,0 +1,170 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8.1
*
* @copyright 2015 - 2023 Timothy J. Warren <tim@timshome.page>
* @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();
}
}

View File

@ -31,8 +31,7 @@ const NUM_PATTERN = '[0-9]+';
* Eugh...url slugs can have weird characters * Eugh...url slugs can have weird characters
* So...if it's not a forward slash, sure it's valid 😅 * So...if it's not a forward slash, sure it's valid 😅
*/ */
const KITSU_SLUG_PATTERN = '[^\/]+'; const SLUG_PATTERN = '[^\/]+';
const SLUG_PATTERN = '[a-zA-Z0-9\- ]+';
// Why doesn't this already exist? // Why doesn't this already exist?
const MILLI_FROM_NANO = 1000 * 1000; const MILLI_FROM_NANO = 1000 * 1000;

View File

@ -22,16 +22,6 @@ use Psr\Log\LoggerInterface;
*/ */
class Container implements ContainerInterface class Container implements ContainerInterface
{ {
/**
* Array of object instances
*/
protected array $instances = [];
/**
* Map of logger instances
*/
protected array $loggers = [];
/** /**
* Constructor * Constructor
* *
@ -41,9 +31,24 @@ class Container implements ContainerInterface
/** /**
* Array of container Generator functions * 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 * 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 * @param array|null $args - Optional arguments for the factory callable
* @throws ContainerException - Error while retrieving the entry. * @throws ContainerException - Error while retrieving the entry.
* @throws NotFoundException - No entry was found for this identifier. * @throws NotFoundException - No entry was found for this identifier.
@ -88,6 +93,11 @@ class Container implements ContainerInterface
{ {
if ($this->has($id)) if ($this->has($id))
{ {
if (array_key_exists($id, $this->classIdMap))
{
$id = $this->classIdMap[$id];
}
// By default, call a factory with the Container // By default, call a factory with the Container
$args = \is_array($args) ? $args : [$this]; $args = \is_array($args) ? $args : [$this];
$obj = ($this->container[$id])(...$args); $obj = ($this->container[$id])(...$args);
@ -112,6 +122,20 @@ class Container implements ContainerInterface
return $this; 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 * 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."); 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; $this->instances[$id] = $value;
return $this; return $this;
@ -137,7 +167,7 @@ class Container implements ContainerInterface
*/ */
public function has(string $id): bool 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);
} }
/** /**

View File

@ -51,6 +51,14 @@ interface ContainerInterface
*/ */
public function set(string $id, callable $value): 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 * Set a specific instance in the container for an existing factory
*/ */

View File

@ -14,7 +14,7 @@
namespace Aviat\Ion\View; namespace Aviat\Ion\View;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\ContainerAware;
use Laminas\Diactoros\Response\HtmlResponse; use Laminas\Diactoros\Response\HtmlResponse;
use Throwable; use Throwable;
use const EXTR_OVERWRITE; use const EXTR_OVERWRITE;
@ -26,11 +26,21 @@ class HtmlView extends HttpView
{ {
use ContainerAware; use ContainerAware;
/**
* Data to send to every template
*/
protected array $baseData = [];
/** /**
* Response mime type * Response mime type
*/ */
protected string $contentType = 'text/html'; protected string $contentType = 'text/html';
/**
* Whether to 'minify' the html output
*/
protected bool $shouldMinify = false;
/** /**
* Create the Html View * Create the Html View
*/ */
@ -42,27 +52,51 @@ class HtmlView extends HttpView
$this->response = new HtmlResponse(''); $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 * Render a basic html Template
* *
* @throws Throwable * @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 = array_merge($this->baseData, $data);
$data['component'] = $this->container->get('component-helper');
$data['helper'] = $helper;
$data['escape'] = $helper->escape();
$data['container'] = $this->container;
ob_start(); return (function () use ($data, $path) {
extract($data, EXTR_OVERWRITE); ob_start();
include_once $path; extract($data, EXTR_OVERWRITE);
$rawBuffer = ob_get_clean(); include_once $path;
$buffer = ($rawBuffer === FALSE) ? '' : $rawBuffer; $rawBuffer = ob_get_clean();
$buffer = ($rawBuffer === FALSE) ? '' : $rawBuffer;
// Very basic html minify, that won't affect content between html tags // Very basic html minify, that won't affect content between html tags
return preg_replace('/>\s+</', '> <', $buffer) ?? $buffer; if ($this->shouldMinify)
{
$buffer = preg_replace('/>\s+</', '> <', $buffer) ?? $buffer;
}
return $buffer;
})();
} }
} }

View File

@ -20,9 +20,8 @@ use InvalidArgumentException;
use Laminas\Diactoros\Response; use Laminas\Diactoros\Response;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Stringable; use \Stringable;
/** /**
* Base view class for Http output * Base view class for Http output
@ -172,7 +171,6 @@ class HttpView implements HttpViewInterface, Stringable
* @throws DoubleRenderException * @throws DoubleRenderException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
#[CodeCoverageIgnore]
protected function output(): void protected function output(): void
{ {
if ($this->hasRendered) if ($this->hasRendered)