Skip to content

Commit

Permalink
Create a proper Container & Router for plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierstoval authored and cedric-anne committed Oct 17, 2024
1 parent fa48566 commit b9da5d9
Show file tree
Hide file tree
Showing 10 changed files with 478 additions and 4 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"phpdocumentor/reflection-docblock": "^5.4",
"phpoffice/phpspreadsheet": "^3.3",
"psr/cache": "^3.0",
"psr/container": "^1.1",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"ralouphie/getallheaders": "^3.0",
Expand Down
4 changes: 2 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion dependency_injection/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
$parameters->set('env(APP_SECRET_FILE)', $projectDir . '/config/glpicrypt.key');
$parameters->set('kernel.secret', env('default:glpi.default_secret:file:APP_SECRET_FILE'));

$services
$services = $services
->defaults()
->autowire()
->autoconfigure()
Expand All @@ -59,6 +59,7 @@
$services->load('Glpi\Config\\', $projectDir . '/src/Glpi/Config');
$services->load('Glpi\Controller\\', $projectDir . '/src/Glpi/Controller');
$services->load('Glpi\Http\\', $projectDir . '/src/Glpi/Http');
$services->load('Glpi\DependencyInjection\\', $projectDir . '/src/Glpi/DependencyInjection');

$services->set(Firewall::class)
->factory([Firewall::class, 'createDefault'])
Expand Down
3 changes: 3 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,9 @@ public static function getLibraries($all = false)
[ 'name' => 'psr/cache',
'check' => 'Psr\\Cache\\CacheItemPoolInterface'
],
[ 'name' => 'psr/container',
'check' => 'Psr\\Container\\ContainerInterface'
],
[ 'name' => 'league/csv',
'check' => 'League\\Csv\\Writer'
],
Expand Down
8 changes: 7 additions & 1 deletion src/Glpi/Config/LegacyConfigurators/InitializePlugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@

namespace Glpi\Config\LegacyConfigurators;

use Glpi\Asset\AssetDefinitionManager;
use Glpi\Config\LegacyConfigProviderInterface;
use Glpi\DependencyInjection\PluginContainer;
use Plugin;
use Update;

