Widgets

Rationale

Widgets are useful if you want to publish content on a page provided by another bundle.

Examples :

  • you want to publish a list of people on the homepage ;
  • you may want to show the group belonging (see Group bundle) below of the vertical menu, only if the bundle is installed.

The administrator of the chill instance may configure the presence of widget. Although, some widget are defined by default (see Declaring a widget by default).

Concepts

A bundle may define place(s) where a widget may be rendered.

In a single place, zero, one or more widget may be displayed.

Some widget may require some configuration, and some does not require any configuration.

Example:

Use case place place defined by... widget provided by...
Publishing a list of people on the homepage homepage defined by Main bundle widget provided by Person bundle

Creating a widget without configuration

To add a widget, you should :

Define the widget class

Define your widget class by implemeting ChillMainBundleTemplatingWidgetWidgetInterface.

Example :

namespace Chill\PersonBundle\Widget;

use Chill\MainBundle\Templating\Widget\WidgetInterface;


/**
* Add a button "add a person"
*
*/
class AddAPersonWidget implements WidgetInterface
{
   public function render(
      \Twig_Environment $env,
      $place,
      array $context,
      array $config
   ) {
       // this will render a link to the page "add a person"
       return $env->render("ChillPersonBundle:Widget:homepage_add_a_person.html.twig");
   }
}

Arguments are :

  • $env the Twig_Environment, which you can use to render your widget ;
  • $place a string representing the place where the widget is rendered ;
  • $context the context given by the template ;
  • $config the configuration which is, in this case, always an empty array (see Creating a widget with configuration).

Note

The html returned by the render function will be considered as html safe. You should strip html before returning it. See also How to escape output in template.

Declare your widget

Declare your widget as a service and add it the tag chill_widget:

service:
    chill_person.widget.add_person:
        class: Chill\PersonBundle\Widget\AddAPersonWidget
        tags:
            - { name: chill_widget, alias: add_person, place: homepage }

The tag must contains those arguments :

  • alias: an alias, which will be used to reference the widget into the config
  • place: a place where this widget is authorized

If you want your widget to be available on multiple places, you should add one tag with each place.

Conclusion

Once your widget is correctly declared, your widget should be available in configuration.

$ php app/console config:dump-reference chill_main
# Default configuration for extension with alias: "chill_main"
chill_main:
        [...]
        # register widgets on place "homepage"
        homepage:

            # the ordering of the widget. May be a number with decimal
            order:                ~ # Required, Example: 10.58

            # the widget alias (see your installed bundles config). Possible values are (maybe incomplete) : person_list, add_person
            widget_alias:         ~ # Required

If you want to add your widget by default, see Declaring a widget by default.

Creating a widget with configuration

You can declare some configuration with your widget, which allow administrators to add their own configuration.

To add some configuration, you will :

  • declare a widget as defined above ;
  • optionnaly declare it as a service ;
  • add a widget factory, which will add configuration to the bundle which provide the place.

Declare your widget class

Declare your widget. You can use some configuration elements in your process, as used here :

<?php
# Chill/PersonBundle/Widget/PersonListWidget.php

namespace Chill\PersonBundle\Widget;

use Chill\MainBundle\Templating\Widget\WidgetInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr;
use Doctrine\DBAL\Types\Type;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\EntityManager;

/**
 * add a widget with person list. 
 * 
 * The configuration is defined by `PersonListWidgetFactory`
 */
class PersonListWidget implements WidgetInterface
{
    
    /**
     * Repository for persons
     * 
     * @var EntityRepository
     */
    protected $personRepository;
    
    /**
     * The entity manager
     *
     * @var EntityManager
     */
    protected $entityManager;
    
    /**
     * the authorization helper
     * 
     * @var AuthorizationHelper;
     */
    protected $authorizationHelper;
    
    /**
     *
     * @var TokenStorage
     */
    protected $tokenStorage;
    
    /**
     *
     * @var UserInterface
     */
    protected $user;
    
    public function __construct(
            EntityRepository $personRepostory,
            EntityManager $em,
            AuthorizationHelper $authorizationHelper,
            TokenStorage $tokenStorage
            ) {
        $this->personRepository = $personRepostory;
        $this->authorizationHelper = $authorizationHelper;
        $this->tokenStorage = $tokenStorage;
        $this->entityManager = $em;
    }

