Controllers¶
You need a Controller any time you want an URL access.
Note
Controllers are the modern way that replace all files previousely present in front/ and ajax/ directories.
Warning
Currently, not all existing front/ or ajax/ files have been migrated to Controllers, mainly because of specific behaviors or lack of time to work on migrating them.
Any new feature added to GLPI >=11 must use Controllers.
For plugin development, please read the plugin-specific implementation.
Creating a controller¶
Minimal requirements to have a working controller:
The controller file must be placed in the
src/Glpi/Controller/folder.The name of the controller must end with
Controller.The controller must extends the
Glpi\Controller\AbstractControllerclass.The controller must define a route using the Route attribute.
The controller must return some kind of response.
Example:
# src/Controller/Form/TagsListController.php
<?php
namespace Glpi\Controller\Form;
use Glpi\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Attribute\Route;
final class TagsListController extends AbstractController
{
#[Route(
"/Form/TagsList",
name: "glpi_form_tags_list",
methods: "GET"
)]
public function __invoke(Request $request): Response
{
if (!Form::canUpdate()) {
throw new AccessDeniedHttpException();
}
$tag_manager = new FormTagsManager();
$filter = $request->query->getString('filter');
return new JsonResponse($tag_manager->getTags($filter));
}
}
Routing¶
Routing is done with the Symfony\Component\Routing\Attribute\Route attribute. Read more from Symfony Routing documentation.
Basic route¶
#[Symfony\Component\Routing\Attribute\Route("/my/route/url", name: "glpi_my_route_name")]
Dynamic route parameter¶
#[Symfony\Component\Routing\Attribute\Route("/Ticket/{id}", name: "glpi_ticket")]
Restricting a route to a specific HTTP method¶
#[Symfony\Component\Routing\Attribute\Route("/Tickets", name: "glpi_tickets", methods: "GET")]
Known limitation for ajax routes¶
Prior to GLPI 12, if an ajax route will be accessed by multiple POST requests without a page reload then you will run into CRSF issues.
This is because GLPI’s solution for this is to check a special CRSF token that is valid for multiples requests, but this special token is only checked if your url start with /ajax.
You will thus need to prefix your route by /ajax until we find a better way to handle this.
Reading query parameters¶
These parameters are found in the $request object:
$request->queryfor$_GET$request->requestfor$_POST$request->filesfor$_FILES
Read more from Symfony Request documentation
Reading a string parameter from $_GET¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
$filter = $request->query->getString('filter');
}
Reading an integer parameter from $_POST¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
$my_int = $request->request->getInt('my_int');
}
Reading an array of values from $_POST¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
$ids = $request->request->get("ids", []);
}
Reading a file¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
// @var \Symfony\Component\HttpFoundation\File\UploadedFile $file
$file = $request->files->get('my_file_input_name');
$content = $file->getContent();
}
Single vs multi action controllers¶
The examples in this documentation use the magic __invoke method to force the controller to have only one action (see https://symfony.com/doc/current/controller/service.html#invokable-controllers).
In general, this is a recommended way to proceed but we do not force it and you are allowed to use multi actions controllers if you need them, by adding another public method and configuring it with the #[Route(...)] attribute.
Handling errors (missing rights, bad request, …)¶
A controller may throw some exceptions if it receive an invalid request. Exceptions will automatically converted to error pages.
If you need exceptions with specific HTTP codes (like 4xx or 5xx codes), you can use any exception that extends Symfony\Component\HttpKernel\Exception\HttpException.
GLPI also provide some custom Http exceptions in the Glpi\Exception\Http\ namespace.
Missing rights¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
if (!Form::canUpdate()) {
throw new \Glpi\Exception\Http\AccessDeniedHttpException();
}
}
Invalid header¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
if ($request->headers->get('Content-Type') !== 'application/json') {
throw new \Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException();
}
}
Invalid input¶
<?php
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
{
$id = $request->request->getInt('id');
if ($id == 0) {
throw new \Glpi\Exception\Http\BadRequestHttpException();
}
}
CSRF protection¶
Prior to GLPI 12, a form input is required in the form of
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">``.
Starting with GLPI 12, CSRF protection is handled using Fetch metadata headers sent by client’s browser. No more token form inputs are needed, you just don’t need to worry about it anymore. For further information about CSRF, read MDN documentation.
In GLPI 11 or 12, csrf/tokens are checked in the CheckCsrfListener.
Firewall¶
By default, the GLPI firewall will not allow unauthenticated user to access your routes. You can change the firewall strategy with the Glpi\Security\Attribute\SecurityStrategy attribute.
<?php
#[Glpi\Security\Attribute\SecurityStrategy(Glpi\Http\Firewall::STRATEGY_NO_CHECK)]
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
Possible responses¶
You may use different responses classes depending on what your controller is doing (sending json content, outputting a file, …).
There is also a render helper method that helps you return a rendered Twig template as a Response object.
Sending JSON¶
<?php
return new Symfony\Component\HttpFoundation\JsonResponse(['name' => 'John', 'age' => 67]);
Sending a file from memory¶
<?php
$filename = "my_file.txt";
$file_content = "my file content";
$disposition = Symfony\Component\HttpFoundation\HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
$filename,
);
$response = new Symfony\Component\HttpFoundation\Response($file_content);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', 'text/plain');
return $response
Sending a file from disk¶
<?php
$file_path = 'path/to/file.txt';
return new Symfony\Component\HttpFoundation\BinaryFileResponse($file_path);
Displaying a twig template¶
<?php
return $this->render('path/to/my/template.html.twig', [
'parameter_1' => 'value_1',
'parameter_2' => 'value_2',
]);
Redirection¶
<?php
return new Symfony\Component\HttpFoundation\RedirectResponse($url);
General best practices¶
Use thin controllers¶
Controller should be thin, which mean they should contain the minimal code needed to glue together the pieces of GLPI needed to handle the request.
A good controller does only the following actions:
Check the rights
Validate the request
Extract what it needs from the request
Call some methods from a dedicated service class that can process the data (using DI in the future, not possible at this time)
Return a
Responseobject
Most of the time, this will take between 5 and 15 instructions, resulting in a small method.
Make your controller final¶
Unless you are making a generic controller that is explicitly made to be extended, set your controller as final.
<?php
❌public class ApiController
✅final public class ApiController
Always restrict the HTTP method¶
If your controller is only meant to be used with a specific HTTP method (e.g. POST), it is best to define it in the Route attribute.
It helps others developers understand how this route must be used and help debugging when misusing the route.
<?php
❌#[Route("/my_route”, name: “glpi_my_route”)]
✅#[Route("/my_route”, name: “glpi_my_route”, methods: “GET”)]
Use uppercase first route names¶
Since our routes will refer to GLPI itemtypes which contains upper cases letters, it is probably clearer to use uppercase first names for all our routes.
<?php
❌/ticket/timeline
✅/Ticket/Timeline
URL generation¶
Ideally, URLs should not be hard-coded but should instead be generated using their route names.
In your Controllers, you can inject the Symfony router in the constructor in order to generate URLs based on route names:
<?php
namespace Glpi\Controller\Custom;
use Glpi\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class MyController extends AbstractController
{
public function __construct(
private readonly UrlGeneratorInterface $router
) {
}
public function __invoke(Request $request): Response
{
$route_name = $this->router->generate('my_route');
// ...
}
}
You can also do it in Twig templates, using the url() or path() functions:
{{ path('my_route') }} {# Shows the url like "/my_route" #}
{{ url('my_route') }} {# Shows the url like "http://localhost/my_route" #}
Check out the Symfony documentation for more details about these functions:
