Skip to content

Commit 816c06b

Browse files
committed
Merge pull request symfony#3258 from symfony/expression-language-framework
Final additions: Expression language framework docs
2 parents 93e8cd2 + 0000250 commit 816c06b

File tree

12 files changed

+478
-5
lines changed

12 files changed

+478
-5
lines changed

book/routing.rst

+94-4
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,11 @@ to the ``{page}`` parameter.
507507
| /blog/my-blog-post | blog | {page} = my-blog-post |
508508
+--------------------+-------+-----------------------+
509509

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

515516
.. configuration-block::
516517

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

721+
.. _book-routing-conditions:
722+
723+
Completely Customized Route Matching with Conditions
724+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
725+
726+
.. versionadded:: 2.4
727+
Route conditions were introduced in Symfony 2.4.
728+
729+
As you've seen, a route can be made to match only certain routing wildcards
730+
(via regular expressions), HTTP methods, or host names. But the routing system
731+
can be extended to have an almost infinite flexibility using ``conditions``:
732+
733+
.. configuration-block::
734+
735+
.. code-block:: yaml
736+
737+
contact:
738+
path: /contact
739+
defaults: { _controller: AcmeDemoBundle:Main:contact }
740+
condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
741+
742+
.. code-block:: xml
743+
744+
<?xml version="1.0" encoding="UTF-8" ?>
745+
<routes xmlns="http://symfony.com/schema/routing"
746+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
747+
xsi:schemaLocation="http://symfony.com/schema/routing
748+
http://symfony.com/schema/routing/routing-1.0.xsd">
749+
750+
<route id="contact"
751+
path="/contact"
752+
condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
753+
>
754+
<default key="_controller">AcmeDemoBundle:Main:contact</default>
755+
</route>
756+
</routes>
757+
758+
.. code-block:: php
759+
760+
use Symfony\Component\Routing\RouteCollection;
761+
use Symfony\Component\Routing\Route;
762+
763+
$collection = new RouteCollection();
764+
$collection->add('contact', new Route(
765+
'/contact', array(
766+
'_controller' => 'AcmeDemoBundle:Main:contact',
767+
),
768+
array(),
769+
array(),
770+
'',
771+
array(),
772+
array(),
773+
'context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"'
774+
));
775+
776+
return $collection;
777+
778+
The ``condition`` is an expression, and you can learn more about its syntax
779+
here: :doc:`/components/expression_language/syntax`. With this, the route
780+
won't match unless the HTTP method is either GET or HEAD *and* if the ``User-Agent``
781+
header matches ``firefox``.
782+
783+
You can do any complex logic you need in the expression by leveraging two
784+
variables that are passed into the expression:
785+
786+
* ``context``: An instance of :class:`Symfony\\Component\\Routing\\RequestContext`,
787+
which holds the most fundamental information about the route being matched;
788+
* ``request``: The Symfony :class:`Symfony\\Component\\HttpFoundation\\Request``
789+
object (see :ref:`component-http-foundation-request`).
790+
791+
.. caution::
792+
793+
Conditions are *not* taken into account when generating a URL.
794+
795+
.. sidebar:: Expressions are Compiled to PHP
796+
797+
Behind the scenes, expressions are compiled down to raw PHP. Our example
798+
would generate the following PHP in the cache directory::
799+
800+
if (rtrim($pathinfo, '/contact') === '' && (
801+
in_array($context->getMethod(), array(0 => "GET", 1 => "HEAD"))
802+
&& preg_match("/firefox/i", $request->headers->get("User-Agent"))
803+
)) {
804+
// ...
805+
}
806+
807+
Because of this, using the ``condition`` key causes no extra overhead
808+
beyond the time it takes for the underlying PHP to execute.
809+
720810
.. index::
721811
single: Routing; Advanced example
722812
single: Routing; _format parameter

book/security.rst

+168
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,8 @@ options:
864864
(internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`
865865
is thrown);
866866

867+
* ``allow_if`` If the expression returns false, then access is denied;
868+
867869
* ``requires_channel`` If the incoming request's channel (e.g. ``http``)
868870
does not match this value (e.g. ``https``), the user will be redirected
869871
(e.g. redirected from ``http`` to ``https``, or vice versa).
@@ -951,6 +953,58 @@ address):
951953

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

