add documentation for widget

This commit is contained in:
Julien Fastré 2016-10-07 00:27:12 +02:00
parent 53a6ca3d6b
commit fe8ade2ca8
8 changed files with 707 additions and 99 deletions

View File

@ -38,7 +38,7 @@ Layout and UI
Layout / Template usage <user-interface/layout-template-usage.rst>
Classes and mixins <user-interface/css-classes.rst>
Delegated blocks <user-interface/delegated-blocks.rst>
Widgets <user-interface/widgets.rst>
Help, I am lost !

View File

@ -1,98 +0,0 @@
.. Copyright (C) 2016 Champs Libres Cooperative SCRLFS
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
Delegating rendering of block to other bundles
##############################################
Sometimes, you may want to delegate part of your layout to another bundle(s), which might, or might not, installed.
Examples :
- you may want to show task in the top bar, only if the bundle "task" is installed (**Note**: this bundle does not exists... yet !)
- you may want to show the group belonging (see :ref:`group-bundle`) below of the vertical menu, only if the bundle is installed.
This is possible using `the symfony dispatcher event <http://symfony.com/doc/current/components/event_dispatcher/index.html>`_.
Inserting a delegated block inside a template
==============================================
Use the twig function :code:`chill_delegated_block`.
Example :
.. code-block:: html+jinja
<div class="my_block">{{ chill_delegated_block('my_block', { 'person': person }) }}</div>
In this example, the block name is :code:`my_block`, and the context is an array : :code:`{ 'person': person }`.
The :code:`div` will be filled with the html produced by the bundles which suscribed to the event :code:`chill_block.my_block`.
Subscribing to a delegated block
=================================
Create a :code:`Subscriber` or a :code:`Listener` as `described in the Symfony documentation <http://symfony.com/doc/current/cookbook/event_dispatcher/event_listener.html>`_.
You should listen to the event :code:`chill_block.block_name`, where `block_name` is the name of the delegated block. For instance, in the example above, the event will be :code:`chill_block.my_block`.
The event passed as argument will be an instance of :class:`Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent`. The context will be available as an array, as described in subscriber example. You may add content to the page using the :method:`Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent::addContent` method.
.. warning::
The code inserted by the function :code:`chill_delegated_block` **should be html safe**. You are encouraged to use an instance of the templating engine (aka Twig) to produce clean html.
Example :
.. code-block:: php
namespace Chill\GroupBundle\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Chill\MainBundle\Templating\Events\DelegatedBlockRenderingEvent;
class TemplatingPostVerticalMenuEventSubscriber implements EventSubscriberInterface
{
// constructor logic will take place here in a real world
public static function getSubscribedEvents()
{
return array('chill_block.person_post_vertical_menu' => array(
array('processRendering', 0) // you may change the priority if you want your content to be inserted upper or below of the other content.
));
}
// here is where we add content to the event.
public function processRendering(DelegatedBlockRenderingEvent $event)
{
// we access the person using $event['person']
$memberships = $memberships = $this->membershipRepository
->findBy(array('person' => $event['person']));
// we add content to the templating using the templating engine
$event->addContent(
$this->templating
->render('ChillGroupBundle:Membership:short_listing.html.twig', array(
'memberships' => $memberships,
))
);
}
}
This tag is registered as a service :
.. code-block:: yaml
services:
chill_group.membership_rendering_event:
class: Chill\GroupBundle\Events\TemplatingPostVerticalMenuEventSubscriber
tags:
- { name: kernel.event_subscriber }
You should have a look at the documentation of the bundle to know which delegated block are available and what is their context.

View File

