diff --git a/ChillMainBundle.php b/ChillMainBundle.php index 2f53f8e69..579908ea4 100644 --- a/ChillMainBundle.php +++ b/ChillMainBundle.php @@ -9,6 +9,8 @@ use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass; use Chill\MainBundle\DependencyInjection\TimelineCompilerClass; use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass; +use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass; + class ChillMainBundle extends Bundle { @@ -20,5 +22,6 @@ class ChillMainBundle extends Bundle $container->addCompilerPass(new TimelineCompilerClass()); $container->addCompilerPass(new RoleProvidersCompilerPass()); $container->addCompilerPass(new ExportsCompilerPass()); + $container->addCompilerPass(new WidgetsCompilerPass()); } } diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php index ef3fed0ae..e3a0f0d25 100644 --- a/DependencyInjection/ChillMainExtension.php +++ b/DependencyInjection/ChillMainExtension.php @@ -1,5 +1,22 @@ + * + * 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; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -7,20 +24,43 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; +use Chill\MainBundle\DependencyInjection\Configuration; /** - * This is the class that loads and manages your bundle configuration - * - * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} + * This class load config for chillMainExtension. */ -class ChillMainExtension extends Extension implements PrependExtensionInterface +class ChillMainExtension extends Extension implements PrependExtensionInterface, + Widget\HasWidgetFactoriesExtensionInterface { + /** + * widget factory + * + * @var WidgetFactoryInterface[] + */ + protected $widgetFactories = array(); + + public function addWidgetFactory(WidgetFactoryInterface $factory) + { + $this->widgetFactories[] = $factory; + } + + /** + * + * @return WidgetFactoryInterface[] + */ + public function getWidgetFactories() + { + return $this->widgetFactories; + } + /** * {@inheritDoc} */ public function load(array $configs, ContainerBuilder $container) { - $configuration = new Configuration(); + // configuration for main bundle + $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); $container->setParameter('chill_main.installation_name', @@ -34,14 +74,25 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface $container->setParameter('chill_main.pagination.item_per_page', $config['pagination']['item_per_page']); + + // add the key 'widget' without the key 'enable' + $container->setParameter('chill_main.widgets', + array('homepage' => $config['widgets']['homepage'])); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); $loader->load('services/logger.yml'); $loader->load('services/repositories.yml'); $loader->load('services/pagination.yml'); + } - + + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($this->widgetFactories, $container); + } + + public function prepend(ContainerBuilder $container) { $bundles = $container->getParameter('kernel.bundles'); @@ -57,7 +108,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface //add installation_name and date_format to globals $chillMainConfig = $container->getExtensionConfig($this->getAlias()); - $config = $this->processConfiguration(new Configuration(), $chillMainConfig); + $config = $this->processConfiguration($this + ->getConfiguration($chillMainConfig, $container), $chillMainConfig); $twigConfig = array( 'globals' => array( 'installation' => array( diff --git a/DependencyInjection/CompilerPass/WidgetsCompilerPass.php b/DependencyInjection/CompilerPass/WidgetsCompilerPass.php new file mode 100644 index 000000000..b28b8fdbb --- /dev/null +++ b/DependencyInjection/CompilerPass/WidgetsCompilerPass.php @@ -0,0 +1,34 @@ + + * + * 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\CompilerPass; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Chill\MainBundle\DependencyInjection\Widget\AbstractWidgetsCompilerPass; + +/** + * Compile the service definition to register widgets. + * + */ +class WidgetsCompilerPass extends AbstractWidgetsCompilerPass { + + public function process(ContainerBuilder $container) + { + $this->doProcess($container, 'chill_main', 'chill_main.widgets'); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 8bb6c9805..21c0d6a84 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -4,14 +4,34 @@ namespace Chill\MainBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + /** - * This is the class that validates and merges configuration from your app/config files - * - * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} + * Configure the main bundle */ class Configuration implements ConfigurationInterface { + + use AddWidgetConfigurationTrait; + + /** + * + * @var ContainerBuilder + */ + private $containerBuilder; + + + public function __construct(array $widgetFactories = array(), + ContainerBuilder $containerBuilder) + { + $this->setWidgetFactories($widgetFactories); + $this->containerBuilder = $containerBuilder; + } + /** * {@inheritDoc} */ @@ -25,18 +45,18 @@ class Configuration implements ConfigurationInterface ->scalarNode('installation_name') ->cannotBeEmpty() ->defaultValue('Chill') - ->end() + ->end() // end of scalar 'installation_name' ->arrayNode('available_languages') ->defaultValue(array('fr')) ->prototype('scalar')->end() - ->end() + ->end() // end of array 'available_languages' ->arrayNode('routing') ->children() ->arrayNode('resources') ->prototype('scalar')->end() - ->end() - ->end() - ->end() + ->end() // end of array 'resources' + ->end() // end of children + ->end() // end of array node 'routing' ->arrayNode('pagination') ->canBeDisabled() ->children() @@ -44,11 +64,20 @@ class Configuration implements ConfigurationInterface ->info('The number of item to show in the page result, by default') ->min(1) ->defaultValue(50) - ->end() - ->end() - ->end() - ->end(); + ->end() // end of integer 'item_per_page' + ->end() // end of children + ->end() // end of pagination + ->arrayNode('widgets') + ->canBeDisabled() + ->children() + ->append($this->addWidgetsConfiguration('homepage', $this->containerBuilder)) + ->end() // end of widgets/children + ->end() // end of widgets + ->end() // end of root/children + ->end() // end of root + ; + return $treeBuilder; } } diff --git a/DependencyInjection/Widget/AbstractWidgetsCompilerPass.php b/DependencyInjection/Widget/AbstractWidgetsCompilerPass.php new file mode 100644 index 000000000..f0a1205e9 --- /dev/null +++ b/DependencyInjection/Widget/AbstractWidgetsCompilerPass.php @@ -0,0 +1,388 @@ + + * + * 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; + + } + } + + +} \ No newline at end of file diff --git a/DependencyInjection/Widget/AddWidgetConfigurationTrait.php b/DependencyInjection/Widget/AddWidgetConfigurationTrait.php new file mode 100644 index 000000000..04c0088ba --- /dev/null +++ b/DependencyInjection/Widget/AddWidgetConfigurationTrait.php @@ -0,0 +1,243 @@ + + * + * 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\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Chill\MainBundle\DependencyInjection\Widget\AbstractWidgetsCompilerPass as WidgetsCompilerPass; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + + +/** + * This trait allow to add automatic configuration for widget inside your config. + * + * Usage + * ====== + * + * 1. Register widget factories + * ---------------------------- + * + * Add widget factories, using `setWidgetFactories` + * + * Example : + * + * ``` + * use Symfony\Component\DependencyInjection\ContainerBuilder; + * use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait; + * + * class MyConfig + * { + * + * use addWidgetConfigurationTrait; + * + * public function __construct(array $widgetFactories = array(), ContainerBuilder $container) + * { + * $this->setWidgetFactories($widgetFactories); + * // will be used on next step + * $this->container = $container; + * } + * + * } + * ``` + * + * + * + * 2. add widget config to your config + * ----------------------------------- + * + * ``` + * use Symfony\Component\DependencyInjection\ContainerBuilder; + * use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait; + * use Symfony\Component\Config\Definition\Builder\TreeBuilder; + * + * class MyConfig + * { + * + * use addWidgetConfigurationTrait; + * + * private $container; + * + * public function __construct(array $widgetFactories = array(), ContainerBuilder $container) + * { + * $this->setWidgetFactories($widgetFactories); + * $this->container; + * } + * + * public function getConfigTreeBuilder() + * { + * $treeBuilder = new TreeBuilder(); + * $root = $treeBuilder->root('my_root'); + * + * $root->children() + * ->arrayNode('widgets') + * ->canBeDisabled() + * ->children() + * ->append($this->addWidgetsConfiguration('homepage', $this->container)) + * ->end() + * ->end() + * ; + * + * return $treeBuilder; + * } + * + * } + * ``` + * + * the above code will add to the config : + * + * ``` + * widgets: + * enabled: true + * + * # register widgets on place "homepage" + * homepage: + * order: ~ # Required + * widget_alias: ~ # One of "person_list"; "add_person", Required + * person_list: + * # options for the person_list + * ``` + * + * + */ +trait AddWidgetConfigurationTrait +{ + /** + * @param WidgetFactoryInterface[] + */ + private $widgetFactories; + + /** + * + * @param WidgetFactoryInterface[] $widgetFactories + */ + public function setWidgetFactories(array $widgetFactories) + { + $this->widgetFactories = $widgetFactories; + } + + /** + * @return WidgetFactoryInterface[] + */ + public function getWidgetFactories() + { + return $this->widgetFactories; + } + + /** + * add configuration nodes for the widget at the given place. + * + * @param type $place + * @param ContainerBuilder $containerBuilder + * @return type + */ + protected function addWidgetsConfiguration($place, ContainerBuilder $containerBuilder) + { + $treeBuilder = new TreeBuilder(); + $root = $treeBuilder->root($place) + ->canBeUnset() + ->info('register widgets on place "'.$place.'"'); + + // if no childen, return the root + if (count(iterator_to_array($this->filterWidgetByPlace($place))) === 0) { + return $root; + } + + $prototypeChildren = $root->prototype('array')->children(); + + $prototypeChildren + ->floatNode(WidgetsCompilerPass::WIDGET_CONFIG_ORDER) + ->isRequired() + ->info("the ordering of the widget. May be a number with decimal") + ->example("10.58") + ->end() + ->enumNode(WidgetsCompilerPass::WIDGET_CONFIG_ALIAS) + ->values($this->getWidgetAliasesbyPlace($place, $containerBuilder)) + ->info("the widget alias (see config for your bundle)") + ->isRequired() + ->end() + ; + + // adding the possible config on each widget under the widget_alias + foreach ($this->filterWidgetByPlace($place) as $factory) { + $builder = new TreeBuilder(); + $widgetOptionsRoot = $builder->root($factory->getWidgetAlias()); + $widgetOptionsRoot->canBeUnset() + ->info(sprintf('the configuration for the widget "%s" (only required if this widget is set in widget_alias)', + $factory->getWidgetAlias())); + $factory->configureOptions($place, $widgetOptionsRoot->children()); + $prototypeChildren->append($widgetOptionsRoot); + } + + return $root; + } + + /** + * get all widget factories for the given place. + * + * @param string $place + * @return \Generator a generator containing a widget factory + */ + protected function filterWidgetByPlace($place) + { + foreach($this->widgetFactories as $factory) { + if (in_array($place, $factory->getAllowedPlaces())) { + yield $factory; + } + } + } + + /** + * get the all possible aliases for the given place. This method + * search within service tags and widget factories + * + * @param type $place + * @param ContainerBuilder $containerBuilder + * @return type + * @throws InvalidConfigurationException if a service's tag does not have the "alias" key + */ + protected function getWidgetAliasesbyPlace($place, ContainerBuilder $containerBuilder) + { + $result = array(); + + foreach ($this->filterWidgetByPlace($place) as $factory) { + $result[] = $factory->getWidgetAlias(); + } + + // append the aliases added without factory + foreach ($containerBuilder + ->findTaggedServiceIds(WidgetsCompilerPass::WIDGET_SERVICE_TAG_NAME) + as $serviceId => $tags) { + foreach ($tags as $tag) { + // throw an error if no alias in definition + if (!array_key_exists(WidgetsCompilerPass::WIDGET_SERVICE_TAG_ALIAS, $tag)) { + throw new InvalidConfigurationException(sprintf( + "The service with id %s does not have any %d key", + $serviceId, + WidgetsCompilerPass::WIDGET_SERVICE_TAG_ALIAS + )); + } + // add the key to the possible results + $result[] = $tag[WidgetsCompilerPass::WIDGET_SERVICE_TAG_ALIAS]; + } + } + + return $result; + } +} \ No newline at end of file diff --git a/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php b/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php new file mode 100644 index 000000000..7e5a12871 --- /dev/null +++ b/DependencyInjection/Widget/Factory/AbstractWidgetFactory.php @@ -0,0 +1,57 @@ + + * + * 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\Factory; + +use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Allow to easily create WidgetFactory. + * + * Extending this factory, the widget will be created using the already defined + * service created "as other services" in your configuration (the most frequent + * way is using `services.yml` file. + * + * If you need to create different service based upon place, position or + * definition, you should implements directly + * `Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface` + * + * + */ +abstract class AbstractWidgetFactory implements WidgetFactoryInterface +{ + + /** + * + * {@inheritdoc} + * + * Will create the definition by returning the definition from the `services.yml` + * file (or `services.xml` or `what-you-want.yml`). + * + * @see \Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface::createDefinition() + */ + public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config) + { + return $containerBuilder->getDefinition($this + ->getServiceId($containerBuilder, $place, $order, $config) + ); + } + +} \ No newline at end of file diff --git a/DependencyInjection/Widget/Factory/WidgetFactoryInterface.php b/DependencyInjection/Widget/Factory/WidgetFactoryInterface.php new file mode 100644 index 000000000..6e7836b01 --- /dev/null +++ b/DependencyInjection/Widget/Factory/WidgetFactoryInterface.php @@ -0,0 +1,103 @@ + + * + * 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\Factory; + + +use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Factory for creating configuration of widgets. + * + * When you need a widget with some configuration, you should implements this + * interface on a factory. The factory will add configuration to the bundle + * giving the places for your widget. + * + * Using this interface, **you do not need** to define the service in your + * container configuration (`services.yml` files). + * + * Once the class is created, you should inject the factory inside the container + * at compile time, in your `Bundle` class : + * + * + * ``` + * namespace Chill\PersonBundle; + * + * use Symfony\Component\HttpKernel\Bundle\Bundle; + * use Symfony\Component\DependencyInjection\ContainerBuilder; + * use Chill\PersonBundle\Widget\PersonListWidgetFactory; + * + * class ChillPersonBundle extends Bundle + * { + * public function build(ContainerBuilder $container) + * { + * parent::build($container); + * // register a widget factory into chill_main : + * $container->getExtension('chill_main') + * ->addWidgetFactory(new PersonListWidgetFactory()); + * } + * } + * ``` + * + * + * + */ +interface WidgetFactoryInterface +{ + /** + * configure options for the widget. Those options will be added in + * configuration in the bundle where the widget will be used. + * + * @param type $place + * @param NodeBuilder $node + */ + public function configureOptions($place, NodeBuilder $node); + + /** + * get the widget alias. This alias will be used in configuration (`config.yml`) + */ + public function getWidgetAlias(); + + /** + * Create a definition for the service which will render the widget. + * + * (Note: you can define the service by yourself, as other services, + * using the `AbstractWidgetFactory`) + * + * @param ContainerBuilder $containerBuilder + * @param type $place + * @param type $order + * @param array $config + */ + public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config); + + /** + * return the service id to build the widget. + * + * @param ContainerBuilder $containerBuilder + * @param string $place + * @param float $order + * @param array $config + * + * @return string the service definition + * + */ + public function getServiceId(ContainerBuilder $containerBuilder, $place, $order, array $config); +} diff --git a/DependencyInjection/Widget/HasWidgetFactoriesExtensionInterface.php b/DependencyInjection/Widget/HasWidgetFactoriesExtensionInterface.php new file mode 100644 index 000000000..b25c487b6 --- /dev/null +++ b/DependencyInjection/Widget/HasWidgetFactoriesExtensionInterface.php @@ -0,0 +1,43 @@ + + * + * 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 Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; + +/** + * Register widget factories to an extension. + * + */ +interface HasWidgetFactoriesExtensionInterface { + + /** + * register a widget factory + * + * @param \Chill\MainBundle\DependencyInjection\Widget\WidgetFactoryInterface $factory + */ + public function addWidgetFactory(WidgetFactoryInterface $factory); + + /** + * get all the widget factories registered for this extension + * + * @return WidgetFactoryInterface[] + */ + public function getWidgetFactories(); +} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 199c9850e..a5c4b4687 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -58,8 +58,8 @@ services: tags: - { name: twig.extension } - chill.main.twig.delegated_block: - class: Chill\MainBundle\Templating\DelegatedBlockRenderingTwig + chill.main.twig.widget: + class: Chill\MainBundle\Templating\Widget\WidgetRenderingTwig arguments: - "@event_dispatcher" tags: diff --git a/Resources/views/layout.html.twig b/Resources/views/layout.html.twig index e93f0cb62..e8c8db00e 100644 --- a/Resources/views/layout.html.twig +++ b/Resources/views/layout.html.twig @@ -137,6 +137,10 @@ + +
+ {{ chill_delegated_block('homepage', {} ) }} +
{{ chill_menu('homepage', { diff --git a/Templating/DelegatedBlockRenderingTwig.php b/Templating/DelegatedBlockRenderingTwig.php deleted file mode 100644 index 77027aaef..000000000 --- a/Templating/DelegatedBlockRenderingTwig.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * 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\Templating; - -use Symfony\Component\EventDispatcher\EventDispatcherInterface; - -/** - * Add the function `chill_delegated_block`. - * - * In a template, you can now allow rendering of a block from other bundle. - * - * The layout template must explicitly call the rendering of other block, - * with the twig function - * - * ``` - * chill_delegated_block('block_name', { 'array' : 'with context' } ) - * ``` - * - * This will launch an event - * `Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent` with - * the event's name 'chill_block.block_name'. - * - * You may add content to the page using the function - * `DelegatedBlockRenderingEvent::addContent`. - * - * See also the documentation of - * `Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent` - * for usage of this event class - * - * - * @author Julien Fastré - */ -class DelegatedBlockRenderingTwig extends \Twig_Extension -{ - /** - * - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - - public function __construct(EventDispatcherInterface $eventDispatcher) - { - $this->eventDispatcher = $eventDispatcher; - } - - - public function getName() - { - return 'chill_main_delegated_block'; - } - - public function getFunctions() - { - return array( - new \Twig_SimpleFunction('chill_delegated_block', - array($this, 'renderingDelegatedBlock'), - array('is_safe' => array('html'))) - ); - } - - public function renderingDelegatedBlock($block, array $context) - { - $event = new Events\DelegatedBlockRenderingEvent($context); - - $this->eventDispatcher->dispatch('chill_block.'.$block, $event); - - return $event->getContent(); - } - -} diff --git a/Templating/Widget/WidgetInterface.php b/Templating/Widget/WidgetInterface.php new file mode 100644 index 000000000..76af9af56 --- /dev/null +++ b/Templating/Widget/WidgetInterface.php @@ -0,0 +1,10 @@ + + * + * 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\Templating\Widget; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Chill\MainBundle\Templating\Widget\WidgetInterface; +use Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent; + +/** + * Add the function `chill_delegated_block`. + * + * In a template, you can now allow rendering of a block from other bundle. + * + * The layout template must explicitly call the rendering of other block, + * with the twig function + * + * ``` + * chill_delegated_block('block_name', { 'array' : 'with context' } ) + * ``` + * + * This will launch an event + * `Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent` with + * the event's name 'chill_block.block_name'. + * + * You may add content to the page using the function + * `DelegatedBlockRenderingEvent::addContent`. + * + * See also the documentation of + * `Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent` + * for usage of this event class + * + * + * @author Julien Fastré + */ +class WidgetRenderingTwig extends \Twig_Extension +{ + + /** + * Contains the widget. This is a double dimension array : + * + * - first key is the place, + * - second key is the ordering ; + * - the value is an array where the widget is the first argument and the + * second is the config + * + * i.e : + * + * $widget['place']['ordering'] = array($widget, $config); + * + * + * + * @var array an array of widget by place and ordering + */ + protected $widget = array(); + + /** + * + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + public function __construct(EventDispatcherInterface $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + } + + + public function getName() + { + return 'chill_main_widget'; + } + + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('chill_delegated_block', + array($this, 'renderingWidget'), + array( + 'is_safe' => array('html'), + 'needs_environment' => true, + 'deprecated' => true, 'alternative' => 'chill_widget' + )), + new \Twig_SimpleFunction('chill_widget', + array($this, 'renderingWidget'), + array('is_safe' => array('html'), 'needs_environment' => true)) + ); + } + + public function renderingWidget(\Twig_Environment $env, $block, array $context = array()) + { + // get the content of widgets + $content = ''; + foreach ($this->getWidgetsArraysOrdered($block) as $a) { + /* @var $widget Widget\WidgetInterface */ + $widget = $a[0]; + $config = $a[1]; + + $content = $widget->render($env, $block, $context, $config); + } + + // for old rendering events (deprecated) + $event = new DelegatedBlockRenderingEvent($context); + + $this->eventDispatcher->dispatch('chill_block.'.$block, $event); + + return $content." ".$event->getContent(); + } + + /** + * add a widget to this class, which become available for a future call. + * + * This function is used by DependencyInjection\CompilerPass\WidgetCompilerPass, + * which add the widget to this class when it is created by from DI, according + * to the given config under `chill_main`. + * + * @param string $place + * @param WidgetInterface $widget + * @param array $config + */ + public function addWidget($place, $ordering, $widget, array $config = array()) + { + $this->widget[$place][$ordering] = array($widget, $config); + } + + /** + * + * @param string $place + * @return array + */ + protected function getWidgetsArraysOrdered($place) + { + if (!array_key_exists($place, $this->widget)) { + $this->widget[$place] = array(); + } + + \ksort($this->widget[$place]); + + return $this->widget[$place]; + } + + + +}