956+
.. _book-security-allow-if:
957+
958+
Securing by an Expression
959+
~~~~~~~~~~~~~~~~~~~~~~~~~
960+
961+
.. versionadded:: 2.4
962+
The ``allow_if`` functionality was introduced in Symfony 2.4.
963+
964+
Once an ``access_control`` entry is matched, you can deny access via the
965+
``roles`` key or use more complex logic with an expression in the ``allow_if``
966+
key:
967+
968+
.. configuration-block::
969+
970+
.. code-block:: yaml
971+
972+
# app/config/security.yml
973+
security:
974+
# ...
975+
access_control:
976+
-
977+
path: ^/_internal/secure
978+
allow_if: "'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')"
979+
980+
.. code-block:: xml
981+
982+
<access-control>
983+
<rule path="^/_internal/secure"
984+
allow-if="'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" />
985+
</access-control>
986+
987+
.. code-block:: php
988+
989+
'access_control' => array(
990+
array(
991+
'path' => '^/_internal/secure',
992+
'allow_if' => '"127.0.0.1" == request.getClientIp() or has_role("ROLE_ADMIN")',
993+
),
994+
),
995+
996+
In this case, when the user tries to access any URL starting with ``/_internal/secure``,
997+
they will only be granted access if the IP address is ``127.0.0.1`` or if
998+
the user has the ``ROLE_ADMIN`` role.
999+
1000+
Inside the expression, you have access to a number of different variables
1001+
and functions including ``request``, which is the Symfony
1002+
:class:`Symfony\\Component\\HttpFoundation\\Request` object (see
1003+
:ref:`component-http-foundation-request`).
1004+
1005+
For a list of the other functions and variables, see
1006+
:ref:`functions and variables <book-security-expression-variables>`.
1007+
9541008
.. _book-security-securing-channel:
9551009

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

1713+
.. _book-security-role-hierarchy:
1714+
16591715
Hierarchical Roles
16601716
~~~~~~~~~~~~~~~~~~
16611717

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

1893+
.. _book-security-template-expression:
1894+
1895+
.. versionadded:: 2.4
1896+
The ``expression`` functionality was introduced in Symfony 2.4.
1897+
1898+
You can also use expressions inside your templates:
1899+
1900+
.. configuration-block::
1901+
1902+
.. code-block:: html+jinja
1903+
1904+
{% if is_granted(expression(
1905+
'"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
1906+
)) %}
1907+
<a href="...">Delete</a>
1908+
{% endif %}
1909+
1910+
.. code-block:: html+php
1911+
1912+
<?php if ($view['security']->isGranted(new Expression(
1913+
'"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
1914+
))): ?>
1915+
<a href="...">Delete</a>
1916+
<?php endif; ?>
1917+
1918+
For more details on expressions and security, see :ref:`book-security-expressions`.
1919+
18371920
Access Control in Controllers
18381921
-----------------------------
18391922

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

