Skip to content

Final additions: Expression language framework docs #3258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 2, 2013
98 changes: 94 additions & 4 deletions book/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -507,10 +507,11 @@ to the ``{page}`` parameter.
| /blog/my-blog-post | blog | {page} = my-blog-post |
+--------------------+-------+-----------------------+

The answer to the problem is to add route *requirements*. The routes in this
example would work perfectly if the ``/blog/{page}`` path *only* matched
URLs where the ``{page}`` portion is an integer. Fortunately, regular expression
requirements can easily be added for each parameter. For example:
The answer to the problem is to add route *requirements* or route *conditions*
(see :ref:`book-routing-conditions`). The routes in this example would work
perfectly if the ``/blog/{page}`` path *only* matched URLs where the ``{page}``
portion is an integer. Fortunately, regular expression requirements can easily
be added for each parameter. For example:

.. configuration-block::

Expand Down Expand Up @@ -717,6 +718,95 @@ You can also match on the HTTP *host* of the incoming request. For more
information, see :doc:`/components/routing/hostname_pattern` in the Routing
component documentation.

.. _book-routing-conditions:

Completely Customized Route Matching with Conditions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 2.4
Route conditions were introduced in Symfony 2.4.

As you've seen, a route can be made to match only certain routing wildcards
(via regular expressions), HTTP methods, or host names. But the routing system
Copy link
Member

Choose a reason for hiding this comment

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

actually, they are not regular expressions.

Copy link
Member Author

Choose a reason for hiding this comment

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

requirements in routes are regular expressions, or am I missing something?

Copy link
Member

Choose a reason for hiding this comment

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

hmm, maybe the sentence is confusing :) "to match only certain routing wildcards" I thought you meaned the path option, not the requirements option

can be extended to have an almost infinite flexibility using ``conditions``:

.. configuration-block::

.. code-block:: yaml

contact:
path: /contact
defaults: { _controller: AcmeDemoBundle:Main:contact }
condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"

.. code-block:: xml

<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="contact"
path="/contact"
condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
>
<default key="_controller">AcmeDemoBundle:Main:contact</default>
</route>
</routes>

.. code-block:: php

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add('contact', new Route(
'/contact', array(
'_controller' => 'AcmeDemoBundle:Main:contact',
),
array(),
array(),
'',
array(),
array(),
'context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"'
));

return $collection;

The ``condition`` is an expression, and you can learn more about its syntax
here: :doc:`/components/expression_language/syntax`. With this, the route
won't match unless the HTTP method is either GET or HEAD *and* if the ``User-Agent``
header matches ``firefox``.

You can do any complex logic you need in the expression by leveraging two
variables that are passed into the expression:

* ``context``: An instance of :class:`Symfony\\Component\\Routing\\RequestContext`,
which holds the most fundamental information about the route being matched;
* ``request``: The Symfony :class:`Symfony\\Component\\HttpFoundation\\Request``
object (see :ref:`component-http-foundation-request`).

.. caution::

Conditions are *not* taken into account when generating a URL.

.. sidebar:: Expressions are Compiled to PHP

Behind the scenes, expressions are compiled down to raw PHP. Our example
would generate the following PHP in the cache directory::

if (rtrim($pathinfo, '/contact') === '' && (
in_array($context->getMethod(), array(0 => "GET", 1 => "HEAD"))
&& preg_match("/firefox/i", $request->headers->get("User-Agent"))
)) {
// ...
}

Because of this, using the ``condition`` key causes no extra overhead
beyond the time it takes for the underlying PHP to execute.

.. index::
single: Routing; Advanced example
single: Routing; _format parameter
Expand Down
168 changes: 168 additions & 0 deletions book/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,8 @@ options:
(internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
is thrown);

* ``allow_if`` If the expression returns false, then access is denied;

* ``requires_channel`` If the incoming request's channel (e.g. ``http``)
does not match this value (e.g. ``https``), the user will be redirected
(e.g. redirected from ``http`` to ``https``, or vice versa).
Expand Down Expand Up @@ -951,6 +953,58 @@ address):

* The second access rule is not examined as the first rule matched.

.. _book-security-allow-if:

Securing by an Expression
~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 2.4
The ``allow_if`` functionality was introduced in Symfony 2.4.

Once an ``access_control`` entry is matched, you can deny access via the
``roles`` key or use more complex logic with an expression in the ``allow_if``
key:

.. configuration-block::

.. code-block:: yaml

# app/config/security.yml
security:
# ...
access_control:
-
path: ^/_internal/secure
allow_if: "'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')"

.. code-block:: xml

<access-control>
<rule path="^/_internal/secure"
allow-if="'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" />
</access-control>

.. code-block:: php

'access_control' => array(
array(
'path' => '^/_internal/secure',
'allow_if' => '"127.0.0.1" == request.getClientIp() or has_role("ROLE_ADMIN")',
),
),

In this case, when the user tries to access any URL starting with ``/_internal/secure``,
they will only be granted access if the IP address is ``127.0.0.1`` or if
the user has the ``ROLE_ADMIN`` role.

Inside the expression, you have access to a number of different variables
and functions including ``request``, which is the Symfony
:class:`Symfony\\Component\\HttpFoundation\\Request` object (see
:ref:`component-http-foundation-request`).

For a list of the other functions and variables, see
:ref:`functions and variables <book-security-expression-variables>`.

.. _book-security-securing-channel:

Securing by Channel
Expand Down Expand Up @@ -1656,6 +1710,8 @@ doesn't need to be defined anywhere - you can just start using it.
Symfony2. If you define your own roles with a dedicated ``Role`` class
(more advanced), don't use the ``ROLE_`` prefix.

.. _book-security-role-hierarchy:

Hierarchical Roles
~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -1834,6 +1890,33 @@ the built-in helper function:
idea to have a main firewall that covers all URLs (as has been shown
in this chapter).

.. _book-security-template-expression:

.. versionadded:: 2.4
The ``expression`` functionality was introduced in Symfony 2.4.

You can also use expressions inside your templates:

.. configuration-block::

.. code-block:: html+jinja

{% if is_granted(expression(
'"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
)) %}
<a href="...">Delete</a>
{% endif %}

.. code-block:: html+php

<?php if ($view['security']->isGranted(new Expression(
'"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
))): ?>
<a href="...">Delete</a>
<?php endif; ?>

For more details on expressions and security, see :ref:`book-security-expressions`.

Access Control in Controllers
-----------------------------

Expand All @@ -1856,6 +1939,91 @@ method of the security context::
A firewall must be active or an exception will be thrown when the ``isGranted``
method is called. See the note above about templates for more details.

.. _book-security-expressions:

Complex Access Controls with Expressions
----------------------------------------

.. versionadded:: 2.4
The expression functionality was introduced in Symfony 2.4.

In addition to a role like ``ROLE_ADMIN``, the ``isGranted`` method also
accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` object::

use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\ExpressionLanguage\Expression;
// ...

public function indexAction()
{
if (!$this->get('security.context')->isGranted(new Expression(
'"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
))) {
throw new AccessDeniedException();
}

// ...
}

In this example, if the current user has ``ROLE_ADMIN`` or if the current
user object's ``isSuperAdmin()`` method returns ``true``, then access will
be granted (note: your User object may not have an ``isSuperAdmin`` method,
that method is invented for this example).

This uses an expression and you can learn more about the expression language
syntax, see :doc:`/components/expression_language/syntax`.

.. _book-security-expression-variables:

Inside the expression, you have access to a number of variables:

* ``user`` The user object (or the string ``anon`` if you're not authenticated);
* ``roles`` The array of roles the user has, including from the
:ref:`role hierarchy <book-security-role-hierarchy>` but not including
the ``IS_AUTHENTICATED_*`` attributes (see the functions below);
* ``object``: The object (if any) that's passed as the second argument to
``isGranted`` ;
* ``token`` The token object;
* ``trust_resolver``: The :class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationTrustResolverInterface`,
object: you'll probably use the ``is_*`` functions below instead.

Additionally, you have access to a number of functions inside the expression:

* ``is_authenticated``: Returns ``true`` if the user is authenticated via "remember-me"
or authenticated "fully" - i.e. returns true if the user is "logged in";
* ``is_anonymous``: Equal to using ``IS_AUTHENTICATED_ANONYMOUSLY`` with
the ``isGranted`` function;
* ``is_remember_me``: Similar, but not equal to ``IS_AUTHENTICATED_REMEMBERED``,
see below;
* ``is_fully_authenticated``: Similar, but not equal to ``IS_AUTHENTICATED_FULLY``,
see below;
* ``has_role``: Checks to see if the user has the given role - equivalent
to an expression like ``'ROLE_ADMIN' in roles``.

.. sidebar:: ``is_remember_me`` is different than checking ``IS_AUTHENTICATED_REMEMBERED``

The ``is_remember_me`` and ``is_authenticated_fully`` functions are *similar*
to using ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY``
with the ``isGranted`` function - but they are **not** the same. The
following shows the difference::

use Symfony\Component\ExpressionLanguage\Expression;
// ...

$sc = $this->get('security.context');
$access1 = $sc->isGranted('IS_AUTHENTICATED_REMEMBERED');

$access2 = $sc->isGranted(new Expression(
'is_remember_me() or is_fully_authenticated()'
));

Here, ``$access1`` and ``$access2`` will be the same value. Unlike the
behavior of ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY``,
the ``is_remember_me`` function *only* returns true if the user is authenticated
via a remember-me cookie and ``is_fully_authenticated`` *only* returns
true if the user has actually logged in during this session (i.e. is
full-fledged).

Impersonating a User
--------------------

Expand Down
2 changes: 2 additions & 0 deletions book/service_container.rst
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,8 @@ service needs the ``my_mailer`` service in order to function. When you define
this dependency in the service container, the container takes care of all
the work of instantiating the classes.

.. _book-services-expressions:

Using the Expression Language
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 3 additions & 1 deletion components/expression_language/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Single: Components; Expression Language

The ExpressionLanguage Component
=================================
================================

The ExpressionLanguage component provides an engine that can compile and
evaluate expressions. An expression is a one-liner that returns a value
Expand Down Expand Up @@ -32,6 +32,8 @@ component is a perfect candidate for the foundation of a *business rule engine*.
The idea is to let the webmaster of a website configure things in a dynamic
way without using PHP and without introducing security problems:

.. _component-expression-language-examples:

.. code-block:: text

# Get the special price if
Expand Down
24 changes: 24 additions & 0 deletions cookbook/expression/expressions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.. index::
single: Expressions in the Framework

How to use Expressions in Security, Routing, Services, and Validation
=====================================================================

.. versionadded:: 2.4
The expression functionality was introduced in Symfony 2.4.

In Symfony 2.4, a powerful :doc:`ExpressionLanguage </components/expression_language/introduction>`
component was added to Symfony. This allows us to add highly customized
logic inside configuration.
Copy link
Contributor

Choose a reason for hiding this comment

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

wonder if we should add a note on how to use or inject expressions, if need be, to other services besides these explicit here

Copy link
Member Author

Choose a reason for hiding this comment

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

I think over time we will start to think of other uses for the expression language, and when we do, we should certainly add more cookbook entries. But fortunately, for right now, we just need to make sure the basic docs are present.


The Symfony Framework leverages expressions out of the box in the following
ways:

* :ref:`Configuring services <book-services-expressions>`;
* :ref:`Route matching conditions <book-routing-conditions>`;
* :ref:`Checking security <book-security-expressions>` and
:ref:`access controls with allow_if <book-security-allow-if>`;
* :doc:`Validation </reference/constraints/Expression>`.

For more information about how to create and work with expressions, see
:doc:`/components/expression_language/syntax`.
Copy link
Contributor

Choose a reason for hiding this comment

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

php-cs-fixer

7 changes: 7 additions & 0 deletions cookbook/expression/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Expressions
===========

.. toctree::
:maxdepth: 2

expressions
1 change: 1 addition & 0 deletions cookbook/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The Cookbook
doctrine/index
email/index
event_dispatcher/index
expression/index
form/index
logging/index
profiler/index
Expand Down
4 changes: 4 additions & 0 deletions cookbook/map.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
* :doc:`/cookbook/event_dispatcher/method_behavior`
* (service container) :doc:`/cookbook/service_container/event_listener`

* :doc:`/cookbook/expression/index`

* :doc:`/cookbook/expression/expressions`

* :doc:`/cookbook/form/index`

* :doc:`/cookbook/form/form_customization`
Expand Down
Loading