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,
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,
],
],

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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,

View File

@ -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));

View File

@ -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();

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
* 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;

View File

@ -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);
}
/**

View File

@ -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
*/

View File

@ -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;
})();
}
}

View File

@ -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)