@ -0,0 +1,334 @@
.. Copyright (C) 2016 Champs Libres Cooperative SCRLFS
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
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 task in the top bar, only if the bundle "task" is installed (**Note**: this bundle does not exists... yet !)
- you may want to show the group belonging (see :ref:`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 `prepending the configuration of the bundle which define the place <http://symfony.com/doc/current/bundles/prepend_extension.html>`_).
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 :ref:`main-bundle` widget provided by :ref:`person-bundle`
=========================================== ======== ============================= =======================================
Creating a widget without configuration
========================================
To add a widget, you should :
- define your widget, implementing :class:`Chill\MainBundle\Templating\Widget\WidgetInterface` ;
- declare your widget with tag `chill_widget`.
Define the widget class
-----------------------
Define your widget class by implemeting :class:`Chill\MainBundle\Templating\Widget\WidgetInterface`.
Example :
.. code-block:: php
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 :
- :code:`$env` the :class:`\Twig_Environment`, which you can use to render your widget ;
- :code:`$place` a string representing the place where the widget is rendered ;
- :code:`$context` the context given by the template ;
- :code:`$config` the configuration which is, in this case, always an empty array (see :ref:`creating-a-widget-with-config`).
.. note::
The html returned by the :code:`render` function will be considered as html safe. You should strip html before returning it. See also `How to escape output in template <http://symfony.com/doc/current/templating/escaping.html>`_.
Declare your widget
-------------------
Declare your widget as a service and add it the tag :code:`chill_widget`:
.. code-block:: yaml
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 :
- :code:`alias`: an alias, which will be used to reference the widget into the config
- :code:`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.
.. code-block:: bash
$ 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 :ref:`declaring-widget-by-default`.
.. _creating-a-widget-with-config:
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 :
.. literalinclude:: ./widgets/ChillPersonAddAPersonWidget.php
:language: php
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 :code:`Factory` during next step.
.. code-block:: yaml
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 `Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface`. For your convenience, an :class:`Chill\MainBundle\DependencyInjection\Widget\Factory\AbstractWidgetFactory` will already implements some easy method.
.. literalinclude:: ./widgets/ChillPersonAddAPersonListWidgetFactory.php
:language: php
.. 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 :code:`getServiceId`. But you can create or adapt programmatically the definition. `See the symfony doc on how to do it <http://symfony.com/doc/current/service_container/definitions.html#working-with-a-definition>`_.
.. code-block:: php
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 :code:`Extension` class which provide the place. This is done in the :code: `Bundle` class.
.. code-block:: php
# 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-widget-by-default:
Declaring a widget by default
=============================
Use the ability `to prepend configuration of other bundle <http://symfony.com/doc/current/bundles/prepend_extension.html>`_. A living example here :
.. literalinclude:: ./widgets/ChillPersonExtension.php
:language: php
Defining a place
================
Add your place in template
--------------------------
A place should be defined by using the :code:`chill_widget` function, which take as argument :
- :code:`place` (string) a string defining the place ;
- :code:`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 :class:`Chill\PersonBundle\Entity\Person` class will be in the context.
Example :
.. code-block:: html+jinja
{# an empty context on homepage #}
{{ chill_widget('homepage', {} }}
.. code-block:: html+jinja
{# 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 :class:`Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait`, which provide the method `addWidgetConfiguration($place, ContainerBuilder $container)`.
Example :
.. literalinclude:: ./widgets/ChillMainConfiguration.php
:language: php
:emphasize-lines: 17, 30, 32, 52
:linenos:
.. _example-chill-main-extension:
You should also adapt the :class:`DependencyInjection\*Extension` class to add ContainerBuilder and WidgetFactories :
.. literalinclude:: ./widgets/ChillMainExtension.php
:language: php
:emphasize-lines: 25-39, 48-49, 56
:linenos:
- line 25-39: we implements the method required by :class:`Chill\MainBundle\DependencyInjection\Widget\HasWidgetExtensionInterface` ;
- line 48-49: we record the configuration of widget into container's parameter ;
- line 56 : we create an instance of :class:`Configuration` (declared above)
Compile the possible widget using Compiler pass
-----------------------------------------------
For your convenience, simply extends :class:`Chill\MainBundle\DependencyInjection\Widget\AbstractWidgetsCompilerPass`. This class provides a `doProcess(ContainerBuildere $container, $extension, $parameterName)` method which will do the job for you:
- :code:`$container` is the container builder
- :code:`$extension` is the extension name
- :code:`$parameterName` is the name of the parameter which contains the configuration for widgets (see :ref:`the example with ChillMain above <example-chill-main-extension>`.
.. code-block:: php
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 <http://symfony.com/doc/current/service_container/compiler_passes.html>`_, you should register your Compiler Pass into your bundle :
.. code-block:: php
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());
}
}

View File

@ -0,0 +1,64 @@
<?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;
}
}

View File

@ -0,0 +1,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);
}
}

View File

@ -0,0 +1,69 @@
<?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';
}
}

View File

@ -0,0 +1,129 @@
<?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
}
}

View File

@ -0,0 +1,51 @@
<?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
)
)
)
));
}
}