|
| 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. |
0 commit comments