397 lines
14 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\DependencyInjection\Widget;
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
use Doctrine\Common\Proxy\Exception\InvalidArgumentException;
use LengthException;
use LogicException;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use UnexpectedValueException;
use function array_key_exists;
use function count;
use function get_class;
use function in_array;
use function is_array;
/**
* Compile the configurations and inject required service into container.
*
* The widgets are services tagged with :
*
* ```
* { name: chill_widget, alias: my_alias, place: my_place }
* ```
*
* Or, if the tag does not exist or if you need to add some config to your
* service depending on the config, you should use a `WidgetFactory` (see
* `WidgetFactoryInterface`.
*
* To reuse this compiler pass, simple execute the doProcess metho in your
* compiler. Example :
*
* ```
* namespace Chill\MainBundle\DependencyInjection\CompilerPass;
*
* use Symfony\Component\DependencyInjection\ContainerBuilder;
* use Chill\MainBundle\DependencyInjection\Widget\AbstractWidgetsCompilerPass;
* class WidgetsCompilerPass extends AbstractWidgetsCompilerPass {
*
* public function process(ContainerBuilder $container)
* {
* $this->doProcess($container, 'chill_main', 'chill_main.widgets');
* }
* }
* ```
*/
abstract class AbstractWidgetsCompilerPass implements CompilerPassInterface
{
/**
* the key to use to identify widget for a given place.
*/
public const WIDGET_CONFIG_ALIAS = 'widget_alias';
/**
* the key to use to order widget for a given place.
*/
public const WIDGET_CONFIG_ORDER = 'order';
/**
* The service which will manage the widgets.
*
* @var string
*/
public const WIDGET_MANAGER = 'chill.main.twig.widget';
/**
* the method wich register the widget into give service.
*/
public const WIDGET_MANAGER_METHOD_REGISTER = 'addWidget';
/**
* the key used to collect the alias in the service definition's tag.
* the alias must be
* injected into the configuration under 'alias' key.
*
* @var string
*/
public const WIDGET_SERVICE_TAG_ALIAS = 'alias';
/**
* the value of the `name` key in service definitions's tag.
*
* @var string
*/
public const WIDGET_SERVICE_TAG_NAME = 'chill_widget';
/**
* the key used to collect the authorized place in the service definition's tag.
*
* @var string
*/
public const WIDGET_SERVICE_TAG_PLACES = 'place';
/**
* cache of ordering by place.
*
* @internal used by function cacheAndGetOrdering
*
* @var array
*/
private $cacheOrdering = [];
/**
* @var WidgetFactoryInterface[]
*/
private $widgetFactories;
private $widgetServices = [];
/**
* process the configuration and the container to add the widget available.
*
* @param string $extension the extension of your bundle
* @param string $containerWidgetConfigParameterName the key under which we can use the widget configuration
*
* @throws LogicException
* @throws UnexpectedValueException if the given extension does not implement HasWidgetExtensionInterface
*/
public function doProcess(
ContainerBuilder $container,
$extension,
$containerWidgetConfigParameterName
) {
if (!$container->hasDefinition(self::WIDGET_MANAGER)) {
throw new LogicException('the service ' . self::WIDGET_MANAGER . ' should' .
' be present. It is required by ' . self::class);
}
$managerDefinition = $container->getDefinition(self::WIDGET_MANAGER);
// collect the widget factories
/** @var HasWidgetFactoriesExtensionInterface $extensionClass */
$extensionClass = $container->getExtension($extension);
// throw an error if extension does not implement HasWidgetFactoriesExtensionInterface
if (!$extensionClass instanceof HasWidgetFactoriesExtensionInterface) {
throw new UnexpectedValueException(sprintf(
'The extension for %s '
. 'do not implements %s. You should implement %s on %s',
$extension,
HasWidgetFactoriesExtensionInterface::class,
HasWidgetFactoriesExtensionInterface::class,
get_class($extensionClass)
));
}
$this->widgetFactories = $extensionClass->getWidgetFactories();
// collect the availabled tagged services
$this->collectTaggedServices($container);
// collect the widgets and their config :
$widgetParameters = $container->getParameter($containerWidgetConfigParameterName);
// and add them to the delegated_block
foreach ($widgetParameters as $place => $widgets) {
foreach ($widgets as $param) {
$alias = $param[self::WIDGET_CONFIG_ALIAS];
// check that the service exists
if (!array_key_exists($alias, $this->widgetServices)) {
throw new InvalidConfigurationException(sprintf('The alias %s' .
' is not defined.', $alias));
}
// check that the widget is allowed at this place
if (!$this->isPlaceAllowedForWidget($place, $alias, $container)) {
throw new InvalidConfigurationException(sprintf(
'The widget with alias %s is not allowed at place %s',
$alias,
$place
));
}
// get the order, eventually corrected
$order = $this->cacheAndGetOrdering($place, $param[self::WIDGET_CONFIG_ORDER]);
// register the widget with config to the service, using the method
// `addWidget`
if ($this->widgetServices[$alias] instanceof WidgetFactoryInterface) {
/** @var WidgetFactoryInterface $factory */
$factory = $this->widgetServices[$alias];
// get the config (under the key which equals to widget_alias
$config = $param[$factory->getWidgetAlias()] ?? [];
// register the service into the container
$serviceId = $this->registerServiceIntoContainer(
$container,
$factory,
$place,
$order,
$config
);
$managerDefinition->addMethodCall(
self::WIDGET_MANAGER_METHOD_REGISTER,
[
$place,
$order,
new Reference($serviceId),
$config,
]
);
} else {
$managerDefinition->addMethodCall(
self::WIDGET_MANAGER_METHOD_REGISTER,
[
$place,
$order,
new Reference($this->widgetServices[$alias]),
[], // the config is alway an empty array
]
);
}
}
}
}
/**
* This method collect all service tagged with `self::WIDGET_SERVICE_TAG`, and
* add also the widget defined by factories.
*
* This method also check that the service is correctly tagged with `alias` and
* `places`, or the factory give a correct alias and more than one place.
*
* @throws InvalidConfigurationException
* @throws InvalidArgumentException
*/
protected function collectTaggedServices(ContainerBuilder $container)
{
// first, check the service tagged in service definition
foreach ($container->findTaggedServiceIds(self::WIDGET_SERVICE_TAG_NAME) as $id => $attrs) {
foreach ($attrs as $attr) {
// check the alias is set
if (!isset($attr[self::WIDGET_SERVICE_TAG_ALIAS])) {
throw new InvalidConfigurationException('you should add an ' . self::WIDGET_SERVICE_TAG_ALIAS .
' key on the service ' . $id);
}
// check the place is set
if (!isset($attr[self::WIDGET_SERVICE_TAG_PLACES])) {
throw new InvalidConfigurationException(sprintf(
'You should add a %s key on the service %s',
self::WIDGET_SERVICE_TAG_PLACES,
$id
));
}
// check the alias does not exists yet
if (array_key_exists($attr[self::WIDGET_SERVICE_TAG_ALIAS], $this->widgetServices)) {
throw new InvalidArgumentException('a service has already be defined with the ' .
self::WIDGET_SERVICE_TAG_ALIAS . ' ' . $attr[self::WIDGET_SERVICE_TAG_ALIAS]);
}
// register the service as available
$this->widgetServices[$attr[self::WIDGET_SERVICE_TAG_ALIAS]] = $id;
}
}
// add the services defined by factories
foreach ($this->widgetFactories as $factory) {
/** @var WidgetFactoryInterface $factory */
$alias = $factory->getWidgetAlias();
// check the alias is not empty
if (empty($alias)) {
throw new LogicException(sprintf(
'the widget factory %s returns an empty alias',
get_class($factory)
));
}
// check the places are not empty
if (!is_array($factory->getAllowedPlaces())) {
throw new UnexpectedValueException("the method 'getAllowedPlaces' "
. 'should return a non-empty array. Unexpected value on ' .
get_class($factory));
}
if (count($factory->getAllowedPlaces()) === 0) {
throw new LengthException("The method 'getAllowedPlaces' should "
. 'return a non-empty array, but returned 0 elements on ' .
get_class($factory) . '::getAllowedPlaces()');
}
// check the alias does not exists yet
if (array_key_exists($alias, $this->widgetServices)) {
throw new InvalidArgumentException('a service has already be defined with the ' .
self::WIDGET_SERVICE_TAG_ALIAS . ' ' . $alias);
}
// register the factory as available
$this->widgetServices[$factory->getWidgetAlias()] = $factory;
}
}
/**
* register the service into container.
*
* @param string $place
* @param float $order
*
* @return string the id of the new service
*/
protected function registerServiceIntoContainer(
ContainerBuilder $container,
WidgetFactoryInterface $factory,
$place,
$order,
array $config
) {
$serviceId = $factory->getServiceId($container, $place, $order, $config);
$definition = $factory->createDefinition(
$container,
$place,
$order,
$config
);
$container->setDefinition($serviceId, $definition);
return $serviceId;
}
/**
* check if the ordering has already be used for the given $place and,
* if yes, correct the ordering by incrementation of 1 until the ordering
* has not be used.
*
* recursive method.
*
* @param string $place
* @param float $ordering
*
* @return float
*/
private function cacheAndGetOrdering($place, $ordering)
{
// create a key in the cache array if not exists
if (!array_key_exists($place, $this->cacheOrdering)) {
$this->cacheOrdering[$place] = [];
}
// check if the order exists
if (array_search($ordering, $this->cacheOrdering[$place], true)) {
// if the order exists, increment of 1 and try again
return $this->cacheAndGetOrdering($place, $ordering + 1);
}
// cache the ordering
$this->cacheOrdering[$place][] = $ordering;
return $ordering;
}
/**
* get the places where the service is allowed.
*
* @param mixed $place
* @param mixed $widgetAlias
*
* @return unknown
*/
private function isPlaceAllowedForWidget($place, $widgetAlias, ContainerBuilder $container)
{
if ($this->widgetServices[$widgetAlias] instanceof WidgetFactoryInterface) {
if (
in_array($place, $this->widgetServices[$widgetAlias]
->getAllowedPlaces(), true)
) {
return true;
}
} else {
$definition = $container->findDefinition($this->widgetServices[$widgetAlias]);
foreach ($definition->getTag(self::WIDGET_SERVICE_TAG_NAME) as $attrs) {
$placeValue = $attrs[self::WIDGET_SERVICE_TAG_PLACES];
if ($placeValue === $place) {
return true;
}
}
}
return false;
}
}