final readonly class InitializePlugins implements LegacyConfigProviderInterface
{
public function __construct(private PluginContainer $pluginContainer)
{
}

public function execute(): void
{
/*
Expand All @@ -53,5 +57,7 @@ public function execute(): void

$plugin = new Plugin();
$plugin->init(true);

$this->pluginContainer->initializeContainer();
}
}
200 changes: 200 additions & 0 deletions src/Glpi/DependencyInjection/PluginContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2024 Teclib' and contributors.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

namespace Glpi\DependencyInjection;

use Glpi\Kernel\Kernel;
use Glpi\Routing\PluginRoutesLoader;
use Plugin;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\Config\Builder\ConfigBuilderGenerator;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Config\FileLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Loader\Psr4DirectoryLoader;
use Symfony\Component\Routing\Router;

class PluginContainer implements ContainerInterface
{
private ?ContainerInterface $internal_container = null;
private Container $symfony_container;
private ParameterBag $container_parameters;
private Kernel $kernel;

public function __construct(
#[Autowire(service: 'service_container')] Container $symfony_container,
#[Autowire(service: 'parameter_bag')] ParameterBag $container_parameters,
#[Autowire(service: 'kernel')] Kernel $kernel,
) {
$this->symfony_container = $symfony_container;
$this->container_parameters = $container_parameters;
$this->kernel = $kernel;
}

public function set(string $id, ?object $service): void
{
$this->internal_container->set($id, $service);
}

public function initialized(string $id): bool
{
return $this->internal_container->initialized($id);
}

public function getParameter(string $name): \UnitEnum|float|array|bool|int|string|null
{
return $this->internal_container->getParameter($name) ?? $this->symfony_container->getParameter($name);
}

public function hasParameter(string $name): bool
{
return $this->internal_container->hasParameter($name) || $this->symfony_container->hasParameter($name);
}

public function setParameter(string $name, \UnitEnum|float|array|bool|int|string|null $value): void
{
$this->internal_container->setParameter($name, $value);
}

public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object
{
$this->initializeContainer();

try {
return $this->internal_container->get($id, self::EXCEPTION_ON_INVALID_REFERENCE);
} catch (NotFoundExceptionInterface) {
}

return $this->symfony_container->get($id, $invalidBehavior);
}

public function has(string $id): bool
{
$this->initializeContainer();

if ($this->internal_container->has($id)) {
return true;
}

return $this->symfony_container->has($id);
}

public function initializeContainer(): void
{
if ($this->internal_container) {
return;
}

$container = new ContainerBuilder(new ParameterBag($this->container_parameters->all()));

$this->configureContainerServices($container);

$container->compile();

$this->internal_container = $container;
}

private function configureContainerServices(ContainerBuilder $container): void
{
$configurator = $this->getContainerConfigurator($container);

// Handy configurator (just like Symfony config files)
$services = $configurator->services();
$services
->defaults()
->autowire()
->autoconfigure()
->instanceof(PublicService::class)->public()
;


// Autoconfig
$container->registerForAutoconfiguration(PublicService::class)->setPublic(true);


// Routes loader
$route_loader = $services->set(PluginRoutesLoader::class);
$route_loader->bind('$env', $this->kernel->getEnvironment());
$services->alias('routing.loader', PluginRoutesLoader::class);


// Plugins routing
$router = $services->set('glpi_plugin_router', Router::class)->public();
$router->bind('$loader', new Reference(PluginRoutesLoader::class));
$router->bind('$resource', 'plugins_routes');
$services->alias(UrlGeneratorInterface::class, 'glpi_plugin_router');

// Plugins services
foreach (Plugin::getPlugins() as $key) {
$path = Plugin::getPhpDir($key);
if (!\is_dir($path . '/src/Controller/')) {
continue;
}
$services->load(\NS_PLUG . \ucfirst($key) . '\\Controller\\', $path . '/src/Controller/');
}
}
/**
* Hacks coming from Symfony's way of handling custom configurators.
*
* @see https://github.com/symfony/symfony/blob/e61a0146e746a4f75498c1cb1c2aa2be5faf8d05/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php
*/
private function getContainerConfigurator(ContainerBuilder $container): ContainerConfigurator
{
$env = $this->kernel->getEnvironment();
$locator = new FileLocator($this->kernel);
$resolver = new LoaderResolver([
new PhpFileLoader($container, $locator, $env, class_exists(ConfigBuilderGenerator::class) ? new ConfigBuilderGenerator($this->kernel->getBuildDir()) : null),
new Psr4DirectoryLoader($locator),
]);

$loader = new DelegatingLoader($resolver);

$file = (new \ReflectionObject($this->kernel))->getFileName();
/* @var PhpFileLoader $kernelLoader */
$kernelLoader = $loader->getResolver()->resolve($file);
$instanceof = \Closure::bind(fn &() => $this->instanceof, $kernelLoader, $kernelLoader)();
$configurator = new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file, $this->kernel->getEnvironment());

return $configurator;
}
}
8 changes: 8 additions & 0 deletions src/Glpi/Http/ListenersPriority.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ final class ListenersPriority
// Also, keep it after the `LegacyRouterListener` to not map to the generic dropdown controller if a
// legacy script exists for the requested URI.
LegacyDropdownRouteListener::class => 300,

// This listener allows matching plugins routes at runtime,
// that's why it's executed right after Symfony's Router,
// and also after GLPI's config is set.
//
// Symfony's Router priority is 32.
// @see \Symfony\Component\HttpKernel\EventListener\RouterListener::getSubscribedEvents()
PluginsRouterListener::class => 31,
];

private function __construct()
Expand Down
Loading

0 comments on commit b9da5d9

Please sign in to comment.