For my my project, I needed use the zend view helpers, including navigation helpers… with the Plates template engine which I use with Zend Expressive implementation. As I don’t like much the Zend View component. My first idea was to provide a Plates extension which would consume the view helper manager but after thinking a bit more, I’ve ended with the solution to override the default Plates engine, making it able to compose the zend view helper manager. Of course, some view helpers are not usable by their nature (they are too close to the zend view renderer or irrelevant with the Plates engine), but most of them are working as expected, except in async context as the helpers are not stateless, through… that’s another problem.
My current working code is as follows:
Plates engine custom implementation which compose a \Zend\View\HelperPluginManager\HelperPluginManager
<?php
declare(strict_types=1);
namespace iMSCP\Layout\Plates;
use League\Plates\Engine as PlatesEngine;
use League\Plates\Template\Func;
use Zend\ServiceManager\ServiceManager;
use Zend\View\Helper\AbstractHelper;
use Zend\View\HelperPluginManager;
use Zend\View\Renderer\RendererInterface;
use Zend\View\Resolver\ResolverInterface;
class Engine extends PlatesEngine implements RendererInterface
{
/**
* Helper plugin manager
*
* @var HelperPluginManager
*/
private $helpers;
/**
* @inheritdoc
*/
public function getEngine()
{
return $this;
}
/**
* @inheritdoc
*/
public function setResolver(ResolverInterface $resolver)
{
return $this;
}
/**
* Set helper plugin manager instance
*
* @param string|HelperPluginManager $helpers
* @return RendererInterface
* @throws \InvalidArgumentException
*/
public function setHelperPluginManager($helpers)
{
if (is_string($helpers)) {
if (!class_exists($helpers)) {
throw new \InvalidArgumentException(sprintf(
'Invalid helper helpers class provided (%s)',
$helpers
));
}
$helpers = new $helpers(new ServiceManager());
}
if (!$helpers instanceof HelperPluginManager) {
throw new \InvalidArgumentException(sprintf(
'Helper helpers must extend Zend\View\HelperPluginManager; got type "%s" instead',
(is_object($helpers) ? get_class($helpers) : gettype($helpers))
));
}
$helpers->setRenderer($this);
$this->helpers = $helpers;
return $this;
}
/**
* Get helper plugin manager instance
*
* @return HelperPluginManager
*/
public function getHelperPluginManager()
{
if (NULL === $this->helpers) {
$this->setHelperPluginManager(new HelperPluginManager(new ServiceManager()));
}
return $this->helpers;
}
/**
* Get a template function (or view helper).
*
* @param string $name
* @return Func
*/
public function getFunction($name)
{
try {
return parent::getFunction($name);
} catch (\LogicException $e) {
$name = strtolower($name);
if(!$this->doesFunctionExist($name)) {
$plugin = $this->getHelperPluginManager()->get($name);
$this->registerFunction($name, is_callable($plugin) ? $plugin : [$plugin, $name]);
}
}
return parent::getFunction($name);
}
/**
* Get plugin instance
*
* @param string $name Name of plugin to return
* @param null|array $options Options to pass to plugin constructor (if not already instantiated)
* @return AbstractHelper
*/
public function plugin($name, array $options = NULL)
{
return $this->getHelperPluginManager()->get($name, $options);
}
/**
* Overloading: proxy to view helpers
*
* Proxies to the attached plugin manager to retrieve, return, and potentially
* execute helpers.
*
* * If the helper does not define __invoke, it will be returned
* * If the helper does define __invoke, it will be called as a functor
*
* @param string $method
* @param array $argv
* @return mixed
*/
public function __call($method, $argv)
{
$plugin = $this->plugin($method);
if (is_callable($plugin)) {
return call_user_func_array($plugin, $argv);
}
return $plugin;
}
/**
* @inheritdoc
* @throws \Throwable
*/
public function render($name, $data = array())
{
return $this->make($name)->render((array)$data);
}
}
Plates custom engine factory
<?php
declare(strict_types=1);
namespace iMSCP\Layout\Plates;
use League\Plates\Extension\ExtensionInterface;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Helper;
use Zend\Expressive\Plates\Exception;
use Zend\Expressive\Plates\Extension;
use Zend\View\HelperPluginManager;
use function class_exists;
use function get_class;
use function gettype;
use function is_array;
use function is_object;
use function is_string;
use function sprintf;
/**
* Create and return a Plates engine instance.
*
*/
class PlatesEngineFactory
{
public function __invoke(ContainerInterface $container): Engine
{
$config = $container->has('config') ? $container->get('config') : [];
$config = isset($config['plates']) ? $config['plates'] : [];
// Create the engine instance:
$engine = new Engine();
$this->injectViewHelperManager($container, $engine);
$this->injectUrlExtension($container, $engine);
$this->injectEscaperExtension($container, $engine);
if (isset($config['extensions']) && is_array($config['extensions'])) {
$this->injectExtensions($container, $engine, $config['extensions']);
}
return $engine;
}
/**
* Inject the ViewHelperManager.
*/
private function injectViewHelperManager(ContainerInterface $container, Engine $engine): void
{
if (!$container->has(HelperPluginManager::class)) {
return;
}
$engine->setHelperPluginManager($container->get(HelperPluginManager::class));
}
/**
* Inject the URL/ServerUrl extensions.
*
* If a service by the name of the UrlExtension class exists, fetches
* and loads it.
*
* Otherwise, instantiates the UrlExtensionFactory, and invokes it with
* the container, loading the result into the engine.
*/
private function injectUrlExtension(ContainerInterface $container, Engine $engine): void
{
if ($container->has(Extension\UrlExtension::class)) {
$engine->loadExtension($container->get(Extension\UrlExtension::class));
return;
}
// If the extension was not explicitly registered, load it only if both helpers were registered
if (!$container->has(Helper\UrlHelper::class) || !$container->has(Helper\ServerUrlHelper::class)) {
return;
}
$extensionFactory = new Extension\UrlExtensionFactory();
$engine->loadExtension($extensionFactory($container));
}
/**
* Inject the Escaper extension.
*
* If a service by the name of the EscaperExtension class exists, fetches
* and loads it.
*
* Otherwise, instantiates the EscaperExtensionFactory, and invokes it with
* the container, loading the result into the engine.
*/
private function injectEscaperExtension(ContainerInterface $container, Engine $engine): void
{
if ($container->has(Extension\EscaperExtension::class)) {
$engine->loadExtension($container->get(Extension\EscaperExtension::class));
return;
}
$extensionFactory = new Extension\EscaperExtensionFactory();
$engine->loadExtension($extensionFactory($container));
}
/**
* Inject all configured extensions into the engine.
*/
private function injectExtensions(ContainerInterface $container, Engine $engine, array $extensions): void
{
foreach ($extensions as $extension) {
$this->injectExtension($container, $engine, $extension);
}
}
/**
* Inject an extension into the engine.
*
* Valid extension specifications include:
*
* - ExtensionInterface instances
* - String service names that resolve to ExtensionInterface instances
* - String class names that resolve to ExtensionInterface instances
*
* If anything else is provided, an exception is raised.
*
* @param ExtensionInterface|string $extension
* @throws Exception\InvalidExtensionException for non-string,
* non-extension $extension values.
* @throws Exception\InvalidExtensionException for string $extension values
* that do not resolve to an extension instance.
*/
private function injectExtension(ContainerInterface $container, Engine $engine, $extension): void
{
if ($extension instanceof ExtensionInterface) {
$engine->loadExtension($extension);
return;
}
if (!is_string($extension)) {
throw new Exception\InvalidExtensionException(sprintf(
'%s expects extension instances, service names, or class names; received %s',
__CLASS__,
(is_object($extension) ? get_class($extension) : gettype($extension))
));
}
if (!$container->has($extension) && !class_exists($extension)) {
throw new Exception\InvalidExtensionException(sprintf(
'%s expects extension service names or class names; "%s" does not resolve to either',
__CLASS__,
$extension
));
}
$extension = $container->has($extension)
? $container->get($extension)
: new $extension();
if (!$extension instanceof ExtensionInterface) {
throw new Exception\InvalidExtensionException(sprintf(
'%s expects extension services to implement %s ; received %s',
__CLASS__,
ExtensionInterface::class,
(is_object($extension) ? get_class($extension) : gettype($extension))
));
}
$engine->loadExtension($extension);
}
}
\Zend\View\HelperPluginManager factory
<?php
declare(strict_types=1);
namespace iMSCP\Layout\Plates;
use Psr\Container\ContainerInterface;
use Zend\View\Helper as ViewHelper;
use Zend\View\HelperPluginManager;
class ViewHelperManagerFactory
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$options = $options ?: [];
$options['factories'] = isset($options['factories']) ? $options['factories'] : [];
$plugins = new HelperPluginManager($container, $options);
// Override extension factories
$plugins = $this->injectOverrideFactories($plugins, $container);
return $plugins;
}
/**
* Inject override factories into the helper manager.
*
* @param HelperPluginManager $plugins
* @param ContainerInterface $container
* @return HelperPluginManager
*/
private function injectOverrideFactories(HelperPluginManager $plugins, ContainerInterface $container)
{
// Configure base path helper
$basePathFactory = $this->createBasePathHelperFactory($container);
$plugins->setFactory(ViewHelper\BasePath::class, $basePathFactory);
$plugins->setFactory('zendviewhelperbasepath', $basePathFactory);
// Configure doctype plates extension
$doctypeFactory = $this->createDoctypeHelperFactory($container);
$plugins->setFactory(ViewHelper\Doctype::class, $doctypeFactory);
$plugins->setFactory('zendviewhelperdoctype', $doctypeFactory);
return $plugins;
}
/**
* Create and return a factory for creating a BasePath helper.
*
* Uses configuration and request services to configure the helper.
*
* @param ContainerInterface $services
* @return callable
*/
private function createBasePathHelperFactory(ContainerInterface $services)
{
return function () use ($services) {
$config = $services->has('config') ? $services->get('config') : [];
$config = $config['plates']['zend_view_helpers'] ?? [];
$helper = new ViewHelper\BasePath;
//if (isset($config['plates']) && isset($config['plates']['base_path'])) {
if (isset($config['base_path'])) {
$helper->setBasePath($config['base_path']);
return $helper;
}
# Should be done through dedicated middleware
/*$request = $services->get('Request');
if (is_callable([$request, 'getBasePath'])) {
$helper->setBasePath($request->getBasePath());
}*/
return $helper;
};
}
/**
* Create and return a Doctype helper factory.
*
* Other plates extensions depend on this to decide which spec to generate their tags
* based on. This is why it must be set early instead of later in the layout phtml.
*
* @param ContainerInterface $container
* @return callable
*/
private function createDoctypeHelperFactory(ContainerInterface $container)
{
return function () use ($container) {
$config = $container->has('config') ? $container->get('config') : [];
//$config = isset($config['plates']) ? $config['plates'] : [];
$config = $config['plates']['zend_view_helpers'] ?? [];
$helper = new ViewHelper\Doctype;
if (isset($config['doctype']) && $config['doctype']) {
$helper->setDoctype($config['doctype']);
}
return $helper;
};
}
}
Configuration provider
<?php
declare(strict_types=1);
namespace iMSCP\Layout;
final class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => $this->getDependencies(),
'plates' => $this->getPlates(),
'templates' => $this->getTemplates(),
];
}
protected function getDependencies(): array
{
return [
'delegators' => [
\Zend\View\HelperPluginManager::class => [
\Zend\Navigation\View\ViewHelperManagerDelegatorFactory::class,
],
],
'factories' => [
\League\Plates\Engine::class => Plates\PlatesEngineFactory::class,
\League\Plates\Extension\Asset::class => Plates\Extension\AssetExtensionFactory::class,
\Zend\View\HelperPluginManager::class => Plates\ViewHelperManagerFactory::class,
],
];
}
protected function getNavigation()
{
return [
'Admin' => [
],
'Reseller' => [
],
'Client' => [
]
];
}
protected function getPlates(): array
{
return [
'zend_view_helpers' => [
'base_path' => '/asset/default',
'doctype' => 'HTML5',
],
'extensions' => [
\League\Plates\Extension\Asset::class,
]
];
}
protected function getTemplates()
{
return [
'paths' => [
'error' => [__DIR__ . '/../templates/default/error'],
'layout' => [__DIR__ . '/../templates/default/layout'],
'partials' => [__DIR__ . '/../templates/default/partials']
],
];
}
}
Any feedback? Does something could be improved?