mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-23 16:22:10 +01:00
1119 lines
36 KiB
ReStructuredText
1119 lines
36 KiB
ReStructuredText
Controller
|
|
==========
|
|
|
|
A controller is a PHP function you create that reads information from the
|
|
``Request`` object and creates and returns a ``Response`` object. The response could
|
|
be an HTML page, JSON, XML, a file download, a redirect, a 404 error or anything
|
|
else. The controller runs whatever arbitrary logic *your application* needs
|
|
to render the content of a page.
|
|
|
|
.. tip::
|
|
|
|
If you haven't already created your first working page, check out
|
|
:doc:`/page_creation` and then come back!
|
|
|
|
A Basic Controller
|
|
------------------
|
|
|
|
While a controller can be any PHP callable (function, method on an object,
|
|
or a ``Closure``), a controller is usually a method inside a controller
|
|
class::
|
|
|
|
// src/Controller/LuckyController.php
|
|
namespace App\Controller;
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
class LuckyController
|
|
{
|
|
#[Route('/lucky/number/{max}', name: 'app_lucky_number')]
|
|
public function number(int $max): Response
|
|
{
|
|
$number = random_int(0, $max);
|
|
|
|
return new Response(
|
|
'<html><body>Lucky number: '.$number.'</body></html>'
|
|
);
|
|
}
|
|
}
|
|
|
|
The controller is the ``number()`` method, which lives inside the
|
|
controller class ``LuckyController``.
|
|
|
|
This controller is quite simple:
|
|
|
|
* *line 2*: Symfony takes advantage of PHP's namespace functionality to
|
|
namespace the entire controller class.
|
|
|
|
* *line 4*: Symfony again takes advantage of PHP's namespace functionality:
|
|
the ``use`` keyword imports the ``Response`` class, which the controller
|
|
must return.
|
|
|
|
* *line 7*: The class can technically be called anything, but it's suffixed
|
|
with ``Controller`` by convention.
|
|
|
|
* *line 10*: The action method is allowed to have a ``$max`` argument thanks to the
|
|
``{max}`` :doc:`wildcard in the route </routing>`.
|
|
|
|
* *line 14*: The controller creates and returns a ``Response`` object.
|
|
|
|
Mapping a URL to a Controller
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
In order to *view* the result of this controller, you need to map a URL to it via
|
|
a route. This was done above with the ``#[Route('/lucky/number/{max}')]``
|
|
:ref:`route attribute <attribute-routes>`.
|
|
|
|
To see your page, go to this URL in your browser: http://localhost:8000/lucky/number/100
|
|
|
|
For more information on routing, see :doc:`/routing`.
|
|
|
|
.. _the-base-controller-class-services:
|
|
.. _the-base-controller-classes-services:
|
|
|
|
The Base Controller Class & Services
|
|
------------------------------------
|
|
|
|
To aid development, Symfony comes with an optional base controller class called
|
|
:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController`.
|
|
It can be extended to gain access to helper methods.
|
|
|
|
Add the ``use`` statement atop your controller class and then modify
|
|
``LuckyController`` to extend it:
|
|
|
|
.. code-block:: diff
|
|
|
|
// src/Controller/LuckyController.php
|
|
namespace App\Controller;
|
|
|
|
+ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
|
|
- class LuckyController
|
|
+ class LuckyController extends AbstractController
|
|
{
|
|
// ...
|
|
}
|
|
|
|
That's it! You now have access to methods like :ref:`$this->render() <controller-rendering-templates>`
|
|
and many others that you'll learn about next.
|
|
|
|
Generating URLs
|
|
~~~~~~~~~~~~~~~
|
|
|
|
The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::generateUrl`
|
|
method is just a helper method that generates the URL for a given route::
|
|
|
|
$url = $this->generateUrl('app_lucky_number', ['max' => 10]);
|
|
|
|
.. _controller-redirect:
|
|
|
|
Redirecting
|
|
~~~~~~~~~~~
|
|
|
|
If you want to redirect the user to another page, use the ``redirectToRoute()``
|
|
and ``redirect()`` methods::
|
|
|
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
// ...
|
|
public function index(): RedirectResponse
|
|
{
|
|
// redirects to the "homepage" route
|
|
return $this->redirectToRoute('homepage');
|
|
|
|
// redirectToRoute is a shortcut for:
|
|
// return new RedirectResponse($this->generateUrl('homepage'));
|
|
|
|
// does a permanent HTTP 301 redirect
|
|
return $this->redirectToRoute('homepage', [], 301);
|
|
// if you prefer, you can use PHP constants instead of hardcoded numbers
|
|
return $this->redirectToRoute('homepage', [], Response::HTTP_MOVED_PERMANENTLY);
|
|
|
|
// redirect to a route with parameters
|
|
return $this->redirectToRoute('app_lucky_number', ['max' => 10]);
|
|
|
|
// redirects to a route and maintains the original query string parameters
|
|
return $this->redirectToRoute('blog_show', $request->query->all());
|
|
|
|
// redirects to the current route (e.g. for Post/Redirect/Get pattern):
|
|
return $this->redirectToRoute($request->attributes->get('_route'));
|
|
|
|
// redirects externally
|
|
return $this->redirect('http://symfony.com/doc');
|
|
}
|
|
|
|
.. danger::
|
|
|
|
The ``redirect()`` method does not check its destination in any way. If you
|
|
redirect to a URL provided by end-users, your application may be open
|
|
to the `unvalidated redirects security vulnerability`_.
|
|
|
|
.. _controller-rendering-templates:
|
|
|
|
Rendering Templates
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
If you're serving HTML, you'll want to render a template. The ``render()``
|
|
method renders a template **and** puts that content into a ``Response``
|
|
object for you::
|
|
|
|
// renders templates/lucky/number.html.twig
|
|
return $this->render('lucky/number.html.twig', ['number' => $number]);
|
|
|
|
Templating and Twig are explained more in the
|
|
:doc:`Creating and Using Templates article </templates>`.
|
|
|
|
.. _controller-accessing-services:
|
|
.. _accessing-other-services:
|
|
|
|
Fetching Services
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
Symfony comes *packed* with a lot of useful classes and functionalities, called :doc:`services </service_container>`.
|
|
These are used for rendering templates, sending emails, querying the database and
|
|
any other "work" you can think of.
|
|
|
|
If you need a service in a controller, type-hint an argument with its class
|
|
(or interface) name and Symfony will inject it automatically. This requires
|
|
your :doc:`controller to be registered as a service </controller/service>`::
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
// ...
|
|
|
|
#[Route('/lucky/number/{max}')]
|
|
public function number(int $max, LoggerInterface $logger): Response
|
|
{
|
|
$logger->info('We are logging!');
|
|
// ...
|
|
}
|
|
|
|
Awesome!
|
|
|
|
What other services can you type-hint? To see them, use the ``debug:autowiring`` console
|
|
command:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ php bin/console debug:autowiring
|
|
|
|
.. tip::
|
|
|
|
If you need control over the *exact* value of an argument, or require a parameter,
|
|
you can use the ``#[Autowire]`` attribute::
|
|
|
|
// ...
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class LuckyController extends AbstractController
|
|
{
|
|
public function number(
|
|
int $max,
|
|
|
|
// inject a specific logger service
|
|
#[Autowire(service: 'monolog.logger.request')]
|
|
LoggerInterface $logger,
|
|
|
|
// or inject parameter values
|
|
#[Autowire('%kernel.project_dir%')]
|
|
string $projectDir
|
|
): Response
|
|
{
|
|
$logger->info('We are logging!');
|
|
// ...
|
|
}
|
|
}
|
|
|
|
You can read more about this attribute in :ref:`autowire-attribute`.
|
|
|
|
Like with all services, you can also use regular
|
|
:ref:`constructor injection <services-constructor-injection>` in your
|
|
controllers.
|
|
|
|
For more information about services, see the :doc:`/service_container` article.
|
|
|
|
Generating Controllers
|
|
----------------------
|
|
|
|
To save time, you can install `Symfony Maker`_ and tell Symfony to generate a
|
|
new controller class:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ php bin/console make:controller BrandNewController
|
|
|
|
created: src/Controller/BrandNewController.php
|
|
created: templates/brandnew/index.html.twig
|
|
|
|
If you want to generate an entire CRUD from a Doctrine :doc:`entity </doctrine>`,
|
|
use:
|
|
|
|
.. code-block:: terminal
|
|
|
|
$ php bin/console make:crud Product
|
|
|
|
created: src/Controller/ProductController.php
|
|
created: src/Form/ProductType.php
|
|
created: templates/product/_delete_form.html.twig
|
|
created: templates/product/_form.html.twig
|
|
created: templates/product/edit.html.twig
|
|
created: templates/product/index.html.twig
|
|
created: templates/product/new.html.twig
|
|
created: templates/product/show.html.twig
|
|
|
|
Managing Errors and 404 Pages
|
|
-----------------------------
|
|
|
|
When things are not found, you should return a 404 response. To do this, throw a
|
|
special type of exception::
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
// ...
|
|
public function index(): Response
|
|
{
|
|
// retrieve the object from database
|
|
$product = ...;
|
|
if (!$product) {
|
|
throw $this->createNotFoundException('The product does not exist');
|
|
|
|
// the above is just a shortcut for:
|
|
// throw new NotFoundHttpException('The product does not exist');
|
|
}
|
|
|
|
return $this->render(/* ... */);
|
|
}
|
|
|
|
The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::createNotFoundException`
|
|
method is just a shortcut to create a special
|
|
:class:`Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException`
|
|
object, which ultimately triggers a 404 HTTP response inside Symfony.
|
|
|
|
If you throw an exception that extends or is an instance of
|
|
:class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException`, Symfony will
|
|
use the appropriate HTTP status code. Otherwise, the response will have a 500
|
|
HTTP status code::
|
|
|
|
// this exception ultimately generates a 500 status error
|
|
throw new \Exception('Something went wrong!');
|
|
|
|
In every case, an error page is shown to the end user and a full debug
|
|
error page is shown to the developer (i.e. when you're in "Debug" mode - see
|
|
:ref:`page-creation-environments`).
|
|
|
|
To customize the error page that's shown to the user, see the
|
|
:doc:`/controller/error_pages` article.
|
|
|
|
.. _controller-request-argument:
|
|
|
|
The Request object as a Controller Argument
|
|
-------------------------------------------
|
|
|
|
What if you need to read query parameters, grab a request header or get access
|
|
to an uploaded file? That information is stored in Symfony's ``Request``
|
|
object. To access it in your controller, add it as an argument and
|
|
**type-hint it with the Request class**::
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
// ...
|
|
|
|
public function index(Request $request): Response
|
|
{
|
|
$page = $request->query->get('page', 1);
|
|
|
|
// ...
|
|
}
|
|
|
|
:ref:`Keep reading <request-object-info>` for more information about using the
|
|
Request object.
|
|
|
|
.. _controller_map-request:
|
|
|
|
Automatic Mapping Of The Request
|
|
--------------------------------
|
|
|
|
It is possible to automatically map request's payload and/or query parameters to
|
|
your controller's action arguments with attributes.
|
|
|
|
Mapping Query Parameters Individually
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Let's say a user sends you a request with the following query string:
|
|
``https://example.com/dashboard?firstName=John&lastName=Smith&age=27``.
|
|
Thanks to the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter`
|
|
attribute, arguments of your controller's action can be automatically fulfilled::
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapQueryParameter] string $firstName,
|
|
#[MapQueryParameter] string $lastName,
|
|
#[MapQueryParameter] int $age,
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
The ``MapQueryParameter`` attribute supports the following argument types:
|
|
|
|
* ``\BackedEnum``
|
|
* ``array``
|
|
* ``bool``
|
|
* ``float``
|
|
* ``int``
|
|
* ``string``
|
|
* Objects that extend :class:`Symfony\\Component\\Uid\\AbstractUid`
|
|
|
|
.. versionadded:: 7.3
|
|
|
|
Support for ``AbstractUid`` objects was introduced in Symfony 7.3.
|
|
|
|
``#[MapQueryParameter]`` can take an optional argument called ``filter``. You can use the
|
|
`Validate Filters`_ constants defined in PHP::
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\w+$/'])] string $firstName,
|
|
#[MapQueryParameter] string $lastName,
|
|
#[MapQueryParameter(filter: \FILTER_VALIDATE_INT)] int $age,
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
.. _controller-mapping-query-string:
|
|
|
|
Mapping The Whole Query String
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Another possibility is to map the entire query string into an object that will hold
|
|
available query parameters. Let's say you declare the following DTO with its
|
|
optional validation constraints::
|
|
|
|
namespace App\Model;
|
|
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
class UserDto
|
|
{
|
|
public function __construct(
|
|
#[Assert\NotBlank]
|
|
public string $firstName,
|
|
|
|
#[Assert\NotBlank]
|
|
public string $lastName,
|
|
|
|
#[Assert\GreaterThan(18)]
|
|
public int $age,
|
|
) {
|
|
}
|
|
}
|
|
|
|
You can then use the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryString`
|
|
attribute in your controller::
|
|
|
|
use App\Model\UserDto;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapQueryString] UserDto $userDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
You can customize the validation groups used during the mapping and also the
|
|
HTTP status to return if the validation fails::
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapQueryString(
|
|
validationGroups: ['strict', 'edit'],
|
|
validationFailedStatusCode: Response::HTTP_UNPROCESSABLE_ENTITY
|
|
)] UserDto $userDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
The default status code returned if the validation fails is 404.
|
|
|
|
If you want to map your object to a nested array in your query using a specific key,
|
|
set the ``key`` option in the ``#[MapQueryString]`` attribute::
|
|
|
|
use App\Model\SearchDto;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapQueryString(key: 'search')] SearchDto $searchDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
.. versionadded:: 7.3
|
|
|
|
The ``key`` option of ``#[MapQueryString]`` was introduced in Symfony 7.3.
|
|
|
|
If you need a valid DTO even when the request query string is empty, set a
|
|
default value for your controller arguments::
|
|
|
|
use App\Model\UserDto;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapQueryString] UserDto $userDto = new UserDto()
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
.. _controller-mapping-request-payload:
|
|
|
|
Mapping Request Payload
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
When creating an API and dealing with other HTTP methods than ``GET`` (like
|
|
``POST`` or ``PUT``), user's data are not stored in the query string
|
|
but directly in the request payload, like this:
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"firstName": "John",
|
|
"lastName": "Smith",
|
|
"age": 28
|
|
}
|
|
|
|
In this case, it is also possible to directly map this payload to your DTO by
|
|
using the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload`
|
|
attribute::
|
|
|
|
use App\Model\UserDto;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapRequestPayload] UserDto $userDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
This attribute allows you to customize the serialization context as well
|
|
as the class responsible of doing the mapping between the request and
|
|
your DTO::
|
|
|
|
public function dashboard(
|
|
#[MapRequestPayload(
|
|
serializationContext: ['...'],
|
|
resolver: App\Resolver\UserDtoResolver
|
|
)]
|
|
UserDto $userDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
You can also customize the validation groups used, the status code to return if
|
|
the validation fails as well as supported payload formats::
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
// ...
|
|
|
|
public function dashboard(
|
|
#[MapRequestPayload(
|
|
acceptFormat: 'json',
|
|
validationGroups: ['strict', 'read'],
|
|
validationFailedStatusCode: Response::HTTP_NOT_FOUND
|
|
)] UserDto $userDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
The default status code returned if the validation fails is 422.
|
|
|
|
.. tip::
|
|
|
|
If you build a JSON API, make sure to declare your route as using the JSON
|
|
:ref:`format <routing-format-parameter>`. This will make the error handling
|
|
output a JSON response in case of validation errors, rather than an HTML page::
|
|
|
|
#[Route('/dashboard', name: 'dashboard', format: 'json')]
|
|
|
|
Make sure to install `phpstan/phpdoc-parser`_ and `phpdocumentor/type-resolver`_
|
|
if you want to map a nested array of specific DTOs::
|
|
|
|
public function dashboard(
|
|
#[MapRequestPayload] EmployeesDto $employeesDto
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
final class EmployeesDto
|
|
{
|
|
/**
|
|
* @param UserDto[] $users
|
|
*/
|
|
public function __construct(
|
|
public readonly array $users = []
|
|
) {}
|
|
}
|
|
|
|
Instead of returning an array of DTO objects, you can tell Symfony to transform
|
|
each DTO object into an array and return something like this:
|
|
|
|
.. code-block:: json
|
|
|
|
[
|
|
{
|
|
"firstName": "John",
|
|
"lastName": "Smith",
|
|
"age": 28
|
|
},
|
|
{
|
|
"firstName": "Jane",
|
|
"lastName": "Doe",
|
|
"age": 30
|
|
}
|
|
]
|
|
|
|
To do so, map the parameter as an array and configure the type of each element
|
|
using the ``type`` option of the attribute::
|
|
|
|
public function dashboard(
|
|
#[MapRequestPayload(type: UserDto::class)] array $users
|
|
): Response
|
|
{
|
|
// ...
|
|
}
|
|
|
|
.. versionadded:: 7.1
|
|
|
|
The ``type`` option of ``#[MapRequestPayload]`` was introduced in Symfony 7.1.
|
|
|
|
.. _controller_map-uploaded-file:
|
|
|
|
Mapping Uploaded Files
|
|
~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Symfony provides an attribute called ``#[MapUploadedFile]`` to map one or more
|
|
``UploadedFile`` objects to controller arguments::
|
|
|
|
namespace App\Controller;
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
class UserController extends AbstractController
|
|
{
|
|
#[Route('/user/picture', methods: ['PUT'])]
|
|
public function changePicture(
|
|
#[MapUploadedFile] UploadedFile $picture,
|
|
): Response {
|
|
// ...
|
|
}
|
|
}
|
|
|
|
In this example, the associated :doc:`argument resolver </controller/value_resolver>`
|
|
fetches the ``UploadedFile`` based on the argument name (``$picture``). If no file
|
|
is submitted, an ``HttpException`` is thrown. You can change this by making the
|
|
controller argument nullable:
|
|
|
|
.. code-block:: php-attributes
|
|
|
|
#[MapUploadedFile]
|
|
?UploadedFile $document
|
|
|
|
The ``#[MapUploadedFile]`` attribute also allows you to pass a list of constraints
|
|
to apply to the uploaded file::
|
|
|
|
namespace App\Controller;
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
class UserController extends AbstractController
|
|
{
|
|
#[Route('/user/picture', methods: ['PUT'])]
|
|
public function changePicture(
|
|
#[MapUploadedFile([
|
|
new Assert\File(mimeTypes: ['image/png', 'image/jpeg']),
|
|
new Assert\Image(maxWidth: 3840, maxHeight: 2160),
|
|
])]
|
|
UploadedFile $picture,
|
|
): Response {
|
|
// ...
|
|
}
|
|
}
|
|
|
|
The validation constraints are checked before injecting the ``UploadedFile`` into
|
|
the controller argument. If there's a constraint violation, an ``HttpException``
|
|
is thrown and the controller's action is not executed.
|
|
|
|
If you need to upload a collection of files, map them to an array or a variadic
|
|
argument. The given constraint will be applied to all files and if any of them
|
|
fails, an ``HttpException`` is thrown:
|
|
|
|
.. code-block:: php-attributes
|
|
|
|
#[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))]
|
|
array $documents
|
|
|
|
#[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))]
|
|
UploadedFile ...$documents
|
|
|
|
Use the ``name`` option to rename the uploaded file to a custom value:
|
|
|
|
.. code-block:: php-attributes
|
|
|
|
#[MapUploadedFile(name: 'something-else')]
|
|
UploadedFile $document
|
|
|
|
In addition, you can change the status code of the HTTP exception thrown when
|
|
there are constraint violations:
|
|
|
|
.. code-block:: php-attributes
|
|
|
|
#[MapUploadedFile(
|
|
constraints: new Assert\File(maxSize: '2M'),
|
|
validationFailedStatusCode: Response::HTTP_REQUEST_ENTITY_TOO_LARGE
|
|
)]
|
|
UploadedFile $document
|
|
|
|
.. versionadded:: 7.1
|
|
|
|
The ``#[MapUploadedFile]`` attribute was introduced in Symfony 7.1.
|
|
|
|
Managing the Session
|
|
--------------------
|
|
|
|
Symfony provides a session service to store information about the user between
|
|
requests. You can access the session through the ``Request`` object (in services,
|
|
:doc:`inject the RequestStack service </service_container/request>`)::
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
public function index(Request $request): Response
|
|
{
|
|
$session = $request->getSession();
|
|
|
|
// store an attribute for reuse during a later user request
|
|
$session->set('user_id', 42);
|
|
|
|
// retrieve an attribute with an optional default value
|
|
$userId = $session->get('user_id', 0);
|
|
|
|
// ...
|
|
}
|
|
|
|
Read the :doc:`session documentation </session>` for more details about
|
|
configuring and using sessions.
|
|
|
|
Flash Messages
|
|
~~~~~~~~~~~~~~
|
|
|
|
Flash messages are special session messages meant to be used exactly once: they
|
|
vanish from the session automatically as soon as you retrieve them. This makes
|
|
them ideal for storing user notifications::
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
// ...
|
|
|
|
public function update(Request $request): Response
|
|
{
|
|
// ... do some data processing
|
|
|
|
$this->addFlash('notice', 'Your changes were saved!');
|
|
// $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add()
|
|
|
|
return $this->redirectToRoute(/* ... */);
|
|
}
|
|
|
|
.. _request-object-info:
|
|
|
|
The Request and Response Object
|
|
-------------------------------
|
|
|
|
As mentioned :ref:`earlier <controller-request-argument>`, Symfony will
|
|
pass the ``Request`` object to any controller argument that is type-hinted with
|
|
the ``Request`` class::
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
public function index(Request $request): Response
|
|
{
|
|
$request->isXmlHttpRequest(); // is it an Ajax request?
|
|
|
|
$request->getPreferredLanguage(['en', 'fr']);
|
|
|
|
// retrieves GET and POST variables respectively
|
|
$request->query->get('page');
|
|
$request->getPayload()->get('page');
|
|
|
|
// retrieves SERVER variables
|
|
$request->server->get('HTTP_HOST');
|
|
|
|
// retrieves an instance of UploadedFile identified by foo
|
|
$request->files->get('foo');
|
|
|
|
// retrieves a COOKIE value
|
|
$request->cookies->get('PHPSESSID');
|
|
|
|
// retrieves an HTTP request header, with normalized, lowercase keys
|
|
$request->headers->get('host');
|
|
$request->headers->get('content-type');
|
|
}
|
|
|
|
The ``Request`` class has several public properties and methods that return any
|
|
information you need about the request.
|
|
|
|
Like the ``Request``, the ``Response`` object has a public ``headers`` property.
|
|
This object is of the type :class:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag`
|
|
and provides methods for getting and setting response headers. The header names are
|
|
normalized. As a result, the name ``Content-Type`` is equivalent to
|
|
the name ``content-type`` or ``content_type``.
|
|
|
|
In Symfony, a controller is required to return a ``Response`` object::
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
// creates a simple Response with a 200 status code (the default)
|
|
$response = new Response('Hello '.$name, Response::HTTP_OK);
|
|
|
|
// creates a CSS-response with a 200 status code
|
|
$response = new Response('<style> ... </style>');
|
|
$response->headers->set('Content-Type', 'text/css');
|
|
|
|
To facilitate this, different response objects are included to address different
|
|
response types. Some of these are mentioned below. To learn more about the
|
|
``Request`` and ``Response`` (and different ``Response`` classes), see the
|
|
:ref:`HttpFoundation component documentation <component-http-foundation-request>`.
|
|
|
|
.. note::
|
|
|
|
Technically, a controller can return a value other than a ``Response``.
|
|
However, your application is responsible for transforming that value into a
|
|
``Response`` object. This is handled using :doc:`events </event_dispatcher>`
|
|
(specifically the :ref:`kernel.view event <component-http-kernel-kernel-view>`),
|
|
an advanced feature you'll learn about later.
|
|
|
|
Accessing Configuration Values
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
To get the value of any :ref:`configuration parameter <configuration-parameters>`
|
|
from a controller, use the ``getParameter()`` helper method::
|
|
|
|
// ...
|
|
public function index(): Response
|
|
{
|
|
$contentsDir = $this->getParameter('kernel.project_dir').'/contents';
|
|
// ...
|
|
}
|
|
|
|
Returning JSON Response
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
To return JSON from a controller, use the ``json()`` helper method. This returns a
|
|
``JsonResponse`` object that encodes the data automatically::
|
|
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
// ...
|
|
|
|
public function index(): JsonResponse
|
|
{
|
|
// returns '{"username":"jane.doe"}' and sets the proper Content-Type header
|
|
return $this->json(['username' => 'jane.doe']);
|
|
|
|
// the shortcut defines three optional arguments
|
|
// return $this->json($data, $status = 200, $headers = [], $context = []);
|
|
}
|
|
|
|
If the :doc:`serializer service </serializer>` is enabled in your
|
|
application, it will be used to serialize the data to JSON. Otherwise,
|
|
the :phpfunction:`json_encode` function is used.
|
|
|
|
Streaming File Responses
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You can use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::file`
|
|
helper to serve a file from inside a controller::
|
|
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
// ...
|
|
|
|
public function download(): BinaryFileResponse
|
|
{
|
|
// send the file contents and force the browser to download it
|
|
return $this->file('/path/to/some_file.pdf');
|
|
}
|
|
|
|
The ``file()`` helper provides some arguments to configure its behavior::
|
|
|
|
use Symfony\Component\HttpFoundation\File\File;
|
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|
// ...
|
|
|
|
public function download(): BinaryFileResponse
|
|
{
|
|
// load the file from the filesystem
|
|
$file = new File('/path/to/some_file.pdf');
|
|
|
|
return $this->file($file);
|
|
|
|
// rename the downloaded file
|
|
return $this->file($file, 'custom_name.pdf');
|
|
|
|
// display the file contents in the browser instead of downloading it
|
|
return $this->file('invoice_3241.pdf', 'my_invoice.pdf', ResponseHeaderBag::DISPOSITION_INLINE);
|
|
}
|
|
|
|
Sending Early Hints
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
`Early hints`_ tell the browser to start downloading some assets even before the
|
|
application sends the response content. This improves perceived performance
|
|
because the browser can prefetch resources that will be needed once the full
|
|
response is finally sent. These resources are commonly Javascript or CSS files,
|
|
but they can be any type of resource.
|
|
|
|
.. note::
|
|
|
|
In order to work, the `SAPI`_ you're using must support this feature, like
|
|
`FrankenPHP`_.
|
|
|
|
You can send early hints from your controller action thanks to the
|
|
:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::sendEarlyHints`
|
|
method::
|
|
|
|
namespace App\Controller;
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\WebLink\Link;
|
|
|
|
class HomepageController extends AbstractController
|
|
{
|
|
#[Route("/", name: "homepage")]
|
|
public function index(): Response
|
|
{
|
|
$response = $this->sendEarlyHints([
|
|
new Link(rel: 'preconnect', href: 'https://fonts.google.com'),
|
|
(new Link(href: '/style.css'))->withAttribute('as', 'style'),
|
|
(new Link(href: '/script.js'))->withAttribute('as', 'script'),
|
|
]);
|
|
|
|
// prepare the contents of the response...
|
|
|
|
return $this->render('homepage/index.html.twig', response: $response);
|
|
}
|
|
}
|
|
|
|
Technically, Early Hints are an informational HTTP response with the status code
|
|
``103``. The ``sendEarlyHints()`` method creates a ``Response`` object with that
|
|
status code and sends its headers immediately.
|
|
|
|
This way, browsers can start downloading the assets immediately; like the
|
|
``style.css`` and ``script.js`` files in the above example. The
|
|
``sendEarlyHints()`` method also returns the ``Response`` object, which you
|
|
must use to create the full response sent from the controller action.
|
|
|
|
.. _controller-server-sent-events:
|
|
|
|
Streaming Server-Sent Events
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
`Server-Sent Events (SSE)`_ is a standard that allows a server to push updates
|
|
to the client over a single HTTP connection. It provides an efficient way to
|
|
send real-time updates from the server to the browser, such as live notifications,
|
|
progress updates, or data feeds.
|
|
|
|
.. versionadded:: 7.3
|
|
|
|
The ``EventStreamResponse`` and ``ServerEvent`` classes were introduced in Symfony 7.3.
|
|
|
|
The :class:`Symfony\\Component\\HttpFoundation\\EventStreamResponse` class
|
|
allows you to stream events to the client using the SSE protocol. It automatically
|
|
sets the required headers (``Content-Type: text/event-stream``, ``Cache-Control: no-cache``,
|
|
``Connection: keep-alive``) and provides an API to send events::
|
|
|
|
use Symfony\Component\HttpFoundation\EventStreamResponse;
|
|
use Symfony\Component\HttpFoundation\ServerEvent;
|
|
|
|
// ...
|
|
|
|
public function liveNotifications(): EventStreamResponse
|
|
{
|
|
return new EventStreamResponse(function (): iterable {
|
|
foreach ($this->getNotifications() as $notification) {
|
|
yield new ServerEvent($notification->toJson());
|
|
|
|
sleep(1); // simulate a delay between events
|
|
}
|
|
});
|
|
}
|
|
|
|
The :class:`Symfony\\Component\\HttpFoundation\\ServerEvent` class is a DTO
|
|
that represents an SSE event following `the WHATWG SSE specification`_. You can
|
|
customize each event using its constructor arguments::
|
|
|
|
// basic event with only data
|
|
yield new ServerEvent('Some message');
|
|
|
|
// event with a custom type (clients listen via addEventListener('my-event', ...))
|
|
yield new ServerEvent(
|
|
data: json_encode(['status' => 'completed']),
|
|
type: 'my-event'
|
|
);
|
|
|
|
// event with an ID (useful for resuming streams with the Last-Event-ID header)
|
|
yield new ServerEvent(
|
|
data: 'Update content',
|
|
id: 'event-123'
|
|
);
|
|
|
|
// event that tells the client to retry after a specific time (in milliseconds)
|
|
yield new ServerEvent(
|
|
data: 'Retry info',
|
|
retry: 5000
|
|
);
|
|
|
|
// event with a comment (can be used for keep-alive)
|
|
yield new ServerEvent(comment: 'keep-alive');
|
|
|
|
For use cases where generators are not practical, you can use the
|
|
:method:`Symfony\\Component\\HttpFoundation\\EventStreamResponse::sendEvent`
|
|
method for manual control::
|
|
|
|
use Symfony\Component\HttpFoundation\EventStreamResponse;
|
|
use Symfony\Component\HttpFoundation\ServerEvent;
|
|
|
|
// ...
|
|
|
|
public function liveProgress(): EventStreamResponse
|
|
{
|
|
return new EventStreamResponse(function (EventStreamResponse $response): void {
|
|
$redis = new \Redis();
|
|
$redis->connect('127.0.0.1');
|
|
$redis->subscribe(['message'], function (/* ... */, string $message) use ($response): void {
|
|
$response->sendEvent(new ServerEvent($message));
|
|
});
|
|
});
|
|
}
|
|
|
|
On the client side, you can listen to events using the native ``EventSource`` API:
|
|
|
|
.. code-block:: javascript
|
|
|
|
const eventSource = new EventSource('/live-notifications');
|
|
|
|
// listen to all events (without a specific type)
|
|
eventSource.onmessage = (event) => {
|
|
console.log('Received:', event.data);
|
|
};
|
|
|
|
// listen to events with a specific type
|
|
eventSource.addEventListener('my-event', (event) => {
|
|
console.log('My event:', JSON.parse(event.data));
|
|
});
|
|
|
|
// handle connection errors
|
|
eventSource.onerror = (error) => {
|
|
console.error('SSE error:', error);
|
|
eventSource.close();
|
|
};
|
|
|
|
.. warning::
|
|
|
|
``EventStreamResponse`` is designed for applications with limited concurrent
|
|
connections. Because SSE keeps HTTP connections open, it consumes server
|
|
resources (memory and connection limits) for each connected client.
|
|
|
|
For high-traffic applications that need to broadcast updates to many clients
|
|
simultaneously, consider using :doc:`Mercure </mercure>`, which is built on
|
|
top of SSE but uses a dedicated hub to manage connections efficiently.
|
|
|
|
Final Thoughts
|
|
--------------
|
|
|
|
In Symfony, a controller is usually a class method which is used to accept
|
|
requests, and return a ``Response`` object. When mapped with a URL, a controller
|
|
becomes accessible and its response can be viewed.
|
|
|
|
To facilitate the development of controllers, Symfony provides an
|
|
``AbstractController``. It can be used to extend the controller class allowing
|
|
access to some frequently used utilities such as ``render()`` and
|
|
``redirectToRoute()``. The ``AbstractController`` also provides the
|
|
``createNotFoundException()`` utility which is used to return a page not found
|
|
response.
|
|
|
|
In other articles, you'll learn how to use specific services from inside your controller
|
|
that will help you persist and fetch objects from a database, process form submissions,
|
|
handle caching and more.
|
|
|
|
Keep Going!
|
|
-----------
|
|
|
|
Next, learn all about :doc:`rendering templates with Twig </templates>`.
|
|
|
|
Learn more about Controllers
|
|
----------------------------
|
|
|
|
.. toctree::
|
|
:maxdepth: 1
|
|
:glob:
|
|
|
|
controller/*
|
|
|
|
.. _`Symfony Maker`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html
|
|
.. _`unvalidated redirects security vulnerability`: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
|
|
.. _`Early hints`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103
|
|
.. _`SAPI`: https://www.php.net/manual/en/function.php-sapi-name.php
|
|
.. _`FrankenPHP`: https://frankenphp.dev
|
|
.. _`Validate Filters`: https://www.php.net/manual/en/filter.constants.php
|
|
.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser
|
|
.. _`phpdocumentor/type-resolver`: https://packagist.org/packages/phpdocumentor/type-resolver
|
|
.. _`Server-Sent Events (SSE)`: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
|
.. _`the WHATWG SSE specification`: https://html.spec.whatwg.org/multipage/server-sent-events.html
|