    /**
     * 
     * @param type $place
     * @param array $context
     * @param array $config
     * @return string
     */
    public function render(\Twig_Environment $env, $place, array $context, array $config)
    { 
        $qb = $this->personRepository
                ->createQueryBuilder('person');
        
        // show only the person from the authorized centers
        $and = $qb->expr()->andX();
        $centers = $this->authorizationHelper
                ->getReachableCenters($this->getUser(), new Role(PersonVoter::SEE));
        $and->add($qb->expr()->in('person.center', ':centers'));
        $qb->setParameter('centers', $centers);
        

        // add the "only active" where clause
        if ($config['only_active'] === true) {
            $qb->join('person.accompanyingPeriods', 'ap');
            $or = new Expr\Orx();
            // add the case where closingDate IS NULL
            $andWhenClosingDateIsNull = new Expr\Andx();
            $andWhenClosingDateIsNull->add((new Expr())->isNull('ap.closingDate'));
            $andWhenClosingDateIsNull->add((new Expr())->gte(':now', 'ap.openingDate'));
            $or->add($andWhenClosingDateIsNull);
            // add the case when now is between opening date and closing date
            $or->add(
                    (new Expr())->between(':now', 'ap.openingDate', 'ap.closingDate')
                );
            $and->add($or);
            $qb->setParameter('now', new \DateTime(), Type::DATE);
        }
        
        // adding the where clause to the query
        $qb->where($and);
        
        $qb->setFirstResult(0)->setMaxResults($config['number_of_items']);
        
        $persons = $qb->getQuery()->getResult();
        
        return $env->render(
            'ChillPersonBundle:Widget:homepage_person_list.html.twig',
            array('persons' => $persons)
            );
    }
    
    /**
     * 
     * @return UserInterface
     */
    private function getUser()
    {
        // return a user
    }

}

Declare your widget as a service

You can declare your widget as a service. Not tag is required, as the service will be defined by the Factory during next step.

services:
   chill_person.widget.person_list:
       class: Chill\PersonBundle\Widget\PersonListWidget
       arguments:
           - "@chill.person.repository.person"
           - "@doctrine.orm.entity_manager"
           - "@chill.main.security.authorization.helper"
           - "@security.token_storage"
   # this widget is defined by the PersonListWidgetFactory

You can eventually skip this step and declare your service into the container through the factory (see above).

Declare your widget factory

The widget factory must implements ChillMainBundleDependencyInjectionWidgetFactoryWidgetFactoryInterface. For your convenience, an ChillMainBundleDependencyInjectionWidgetFactoryAbstractWidgetFactory will already implements some easy method.

<?php

# Chill/PersonBundle/Widget/PersonListWidgetFactory

namespace Chill\PersonBundle\Widget;

use Chill\MainBundle\DependencyInjection\Widget\Factory\AbstractWidgetFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;

/**
 * add configuration for the person_list widget.
 */
class PersonListWidgetFactory extends AbstractWidgetFactory
{
   /*
    * append the option to the configuration
    * see http://symfony.com/doc/current/components/config/definition.html
    *
    */
    public function configureOptions($place, NodeBuilder $node)
    {
        $node->booleanNode('only_active')
                ->defaultTrue()
                ->end();
        $node->integerNode('number_of_items')
            ->defaultValue(50)
            ->end();
        $node->scalarNode('filtering_class')
                ->defaultNull()
                ->end();
            
    }
     
   /*
    * return an array with the allowed places where the widget can be rendered
    *
    * @return string[]
    */   
    public function getAllowedPlaces()
    {
        return array('homepage');
    }
    
   /* 
    * return the widget alias
    *
    * @return string
    */
    public function getWidgetAlias()
    {
        return 'person_list';
    }
    
    /*
     * return the service id for the service which will render the widget.
     *
     * this service must implements `Chill\MainBundle\Templating\Widget\WidgetInterface`
     * 
     * the service must exists in the container, and it is not required that the service 
     * has the `chill_main` tag.
     */
    public function getServiceId(ContainerBuilder $containerBuilder, $place, $order, array $config)
    {
        return 'chill_person.widget.person_list';
    }
    
}

Note

You can declare your widget into the container by overriding the createDefinition method. By default, this method will return the already existing service definition with the id given by getServiceId. But you can create or adapt programmatically the definition. See the symfony doc on how to do it.

public function createDefinition(ContainerBuilder $containerBuilder, $place, $order, array $config)
{
    $definition = new \Symfony\Component\DependencyInjection\Definition('my\Class');
    // create or adapt your definition here

    return $definition;
}

You must then register your factory into the Extension class which provide the place. This is done in the :code: Bundle class.

