Skip to content
This repository has been archived by the owner on Sep 16, 2021. It is now read-only.

documentation for publish workflow #171

Merged
merged 6 commits into from
Jul 14, 2013
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 330 additions & 19 deletions bundles/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,328 @@ Configuration

# app/config/config.yml
cmf_core:
document_manager_name: default
role: IS_AUTHENTICATED_ANONYMOUSLY # used by the publish workflow checker
document_manager_name: null # used for the twig functions to fetch documents
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be manager_name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed it should, but its not. i'll do a separate PR once this change is merged.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rmsint: it would be consistent, yes. but unfortunately its like this in
the actual bundle atm. we should clean this up however. care to do a pr?
or open an issue so we don't forget?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I will do a PR on the CoreBundle to change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use ~?

publish_workflow:
enabled: true
view_non_published_role: ROLE_CAN_VIEW_NON_PUBLISHED
request_listener: true

.. code-block:: xml

<!-- app/config/config.xml -->
<!-- role: used by the publish workflow checker -->
<config xmlns="http://symfony.com/schema/dic/core"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

symfony.com => cmf.symfony.com

document-manager-name="default"
role="IS_AUTHENTICATED_ANONYMOUSLY"
/>
document-manager-name="null">
<publish-workflow
enabled="true"
view-non-published-role="ROLE_CAN_VIEW_NON_PUBLISHED"
request-listener="true"
/>
<config/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</config>


.. code-block:: php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// app/config/config.php

$container->loadFromExtension('cmf_core', array(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be indented one level back

'document_manager_name' => null,
'publish_workflow' => array(
'enabled' => true,
'view_non_published_role' => 'ROLE_CAN_VIEW_NON_PUBLISHED',
'request_listener' => true,
),
));

The publish workflow is enabled by default. If you do not want to use it, you
can set ``cmf_core.publish_workflow.enabled: false`` to gain some performance.

.. _bundle-core-publish_workflow:

Publish Workflow Checker
------------------------
Publish Workflow
----------------

The Bundle provides a ``cmf_core.publish_workflow_checker`` service
which implements ``PublishWorkflowCheckerInterface``. This interface defines a
single method ``checkIsPublished()``.
The publish workflow system allows to control what content is available on the
site. This is similar to the `Symfony2 security component`_. But contrary to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symfony2 Security component

security context, the publish check can be executed even when no firewall is in
place and the security context thus has no token (see `Symfony2 Authorization`_).

.. code-block:: php
The publish workflow is also tied into the security workflow: The core bundle
registers a security voter that forwards security checks to the publish
workflow. This means that if you always have a firewall, you can just use
the normal security context and the twig function ``is_granted`` to check for
publication.

$publishWorkflowChecker = $container->get('cmf_core.publish_workflow_checker');
A good introduction to the Symfony core security is the `Security Chapter`_ in
the Symfony2 book.

// if to ignore the role when deciding if to consider the document as published
$ignoreRole = false;

