04/01/2017 - SYMFONY
In this example we're going to limit our API with rate limiting feature where users will be consuming API maximum of given times.
These are what I can think of for now so if you see something is missing then go ahead to implement.
If anything unexpected happens along the way, 404 Not Found
exception is thrown containing "An unknown error occurred." message.
api:
resource: "@ApiBundle/Controller/"
type: annotation
prefix: /1/
website:
resource: "@WebsiteBundle/Controller/"
type: annotation
prefix: /
namespace Application\WebsiteBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Response;
/**
* @Route("", service="website.controller.default")
*/
class DefaultController extends Controller
{
/**
* @Method({"GET"})
* @Route("")
*
* @return Response
*/
public function indexAction()
{
return new Response('You are not consuming the API...');
}
}
services:
website.controller.default:
class: Application\WebsiteBundle\Controller\DefaultController
You can call http://rate-limiter.dev/app_dev.php/
as many times as you wish without a problem.
namespace Application\ApiBundle\Controller;
abstract class AbstractController
{
}
namespace Application\ApiBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Response;
/**
* @Route("", service="api.controller.default")
*/
class DefaultController extends AbstractController
{
/**
* @Method({"GET"})
* @Route("")
*
* @return Response
*/
public function indexAction()
{
return new Response('You are consuming the API...');
}
}
namespace Application\ApiBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Response;
/**
* @Route("/account", service="api.controller.account")
*/
class AccountController extends AbstractController
{
/**
* @Method({"GET"})
* @Route("")
*
* @return Response
*/
public function indexAction()
{
return new Response('You are consuming the API...');
}
}
services:
api.controller.default:
class: Application\ApiBundle\Controller\DefaultController
api.controller.account:
class: Application\ApiBundle\Controller\AccountController
namespace Application\ApiBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Application\ApiBundle\Repository\UserRepository")
* @ORM\Table(name="user",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="username_unq", columns={"username"})
* }
* )
*/
class User
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="`username`", type="string", length=100)
*/
private $username;
/**
* @ORM\OneToOne(targetEntity="RateLimit", cascade={"persist", "remove"})
* @ORM\JoinColumn(name="rate_limit_id", referencedColumnName="id")
*/
private $rateLimit;
...
}
namespace Application\ApiBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="rate_limit")
*/
class RateLimit
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(
* name="`limit`",
* type="integer",
* options={"comment":"Max number of allowed calls in given period of time."}
* )
*/
private $limit;
/**
* @ORM\Column(
* name="`total_call`",
* type="integer",
* options={"comment":"Current total calls in given period of time."}
* )
*/
private $totalCall = 0;
...
}
namespace Application\ApiBundle\Exception;
use RuntimeException;
class RateLimitException extends RuntimeException
{
}
namespace Application\ApiBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
class UserRepository extends EntityRepository
{
public function findOneById($id)
{
return $this->createQueryBuilder('u')
->select('r.id, r.limit, r.totalCall')
->innerJoin('u.rateLimit', 'r')
->where('u.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult(Query::HYDRATE_ARRAY);
}
}
services:
api.repository.user:
class: Application\ApiBundle\Repository\UserRepository
factory: [ "@doctrine.orm.entity_manager", getRepository ]
arguments:
- Application\ApiBundle\Entity\User
namespace Application\ApiBundle\EventListener;
use Application\ApiBundle\Controller\AbstractController;
use Application\ApiBundle\Exception\RateLimitException;
use Application\ApiBundle\Util\RateLimiter;
use Exception;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ControllerListener
{
private $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function onKernelController(FilterControllerEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if (!$controller[0] instanceof AbstractController) {
return;
}
try {
$this->rateLimiter->check(1);
} catch (RateLimitException $e) {
throw new BadRequestHttpException($e->getMessage());
} catch (Exception $e) {
throw new NotFoundHttpException('An unknown error occurred.');
}
}
}
services:
api.listener.controller:
class: Application\ApiBundle\EventListener\ControllerListener
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
arguments:
- "@api.util.rate_limiter"
namespace Application\ApiBundle\Util;
use Application\ApiBundle\Entity\RateLimit;
use Application\ApiBundle\Exception\RateLimitException;
use Application\ApiBundle\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
class RateLimiter
{
private $userRepository;
private $entityManager;
public function __construct(
UserRepository $userRepository,
EntityManagerInterface $entityManager
) {
$this->userRepository = $userRepository;
$this->entityManager = $entityManager;
}
public function check($userId)
{
$userRateLimit = $this->userRepository->findOneById($userId);
if (is_null($userRateLimit)) {
throw new RateLimitException('Rate limit not found');
}
if ($userRateLimit['totalCall'] >= $userRateLimit['limit']) {
throw new RateLimitException('Rate limit exceeded.');
}
$this->updateTotalCall($userRateLimit);
}
private function updateTotalCall(array $userRateLimit)
{
/** @var RateLimit $rateLimit */
$rateLimit = $this->entityManager->getReference(RateLimit::class, $userRateLimit['id']);
$rateLimit->setTotalCall($userRateLimit['totalCall']+1);
$this->entityManager->flush();
}
}
services:
api.util.rate_limiter:
class: Application\ApiBundle\Util\RateLimiter
arguments:
- "@api.repository.user"
- "@doctrine.orm.entity_manager"
mysql> SELECT
-> `user`.`id` AS `UserID`,
-> `user`.`username` AS `UserUsername`,
-> `rate_limit`.`limit` AS `Limit`,
-> `rate_limit`.`total_call` AS `TotalCall`
-> FROM `user`
-> LEFT JOIN `rate_limit` ON `user`.`rate_limit_id` = `rate_limit`.`id`;
+--------+----------------+-------+-----------+
| UserID | UserUsername | Limit | TotalCall |
+--------+----------------+-------+-----------+
| 1 | AlPacino | 10 | 0 | # Can make maximum 10 calls at any time
| 2 | AndyGarcia | 2 | 2 | # Cannot make any more calls as TotalCall equals to Limit
| 3 | RobertDeNiro | NULL | NULL | # Cannot make any calls because Rate Limit data is not set yet
+--------+----------------+-------+-----------+
3 rows in set (0.00 sec)
# REQUEST
http://rate-limit.dev/app_dev.php/1/
http://rate-limit.dev/app_dev.php/1/account
# RESPONSE
200 OK
You are consuming the API...
# REQUEST
http://rate-limit.dev/app_dev.php/1/
http://rate-limit.dev/app_dev.php/1/account
# RESPONSE
400 Bad Request
Rate limit exceeded.
# REQUEST
http://rate-limit.dev/app_dev.php/1/
http://rate-limit.dev/app_dev.php/1/account
# RESPONSE
400 Bad Request
Rate limit not found.