# Chill/PersonBundle/ChillPersonBundle.php

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);

        $container->getExtension('chill_main')
            ->addWidgetFactory(new PersonListWidgetFactory());
    }
}

Declaring a widget by default

Use the ability to prepend configuration of other bundle. A living example here :

<?php

# Chill/PersonBundle/DependencyInjection/ChillPersonExtension.php

namespace Chill\PersonBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
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\MissingBundleException;
use Chill\PersonBundle\Security\Authorization\PersonVoter;

/**
 * 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}
 */
class ChillPersonExtension extends Extension implements PrependExtensionInterface
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
      // ...
    }
    
    
    /**
     * 
     * Add a widget "add a person" on the homepage, automatically
     * 
     * @param \Chill\PersonBundle\DependencyInjection\containerBuilder $container
     */
    public function prepend(ContainerBuilder $container) 
    {
        $container->prependExtensionConfig('chill_main', array(
            'widgets' => array(
                'homepage' => array(
                    array(
                        'widget_alias' => 'add_person',
                        'order' => 2
                    )
                )
            )
        ));
    }
}

Defining a place

Add your place in template

A place should be defined by using the chill_widget function, which take as argument :

  • place (string) a string defining the place ;
  • context (array) an array defining the context.

The context should be documented by the bundle. It will give some information about the context of the page. Example: if the page concerns a people, the ChillPersonBundleEntityPerson class will be in the context.

Example :

{# an empty context on homepage #}
{{ chill_widget('homepage', {} }}
{# defining a place 'right column' with the person currently viewed
{{ chill_widget('right_column', { 'person' : person } }}

Declare configuration for you place

In order to let other bundle, or user, to define the widgets inside the given place, you should open a configuration. You can use the Trait ChillMainBundleDependencyInjectionWidgetAddWidgetConfigurationTrait, which provide the method addWidgetConfiguration($place, ContainerBuilder $container).

Example :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
   # Chill\MainBundle\DependencyInjection\Configuration.php
  
   namespace Chill\MainBundle\DependencyInjection;

   use Symfony\Component\Config\Definition\Builder\TreeBuilder;
   use Symfony\Component\Config\Definition\ConfigurationInterface;
   use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait;
   use Symfony\Component\DependencyInjection\ContainerBuilder;

   /**
    * Configure the main bundle
    */
   class Configuration implements ConfigurationInterface
   {
       
       use AddWidgetConfigurationTrait;
       
       /**
	*
	* @var ContainerBuilder
	*/
       private $containerBuilder;

       
       public function __construct(array $widgetFactories = array(), 
	       ContainerBuilder $containerBuilder)
       {
           // we register here widget factories (see below)
	   $this->setWidgetFactories($widgetFactories);
           // we will need the container builder later...
	   $this->containerBuilder = $containerBuilder;
       }
       
       /**
	* {@inheritDoc}
	*/
       public function getConfigTreeBuilder()
       {
	   $treeBuilder = new TreeBuilder();
	   $rootNode = $treeBuilder->root('chill_main');

	   $rootNode
	       ->children()

                   // ...

		   ->arrayNode('widgets')
		       ->canBeDisabled()
		       ->children()
                            // we declare here all configuration for homepage place
			    ->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;
       }
   }

You should also adapt the DependencyInjection*Extension class to add ContainerBuilder and WidgetFactories :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

#Chill\MainBundle\DependencyInjection\ChillMainExtension.php

namespace Chill\MainBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
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 class load config for chillMainExtension.
 */
class ChillMainExtension extends Extension implements 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;
    }
    
    public function load(array $configs, ContainerBuilder $container)
    {
        // configuration for main bundle
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);
        
        // add the key 'widget' without the key 'enable'
        $container->setParameter('chill_main.widgets', 
            array('homepage' => $config['widgets']['homepage']));

        // ... 
    }
    
    public function getConfiguration(array $config, ContainerBuilder $container)
    {
        return new Configuration($this->widgetFactories, $container);
    }
    
}

Compile the possible widget using Compiler pass

For your convenience, simply extends ChillMainBundleDependencyInjectionWidgetAbstractWidgetsCompilerPass. This class provides a doProcess(ContainerBuildere $container, $extension, $parameterName) method which will do the job for you:

  • $container is the container builder
  • $extension is the extension name
  • $parameterName is the name of the parameter which contains the configuration for widgets (see the example with ChillMain above.
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');
    }
}

As explained in the symfony docs, you should register your Compiler Pass into your bundle :

namespace Chill\MainBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;


class ChillMainBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new WidgetsCompilerPass());
    }
}