371 lines
16 KiB
PHP
371 lines
16 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\Backend\Module;
|
|
|
|
use TYPO3\CMS\Backend\Routing\Route;
|
|
use TYPO3\CMS\Backend\Routing\RouterConfigurationEvent;
|
|
use TYPO3\CMS\Core\Routing\RouteCollection;
|
|
|
|
/**
|
|
* @internal Always use the ModuleProvider API to access modules
|
|
*/
|
|
final class ModuleRegistry
|
|
{
|
|
/**
|
|
* @var ModuleInterface[]
|
|
*/
|
|
private array $modules = [];
|
|
|
|
/**
|
|
* This contains all available "aliases" as key, and the real module identifier as value
|
|
* @var array<string, string>
|
|
*/
|
|
private array $moduleAliases = [];
|
|
|
|
/**
|
|
* @param ModuleInterface[] $modules
|
|
*/
|
|
public function __construct(array $modules)
|
|
{
|
|
array_walk($modules, $this->addModule(...));
|
|
$this->modules = $this->applyHierarchy($this->modules);
|
|
$this->populateAliasMapping();
|
|
}
|
|
|
|
private function addModule(ModuleInterface $module): void
|
|
{
|
|
$identifier = $module->getIdentifier();
|
|
if (isset($this->modules[$identifier])) {
|
|
throw new \LogicException(
|
|
'A module with the identifier ' . $identifier . ' is already registered.',
|
|
1642174843
|
|
);
|
|
}
|
|
$this->modules[$module->getIdentifier()] = $module;
|
|
}
|
|
|
|
public function hasModule(string $identifier): bool
|
|
{
|
|
return isset($this->modules[$identifier]) || isset($this->moduleAliases[$identifier]);
|
|
}
|
|
|
|
public function getModule(string $identifier): ModuleInterface
|
|
{
|
|
if (!$this->hasModule($identifier)) {
|
|
throw new \InvalidArgumentException(
|
|
'Module with identifier ' . $identifier . ' does not exist.',
|
|
1642375889
|
|
);
|
|
}
|
|
|
|
// Resolve the alias to the real module
|
|
if (isset($this->moduleAliases[$identifier])) {
|
|
$identifier = $this->moduleAliases[$identifier];
|
|
}
|
|
return $this->modules[$identifier];
|
|
}
|
|
|
|
/**
|
|
* @return ModuleInterface[]
|
|
*/
|
|
public function getModules(): array
|
|
{
|
|
return $this->modules;
|
|
}
|
|
|
|
/**
|
|
* Handle router configuration when the router is set up.
|
|
*
|
|
* The event registration is wired in `TYPO3\CMS\Backend\ServiceProvider::addEventListeners`
|
|
* instead of using an `AsEventListener` because this service is not autowired, but has a
|
|
* custom factory in `TYPO3\CMS\Backend\ServiceProvider`.
|
|
*/
|
|
public function registerRoutesForModules(RouterConfigurationEvent $event): void
|
|
{
|
|
$router = $event->router;
|
|
foreach ($this->modules as $module) {
|
|
if (!$module->hasParentModule() && !$module->isStandalone()) {
|
|
// Skip first level modules, which are not standalone
|
|
continue;
|
|
}
|
|
$routeCollection = new RouteCollection();
|
|
foreach ($module->getDefaultRouteOptions() as $routeIdentifier => $routeOptions) {
|
|
$path = (string)(($routeOptions['path'] ?? false) ?: ('/' . $routeIdentifier));
|
|
$methods = (array)($routeOptions['methods'] ?? []);
|
|
unset($routeOptions['path'], $routeOptions['methods']);
|
|
if ($routeIdentifier === '_default') {
|
|
// Add the first
|
|
$route = new Route($module->getPath(), $routeOptions);
|
|
if ($methods !== []) {
|
|
$route->setMethods($methods);
|
|
}
|
|
$router->addRoute($module->getIdentifier(), $route, $module->getAliases());
|
|
} else {
|
|
$route = new Route($path, $routeOptions);
|
|
if ($methods !== []) {
|
|
$route->setMethods($methods);
|
|
}
|
|
$routeCollection->add($routeIdentifier, $route);
|
|
}
|
|
}
|
|
$routeCollection->addNamePrefix($module->getIdentifier() . '.');
|
|
$routeCollection->addPrefix($module->getPath());
|
|
$router->addRouteCollection($routeCollection);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies sorting (based on the "position" configuration) to all
|
|
* registered modules and resolves the hierarchy (creating relations
|
|
* by attaching modules to the $parentModule and $subModules properties).
|
|
*
|
|
* @param ModuleInterface[] $modules
|
|
* @return ModuleInterface[]
|
|
*/
|
|
private function applyHierarchy(array $modules): array
|
|
{
|
|
// Fetch top-level (parent) modules and fill them with sorted sub modules
|
|
$topLevelModules = [];
|
|
foreach ($modules as $identifier => $module) {
|
|
if ($module->getParentIdentifier() === '') {
|
|
$topLevelModules[$identifier] = $module;
|
|
}
|
|
$subModules = array_filter(
|
|
$modules,
|
|
static fn(ModuleInterface $mod): bool => $mod->getParentIdentifier() === $identifier
|
|
);
|
|
if ($subModules === []) {
|
|
continue;
|
|
}
|
|
// Sort sub modules and connect them with their parent module
|
|
$subModules = $this->applySorting($subModules);
|
|
foreach ($subModules as $subModule) {
|
|
$module->addSubModule($subModule);
|
|
$subModule->setParentModule($module);
|
|
}
|
|
}
|
|
|
|
// Promote single submodules to standalone modules and rebuild top-level modules afterwards
|
|
$topLevelModules = array_filter(
|
|
$this->promoteSingleSubmodulesToStandalone($modules, $topLevelModules),
|
|
static fn(ModuleInterface $module): bool => !$module->hasParentModule() || $module->isStandalone()
|
|
);
|
|
|
|
// Sort top level modules and return all modules (flat) with the correct sorting
|
|
return $this->flattenModules($this->applySorting($topLevelModules));
|
|
}
|
|
|
|
/**
|
|
* Promotes submodules to standalone if their parent module has only one submodule and is not
|
|
* standalone itself. This makes single submodules behave like standalone top-level modules.
|
|
*
|
|
* @param ModuleInterface[] $modules
|
|
* @param ModuleInterface[] $topLevelModules
|
|
* @return ModuleInterface[]
|
|
*/
|
|
private function promoteSingleSubmodulesToStandalone(array $modules, array $topLevelModules): array
|
|
{
|
|
foreach ($topLevelModules as $parentIdentifier => $parentModule) {
|
|
// Skip already standalone modules
|
|
if ($parentModule->isStandalone()) {
|
|
continue;
|
|
}
|
|
// Only promote if explicitly enabled via appearance setting
|
|
if (!($parentModule->getAppearance()['promotesSingleSubmoduleToStandalone'] ?? false)) {
|
|
continue;
|
|
}
|
|
$subModules = $parentModule->getSubModules();
|
|
if (count($subModules) !== 1) {
|
|
continue;
|
|
}
|
|
|
|
/** @var BaseModule $subModule */
|
|
$subModule = reset($subModules);
|
|
|
|
// Promote the submodule to standalone, inheriting parent properties
|
|
$subModule->promoteToStandalone(
|
|
$parentModule->getNavigationComponent(),
|
|
$parentModule->getPosition(),
|
|
$parentModule->getAliases()
|
|
);
|
|
|
|
// Remove parent module from registry
|
|
unset($modules[$parentIdentifier]);
|
|
}
|
|
|
|
return $modules;
|
|
}
|
|
|
|
/**
|
|
* Ensures that modules within one level of hierarchy are ordered properly the way
|
|
* they were given respecting their "top", "bottom", "before" or "after" definition.
|
|
*
|
|
* @param ModuleInterface[] $modules
|
|
* @return ModuleInterface[]
|
|
*/
|
|
private function applySorting(array $modules): array
|
|
{
|
|
$modulePositionInformation = [];
|
|
// First create a list of all needed data, that is the identifier, and its position
|
|
foreach ($modules as $identifier => $module) {
|
|
// @todo Should we enforce ['after' => '*'] in case ->getPosition() is empty?
|
|
$modulePositionInformation[$identifier] = $module->getPosition();
|
|
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyBefore'] = [];
|
|
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyAfter'] = [];
|
|
}
|
|
|
|
// Identifiers of modules to be added at the top
|
|
$highPriorityModules = [];
|
|
// Identifiers of modules to be added at the bottom
|
|
$lowPriorityModules = [];
|
|
|
|
// Sort out the "top" and "bottom", and also build a graph of directly dependant (before/after) modules
|
|
foreach ($modulePositionInformation as $identifier => $positionInformation) {
|
|
if ($positionInformation['before'] ?? false) {
|
|
if ($positionInformation['before'] === '*') {
|
|
// Module should be added on top
|
|
$highPriorityModules[] = $identifier;
|
|
} elseif (isset($modules[$positionInformation['before']])) {
|
|
// Build the dependencies in case a valid module identifier is configured
|
|
$modulePositionInformation[$positionInformation['before']]['modulesToBeAddedDirectlyBefore'][] = $identifier;
|
|
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyAfter'][] = $positionInformation['before'];
|
|
}
|
|
} elseif ($positionInformation['after'] ?? false) {
|
|
if ($positionInformation['after'] === '*') {
|
|
// Module should be added at the bottom
|
|
$lowPriorityModules[] = $identifier;
|
|
} elseif (isset($modules[$positionInformation['after']])) {
|
|
// Build the dependencies in case a valid module identifier is configured
|
|
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyBefore'][] = $positionInformation['after'];
|
|
$modulePositionInformation[$positionInformation['after']]['modulesToBeAddedDirectlyAfter'][] = $identifier;
|
|
}
|
|
}
|
|
}
|
|
|
|
// First add the top items and their dependant modules
|
|
$orderedModuleIdentifiers = $this->populateOrderingsForDependencies($highPriorityModules, $modulePositionInformation);
|
|
// Now add the bottom items and their dependant modules
|
|
// They will be cut out later, however, this is done now to also add their dependencies NOW (and not when looping over all items again)
|
|
$orderedModuleIdentifiers = $this->populateOrderingsForDependencies($lowPriorityModules, $modulePositionInformation, $orderedModuleIdentifiers);
|
|
$lastLowPriorityModule = end($orderedModuleIdentifiers);
|
|
// Loop through all items and see which have not been added yet. Keep the original sorting.
|
|
$orderedModuleIdentifiers = $this->populateOrderingsForDependencies(array_keys($modulePositionInformation), $modulePositionInformation, $orderedModuleIdentifiers);
|
|
// Find the lowest priority module and move everything after that module to the very end
|
|
if ($lowPriorityModules !== [] && $lastLowPriorityModule) {
|
|
$firstLowPriorityModule = reset($lowPriorityModules);
|
|
if ($firstLowPriorityModule !== $lastLowPriorityModule) {
|
|
$firstPosition = array_search($firstLowPriorityModule, $orderedModuleIdentifiers, true);
|
|
$lastPosition = array_search($lastLowPriorityModule, $orderedModuleIdentifiers, true);
|
|
if ($firstPosition !== false && $lastPosition !== false) {
|
|
$extractedItems = array_slice($orderedModuleIdentifiers, $firstPosition, $lastPosition);
|
|
$orderedModuleIdentifiers = array_merge($orderedModuleIdentifiers, $extractedItems);
|
|
}
|
|
}
|
|
}
|
|
// Use the ordered list and replace all valid identifiers with the corresponding
|
|
return array_replace(array_intersect_key(array_flip($orderedModuleIdentifiers), $modules), $modules);
|
|
}
|
|
|
|
/**
|
|
* Given module identifiers are added based on their module position information to the
|
|
* $alreadyOrderedModuleIdentifiers array (if not already present). In case the modules
|
|
* to be added have dependencies to other modules and those modules exist, corresponding
|
|
* modules are added to $alreadyOrderedModuleIdentifiers on the correct position as well.
|
|
*/
|
|
private function populateOrderingsForDependencies(
|
|
array $moduleIdentifiersToBeAdded,
|
|
array $modulePositionInformation,
|
|
array $alreadyOrderedModuleIdentifiers = []
|
|
): array {
|
|
foreach ($moduleIdentifiersToBeAdded as $identifier) {
|
|
// already placed somewhere
|
|
if (in_array($identifier, $alreadyOrderedModuleIdentifiers, true)) {
|
|
continue;
|
|
}
|
|
// Check if the current module has dependencies, which should be added BEFORE
|
|
foreach ($modulePositionInformation[$identifier]['modulesToBeAddedDirectlyBefore'] ?? [] as $dependantIdentifier) {
|
|
// already placed somewhere
|
|
if (in_array($dependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
|
|
continue;
|
|
}
|
|
// Check if the dependent module has dependencies, which should be added BEFORE
|
|
foreach ($modulePositionInformation[$dependantIdentifier]['modulesToBeAddedDirectlyBefore'] ?? [] as $dependantDependantIdentifier) {
|
|
// already placed somewhere
|
|
if (in_array($dependantDependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
|
|
continue;
|
|
}
|
|
// Add the sub dependency right away
|
|
$alreadyOrderedModuleIdentifiers[] = $dependantDependantIdentifier;
|
|
}
|
|
// Add the dependant module now
|
|
$alreadyOrderedModuleIdentifiers[] = $dependantIdentifier;
|
|
}
|
|
// Add the actual module now
|
|
$alreadyOrderedModuleIdentifiers[] = $identifier;
|
|
// Check if the current module has dependencies, which should be added AFTER
|
|
foreach ($modulePositionInformation[$identifier]['modulesToBeAddedDirectlyAfter'] ?? [] as $dependantIdentifier) {
|
|
// already placed somewhere
|
|
if (in_array($dependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
|
|
continue;
|
|
}
|
|
// Add the dependant module right away
|
|
$alreadyOrderedModuleIdentifiers[] = $dependantIdentifier;
|
|
// Check if the dependent module has dependencies, which should be added AFTER
|
|
foreach ($modulePositionInformation[$dependantIdentifier]['modulesToBeAddedDirectlyAfter'] ?? [] as $dependantDependantIdentifier) {
|
|
// already placed somewhere
|
|
if (in_array($dependantDependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
|
|
continue;
|
|
}
|
|
// Add the sub dependency right away
|
|
$alreadyOrderedModuleIdentifiers[] = $dependantDependantIdentifier;
|
|
}
|
|
}
|
|
}
|
|
return $alreadyOrderedModuleIdentifiers;
|
|
}
|
|
|
|
/**
|
|
* Create a flat modules array (looping through each level by calling "getSubmodules()" on the parent)
|
|
*/
|
|
private function flattenModules(array $modules, $flatModules = []): array
|
|
{
|
|
foreach ($modules as $module) {
|
|
$flatModules[$module->getIdentifier()] = $module;
|
|
if ($module->hasSubmodules()) {
|
|
$flatModules = $this->flattenModules($module->getSubmodules(), $flatModules);
|
|
}
|
|
}
|
|
return $flatModules;
|
|
}
|
|
|
|
private function populateAliasMapping(): void
|
|
{
|
|
foreach ($this->modules as $moduleIdentifier => $module) {
|
|
foreach ($module->getAliases() as $aliasIdentifier) {
|
|
// Note: The last module defining the same alias wins in general
|
|
$this->moduleAliases[$aliasIdentifier] = $moduleIdentifier;
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getModuleAliases(): array
|
|
{
|
|
return $this->moduleAliases;
|
|
}
|
|
}
|