Skip to content

Commit 54d25be

Browse files
authored
Add documentation on controllers
1 parent 8a76721 commit 54d25be

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-0
lines changed

source/devapi/controllers.rst

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
Controllers
2+
-----------
3+
4+
You need a `Controller` any time you want an URL access.
5+
6+
.. note::
7+
8+
`Controllers` is the "modern" way that replaces all files previousely present in ``front/`` and ``ajax/`` directories.
9+
10+
.. warning::
11+
12+
Currently, not all existing front or ajax files has been migrated to `Controllers`, mainly because of specific stuff or no time to work on that yet.
13+
14+
Any new feature added to GLPI >= 11 **must** use `Controllers`.
15+
16+
Creating a controller
17+
^^^^^^^^^^^^^^^^^^^^^
18+
19+
Minimal requirements to have a working controller:
20+
21+
* The controller file must be placed in the src/Glpi/Controller/** folder.
22+
* The name of the controller must end with Controller.
23+
* The controller must extends the ``Glpi\Controller\AbstractController`` class.
24+
* The controller must define a route using the Route attribute.
25+
* The controller must return some kind of response.
26+
27+
Example:
28+
29+
.. code-block:: php
30+
31+
# src/Controller/Form/TagsListController.php
32+
<?php
33+
34+
namespace Glpi\Controller\Form;
35+
36+
use Glpi\Controller\AbstractController;
37+
use Symfony\Component\HttpFoundation\Request;
38+
use Symfony\Component\HttpFoundation\Response;
39+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
40+
use Symfony\Component\Routing\Attribute\Route;
41+
42+
final class TagsListController extends AbstractController
43+
{
44+
#[Route(
45+
"/Form/TagsList",
46+
name: "glpi_form_tags_list",
47+
methods: "GET"
48+
)]
49+
public function __invoke(Request $request): Response
50+
{
51+
if (!Form::canUpdate()) {
52+
throw new AccessDeniedHttpException();
53+
}
54+
55+
$tag_manager = new FormTagsManager();
56+
$filter = $request->query->getString('filter');
57+
58+
return new JsonResponse($tag_manager->getTags($filter));
59+
}
60+
}
61+
62+
Routing
63+
^^^^^^^
64+
65+
Routing is done with the ``Symfony\Component\Routing\Attribute\Route`` attribute. Read more from `Symfony Routing documentation <https://symfony.com/doc/current/routing.html>`_.
66+
67+
Basic route
68+
+++++++++++
69+
70+
.. code-block:: php
71+
72+
#[Symfony\Component\Routing\Attribute\Route("/my/route/url", name: "glpi_my_route_name")]
73+
74+
Dynamic route parameter
75+
+++++++++++++++++++++++
76+
77+
.. code-block:: php
78+
79+
#[Symfony\Component\Routing\Attribute\Route("/Ticket/{$id}", name: "glpi_ticket")]
80+
81+
Restricting a route to a specific HTTP method
82+
+++++++++++++++++++++++++++++++++++++++++++++
83+
84+
.. code-block:: php
85+
86+
#[Symfony\Component\Routing\Attribute\Route("/Tickets", name: "glpi_tickets", methods: "GET")]
87+
88+
Known limitation for ajax routes
89+
++++++++++++++++++++++++++++++++
90+
91+
If an ajax route will be accessed by multiple POST requests without a page reload then you will run into CRSF issues.
92+
93+
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``.
94+
95+
You will thus need to prefix your route by ``/ajax`` until we find a better way to handle this.
96+
97+
Reading query parameters
98+
^^^^^^^^^^^^^^^^^^^^^^^^
99+
100+
These parameters are found in the $request object:
101+
102+
* ``$request->query`` for ``$_GET``
103+
* ``$request->request`` for ``$_POST``
104+
* ``$request->files`` for ``$_FILES``
105+
106+
Read more from `Symfony Request documentation <https://symfony.com/doc/current/components/http_foundation.html#request>`_
107+
108+
Reading a string parameter from $_GET
109+
+++++++++++++++++++++++++++++++++++++
110+
111+
.. code-block:: php
112+
113+
<?php
114+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
115+
{
116+
$filter = $request->query->getString('filter');
117+
}
118+
119+
Reading an integer parameter from $_POST
120+
++++++++++++++++++++++++++++++++++++++++
121+
122+
.. code-block:: php
123+
124+
<?php
125+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
126+
{
127+
$my_int = $request->request->getInt('my_int');
128+
}
129+
130+
Reading an array of values from $_POST
131+
++++++++++++++++++++++++++++++++++++++
132+
133+
.. code-block:: php
134+
135+
<?php
136+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
137+
{
138+
$ids = $request->request->all()["ids"] ?? [];
139+
}
140+
141+
Reading a file
142+
++++++++++++++
143+
144+
.. code-block:: php
145+
146+
<?php
147+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
148+
{
149+
// @var \Symfony\Component\HttpFoundation\File\UploadedFile $file
150+
$file = $request->files->get('my_file_input_name');
151+
$content = $file->getContent();
152+
}
153+
154+
Single vs multi action controllers
155+
++++++++++++++++++++++++++++++++++
156+
157+
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).
158+
159+
In general, this is recommended way to proceed but we do not force it and you are allowed to use multi actions controllers if you need them.
160+
161+
Handling errors (missing rights, bad request, …)
162+
++++++++++++++++++++++++++++++++++++++++++++++++
163+
164+
A controller may throw some exceptions if it receive an invalid request.
165+
166+
You can use any exception that extends ``Symfony\Component\HttpKernel\Exception``, see below examples.
167+
168+
Missing rights
169+
++++++++++++++
170+
171+
.. code-block:: php
172+
173+
<?php
174+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
175+
{
176+
if (!Form::canUpdate()) {
177+
throw new Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
178+
}
179+
}
180+
181+
Invalid input
182+
+++++++++++++
183+
184+
.. code-block:: php
185+
186+
<?php
187+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
188+
{
189+
$id = $request->request->getInt('id');
190+
if ($id == 0) {
191+
throw new Symfony\Component\HttpKernel\Exception\BadRequestHttpException();
192+
}
193+
}
194+
195+
Firewall
196+
^^^^^^^^
197+
198+
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.
199+
200+
.. code-block:: php
201+
202+
<?php
203+
#[Glpi\Security\Attribute\SecurityStrategy(Glpi\Http\Firewall::STRATEGY_NO_CHECK)]
204+
public function __invoke(Symfony\Component\HttpFoundation\Request $request): Response
205+
206+
Possible responses
207+
^^^^^^^^^^^^^^^^^^
208+
209+
You may use different responses classes depending on what your controller is doing (sending json content, outputting a file, …).
210+
211+
There is also a render helper method that helps you return a rendered twig content as a response.
212+
213+
Sending JSON
214+
++++++++++++
215+
216+
.. code-block:: php
217+
218+
<?php
219+
return new Symfony\Component\HttpFoundation\JsonResponse(['name' => 'John', 'age' => 67]);
220+
221+
Sending a file from memory
222+
++++++++++++++++++++++++++
223+
224+
.. code-block:: php
225+
226+
<?php
227+
$filename = "my_file.txt";
228+
$file_content = "my file content";
229+
230+
$disposition = Symfony\Component\HttpFoundation\HeaderUtils::makeDisposition(
231+
HeaderUtils::DISPOSITION_ATTACHMENT,
232+
$filename,
233+
);
234+
235+
$response = new Symfony\Component\HttpFoundation;\Response($file_content);
236+
$response->headers->set('Content-Disposition', $disposition);
237+
$response->headers->set('Content-Type', 'text/plain');
238+
return $response
239+
240+
Sending a file from disk
241+
++++++++++++++++++++++++
242+
243+
.. code-block:: php
244+
245+
<?php
246+
$file = 'path/to/file.txt';
247+
return new Symfony\Component\HttpFoundation\BinaryFileResponse($file);
248+
249+
Displaying a twig template
250+
++++++++++++++++++++++++++
251+
252+
.. code-block:: php
253+
254+
<?php
255+
return $this->render('/path/to/my/template.html.twig', [
256+
'parameter_1' => 'value_1',
257+
'parameter_2' => 'value_2',
258+
]);
259+
260+
Redirection
261+
+++++++++++
262+
263+
.. code-block:: php
264+
265+
<?php
266+
return new Symfony\Component\HttpFoundation\RedirectResponse($url);
267+
268+
General best practices
269+
^^^^^^^^^^^^^^^^^^^^^^
270+
271+
Use thin controllers
272+
++++++++++++++++++++
273+
274+
Controller should be *thin*, which mean they should contains the minimal code needed to *glue* together the pieces of GLPI needed to handle the request.
275+
276+
A good controller does only the following actions:
277+
278+
* Check the rights
279+
* Validate the request
280+
* Extract what it needs from the request
281+
* Call some methods from a dedicated service class that can process the data (using DI in the future, not possible at this time)
282+
* Return a response
283+
284+
Most of the time, this will take between 5 and 15 instructions, resulting in a small method.
285+
286+
Make your controller final
287+
++++++++++++++++++++++++++
288+
289+
Unless you are making a generic controller that is explicitly made to be extended, set your controller as ``final``.
290+
291+
.. code-block:: php
292+
293+
<?php
294+
❌public class ApiController
295+
✅final public class ApiController
296+
297+
Always restrict the HTTP method
298+
+++++++++++++++++++++++++++++++
299+
300+
If your controller is only meant to be used with a specific HTTP method (e.g. `POST`), it is best to define it.
301+
302+
It helps others developers to understand how this route must be used and help debugging when miss-using the route.
303+
304+
.. code-block:: php
305+
306+
<?php
307+
❌#[Route("/my_route”, name: “glpi_my_route”)]
308+
✅#[Route("/my_route”, name: “glpi_my_route”, methods: “GET”)]
309+
310+
Use uppercase first route names
311+
+++++++++++++++++++++++++++++++
312+
313+
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.
314+
315+
.. code-block:: php
316+
317+
<?php
318+
❌/ticket/timeline
319+
✅/Ticket/Timeline
320+
321+
URL generation
322+
++++++++++++++
323+
324+
Ideally, URLs should not be hard-coded but should instead be generated using their route names.
325+
326+
This is not yet possible in many places so we have to rely on hard-coded urls at this time.

source/devapi/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Apart from the current documentation, you can also generate the full PHP documen
99
mainobjects
1010
database/index
1111
search
12+
controllers
1213
hlapi/index
1314
massiveactions
1415
rules

0 commit comments

Comments
 (0)