1942+
.. _book-security-expressions:
1943+
1944+
Complex Access Controls with Expressions
1945+
----------------------------------------
1946+
1947+
.. versionadded:: 2.4
1948+
The expression functionality was introduced in Symfony 2.4.
1949+
1950+
In addition to a role like ``ROLE_ADMIN``, the ``isGranted`` method also
1951+
accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` object::
1952+
1953+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
1954+
use Symfony\Component\ExpressionLanguage\Expression;
1955+
// ...
1956+
1957+
public function indexAction()
1958+
{
1959+
if (!$this->get('security.context')->isGranted(new Expression(
1960+
'"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
1961+
))) {
1962+
throw new AccessDeniedException();
1963+
}
1964+
1965+
// ...
1966+
}
1967+
1968+
In this example, if the current user has ``ROLE_ADMIN`` or if the current
1969+
user object's ``isSuperAdmin()`` method returns ``true``, then access will
1970+
be granted (note: your User object may not have an ``isSuperAdmin`` method,
1971+
that method is invented for this example).
1972+
1973+
This uses an expression and you can learn more about the expression language
1974+
syntax, see :doc:`/components/expression_language/syntax`.
1975+
1976+
.. _book-security-expression-variables:
1977+
1978+
Inside the expression, you have access to a number of variables:
1979+
1980+
* ``user`` The user object (or the string ``anon`` if you're not authenticated);
1981+
* ``roles`` The array of roles the user has, including from the
1982+
:ref:`role hierarchy <book-security-role-hierarchy>` but not including
1983+
the ``IS_AUTHENTICATED_*`` attributes (see the functions below);
1984+
* ``object``: The object (if any) that's passed as the second argument to
1985+
``isGranted`` ;
1986+
* ``token`` The token object;
1987+
* ``trust_resolver``: The :class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationTrustResolverInterface`,
1988+
object: you'll probably use the ``is_*`` functions below instead.
1989+
1990+
Additionally, you have access to a number of functions inside the expression:
1991+
1992+
* ``is_authenticated``: Returns ``true`` if the user is authenticated via "remember-me"
1993+
or authenticated "fully" - i.e. returns true if the user is "logged in";
1994+
* ``is_anonymous``: Equal to using ``IS_AUTHENTICATED_ANONYMOUSLY`` with
1995+
the ``isGranted`` function;
1996+
* ``is_remember_me``: Similar, but not equal to ``IS_AUTHENTICATED_REMEMBERED``,
1997+
see below;
1998+
* ``is_fully_authenticated``: Similar, but not equal to ``IS_AUTHENTICATED_FULLY``,
1999+
see below;
2000+
* ``has_role``: Checks to see if the user has the given role - equivalent
2001+
to an expression like ``'ROLE_ADMIN' in roles``.
2002+
2003+
.. sidebar:: ``is_remember_me`` is different than checking ``IS_AUTHENTICATED_REMEMBERED``
2004+
2005+
The ``is_remember_me`` and ``is_authenticated_fully`` functions are *similar*
2006+
to using ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY``
2007+
with the ``isGranted`` function - but they are **not** the same. The
2008+
following shows the difference::
2009+
2010+
use Symfony\Component\ExpressionLanguage\Expression;
2011+
// ...
2012+
2013+
$sc = $this->get('security.context');
2014+
$access1 = $sc->isGranted('IS_AUTHENTICATED_REMEMBERED');
2015+
2016+
$access2 = $sc->isGranted(new Expression(
2017+
'is_remember_me() or is_fully_authenticated()'
2018+
));
2019+
2020+
Here, ``$access1`` and ``$access2`` will be the same value. Unlike the
2021+
behavior of ``IS_AUTHENTICATED_REMEMBERED`` and ``IS_AUTHENTICATED_FULLY``,
2022+
the ``is_remember_me`` function *only* returns true if the user is authenticated
2023+
via a remember-me cookie and ``is_fully_authenticated`` *only* returns
2024+
true if the user has actually logged in during this session (i.e. is
2025+
full-fledged).
2026+
18592027
Impersonating a User
18602028
--------------------
18612029

book/service_container.rst

+2
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,8 @@ service needs the ``my_mailer`` service in order to function. When you define
627627
this dependency in the service container, the container takes care of all
628628
the work of instantiating the classes.
629629

630+
.. _book-services-expressions:
631+
630632
Using the Expression Language
631633
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
632634

components/expression_language/introduction.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Single: Components; Expression Language
44

55
The ExpressionLanguage Component
6-
=================================
6+
================================
77

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

35+
.. _component-expression-language-examples:
36+
3537
.. code-block:: text
3638
3739
# Get the special price if

cookbook/expression/expressions.rst

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.. index::
2+
single: Expressions in the Framework
3+
4+
How to use Expressions in Security, Routing, Services, and Validation
5+
=====================================================================
6+
7+
.. versionadded:: 2.4
8+
The expression functionality was introduced in Symfony 2.4.
9+
10+
In Symfony 2.4, a powerful :doc:`ExpressionLanguage </components/expression_language/introduction>`
11+
component was added to Symfony. This allows us to add highly customized
12+
logic inside configuration.
13+
14+
The Symfony Framework leverages expressions out of the box in the following
15+
ways:
16+
17+
* :ref:`Configuring services <book-services-expressions>`;
18+
* :ref:`Route matching conditions <book-routing-conditions>`;
19+
* :ref:`Checking security <book-security-expressions>` and
20+
:ref:`access controls with allow_if <book-security-allow-if>`;
21+
* :doc:`Validation </reference/constraints/Expression>`.
22+
23+
For more information about how to create and work with expressions, see
24+
:doc:`/components/expression_language/syntax`.

cookbook/expression/index.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Expressions
2+
===========
3+
4+
.. toctree::
5+
:maxdepth: 2
6+
7+
expressions

cookbook/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The Cookbook
1515
doctrine/index
1616
email/index
1717
event_dispatcher/index
18+
expression/index
1819
form/index
1920
logging/index
2021
profiler/index

cookbook/map.rst.inc

+4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@
7878
* :doc:`/cookbook/event_dispatcher/method_behavior`
7979
* (service container) :doc:`/cookbook/service_container/event_listener`
8080

81+
* :doc:`/cookbook/expression/index`
82+
83+
* :doc:`/cookbook/expression/expressions`
84+
8185
* :doc:`/cookbook/form/index`
8286

8387
* :doc:`/cookbook/form/form_customization`

0 commit comments

Comments
 (0)