244 lines
9.6 KiB
PHP
244 lines
9.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Extbase\Routing;
|
|
|
|
use TYPO3\CMS\Core\Routing\Enhancer\PluginEnhancer;
|
|
use TYPO3\CMS\Core\Routing\Route;
|
|
use TYPO3\CMS\Core\Routing\RouteCollection;
|
|
|
|
/**
|
|
* Allows to have a plugin with multiple controllers + actions for one specific plugin that has a namespace.
|
|
*
|
|
* A typical configuration looks like this:
|
|
*
|
|
* routeEnhancers:
|
|
* BlogExample:
|
|
* type: Extbase
|
|
* extension: BlogExample
|
|
* plugin: Pi1
|
|
* routes:
|
|
* - { routePath: '/blog/{page}', _controller: 'Blog::list', _arguments: {'page': '@widget_0/currentPage'} }
|
|
* - { routePath: '/blog/{slug}', _controller: 'Blog::detail' }
|
|
* requirements:
|
|
* page: '[0-9]+'
|
|
* slug: '.*'
|
|
*/
|
|
class ExtbasePluginEnhancer extends PluginEnhancer
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $routesOfPlugin;
|
|
|
|
public function __construct(array $configuration)
|
|
{
|
|
parent::__construct($configuration);
|
|
$this->routesOfPlugin = $this->configuration['routes'] ?? [];
|
|
// Only set the namespace if the plugin+extension keys are given. This allows to also use "namespace" property
|
|
// instead from the parent constructor.
|
|
if (
|
|
$this->namespace === ''
|
|
&& isset($this->configuration['extension'])
|
|
&& isset($this->configuration['plugin'])
|
|
) {
|
|
$extensionName = $this->configuration['extension'];
|
|
$pluginName = $this->configuration['plugin'];
|
|
$extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
|
|
$pluginSignature = strtolower($extensionName . '_' . $pluginName);
|
|
$this->namespace = 'tx_' . $pluginSignature;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function enhanceForMatching(RouteCollection $collection): void
|
|
{
|
|
$i = 0;
|
|
/** @var Route $defaultPageRoute */
|
|
$defaultPageRoute = $collection->get('default');
|
|
foreach ($this->routesOfPlugin as $configuration) {
|
|
$route = $this->getVariant($defaultPageRoute, $configuration);
|
|
$collection->add($this->namespace . '_' . $i++, $route);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function getVariant(Route $defaultPageRoute, array $configuration): Route
|
|
{
|
|
$arguments = $configuration['_arguments'] ?? [];
|
|
unset($configuration['_arguments']);
|
|
|
|
$variableProcessor = $this->getVariableProcessor();
|
|
$routePath = $this->modifyRoutePath($configuration['routePath']);
|
|
$routePath = $variableProcessor->deflateRoutePath($routePath, $this->namespace, $arguments);
|
|
unset($configuration['routePath']);
|
|
$options = array_merge($defaultPageRoute->getOptions(), ['_enhancer' => $this, 'utf8' => true, '_arguments' => $arguments]);
|
|
$route = new Route(rtrim($defaultPageRoute->getPath(), '/') . '/' . ltrim($routePath, '/'), [], [], $options);
|
|
|
|
$defaults = array_merge_recursive(
|
|
$defaultPageRoute->getDefaults(),
|
|
$variableProcessor->deflateKeys($this->configuration['defaults'] ?? [], $this->namespace, $arguments)
|
|
);
|
|
// only keep `defaults` that are actually used in `routePath`
|
|
$defaults = $this->filterValuesByPathVariables(
|
|
$route,
|
|
$defaults
|
|
);
|
|
// apply '_controller' to route defaults
|
|
$defaults = array_merge_recursive(
|
|
$defaults,
|
|
array_intersect_key($configuration, ['_controller' => true])
|
|
);
|
|
$route->setDefaults($defaults);
|
|
$this->applyRouteAspects($route, $this->aspects, $this->namespace);
|
|
$this->applyRequirements($route, $this->configuration['requirements'] ?? [], $this->namespace);
|
|
$this->applyStaticVariables($route, $this->configuration['static'] ?? [], $this->namespace);
|
|
return $route;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function enhanceForGeneration(RouteCollection $collection, array $originalParameters): void
|
|
{
|
|
if (!is_array($originalParameters[$this->namespace] ?? null)) {
|
|
return;
|
|
}
|
|
// apply default controller and action names if not set in parameters
|
|
if (!$this->hasControllerActionValues($originalParameters[$this->namespace])
|
|
&& !empty($this->configuration['defaultController'])
|
|
) {
|
|
$this->applyControllerActionValues(
|
|
$this->configuration['defaultController'],
|
|
$originalParameters[$this->namespace],
|
|
true
|
|
);
|
|
}
|
|
|
|
$i = 0;
|
|
/** @var Route $defaultPageRoute */
|
|
$defaultPageRoute = $collection->get('default');
|
|
foreach ($this->routesOfPlugin as $configuration) {
|
|
$variant = $this->getVariant($defaultPageRoute, $configuration);
|
|
// The enhancer tells us: This given route does not match the parameters
|
|
if (!$this->verifyRequiredParameters($variant, $originalParameters)) {
|
|
continue;
|
|
}
|
|
$parameters = $originalParameters;
|
|
unset($parameters[$this->namespace]['action']);
|
|
unset($parameters[$this->namespace]['controller']);
|
|
$compiledRoute = $variant->compile();
|
|
// contains all given parameters, even if not used as variables in route
|
|
$deflatedParameters = $this->deflateParameters($variant, $parameters);
|
|
$variables = array_flip($compiledRoute->getPathVariables());
|
|
$mergedParams = array_replace($variant->getDefaults(), $deflatedParameters);
|
|
// all params must be given, otherwise we exclude this variant
|
|
// (it is allowed that $variables is empty - in this case variables are
|
|
// "given" implicitly through controller-action pair in `_controller`)
|
|
if (array_diff_key($variables, $mergedParams)) {
|
|
continue;
|
|
}
|
|
$variant->addOptions(['deflatedParameters' => $deflatedParameters]);
|
|
$collection->add($this->namespace . '_' . $i++, $variant);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A route has matched the controller/action combination, so ensure that these properties
|
|
* are set to tx_blogexample_pi1[controller] and tx_blogexample_pi1[action].
|
|
*
|
|
* @param array $parameters Actual parameter payload to be used
|
|
* @param array $internals Internal instructions (_route, _controller, ...)
|
|
*/
|
|
public function inflateParameters(array $parameters, array $internals = []): array
|
|
{
|
|
$parameters = $this->getVariableProcessor()
|
|
->inflateNamespaceParameters($parameters, $this->namespace);
|
|
$parameters[$this->namespace] = $parameters[$this->namespace] ?? [];
|
|
|
|
// Invalid if there is no controller given, so this enhancers does not do anything
|
|
if (empty($internals['_controller'] ?? null)) {
|
|
return $parameters;
|
|
}
|
|
$this->applyControllerActionValues(
|
|
$internals['_controller'],
|
|
$parameters[$this->namespace],
|
|
false
|
|
);
|
|
return $parameters;
|
|
}
|
|
|
|
/**
|
|
* Check if controller+action combination matches
|
|
*/
|
|
protected function verifyRequiredParameters(Route $route, array $parameters): bool
|
|
{
|
|
if (!is_array($parameters[$this->namespace])) {
|
|
return false;
|
|
}
|
|
if (!$route->hasDefault('_controller')) {
|
|
return false;
|
|
}
|
|
$controller = $route->getDefault('_controller');
|
|
[$controllerName, $actionName] = explode('::', $controller);
|
|
if (!isset($parameters[$this->namespace]['controller']) || $controllerName !== $parameters[$this->namespace]['controller']) {
|
|
return false;
|
|
}
|
|
if (!isset($parameters[$this->namespace]['action']) || $actionName !== $parameters[$this->namespace]['action']) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Check if action and controller are not empty.
|
|
*/
|
|
protected function hasControllerActionValues(array $target): bool
|
|
{
|
|
return !empty($target['controller']) && !empty($target['action']);
|
|
}
|
|
|
|
/**
|
|
* Add controller and action parameters so they can be used later-on.
|
|
*
|
|
* @param array $target Reference to target array to be modified
|
|
* @param bool $tryUpdate Try updating action value - but only if controller value matches
|
|
*/
|
|
protected function applyControllerActionValues(string $controllerActionValue, array &$target, bool $tryUpdate = false)
|
|
{
|
|
if (!str_contains($controllerActionValue, '::')) {
|
|
return;
|
|
}
|
|
[$controllerName, $actionName] = explode('::', $controllerActionValue, 2);
|
|
// use default action name if controller matches
|
|
if ($tryUpdate && empty($target['action']) && $controllerName === ($target['controller'] ?? null)) {
|
|
$target['action'] = $actionName;
|
|
// use default controller name if action is defined (implies: non-default-controllers must be given)
|
|
} elseif ($tryUpdate && empty($target['controller']) && !empty($target['action'])) {
|
|
$target['controller'] = $controllerName;
|
|
// fallback and override
|
|
} else {
|
|
$target['controller'] = $controllerName;
|
|
$target['action'] = $actionName;
|
|
}
|
|
}
|
|
}
|