at the end of the day, it was inevitable
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace ApiBundle\Controller;
|
||||
|
||||
use ApiBundle\ApiBundleServices;
|
||||
use ApiBundle\Response\View;
|
||||
use ApiBundle\Security\AccessChecker\AccessCheckerInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Registry;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* Class AbstractApiController
|
||||
* @package ApiBundle\Controller
|
||||
*/
|
||||
class AbstractApiController
|
||||
{
|
||||
|
||||
/**
|
||||
* Default page value in pagination if 'page' filter not provided.
|
||||
*/
|
||||
const DEFAULT_PAGE = 1;
|
||||
|
||||
/**
|
||||
* Default limit (max entities per page) value in pagination if 'limit'
|
||||
* filter not provided.
|
||||
*/
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container A ContainerInterface instance.
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service from container.
|
||||
*
|
||||
* @param string $id The service identifier.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function get($id)
|
||||
{
|
||||
return $this->container->get($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a container configuration parameter by its name.
|
||||
*
|
||||
* @param string $name The parameter name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getParameter($name)
|
||||
{
|
||||
return $this->container->getParameter($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a Form instance from the type of the form.
|
||||
*
|
||||
* @param string $type The fully qualified class name of the form type.
|
||||
* @param mixed $data The initial data for the form.
|
||||
* @param array $options Options for the form.
|
||||
*
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function createForm($type, $data = null, array $options = [])
|
||||
{
|
||||
return $this->container->get('form.factory')
|
||||
->create($type, $data, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return default entity manager.
|
||||
*
|
||||
* @return ObjectManager
|
||||
*/
|
||||
protected function getManager()
|
||||
{
|
||||
/** @var Registry $doctrine */
|
||||
$doctrine = $this->container->get('doctrine');
|
||||
|
||||
$manager = $doctrine->getManager();
|
||||
|
||||
if (! $manager instanceof ObjectManager) {
|
||||
throw new \LogicException('Should be instance of '. ObjectManager::class);
|
||||
}
|
||||
|
||||
return $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current User entity instance.
|
||||
*
|
||||
* @return \UserBundle\Entity\User
|
||||
*/
|
||||
protected function getCurrentUser()
|
||||
{
|
||||
/** @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface $storage */
|
||||
$storage = $this->container->get('security.token_storage');
|
||||
|
||||
return $storage->getToken()->getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action Action name.
|
||||
* @param object|object[] $entity A Entity instance or array of entity instances.
|
||||
*
|
||||
* @return string[] Array of restriction reasons.
|
||||
*/
|
||||
protected function checkAccess($action, $entity)
|
||||
{
|
||||
/** @var AccessCheckerInterface $checker */
|
||||
$checker = $this->container->get(ApiBundleServices::ACCESS_CHECKER);
|
||||
|
||||
if ($entity instanceof \Traversable) {
|
||||
$entity = iterator_to_array($entity);
|
||||
} elseif (is_object($entity)) {
|
||||
$entity = [ $entity ];
|
||||
}
|
||||
|
||||
if (! is_array($entity)) {
|
||||
throw new \InvalidArgumentException('Expects single object or array of objects.');
|
||||
}
|
||||
|
||||
$grantChecker = \nspl\f\partial([ $checker, 'isGranted' ], $action);
|
||||
|
||||
return \nspl\a\flatten(\nspl\a\map($grantChecker, $entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate proper server response.
|
||||
*
|
||||
* @param mixed $data A data sent to client.
|
||||
* @param integer $code Response http code.
|
||||
* @param array $groups Serialization groups.
|
||||
*
|
||||
* @return \ApiBundle\Response\ViewInterface
|
||||
*/
|
||||
protected function generateResponse(
|
||||
$data = null,
|
||||
$code = null,
|
||||
array $groups = []
|
||||
) {
|
||||
return new View($data, $groups, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate given data.
|
||||
*
|
||||
* @param Request $request A Request instance.
|
||||
* @param mixed $results Any values which have proper pagination listener.
|
||||
* @param integer $defaultLimit Default limit.
|
||||
*
|
||||
* @return \Knp\Component\Pager\Pagination\PaginationInterface
|
||||
*/
|
||||
protected function paginate(Request $request, $results, $defaultLimit = self::DEFAULT_LIMIT)
|
||||
{
|
||||
/** @var PaginatorInterface $paginator */
|
||||
$paginator = $this->get('knp_paginator');
|
||||
|
||||
$page = $request->query->getInt('page', self::DEFAULT_PAGE);
|
||||
$limit = $request->query->getInt('limit', $defaultLimit);
|
||||
|
||||
return $paginator->paginate($results, $page, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards the request to another controller.
|
||||
*
|
||||
* @param string $controller The controller name (a string like BlogBundle:Post:index).
|
||||
* @param array $path An array of path parameters.
|
||||
* @param array $query An array of query parameters.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response A Response instance
|
||||
*/
|
||||
protected function forward($controller, array $path = [], array $query = [])
|
||||
{
|
||||
$request = $this->container->get('request_stack')->getCurrentRequest();
|
||||
$path['_forwarded'] = $request->attributes;
|
||||
$path['_controller'] = $controller;
|
||||
$subRequest = $request->duplicate($query, null, $path);
|
||||
|
||||
return $this->container
|
||||
->get('http_kernel')
|
||||
->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace ApiBundle\Controller;
|
||||
|
||||
use ApiBundle\Entity\ManageableEntityInterface;
|
||||
use ApiBundle\Form\EntitiesBatchType;
|
||||
use ApiBundle\Security\Inspector\InspectorInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Class AbstractApiController
|
||||
* @package AppBundle\Controller
|
||||
*/
|
||||
class AbstractCRUDController extends AbstractApiController
|
||||
{
|
||||
|
||||
/**
|
||||
* Entity fqcn.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container A ContainerInterface instance.
|
||||
* @param string $entity Managed entity fqcn.
|
||||
*/
|
||||
public function __construct(
|
||||
ContainerInterface $container,
|
||||
$entity
|
||||
) {
|
||||
parent::__construct($container);
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new entity.
|
||||
*
|
||||
* @param Request $request A Request instance.
|
||||
* @param ManageableEntityInterface $entity A ManageableEntityInterface
|
||||
* instance.
|
||||
*
|
||||
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
|
||||
*/
|
||||
protected function createEntity(Request $request, ManageableEntityInterface $entity)
|
||||
{
|
||||
$form = $this->createForm($entity->getCreateFormClass(), $entity);
|
||||
|
||||
// Submit data into form.
|
||||
$form->submit($request->request->all());
|
||||
if ($form->isValid()) {
|
||||
// Check that current user can create this entity.
|
||||
// If user don't have rights to create this entity we should send all
|
||||
// founded restrictions to client.
|
||||
$reasons = $this->checkAccess(InspectorInterface::CREATE, $entity);
|
||||
if (count($reasons) > 0) {
|
||||
// User don't have rights to create this entity so send all
|
||||
// founded restriction reasons to client.
|
||||
return $this->generateResponse($reasons, 403);
|
||||
}
|
||||
|
||||
$em = $this->getManager();
|
||||
$em->persist($entity);
|
||||
$em->flush();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
// Client send invalid data.
|
||||
return $this->generateResponse($form, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about single entity.
|
||||
*
|
||||
* @param integer|ManageableEntityInterface|null $id A entity id.
|
||||
*
|
||||
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
|
||||
*/
|
||||
protected function getEntity($id)
|
||||
{
|
||||
$foundedEntity = $id;
|
||||
if (is_numeric($id)) {
|
||||
$repository = $this->getManager()->getRepository($this->entity);
|
||||
|
||||
$foundedEntity = $repository->find($id);
|
||||
}
|
||||
|
||||
if ($foundedEntity === null) {
|
||||
$name = \app\c\getShortName($this->entity);
|
||||
// Remove 'Abstract' prefix if it exists.
|
||||
if (strpos($name, 'Abstract') !== false) {
|
||||
$name = substr($name, 8);
|
||||
}
|
||||
|
||||
return $this->generateResponse("Can't find {$name} with id {$id}.", 404);
|
||||
}
|
||||
|
||||
// Check that current user can read this entity.
|
||||
// If user don't have rights to read this entity we should send all
|
||||
// founded restrictions to client.
|
||||
$reasons = $this->checkAccess(InspectorInterface::READ, $foundedEntity);
|
||||
if (count($reasons) > 0) {
|
||||
return $this->generateResponse($reasons, 403);
|
||||
}
|
||||
|
||||
return $foundedEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity.
|
||||
*
|
||||
* @param Request $request A Request instance.
|
||||
* @param integer|ManageableEntityInterface|null $entity A entity id.
|
||||
*
|
||||
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
|
||||
*/
|
||||
protected function putEntity(Request $request, $entity)
|
||||
{
|
||||
$em = $this->getManager();
|
||||
|
||||
$foundedEntity = $entity;
|
||||
if (is_numeric($entity)) {
|
||||
$repository = $em->getRepository($this->entity);
|
||||
/** @var \ApiBundle\Entity\ManageableEntityInterface $entity */
|
||||
$foundedEntity = $repository->find($entity);
|
||||
}
|
||||
|
||||
if ($foundedEntity === null) {
|
||||
$name = \app\c\getShortName($this->entity);
|
||||
// Remove 'Abstract' prefix if it exists.
|
||||
if (strpos($name, 'Abstract') !== false) {
|
||||
$name = substr($name, 8);
|
||||
}
|
||||
|
||||
return $this->generateResponse("Can't find {$name} with id {$entity}.", 404);
|
||||
}
|
||||
|
||||
$form = $this->createForm($foundedEntity->getUpdateFormClass(), $foundedEntity, [
|
||||
'method' => 'PUT',
|
||||
]);
|
||||
$form->submit($request->request->all());
|
||||
if ($form->isValid()) {
|
||||
// Check that current user can update this entity.
|
||||
// If user don't have rights to update this entity we should send all
|
||||
// founded restrictions to client.
|
||||
$reasons = $this->checkAccess(InspectorInterface::UPDATE, $foundedEntity);
|
||||
if (count($reasons) > 0) {
|
||||
return $this->generateResponse($reasons, 403);
|
||||
}
|
||||
|
||||
$em->persist($foundedEntity);
|
||||
$em->flush();
|
||||
|
||||
return $foundedEntity;
|
||||
}
|
||||
|
||||
return $this->generateResponse($form, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entity.
|
||||
*
|
||||
* @param integer|ManageableEntityInterface|null $entity A entity id.
|
||||
*
|
||||
* @return \ApiBundle\Response\ViewInterface
|
||||
*/
|
||||
protected function deleteEntity($entity)
|
||||
{
|
||||
$em = $this->getManager();
|
||||
|
||||
$foundedEntity = $entity;
|
||||
if (is_numeric($entity)) {
|
||||
$repository = $em->getRepository($this->entity);
|
||||
/** @var \ApiBundle\Entity\ManageableEntityInterface $entity */
|
||||
$foundedEntity = $repository->find($entity);
|
||||
}
|
||||
|
||||
if ($foundedEntity === null) {
|
||||
$name = \app\c\getShortName($this->entity);
|
||||
// Remove 'Abstract' prefix if it exists.
|
||||
if (strpos($name, 'Abstract') !== false) {
|
||||
$name = substr($name, 8);
|
||||
}
|
||||
|
||||
return $this->generateResponse("Can't find {$name} with id {$entity}.", 404);
|
||||
}
|
||||
// Check that current user can delete this entity.
|
||||
// If user don't have rights to delete this entity we should send all
|
||||
// founded restrictions to client.
|
||||
$reasons = $this->checkAccess(InspectorInterface::DELETE, $foundedEntity);
|
||||
if (count($reasons) > 0) {
|
||||
return $this->generateResponse($reasons, 403);
|
||||
}
|
||||
|
||||
$em->remove($foundedEntity);
|
||||
$em->flush();
|
||||
|
||||
return $this->generateResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request A Request instance.
|
||||
* @param string|callable $permission A requested permission.
|
||||
* @param string $formClass Form class fqcn.
|
||||
* @param callable $processor Function which process founded entities.
|
||||
*
|
||||
* @return \ApiBundle\Response\ViewInterface
|
||||
*/
|
||||
protected function batchProcessing(
|
||||
Request $request,
|
||||
$permission,
|
||||
$formClass,
|
||||
callable $processor
|
||||
) {
|
||||
$this->checkFormClass($formClass);
|
||||
|
||||
$form = $this->createForm($formClass, null, [ 'class' => $this->entity ]);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$data = $form->getData();
|
||||
|
||||
if (is_callable($permission)) {
|
||||
$permission = call_user_func_array($permission, $data);
|
||||
}
|
||||
|
||||
if (! is_string($permission)) {
|
||||
throw new \InvalidArgumentException('$permission should be string or callable');
|
||||
}
|
||||
|
||||
$reasons = $this->checkAccess($permission, $data['entities']);
|
||||
if (count($reasons) > 0) {
|
||||
return $this->generateResponse($reasons, 403);
|
||||
}
|
||||
|
||||
$response = call_user_func_array($processor, $data);
|
||||
if ($response === null) {
|
||||
$response = $this->generateResponse();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->generateResponse($form, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $formClass Form class fqcn.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function checkFormClass($formClass)
|
||||
{
|
||||
if (! is_string($formClass) || ! class_exists($formClass)) {
|
||||
throw new \InvalidArgumentException('$formClass should be fqcn');
|
||||
}
|
||||
|
||||
if (($formClass !== EntitiesBatchType::class)
|
||||
&& ! in_array(EntitiesBatchType::class, class_parents($formClass), true)) {
|
||||
throw new \InvalidArgumentException('Invalid form class '. $formClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace ApiBundle\Controller\Annotation;
|
||||
|
||||
use Common\Annotation\AbstractAppAnnotation;
|
||||
|
||||
/**
|
||||
* Class Roles
|
||||
* @package ApiBundle\Controller\Annotation
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class Roles extends AbstractAppAnnotation
|
||||
{
|
||||
|
||||
/**
|
||||
* Expected role or array of roles.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $roles;
|
||||
|
||||
/**
|
||||
* Return name of default property.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDefault()
|
||||
{
|
||||
return 'roles';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize annotation parameters.
|
||||
* Called after all parameters set in constrictor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function normalize()
|
||||
{
|
||||
$this->roles = (array) $this->roles;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user