6

I have a common structure for Symfony controller (using FOSRestBundle)

/**
 * @Route\Get("users/{id}", requirements={"userId" = "(\d+)"})
 */
public function getUserAction(User $user)
{
}

Now if I request http://localhost/users/1 everything is fine. But if I request http://localhost/users/11111111111111111 I get 500 error and Exception

ERROR:  value \"11111111111111111\" is out of range for type integer"

Is there a way to check id before it is transferred to database?

As a solution I can specify length of id

/**
 * @Route\Get("users/{id}", requirements={"userId" = "(\d{,10})"})
 */

but then Symfony will say that there is no such route, instead of showing that the id is incorrect.

klipach
  • 818
  • 7
  • 21
  • 1
    Have you already tried to look for a string instead of an integer? regexp: (\w+) instead of (\d+) ? You may have to cast this ID-param to a double and check whether it's a number within your controller afterwards. – Blauharley Apr 04 '18 at 12:33
  • (\w+) wouldn't help, ids are integers – klipach Apr 04 '18 at 12:41
  • Maybe this one can help to you? http://symfony.com/doc/3.4/reference/constraints/Range.html – Evgeny Ruban Apr 04 '18 at 20:02
  • I suggest to read you this answers: https://stackoverflow.com/questions/670662/whats-the-maximum-size-for-an-int-in-php – Imanali Mamadiev Apr 05 '18 at 03:45
  • How can I apply range for route? PHP_INT_SIZE - is not related to my question. My problem is that I want to check $id value before it is passed to DoctrineConvertor and throw an exception before id is passed to DB (to avoid db call and db exception) – klipach Apr 05 '18 at 07:58

1 Answers1

0

By telling Symfony that the getUserAction() argument is a User instance, it will take for granted that the {id} url parameter must be matched to the as primary key, handing it over to the Doctrine ParamConverter to fetch the corresponding User.

There are at least two workarounds.

1. Use the ParamConverter repository_method config

In the controller function's comment, we can add the @ParamConverter annotation and tell it to use the repository_method option.

This way Symfony will hand the url parameter to a function in our entity repository, from which we'll be able to check the integrity of the url parameter.

In UserRepository, let's create a function getting an entity by primary key, checking first the integrity of the argument. That is, $id must not be larger than the largest integer that PHP can handle (the PHP_INT_MAX constant).

Please note: $id is a string, so it's safe to compare it to PHP_INT_MAX, because PHP will automatically typecast PHP_INT_MAX to a string and compare it to $id. If it were an integer, the test would always fail (by design, all integers are less than or equal to PHP_INT_MAX).

// ...
use Symfony\Component\Form\Exception\OutOfBoundsException;

class UserRepository extends ...
{
    // ...

    public function findSafeById($id) {
      if ($id > PHP_INT_MAX) {
        throw new OutOfBoundsException($id . " is too large to fit in an integer");
      }

      return $this->find($id);
    }
}        

This is only an example: we can do anything we like before throwing the exception (for example logging the failed attempt).

Then, in our controller, let's include the ParamConverter annotation:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

and modify the function comment adding the annotation:

@ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"}) 

Our controller function should look like:

  /**
   * @Get("users/{id}")
   * @ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
   */
  public function getUserAction(User $user) {
      // Return a "OK" response with the content you like
  }

This technique allows customizing the exception, but does not give you control over the response - you'll still get a 500 error in production.

Documentation: see here.

2. Parse the route "the old way"

This way was the only viable one up to Symfony 3, and gives you a more fine-grained control over the generated response.

Let's change the action prototype like this:

/**
 * @Route\Get("users/{id}", requirements={"id" = "(\d+)"})
 */
public function getUserAction($id)
{
}

Now, in the action we'll receive the requested $id and we'll be able to check whether it's ok. If not, we throw an exception and/or return some error response (we can choose the HTTP status code, the format and anything else).

Below you find a sample implementation of this procedure.

use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\Form\Exception\OutOfBoundsException;
use Symfony\Component\HttpFoundation\JsonResponse;

class MyRestController extends FOSRestController {
    /**
     * @Get("users/{id}", requirements={"id" = "(\d+)"})
     */
    public function getUserAction($id) {

      try {
        if ($id > PHP_INT_MAX) {
          throw new OutOfBoundsException($id . " is too large to fit in an integer");
        }

        // Replace App\Entity\User with your actual Entity alias
        $user = $this->getDoctrine()->getRepository('App\Entity\User')->find($id);
        if (!$user) {
          throw new \Doctrine\ORM\NoResultException("User not found");
        }

        // Return a "OK" response with the content you like
        return new JsonResponse(['key' => 123]);

      } catch (Exception $e) {
        return new JsonResponse(['message' => $e->getMessage()], 400);
      }
    }
Paolo Stefan
  • 10,112
  • 5
  • 45
  • 64
  • This way is valid, but this removes all the magic of the doctrine param converter and needs a lot of changes in the code. My question is if it is possible just to add the check after id is extracted from URL and before it is passed to the doctrine param converter. – klipach May 04 '18 at 14:43
  • There is actually another way, but it's not very different from the standard one - I've edited my answer (now it's nearly a book chapter :) – Paolo Stefan May 04 '18 at 15:46