if ($publishWorkflowChecker->checkIsPublished($document, $ignoreRole)) {
Check if content is published
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if Content is Published

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The Bundle provides the ``cmf_core.publish_workflow.checker`` service which
implements the :class:`Symfony\\Component\\Security\\Core\\SecurityContextInterface`
of the Symfony security component. The method to check publication is, like
with the security context,
:method:`Symfony\\Component\\Security\\Core\\SecurityContextInterface::isGranted()``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double backticks on the end should be single

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and you must remove ()


This method is used as when doing `ACL checks`_: The first argument is the
desired action, the second the content object you want to do the action on.

In 1.0, the only actions supported by the default voters are ``VIEW`` and
``VIEW_ANONYMOUS``. Having the right to view means that the current user is
allowed to see this content either because it is published or because of his
specific permissions. In some contexts, your application might not want to
show unpublished content even to a privileged user so as not to confuse him.
For this, the "view anonymous" permission is used.

The workflow checker is configured with a role that is allowed to bypass
publication checks so that it can see unpublished content. This role should be
given to editors. The default name of the role is ``ROLE_CAN_VIEW_NON_PUBLISHED``.

.. configuration-block::

.. code-block:: yaml

# app/config/security.yml
security:
role_hierarchy:
ROLE_EDITOR: ROLE_CAN_VIEW_NON_PUBLISHED

.. code-block:: xml

<!-- app/config/security.xml -->
<config>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<config xmlns="http://symfony.com/schema/dic/security">

<role id="ROLE_EDITOR">ROLE_CAN_VIEW_NON_PUBLISHED</role>
</config>

.. code-block:: php

// app/config/security.php
$container->loadFromExtension('security', array(
'role_hierarchy' => array(
'ROLE_EDITOR' => 'ROLE_CAN_VIEW_NON_PUBLISHED',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unneeded spaces

),
));

Once a user with ``ROLE_EDITOR`` is logged in - meaning there is a firewall in place for the path
in question - he will have the permission to view unpublished content as well::

use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker;

// check if current user is allowed to see this document
$publishWorkflowChecker = $container->get('cmf_core.publish_workflow.checker');
if ($publishWorkflowChecker->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $document)) {
// ...
}
// check if the document is published, do not make an exception if current
// user would have the right to see this content
if ($publishWorkflowChecker->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $document)) {
// ...
}

To check publication in a template, use the twig function ``cmf_is_published``:

.. code-block:: jinja

{# check if document is published, regardless of current users role #}
{% if cmf_is_published(page) %}
{# output the document #}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{# ... output the document %}

Same on line 155

{% endif %}

{# check if current logged in user is allowed to view the document either
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text should start on a new line

because it is published or because the current user may view unpublished
documents.
#}
{% if is_granted('VIEW', page) %}
{# output the document #}
{% endif %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHP:

<!-- check if document is published, regardless of current users role -->
<?php if ($view['cmf']->isPublished($page)) : ?>
    <!-- ... output the document -->
<?php endif ?>

<!--
    check if current logged in user is allowed to view the document either  
    because it is published or because the current user may view unpublished  
    documents.  
-->
<?php if ($view['security']->isGranted('VIEW', $page)) : ?>
    <!-- ... output the document -->
<?php endif ?>  


Code that loads content should do the publish checks. Note that the twig
functions already check for publication. Thanks to a
:ref:`request listener <bundle-core-workflow-request_listener>`, routes and
the main content provided by the
:ref:`DynamicRouter <bundles-routing-dynamic_router>` are checked automatically
as well.

It is possible to set the security token explicitly on the workflow checker.
But by default, the checker will acquire the token from the default security
context, and if there is none (typically when there is no firewall in place for
that URL), an
:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken`
is created on the fly.

If you check for ``VIEW`` and not ``VIEW_ANONYMOUS``, the first check is
whether the security context knows the current user and if that user is granted
the bypass role. If so, access is granted, otherwise the decision is delegated to a
:class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager`
which calls all voters with the requested attributes, the object and the token.

The decision manager is configured for an unanimous vote with "allow if all
abstain". This means a single voter saying ``ACCESS_DENIED`` is enough for
the content to be considered not published. If all voters abstain (for example
when the content in question does not implement any workflow features) the
content is still considered published.

Publish Voters
~~~~~~~~~~~~~~

A voter has to implement the
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`.
It will get passed a content object and has to decide whether it is published
according to its rules. The CoreBundle provides a couple of generic voters
that check the content for having an interface exposing the methods they need.
If the content implements the interface, they check the parameter and return
``ACCESS_GRANTED`` resp ``ACCESS_DENIED``, otherwise they return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like abbrevations in the docs

``ACCESS_ABSTAIN``.

As voting is unanimous, each voter returns ``ACCESS_GRANTED`` if its criteria
is met, but if a single voter returns ``ACCESS_DENIED``, the content is
considered not published.

You can also implement your :ref:`own voters <bundle-core-workflow_custom_voters>`
for additional publication behaviour.

PublishableVoter
................

This voter checks on the ``PublishableInterface`` which simply has a method to
return a boolean value.

* **isPublishable**: If the object should be considered for publication or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lasr items of a list should end with a dot

TimePeriodVoter
...............

This voter checks on the ``PublishTimePeriodInterface`` which defines a start
and end date. A date may be null to indicate "always started" resp.
"never ending".

* **getPublishStartDate**: If non-null, the date from which the document
should start being published;
* **getPublishEndDate**: If non-null, the date from which the document
should stop being published.

.. _bundle-core-workflow_custom_voters:

Custom Voters
.............

To build voters with custom logic, you need to implement
:class:`Symfony\\Component\\Security\\Core\\Authentication\\Voter\\VoterInterface`
and define a service with the tag ``cmf_published_voter``. This is similar
to the ``security.voter`` tag, but adds your voter to the publish workflow. As
with the security voters, you can specify a priority, though it is of limited
use as the access decision must be unanimous. If you have more expensive checks,
you can lower the priority of those voters.

.. configuration-block::

.. code-block:: yaml

services:
acme.security.publishable_voter:
class: %my_namespace.security.publishable_voter.class%
tags:
- { name: cmf_published_voter, priority: 30 }

.. code-block:: xml

<service id="acme.security.publishable_voter" class="%acme.security.publishable_voter.class%">
<tag name="cmf_published_voter" priority="30"/>
</service>

.. code-block:: php

use Symfony\Component\DependencyInjection\Definition;

$container->register('acme.security.publishable_voter', '%acme.security.publishable_voter.class%')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

->register(...) should go on a new line and make sure you don't break the 85 characters in code examples.

->addTag('cmf_published_voter', array('priority' => 30));

As the workflow checker will create an
:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken` on
the fly if the security context has none, voters must be able to handle this
situation when accessing the user. Also when accessing the security context,
they first must check if it has a token and otherwise not call it to avoid
triggering an exception. If a voter only gives access if there is a current
user fulfills some requirement, it simply has to return ``ACCESS_DENIED`` if
there is no current user.

.. _bundle-core-workflow-request_listener:

Publication Request Listener
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :ref:`DynamicRouter <bundles-routing-dynamic_router>` places the route
object and the main content - if the route has a main content - into the
request attributes. Unless you disable the
``cmf_core.publish_workflow.request_listener``, this listener will listen
on all requests and check publication of both the route object and the main
content object.

This means that custom templates for ``templates_by_class`` and the controllers
of ``controllers_by_class`` need not check for publication explicitly as its
already done.

Editing publication information: Publish Workflow Sonata Admin Extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

There is a write interface for each publish workflow too, defining setter
methods. The core bundle provides extensions for SonataAdminBundle to easily
add editing of the publish workflow fields to all or selected admins.

Instead of implementing ``PublishableInterface`` resp.
``PublishTimePeriodInterface`` you models instead need to implement the
``PublishableWriteInterface`` and / or ``PublishTimePeriodWriteInterface``.

To enable the extensions in your admin classes, simply define the extension
configuration in the ``sonata_admin`` section of your project configuration:

.. configuration-block::

.. code-block:: yaml

# app/config/config.yml
sonata_admin:
# ...
extensions:
symfony_cmf_core.publish_workflow.admin_extension.publishable:
implements:
- Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface
symfony_cmf_core.publish_workflow.admin_extension.time_period:
implements:
- Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodWriteInterface

.. code-block:: xml

<!-- app/config/config.xml -->
<config>
<sonata-admin>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these 2 lines should be:

<config xmlns="http://sonata-project.org/schema/dic/admin">

<!-- ... -->
<extensions>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this wrapper element

<extension id="symfony_cmf_core.publish_workflow.admin_extension.publishable">
<implements>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<implement>

Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface
</implements>
</extension>
<extension id="symfony_cmf_core.publish_workflow.admin_extension.time_period">
<implements>
Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodWriteInterface
</implements>
</extension>
</extensions>
</sonata-admin>
</config>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wouterj there was some guessing involved here. is this xml config more or less correct?


.. code-block:: php

// app/config/config.php
$container->loadFromExtension('sonata_admin', array(
'extensions' => array(
'symfony_cmf_core.admin_extension.publish_workflow' => array(
'implements' => array(
'Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowInterface',
),
),
),
));

See the `Sonata Admin extension documentation`_ for more information.

Dependency Injection Tags
-------------------------
Expand Down Expand Up @@ -86,15 +377,30 @@ service must have the ``setRequest`` method or you will get a fatal error::
service definition and call ``$this->container->get('request')`` in your
code when you actually need the request.

cmf_published_voter
~~~~~~~~~~~~~~~~~~~

Used to activate :ref:`custom voters <bundle-core-workflow_custom_voters>` for the
:ref:`publish workflow <bundle-core-publish_workflow>` . Tagging a service with
``cmf_published_voter`` integrates it into the access decision of the publish
workflow.

This tag has the attribute *priority*. The lower the priority number, the
earlier the voter gets to vote.

Twig extension
--------------

Implements the following functions:
This provides a set of useful twig functions for your templates. The functions
respect the :ref:`publish workflow <bundle-core-publish_workflow>` if it is
enabled.

* **cmf_find**: returns the document for the provided path
* **cmf_find_many**: returns an array of documents for the provided paths
* **cmf_is_published**: checks if the provided document is published
* **cmf_is_published**: checks if the provided document is published,
regardless of an eventual bypass permission of the current user. If you need
the bypass role, you will have a firewall configured for the route the
template is rendered in and can simply use ``{{ is_granted('VIEW', document) }}``
* **cmf_prev**: returns the previous document by examining the child nodes of
the provided parent
* **cmf_prev_linkable**: returns the previous linkable document by examining
Expand Down Expand Up @@ -144,3 +450,8 @@ Implements the following functions:
{% endif %}

.. _`CoreBundle`: https://github.com/symfony-cmf/CoreBundle#readme
.. _`Symfony2 security component`: http://www.symfony.com/doc/current/components/security/index.html
.. _`Symfony2 Authorization`: http://www.symfony.com/doc/current/components/security/authorization.html
.. _`Security Chapter`: http://www.symfony.com/doc/current/book/security.html
.. _`ACL checks`: http://www.symfony.com/doc/current/cookbook/security/acl.html
.. _`Sonata Admin extension documentation`: http://sonata-project.org/bundles/admin/master/doc/reference/extensions.html
2 changes: 2 additions & 0 deletions bundles/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ will look like this

See also official Symfony2 `documentation for DependencyInjection tags`_

.. _bundles-routing-dynamic_router:

Dynamic Router
--------------

Expand Down