04/01/2017 - SYMFONY
Bu örneğimizde API uygulamamıza kullanım kısıtlaması (rate limiting) özelliği ekleyip, kullanıcılara sadece verilen süre içinde toplam kullanım hakkı vereceğiz.
Aşağıdakiler şu an için aklıma gelenler ama size göre eksik olanlar varsa kendiniz eklemeler de yapabilirsiniz.
Eğer herhangi bir nedenden dolayı beklenmedik bir hata oluşursa, sistem içinde "An unknown error occurred." hata mesajı barındıran bir 404 Not Found
hatası oluşturur.
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
Hiç problem yaşamadan http://rate-limiter.dev/app_dev.php/
adresine istediğiniz kadar ulaşabilirsiniz.
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.