* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Chill\MainBundle\DependencyInjection\Widget; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Doctrine\Common\Proxy\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Definition; use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; use Chill\MainBundle\DependencyInjection\Widget\HasWidgetFactoriesExtensionInterface; /** * 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 { private $widgetServices = array(); /** * * @var WidgetFactoryInterface[] */ private $widgetFactories; /** * The service which will manage the widgets * * @var string */ const WIDGET_MANAGER = 'chill.main.twig.widget'; /** * the method wich register the widget into give service. */ const WIDGET_MANAGER_METHOD_REGISTER = 'addWidget'; /** * the value of the `name` key in service definitions's tag * * @var string */ const WIDGET_SERVICE_TAG_NAME = 'chill_widget'; /** * 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 */ const WIDGET_SERVICE_TAG_ALIAS = 'alias'; /** * the key used to collect the authorized place in the service definition's tag * * @var string */ const WIDGET_SERVICE_TAG_PLACES = 'place'; /** * the key to use to order widget for a given place */ const WIDGET_CONFIG_ORDER = 'order'; /** * the key to use to identify widget for a given place */ const WIDGET_CONFIG_ALIAS = 'widget_alias'; /** * process the configuration and the container to add the widget available * * @param ContainerBuilder $container * @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 * @throws \InvalidConfigurationException if there are errors in the config */ 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 $extensionClass HasWidgetFactoriesExtensionInterface */ $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 $factory WidgetFactoryInterface */ $factory = $this->widgetServices[$alias]; // get the config (under the key which equals to widget_alias $config = isset($param[$factory->getWidgetAlias()]) ? $param[$factory->getWidgetAlias()] : array(); // register the service into the container $serviceId =$this->registerServiceIntoContainer($container, $factory, $place, $order, $config); $managerDefinition->addMethodCall(self::WIDGET_MANAGER_METHOD_REGISTER, array( $place, $order, new Reference($serviceId), $config )); } else { $managerDefinition->addMethodCall(self::WIDGET_MANAGER_METHOD_REGISTER, array( $place, $order, new Reference($this->widgetServices[$alias]), array() // the config is alway an empty array )); } } } } /** * register the service into container. * * @param ContainerBuilder $container * @param WidgetFactoryInterface $factory * @param string $place * @param float $order * @param array $config * @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; } /** * cache of ordering by place. * * @internal used by function cacheAndGetOrdering * @var array */ private $cacheOrdering = array(); /** * 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] = array(); } // check if the order exists if (array_search($ordering, $this->cacheOrdering[$place])) { // if the order exists, increment of 1 and try again return $this->cacheAndGetOrdering($place, $ordering + 1); } else { // cache the ordering $this->cacheOrdering[$place][] = $ordering; return $ordering; } } /** * get the places where the service is allowed * * @param Definition $definition * @return unknown */ private function isPlaceAllowedForWidget($place, $widgetAlias, ContainerBuilder $container) { if ($this->widgetServices[$widgetAlias] instanceof WidgetFactoryInterface) { if (in_array($place, $this->widgetServices[$widgetAlias] ->getAllowedPlaces())) { 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; } /** * 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. * * @param ContainerBuilder $container * @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 $factory WidgetFactoryInterface */ $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; } } }