at the end of the day, it was inevitable

This commit is contained in:
Mo Elzubeir
2022-12-09 08:36:26 -06:00
commit 1218570914
1768 changed files with 887087 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace ApiBundle;
use ApiBundle\DependencyInjection\Compiler\InspectorsCollectCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Class ApiBundle
* @package ApiBundle
*/
class ApiBundle extends Bundle
{
/**
* Builds the bundle.
*
* It is only ever called once when the cache is empty.
*
* @param ContainerBuilder $container A ContainerBuilder instance.
*
* @return void
*/
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new InspectorsCollectCompilerPass());
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace ApiBundle;
/**
* Class ApiBundleServices
* @package ApiBundle
*/
class ApiBundleServices
{
/**
* Check access to entity for current user.
*
* Must implements {@see \ApiBundle\Security\AccessChecker\AccessCheckerInterface}
* interface.
*/
const ACCESS_CHECKER = 'api.access_checker';
}
@@ -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;
}
}
@@ -0,0 +1,67 @@
<?php
namespace ApiBundle\DependencyInjection\Compiler;
use ApiBundle\Security\Inspector\Factory\LazyInspectorFactory;
use ApiBundle\Security\Inspector\InspectorInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Class InspectorsCollectCompilerPass
* Register inspectors into LazyInspectorFactory.
* All inspectors must be tagged by 'socialhose.inspector'.
*
* @package ApiBundle\DependencyInjection\Compiler
*/
class InspectorsCollectCompilerPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* @param ContainerBuilder $container A ContainerBuilder instance.
*
* @return void
*/
public function process(ContainerBuilder $container)
{
if (! $container->hasDefinition('api.inspector_factory')) {
// Work only if we have definition of inspector factory.
return;
}
$factory = $container->getDefinition('api.inspector_factory');
if ($factory->getClass() !== LazyInspectorFactory::class) {
// Works only for lazy inspector factory.
return;
}
// Get all tagged inspectors and create map between supported class and
// inspector service id.
$inspectorsIds = [];
$inspectors = $container->findTaggedServiceIds('socialhose.inspector');
$inspectors = array_keys($inspectors);
foreach ($inspectors as $id) {
/** @var InspectorInterface $class */
$class = $container->getDefinition($id)->getClass();
$reflection = new \ReflectionClass($class);
if (! $reflection->implementsInterface(InspectorInterface::class)) {
// Tagged service not implements inspector interface.
$message = "Inspector {$id} must implements "
. InspectorInterface::class;
throw new \InvalidArgumentException($message);
}
$supported = (array) $class::supportedClass();
foreach ($supported as $item) {
$inspectorsIds[$item] = $id;
}
}
// Inject founded inspectors into factory.
$factory->replaceArgument(1, $inspectorsIds);
}
}
@@ -0,0 +1,29 @@
<?php
namespace ApiBundle\Entity;
use AppBundle\Entity\EntityInterface;
/**
* Interface ManageableEntityInterface
* Interface for entities which can be managed by api methods.
*
* @package ApiBundle\Entity
*/
interface ManageableEntityInterface extends EntityInterface
{
/**
* Return fqcn of form used for creating this entity.
*
* @return string
*/
public function getCreateFormClass();
/**
* Return fqcn of form used for updating this entity.
*
* @return string
*/
public function getUpdateFormClass();
}
@@ -0,0 +1,25 @@
<?php
namespace ApiBundle\Entity;
/**
* Interface NormalizableEntityInterface
* @package ApiBundle\Entity
*/
interface NormalizableEntityInterface
{
/**
* Return metadata for current entity.
*
* @return \ApiBundle\Serializer\Metadata\Metadata
*/
public function getMetadata();
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups();
}
@@ -0,0 +1,76 @@
<?php
namespace ApiBundle\EventListener;
use Common\Annotation\AppAnnotationInterface;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Util\ClassUtils;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* Class AnnotationFetchListener
* @package ApiBundle\EventListener
*/
class AnnotationFetchListener
{
/**
* @var Reader
*/
private $reader;
/**
* AnnotationFetchListener constructor.
*
* @param Reader $reader A Reader instance.
*/
public function __construct(Reader $reader)
{
$this->reader = $reader;
}
/**
* @param FilterControllerEvent $event A FilterControllerEvent instance.
*
* @return void
*/
public function handle(FilterControllerEvent $event)
{
$controller = $event->getController();
$className = ClassUtils::getClass($controller[0]);
$class = new \ReflectionClass($className);
$method = $class->getMethod($controller[1]);
$classAnnotation = $this
->getAnnotations($this->reader->getClassAnnotations($class));
$methodAnnotation = $this
->getAnnotations($this->reader->getMethodAnnotations($method));
$annotations = array_merge($classAnnotation, $methodAnnotation);
$request = $event->getRequest();
foreach ($annotations as $key => $annotation) {
$request->attributes->set($key, $annotation);
}
}
/**
* Get annotations from array of fetched annotations.
*
* @param array $annotations Array of fetched annotations.
*
* @return array
*/
protected function getAnnotations(array $annotations)
{
$cwAnnotation = [];
foreach ($annotations as $annotation) {
if ($annotation instanceof AppAnnotationInterface) {
$key = '_'. strtolower(\app\c\getShortName($annotation));
$cwAnnotation[$key] = $annotation;
}
}
return $cwAnnotation;
}
}
@@ -0,0 +1,235 @@
<?php
namespace ApiBundle\EventListener;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Response\ViewInterface;
use AppBundle\HttpFoundation\AppResponse;
use Knp\Component\Pager\Pagination\AbstractPagination;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class ApiSubscriber
* @package ApiBundle\EventListener
*/
class ApiSubscriber implements EventSubscriberInterface
{
/**
* @var NormalizerInterface
*/
private $normalizer;
/**
* @var LoggerInterface
*/
private $logger;
/**
* ApiSubscriber constructor.
*
* @param NormalizerInterface $normalizer A NormalizerInterface instance.
* @param LoggerInterface $logger A LoggerInterface instance.
*/
public function __construct(
NormalizerInterface $normalizer,
LoggerInterface $logger
) {
$this->normalizer = $normalizer;
$this->logger = $logger;
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The array keys are event names and the value can be:
*
* * The method name to call (priority defaults to 0)
* * An array composed of the method name to call and the priority
* * An array of arrays composed of the method names to call and
* respective
* priorities, or 0 if unset
*
* For instance:
*
* * array('eventName' => 'methodName')
* * array('eventName' => array('methodName', $priority))
* * array('eventName' => array(array('methodName1', $priority),
* array('methodName2')))
*
* @return array The event names to listen to
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => [ 'onRequest', 100 ],
KernelEvents::VIEW => 'onView',
KernelEvents::EXCEPTION => 'onException',
];
}
/**
* Process application/json request.
* Fetch json data, parse and store them as request parameters.
*
* @param GetResponseEvent $event A GetResponseEvent instance.
*
* @return void
*
* @throws HttpException Then receive invalid json.
*/
public function onRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$uri = $request->getUri();
$isApiMethod = (strpos($uri, '/api') !== false)
|| (strpos($uri, '/security/') !== false);
$content = trim($request->getContent());
if ($isApiMethod && (strlen($content) > 0)) {
// Make transformation only for api methods.
$content = json_decode($content, true);
if (isset($content['_format'])) {
unset($content['_format']);
}
// Check json parse error.
$code = json_last_error();
if ($code !== JSON_ERROR_NONE) {
$event->setResponse(AppResponse::badRequest(
'Invalid json ('. $code .'): '. json_last_error_msg()
));
return;
}
$request->request->replace($content);
}
}
/**
* @param GetResponseForControllerResultEvent $event A
* GetResponseForControllerResultEvent
* instance.
*
* @return void
*/
public function onView(GetResponseForControllerResultEvent $event)
{
// Works only if current response returned by one of api controllers.
$uri = $event->getRequest()->getUri();
$isApiEndpoint = (strpos($uri, '/api') !== false) || (strpos($uri, '/security') !== false);
if ($isApiEndpoint) {
$result = $event->getControllerResult();
if ($result instanceof ViewInterface) {
$event->setResponse($result->serialize($this->normalizer));
} else {
$result = $this->normalize($result);
$event->setResponse(AppResponse::create($result));
}
}
}
/**
* @param GetResponseForExceptionEvent $event A
* GetResponseForExceptionEvent
* instance.
*
* @return void
*/
public function onException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
$request = $event->getRequest();
$uri = $request->getUri();
if (strpos($uri, '/api') !== false) {
if ($exception instanceof HttpException) {
//
// Handle throwException which have status code.
// We just get error message and status code, form it in proper
// structure and send to client.
//
// But for 'no route found' message we create 405 response instead
// of 404.
//
if ($exception->getPrevious()
&& ($exception->getPrevious() instanceof ResourceNotFoundException)) {
$response = AppResponse::create('Unknown method.', 405);
} else {
$response = AppResponse::create()
->setStatusCode($exception->getStatusCode())
->setData($exception->getMessage());
}
$event->setResponse($response);
} else {
//
// If throwException occurred in one of api methods, we log it and send
// some message to client.
//
$response = AppResponse::create(null, 500);
$message = $exception->getMessage() . ' in '
. $exception->getFile() . ' at ' . $exception->getLine()
. ' occurred while processing '
. $request->attributes->get('_controller') . ':'
. $request->attributes->get('_action');
$response->setData($message);
// To log message we also add serialized request.
$this->logger->error($message, [
'trace' => $exception->getTrace(),
]);
$event->setResponse($response);
}
}
}
/**
* Normalize response data.
*
* @param mixed $data Response data.
*
* @return array
*/
private function normalize($data)
{
$groups = [];
switch (true) {
case is_array($data):
return array_map([ $this, 'normalize' ], $data);
case $data instanceof AbstractPagination:
$entity = $data->current();
if ($entity instanceof NormalizableEntityInterface) {
$groups = $entity->defaultGroups();
}
return $this->normalizer->normalize($data, null, $groups);
case is_object($data):
if ($data instanceof NormalizableEntityInterface) {
$groups = $data->defaultGroups();
}
return $this->normalizer->normalize($data, null, $groups);
}
return $data;
}
}
@@ -0,0 +1,69 @@
<?php
namespace ApiBundle\EventListener;
use ApiBundle\Controller\Annotation\Roles;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Security\Core\Role\RoleInterface;
/**
* Class RolesListener
* @package ApiBundle\EventListener
*/
class RolesListener
{
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* @var RoleHierarchyInterface
*/
private $hierarchy;
/**
* RolesListener constructor.
*
* @param TokenStorageInterface $storage A TokenStorageInterface instance.
* @param RoleHierarchyInterface $hierarchy A RoleHierarchyInterface instance.
*/
public function __construct(
TokenStorageInterface $storage,
RoleHierarchyInterface $hierarchy
) {
$this->storage = $storage;
$this->hierarchy = $hierarchy;
}
/**
* @param FilterControllerEvent $event A FilterControllerEvent instance.
*
* @return void
*/
public function handle(FilterControllerEvent $event)
{
$request = $event->getRequest();
$roles = $request->attributes->get('_roles');
if (! $roles instanceof Roles) {
return;
}
$token = $this->storage->getToken();
$expected = (array) $roles->roles;
$actual = array_map(function (RoleInterface $role) {
return $role->getRole();
}, $this->hierarchy->getReachableRoles($token->getRoles()));
if (count(array_diff($expected, $actual)) > 0) {
$message = 'You don\'t have enough roles to call this method. Required '
. implode(', ', $expected) .'.';
throw new AccessDeniedHttpException($message);
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace ApiBundle\Form;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class ActivatedEntitiesBatchType
*
* @package ApiBundle\Form
*/
class ActivatedEntitiesBatchType extends EntitiesBatchType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('active', CheckboxType::class);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace ApiBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class EntitiesBatchType
*
* Base form type for batch processing form types.
*
* @package ApiBundle\Form
*/
class EntitiesBatchType extends AbstractType implements DataMapperInterface
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ids', EntityType::class, [
'class' => $options['class'],
'multiple' => true,
])
->setDataMapper($this);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('class');
}
/**
* Maps properties of some data to a list of forms.
*
* @param mixed $data Structured data.
* @param FormInterface[]|array $forms A list of {@link FormInterface} instances.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function mapDataToForms($data, $forms)
{
// Do nothing because it's not necessary.
}
/**
* Maps the data of a list of forms into the properties of some data.
*
* @param FormInterface[]|\Traversable $forms A list of {@link FormInterface}
* instances.
* @param mixed $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
$data = [];
$data['entities'] = $forms['ids']->getData();
unset($forms['ids']);
foreach ($forms as $form) {
$data[$form->getName()] = $form->getData();
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace ApiBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use UserBundle\Repository\NotificationRepository;
/**
* Class NotificationSubscribeBatchType
*
* @package ApiBundle\Form
*/
class NotificationSubscribeBatchType extends EntitiesBatchType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
//
// Use custom query builder.
//
$idsOptions = $builder->get('ids')->getOptions();
$idsOptions['query_builder'] = function (NotificationRepository $repository) {
return $repository->getQueryBuilderForSubscription();
};
$builder->remove('ids')->add('ids', EntityType::class, $idsOptions);
$builder->add('subscribed', CheckboxType::class);
}
}
@@ -0,0 +1,34 @@
<?php
namespace ApiBundle\Form;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class PublishedEntitiesBatchType
*
* @package ApiBundle\Form
*/
class PublishedEntitiesBatchType extends EntitiesBatchType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('published', CheckboxType::class);
}
}
@@ -0,0 +1,67 @@
<?php
namespace ApiBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use UserBundle\Entity\Notification\Notification;
use UserBundle\Entity\User;
use UserBundle\Repository\NotificationRepository;
/**
* Class SubscribeToNotificationsBatchType
*
* @package ApiBundle\Form
*/
class SubscribeToNotificationsBatchType extends EntitiesBatchType
{
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* SubscribeToNotificationsBatchType constructor.
*
* @param TokenStorageInterface $storage A TokenStorageInterface instance.
*/
public function __construct(TokenStorageInterface $storage)
{
$this->storage = $storage;
}
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->remove('ids')->add('ids', EntityType::class, [
'class' => Notification::class,
'multiple' => true,
'query_builder' => function (NotificationRepository $repository) {
$user = \app\op\invokeIf($this->storage->getToken(), 'getUser');
if ($user instanceof User) {
return $repository->getQueryBuilderForForm($user);
}
return $repository->createQueryBuilder('Notification');
},
]);
$builder->add('subscribe', CheckboxType::class);
}
}
@@ -0,0 +1,19 @@
services:
#
# Base api controllers.
#
api.controller.abstract:
class: 'ApiBundle\Controller\AbstractApiController'
arguments:
- '@service_container'
abstract: true
#
# Base CRUD controllers.
#
api.controller.abstract_crud:
class: 'ApiBundle\Controller\AbstractCRUDController'
arguments:
- '@service_container'
- # injects by concrete class.
abstract: true
@@ -0,0 +1,18 @@
services:
#
# Inspectors factory.
#
api.inspector_factory:
class: 'ApiBundle\Security\Inspector\Factory\LazyInspectorFactory'
arguments:
- '@service_container'
- # injected by compiler pass.
#
# Access checker.
#
api.access_checker:
class: 'ApiBundle\Security\AccessChecker\AccessChecker'
arguments:
- '@security.token_storage'
- '@api.inspector_factory'
@@ -0,0 +1,56 @@
services:
#
# Entity normalizer.
#
api.normalizer.entity:
class: 'ApiBundle\Serializer\Normalizer\EntityNormalizer'
tags:
- { name: serializer.normalizer }
#
# Enum normalizer.
#
api.normalizer.enum:
class: 'ApiBundle\Serializer\Normalizer\EnumNormalizer'
tags:
- { name: serializer.normalizer }
#
# ThemeOptionFont normalizer.
#
api.normalizer.theme_option_font:
class: 'ApiBundle\Serializer\Normalizer\ThemeOptionFontNormalizer'
tags:
- { name: serializer.normalizer }
#
# Form normalizer.
#
api.normalizer.form:
class: 'ApiBundle\Serializer\Normalizer\FormNormalizer'
tags:
- { name: serializer.normalizer }
#
# Paginator normalizer.
#
api.normalizer.paginator:
class: 'ApiBundle\Serializer\Normalizer\PaginationNormalizer'
tags:
- { name: serializer.normalizer }
#
# Index document normalizer.
#
api.normalizer.index_document:
class: 'ApiBundle\Serializer\Normalizer\DocumentNormalizer'
tags:
- { name: serializer.normalizer }
#
# Empty object normalizer.
#
api.normalizer.empty_object:
class: 'ApiBundle\Serializer\Normalizer\EmptyObjectNormalizer'
tags:
- { name: serializer.normalizer }
@@ -0,0 +1,62 @@
imports:
- { resource: controllers.yml }
- { resource: inspector.yml }
- { resource: normalizers.yml }
services:
#
# Subscriber for api events.
#
api.listeners.api:
class: 'ApiBundle\EventListener\ApiSubscriber'
arguments:
- '@serializer'
- '@monolog.logger.api_error'
tags:
- { name: kernel.event_subscriber }
#
# App annotation fetch listener.
#
api.listeners.annotation:
class: 'ApiBundle\EventListener\AnnotationFetchListener'
arguments:
- '@annotation_reader'
tags:
-
name: kernel.event_listener
event: kernel.controller
method: handle
#
# Roles annotation checker listener.
#
api.listeners.security:
class: 'ApiBundle\EventListener\RolesListener'
arguments:
- '@security.token_storage'
- '@security.role_hierarchy'
tags:
-
name: kernel.event_listener
event: kernel.controller
method: handle
priority: -10
#
# Formatter for api errors log.
#
api.log_formatter.api_errors:
class: Monolog\Formatter\LineFormatter
arguments:
- "[%%datetime%%] %%message%%\n%%context.headers%%\n%%context.request%%\n\n"
- ~
- true
api.form.subscribe_to_notifications:
class: 'ApiBundle\Form\SubscribeToNotificationsBatchType'
arguments:
- '@security.token_storage'
tags:
- { name: form.type }
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace ApiBundle\Response;
use AppBundle\HttpFoundation\AppResponse;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class View
* Abstract under controller results and api response.
*
* @package ApiBundle\Response
*/
class View implements ViewInterface
{
/**
* @var mixed
*/
private $data;
/**
* @var string[]
*/
private $groups;
/**
* @var integer
*/
private $code;
/**
* View constructor.
*
* @param mixed $data Response data.
* @param array $groups Serialization groups.
* @param integer $code HTTP status code.
*/
public function __construct(
$data,
array $groups = [],
$code = null
) {
if (($data === null) && ($code === null)) {
$code = AppResponse::HTTP_NO_CONTENT;
}
$this->data = $data;
$this->groups = $groups;
$this->code = $code ?: AppResponse::HTTP_OK;
}
/**
* Serialize this response into proper response.
*
* @param NormalizerInterface $normalizer A NormalizerInterface instance.
*
* @return AppResponse
*/
public function serialize(NormalizerInterface $normalizer)
{
if (($this->data === null)
|| (is_array($this->data) && (count($this->data) === 0))) {
// We got empty response, send without serialization.
return AppResponse::create(null, $this->code);
}
if (is_array($this->data) || is_object($this->data)) {
//
// TODO: refactor it. Low priority.
//
if (($this->code >= 400) && ! is_array($this->data)
&& (! $this->data instanceof FormInterface)) {
$this->data = [ $this->data ];
}
return AppResponse::create(
$normalizer->normalize($this->data, null, $this->groups),
$this->code
);
}
// Scalar values we just return.
return AppResponse::create($this->data, $this->code);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace ApiBundle\Response;
use AppBundle\HttpFoundation\AppResponse;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Interface ViewInterface
* @package ApiBundle\Response
*/
interface ViewInterface
{
/**
* Serialize this response into proper response.
*
* @param NormalizerInterface $normalizer A NormalizerInterface instance.
*
* @return AppResponse
*/
public function serialize(NormalizerInterface $normalizer);
}
@@ -0,0 +1,55 @@
<?php
namespace ApiBundle\Security\AccessChecker;
use ApiBundle\Security\Inspector\Factory\InspectorFactoryInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Class AccessChecker
* @package ApiBundle\Security\AccessChecker
*/
class AccessChecker implements AccessCheckerInterface
{
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* @var InspectorFactoryInterface
*/
private $factory;
/**
* AccessChecker constructor.
*
* @param TokenStorageInterface $storage A TokenStorageInterface instance.
* @param InspectorFactoryInterface $factory A InspectorFactoryInterface
* instance.
*/
public function __construct(
TokenStorageInterface $storage,
InspectorFactoryInterface $factory
) {
$this->storage = $storage;
$this->factory = $factory;
}
/**
* Checks that current user can make given action with specified entity.
*
* @param string $action Action name.
* @param object $entity A Entity instance.
*
* @return string[] Array of restriction reasons.
*/
public function isGranted($action, $entity)
{
$inspector = $this->factory->create($entity);
$user = $this->storage->getToken()->getUser();
return $inspector->inspect($user, $entity, $action);
}
}
@@ -0,0 +1,23 @@
<?php
namespace ApiBundle\Security\AccessChecker;
/**
* Interface AccessCheckerInterface
* Check access to entity for current user.
*
* @package ApiBundle\Security\AccessChecker
*/
interface AccessCheckerInterface
{
/**
* Checks that given user can make given action with specified entity.
*
* @param string $action Action name.
* @param object $entity A Entity instance.
*
* @return string[] Array of restriction reasons.
*/
public function isGranted($action, $entity);
}
@@ -0,0 +1,132 @@
<?php
namespace ApiBundle\Security\Inspector;
use UserBundle\Entity\User;
/**
* Class InspectorInterface
* Base class for all inspectors.
*
* @package ApiBundle\Security\Inspector
*/
abstract class AbstractInspector implements InspectorInterface
{
/**
* @var array
*/
protected $reasons = [];
/**
* Checks that given user can make given action with specified entity.
*
* @param User $user A User entity instance.
* @param object $entity A Entity instance or array of instances.
* @param string $action Action name.
*
* @return string[] Array of restriction reasons.
*/
public function inspect(User $user, $entity, $action)
{
$classes = (array) static::supportedClass();
$checker = \nspl\f\partial('\app\op\isInstanceOf', $entity);
if (! \nspl\a\any($classes, $checker)) {
throw new \InvalidArgumentException('Can inspect only '. implode(', ', $classes));
}
// Clear reasons.
$this->reasons = [];
switch ($action) {
case self::CREATE:
$this->canCreate($user, $entity);
break;
case self::READ:
$this->canRead($user, $entity);
break;
case self::UPDATE:
$this->canUpdate($user, $entity);
break;
case self::DELETE:
$this->canDelete($user, $entity);
break;
}
return $this->reasons;
}
/**
* @param string $reason Restriction reason.
*
* @return AbstractInspector
*/
protected function addReason($reason)
{
$this->reasons[] = $reason;
return $this;
}
/**
* Add reason only if condition is true.
*
* @param string $reason Restriction reason.
* @param boolean $condition Some boolean condition.
*
* @return AbstractInspector
*/
protected function addReasonIf($reason, $condition)
{
if ($condition) {
$this->reasons[] = $reason;
}
return $this;
}
/**
* Check that user can create specified entity.
*
* @param User $user A user who try to create entity.
* @param object $entity A Entity instance.
*
* @return void
*/
abstract protected function canCreate(User $user, $entity);
/**
* Check that user can read specified entity.
*
* @param User $user A user who try to read entity.
* @param object $entity A Entity instance.
*
* @return void
*/
abstract protected function canRead(User $user, $entity);
/**
* Check that user can update specified entity.
*
* @param User $user A user who try to update entity.
* @param object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
abstract protected function canUpdate(User $user, $entity);
/**
* Check that user can delete specified entity.
*
* @param User $user A user who try to delete entity.
* @param object $entity A Entity instance.
*
* @return void
*/
abstract protected function canDelete(User $user, $entity);
}
@@ -0,0 +1,24 @@
<?php
namespace ApiBundle\Security\Inspector\Factory;
use ApiBundle\Security\Inspector\InspectorInterface;
/**
* Interface InspectorFactoryInterface
* Create entity inspector instance.
*
* @package ApiBundle\Security\Inspector\Factory
*/
interface InspectorFactoryInterface
{
/**
* Create proper inspector for given entity instance.
*
* @param object|string $class A Entity instance or fqcn.
*
* @return InspectorInterface
*/
public function create($class);
}
@@ -0,0 +1,73 @@
<?php
namespace ApiBundle\Security\Inspector\Factory;
use ApiBundle\Security\Inspector\InspectorInterface;
use Doctrine\Common\Util\ClassUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class LazyInspectorFactory
* Default implementation of InspectorFactoryInterface.
* Use lazy loading for creating inspectors.
*
* @package ApiBundle\Security\Inspector\Factory
*/
class LazyInspectorFactory implements InspectorFactoryInterface
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var array
*/
private $inspectorsIds;
/**
* InspectorFactory constructor.
*
* @param ContainerInterface $container A ContainerInterface instance.
* @param array $inspectorsIds Registered inspectors services
* ids.
*/
public function __construct(ContainerInterface $container, array $inspectorsIds)
{
$this->container = $container;
$this->inspectorsIds = $inspectorsIds;
}
/**
* Create proper inspector for given entity instance.
*
* @param object|string $class A Entity instance or fqcn.
*
* @return InspectorInterface
*/
public function create($class)
{
if (is_object($class)) {
$class = get_class($class);
}
$class = ClassUtils::getRealClass($class);
if (!is_string($class) || ! class_exists($class)) {
throw new \InvalidArgumentException('Expects object or valid fqcn.');
}
if (! array_key_exists($class, $this->inspectorsIds)) {
$message = "Can't find inspector for entity '{$class}'";
throw new \InvalidArgumentException($message);
}
$inspector = $this->container->get($this->inspectorsIds[$class]);
if (! $inspector instanceof InspectorInterface) {
$message = 'Inspector must implements '. InspectorInterface::class;
throw new \RuntimeException($message);
}
return $inspector;
}
}
@@ -0,0 +1,38 @@
<?php
namespace ApiBundle\Security\Inspector;
use UserBundle\Entity\User;
/**
* Interface InspectorInterface
* Inspect entity and decides to give access or not to inspected entity.
*
* @package ApiBundle\Security\Inspector
*/
interface InspectorInterface
{
const CREATE = 'create';
const READ = 'read';
const UPDATE = 'update';
const DELETE = 'delete';
/**
* Checks that given user can make given action with specified entity.
*
* @param User $user A User entity instance.
* @param object $entity A Entity instance or array of instances.
* @param string $action Action name.
*
* @return string[] Array of restriction reasons.
*/
public function inspect(User $user, $entity, $action);
/**
* Return supported entity fqcn.
*
* @return string
*/
public static function supportedClass();
}
@@ -0,0 +1,112 @@
<?php
namespace ApiBundle\Serializer\Metadata;
/**
* Class Metadata
* @package ApiBundle\Serializer\Metadata
*/
class Metadata
{
/**
* @var string
*/
private $fqcn;
/**
* @var PropertyMetadata[]
*/
private $properties;
/**
* Metadata constructor.
*
* @param string $fqcn Entity fqcn.
* @param array $properties Array of PropertyMetadata's.
*/
public function __construct($fqcn, array $properties = [])
{
$this->fqcn = $fqcn;
$this->properties = $properties;
}
/**
* @return string
*/
public function getFqcn()
{
return $this->fqcn;
}
/**
* @param string $interfaceName Full qualified interface name.
*
* @return boolean
*/
public function implementsInterface($interfaceName)
{
return in_array($interfaceName, class_implements($this->fqcn), true);
}
/**
* Get all properties metadata or filter by it specified groups if it's set.
*
* @param array|string|null $groups A serialized group.
*
* @return PropertyMetadata[]
*/
public function getProperties($groups = null)
{
if ($groups === null) {
return $this->properties;
}
$groups = (array) $groups;
return array_filter(
$this->properties,
function (PropertyMetadata $metadata) use ($groups) {
return count(array_intersect($metadata->getGroups(), $groups)) > 0;
}
);
}
/**
* Add to this metadata properties from specified metadata which not exists
* in this.
*
* @param Metadata $metadata A Metadata instance.
*
* @return Metadata
*/
public function admix(Metadata $metadata)
{
$registeredProperties = array_map(function (PropertyMetadata $property) {
return $property->getName();
}, $this->properties);
$filter = function (PropertyMetadata $property) use ($registeredProperties) {
return ! in_array($property->getName(), $registeredProperties, true);
};
$uniqueProperties = array_filter($metadata->getProperties(), $filter);
$this->properties = array_merge($this->properties, $uniqueProperties);
return $this;
}
/**
* @param array $metadataList Array of Metadata instances.
*
* @return Metadata
*/
public function admixList(array $metadataList)
{
/** @var Metadata $metadata */
foreach ($metadataList as $metadata) {
$this->admix($metadata);
}
return $this;
}
}
@@ -0,0 +1,514 @@
<?php
namespace ApiBundle\Serializer\Metadata;
use Doctrine\Common\Collections\Collection;
/**
* Class PropertyMetadata
* @package ApiBundle\Serializer\Metadata
*/
class PropertyMetadata
{
/**
* Integer scalar value.
*/
const TYPE_INTEGER = 'integer';
/**
* Float (double) scalar value.
*/
const TYPE_DOUBLE = 'double';
/**
* String scalar value.
*/
const TYPE_STRING = 'string';
/**
* Boolean scalar value.
*/
const TYPE_BOOLEAN = 'boolean';
/**
* Array value.
* Elements maybe other array or any scalar values.
*/
const TYPE_ARRAY = 'array';
/**
* Array of associated entities.
* Metadata must contains associated entity fqcn in 'actualType' field.
*/
const TYPE_COLLECTION = 'collection';
/**
* Single associated entity.
* Metadata must contains associated entity fqcn in 'actualType' field.
*/
const TYPE_ENTITY = 'entity';
/**
* One of enum instance.
*
* @see \AppBundle\Enum\AbstractEnum
*/
const TYPE_ENUM = 'enum';
/**
* \DateTime instance.
*/
const TYPE_DATE = 'date';
/**
* Some object.
*/
const TYPE_OBJECT = 'object';
/**
* Custom object.
*/
const TYPE_GROUP = 'group';
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $type;
/**
* Actual property type, used for object type.
*
* @var string
*/
private $actualType;
/**
* @var string
*/
private $field;
/**
* @var array
*/
private $groups;
/**
* @var boolean
*/
private $nullable = false;
/**
* @var PropertyMetadata[]
*/
private $subProperties = [];
/**
* PropertyMetadata constructor.
*
* @param string $name Property name.
* @param string $type Property type.
* @param \Closure|callable|string $field Entity field name or function for
* fetching data.
* @param array $groups Serialized group names.
*/
public function __construct($name, $type, $field, array $groups)
{
$this->name = $name;
$this->type = $type;
$this->field = $field;
$this->groups = array_map('trim', $groups);
}
/**
* Create property metadata for integer field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createInteger($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_INTEGER, $name, $groups);
}
/**
* Create property metadata for string field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createString($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_STRING, $name, $groups);
}
/**
* Create property metadata for string field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createDouble($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_DOUBLE, $name, $groups);
}
/**
* Create property metadata for object field.
*
* @param string $name Property name.
* @param string $actualType Entity fqcn.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createEntity($name, $actualType, array $groups)
{
$property = new PropertyMetadata($name, self::TYPE_ENTITY, $name, $groups);
return $property->setActualType($actualType);
}
/**
* Create property metadata for string field.
*
* @param string $name Property name.
* @param string $enumClass Enum class.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createEnum($name, $enumClass, array $groups)
{
$property = new PropertyMetadata($name, self::TYPE_ENUM, $name, $groups);
return $property->setActualType($enumClass);
}
/**
* Create property metadata for array field.
*
* @param string $name Property name.
* @param string $actualType Entity fqcn.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createCollection($name, $actualType, array $groups)
{
$property = new PropertyMetadata($name, self::TYPE_COLLECTION, $name, $groups);
return $property->setActualType($actualType);
}
/**
* Create property metadata for array field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createArray($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_ARRAY, $name, $groups);
}
/**
* Create property metadata for boolean field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createBoolean($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_BOOLEAN, $name, $groups);
}
/**
* Create property metadata for date field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createDate($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_DATE, $name, $groups);
}
/**
* Create property metadata for date field.
*
* @param string $name Property name.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function createObject($name, array $groups)
{
return new PropertyMetadata($name, self::TYPE_OBJECT, $name, $groups);
}
/**
* Create property metadata for date field.
*
* @param string $name Property name.
* @param array $subProperties Sub properties metadata.
* @param array $groups Serialized group names.
*
* @return PropertyMetadata
*/
public static function groupProperties($name, array $subProperties, array $groups)
{
$instance = new PropertyMetadata($name, self::TYPE_GROUP, null, $groups);
return $instance->setSubProperties($subProperties);
}
/**
* Set name.
*
* @param string $name Property name.
*
* @return PropertyMetadata
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set type.
*
* @param string $type Property type.
*
* @return PropertyMetadata
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type.
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Set actual type.
*
* @param string $actualType Actual property type.
*
* @return PropertyMetadata
*/
public function setActualType($actualType)
{
$this->actualType = $actualType;
return $this;
}
/**
* Get actual type.
*
* @return string
*/
public function getActualType()
{
return $this->actualType;
}
/**
* @return boolean
*/
public function isScalar()
{
return ($this->type !== self::TYPE_COLLECTION)
&& ($this->type !== self::TYPE_ENTITY)
&& ($this->type !== self::TYPE_GROUP)
&& ($this->type !== self::TYPE_OBJECT);
}
/**
* Set field.
*
* @param \Closure|callable|string $field Entity field name or function for
* fetching data.
*
* @return PropertyMetadata
*/
public function setField($field)
{
$this->field = $field;
return $this;
}
/**
* Get field.
*
* @return \Closure|callable|string
*/
public function getField()
{
return $this->field;
}
/**
* @param object $object Object instance on with we need getter.
*
* @return \Closure
*/
public function getGetter($object)
{
$getter = $this->field;
if (is_string($getter)) {
//
// We got concrete field name.
// Create getter for it.
//
$getter = function () use ($getter) {
$value = $this->{$getter};
if ($value instanceof Collection) {
// Convert doctrine collection into array.
$value = $value->toArray();
} elseif ($value instanceof \DateTimeInterface) {
// Format date time instances.
$value = $value->format('c');
}
return $value;
};
} elseif ($this->type === self::TYPE_GROUP) {
//
// For object we should iterate other sub properties and get values
// from it.
//
// We should store current sub properties in order to inject them
// into closure.
//
$subProperties = $this->subProperties;
$getter = function () use ($object, $subProperties) {
$results = [];
foreach ($subProperties as $subProperty) {
$getter = $subProperty->getGetter($object);
$results[$subProperty->getName()] = $getter();
}
return $results;
};
}
// Field is function.
return $getter->bindTo($object, $object);
}
/**
* Set groups.
*
* @param array $groups Serialization groups.
*
* @return PropertyMetadata
*/
public function setGroups(array $groups)
{
$this->groups = $groups;
return $this;
}
/**
* Get groups.
*
* @return array
*/
public function getGroups()
{
return $this->groups;
}
/**
* Set nullable.
*
* @param boolean $nullable Can property value be null.
*
* @return PropertyMetadata
*/
public function setNullable($nullable)
{
$this->nullable = $nullable;
return $this;
}
/**
* Get nullable
*
* @return boolean
*/
public function isNullable()
{
return $this->nullable;
}
/**
* Set sub properties
*
* @param array $properties Array of PropertyMetadata instances.
*
* @return PropertyMetadata
*/
public function setSubProperties(array $properties)
{
$this->subProperties = $properties;
return $this;
}
/**
* Get sub properties
*
* @return PropertyMetadata[]
*/
public function getSubProperties()
{
return $this->subProperties;
}
}
@@ -0,0 +1,77 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use CacheBundle\Comment\Manager\CommentManagerInterface;
use IndexBundle\Model\AbstractDocument;
use IndexBundle\Model\ArticleDocumentInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class DocumentNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class DocumentNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof AbstractDocument;
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object|AbstractDocument $object Object to normalize.
* @param string $format Format the normalization
* result will be encoded as.
* @param array $context Context options for the
* normalizer.
*
* @return array
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function normalize($object, $format = null, array $context = array())
{
if ($object instanceof ArticleDocumentInterface) {
$object->addNormalizerListener(function (array $data) use ($format) {
$data['comments'] = [
'data' => $this->normalizer->normalize($data['comments'], $format, [
'id',
'comment',
]),
'count' => count($data['comments']),
'totalCount' => $data['commentsCount'],
'limit' => CommentManagerInterface::NEW_COMMENT_POOL_SIZE,
];
unset($data['commentsCount']);
return \nspl\a\map(function ($value) {
if ($value instanceof \DateTimeInterface) {
$value = $value->format('c');
}
return $value;
}, $data);
});
}
return $object->getNormalizedData();
}
}
@@ -0,0 +1,52 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use IndexBundle\Model\AbstractDocument;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class EmptyObjectNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class EmptyObjectNormalizer implements NormalizerInterface
{
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
if (! $data instanceof \stdClass) {
return false;
}
return count(get_object_vars($data)) === 0;
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object|AbstractDocument $object Object to normalize.
* @param string $format Format the normalization
* result will be encoded as.
* @param array $context Context options for the
* normalizer.
*
* @return object
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function normalize($object, $format = null, array $context = array())
{
return (object) [];
}
}
@@ -0,0 +1,82 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\EntityInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class EntityNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class EntityNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof NormalizableEntityInterface;
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object|NormalizableEntityInterface $object Object to normalize.
* @param string $format Format the normalization
* result will be encoded as.
* @param array $context Context options for the
* normalizer.
*
* @return array
*/
public function normalize($object, $format = null, array $context = array())
{
$metadata = $object->getMetadata()->getProperties($context);
$result = [];
// If we normalize entity we should add 'type' property to it.
if ($object instanceof EntityInterface) {
$result['type'] = $object->getEntityType();
}
foreach ($metadata as $property) {
$getter = $property->getGetter($object);
$value = $getter();
//
// Normalize non scalar value such as:
// - Collection of associated entity.
// - Single associated entity.
// - Array of scalar values. Just in case.
//
if (! $property->isScalar()) {
$value = $this->normalizer->normalize($value, $format, $context);
if ($property->getType() === PropertyMetadata::TYPE_OBJECT) {
$value = (object) $value;
}
} elseif ($property->getType() === PropertyMetadata::TYPE_ENUM) {
$value = (string) $value;
}
$result[$property->getName()] = $value;
}
return $result;
}
}
@@ -0,0 +1,47 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use AppBundle\Enum\AbstractEnum;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class EnumNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class EnumNormalizer implements NormalizerInterface
{
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof AbstractEnum;
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object|AbstractEnum $object Object to normalize.
* @param string $format Format the normalization result will
* be encoded as.
* @param array $context Context options for the normalizer.
*
* @return array
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function normalize($object, $format = null, array $context = [])
{
return $object->getValue();
}
}
@@ -0,0 +1,328 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use AppBundle\Utils\TransKey\TransKeyGeneratorInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Validator\Constraints\AbstractComparison;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\Constraints;
/**
* Class FormNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class FormNormalizer implements NormalizerInterface
{
/**
* Already founded form element keys.
*
* @var string[]
*/
private $transKeysCache = [];
/**
* Map between comparison constraint fqcn and generator config.
*
* Config has next keys:
* * key - it value must start from upper case character, and should by an
* antonym for constraint.
* * valueName - name of value in response with which we got conflict, for
* example: bound value for comparison. May be omitted.
*
* @var string[]
*/
private static $comparisonConfig = [
Constraints\GreaterThanOrEqual::class => [
'key' => 'Lower',
'valueName' => 'than',
],
Constraints\GreaterThan::class => [
'key' => 'LowerOrEqual',
'valueName' => 'than',
],
Constraints\LessThan::class => [
'key' => 'GreaterOrEqual',
'valueName' => 'than',
],
Constraints\LessThanOrEqual::class => [
'key' => 'Greater',
'valueName' => 'than',
],
];
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof FormInterface;
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object|FormInterface $object Object to normalize.
* @param string $format Format the normalization result will
* be encoded as.
* @param array $context Context options for the normalizer.
*
* @return array
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function normalize($object, $format = null, array $context = [])
{
if (! $object->isSubmitted()) {
return [ 'Form not submitted.' ];
}
return array_map(function (FormError $error) {
return $this->explainError($error);
}, iterator_to_array($object->getErrors(true, true)));
}
/**
* Generate form key in camelCase.
*
* @param FormInterface $form A FormInterface instance.
*
* @return string
*/
private function getTransKey(FormInterface $form)
{
$hash = spl_object_hash($form);
if (! isset($this->transKeysCache[$hash])) {
/** @var TransKeyGeneratorInterface $generator */
$generator = $form->getConfig()->getOption('key');
$this->transKeysCache[$hash] = $generator->generate($form);
}
return $this->transKeysCache[$hash];
}
/**
* Explain occurred form error.
*
* @param FormError $error A FormError instance.
*
* @return array
*/
private function explainError(FormError $error)
{
$form = $error->getOrigin();
$cause = $error->getCause();
//
// Prepare current translation key and translation parameters.
//
$transKey = $this->getTransKey($form);
$parameters = [];
//
// Check cause.
//
if ($cause instanceof ConstraintViolation) {
//
// We got constraint violation so we should check that we known about
// constrain which spawned this violation and make proper explanation.
//
$isOrder = is_numeric($form->getName());
if ($isOrder || ($form->getParent() && is_numeric(\app\op\invokeIf($form->getParent(), 'getName')))) {
//
// Entry type of CollectionType had order number instead of name
// and we should store this number in parameters and send to
// client.
//
$order = (int) ($isOrder ? $form->getName() : \app\op\invokeIf($form->getParent(), 'getName'));
$parameters['order'] = $order;
}
$parameters = array_merge($parameters, $this->getParametersForViolation(
$cause,
$form,
$transKey
));
}
return [
'message' => $error->getMessage(),
'transKey' => $transKey,
// This hardcoded by requesting from Frontend Developers.
'type' => 'error',
'parameters' => $parameters,
];
}
/**
* @param ConstraintViolation $violation Founded violation.
* @param FormInterface $form Form on which violation found.
* @param string $transKey Translation key for this form.
*
* @return array
*/
private function getParametersForViolation(
ConstraintViolation $violation,
FormInterface $form,
&$transKey
) {
$parameters = $this->getParametersForViolationByCode($violation, $form, $transKey);
if ($parameters === null) {
$parameters = $this->getParametersForViolationByClass($violation, $transKey);
}
return $parameters !== null ? $parameters : [];
}
/**
* @param ConstraintViolation $violation Founded violation.
* @param FormInterface $form Form on which violation found.
* @param string $transKey Translation key for this form.
*
* @return array|null
*/
private function getParametersForViolationByCode(
ConstraintViolation $violation,
FormInterface $form,
&$transKey
) {
$code = $violation->getCode();
$current = $violation->getInvalidValue();
$parameters = null;
switch (true) {
//
// Firstly we should check possible form constraint errors.
//
case $code === FormConstraint::NO_SUCH_FIELD_ERROR:
$transKey .= 'UnknownField';
$parameters = [
'name' => key($current),
];
break;
case $code === FormConstraint::NOT_SYNCHRONIZED_ERROR:
$transKey .= 'Invalid';
$parameters = [
'current' => $current,
];
if ($this->isChoiceType($form)) {
// We should return available choice values and also should
// return only invalid values.
$choices = $form->getConfig()->getOption('choices');
$parameters['available'] = $choices;
if (is_array($choices) && $form->getConfig()->getOption('multiple')) {
$parameters['invalid'] = array_diff($current, $choices);
}
}
break;
//
// Finally we should check UniqueEntity constraint.
// For some reasons violation with 'Form::NO_SUCH_FIELD_ERROR'
// code has UniqueEntity constraint so we should check check
// 'Form::NO_SUCH_FIELD_ERROR' first to avoid strange error
// messages.
//
case $code === UniqueEntity::NOT_UNIQUE_ERROR:
$transKey .= 'NotUnique';
$parameters = [
'current' => $current,
];
break;
}
return $parameters;
}
/**
* @param ConstraintViolation $violation Founded violation.
* @param string $transKey Translation key for this form.
*
* @return array|null
*/
private function getParametersForViolationByClass(
ConstraintViolation $violation,
&$transKey
) {
$constraint = $violation->getConstraint();
$class = get_class($constraint);
$current = $violation->getInvalidValue();
$parameters = null;
switch (true) {
//
// Length constraint.
//
case $constraint instanceof Constraints\Length:
$transKey .= 'TooShort';
$parameters = [
'min' => $constraint->min,
];
break;
//
// Next we check comparison errors.
// For comparison error we also should check that we hav config
// for it.
//
case ($constraint instanceof AbstractComparison)
&& isset(self::$comparisonConfig[$class]):
$config = self::$comparisonConfig[$class];
$transKey .= $config['key'];
$parameters = [
'current' => $current,
$config['valueName'] => $constraint->value,
];
break;
//
// Required field is blank.
//
case ($constraint instanceof Constraints\NotBlank):
$transKey .= 'Empty';
$parameters = [
'current' => $current,
];
break;
}
return $parameters;
}
/**
* Check that specified form is choice type or extend it.
*
* @param FormInterface $form A FormInterface instance.
*
* @return boolean
*/
private function isChoiceType(FormInterface $form)
{
$type = $form->getConfig()->getType();
$innerType = $type->getInnerType();
return ($type instanceof ChoiceType)
|| ($innerType instanceof ChoiceType)
|| ($innerType->getParent() === ChoiceType::class);
}
}
@@ -0,0 +1,72 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class PaginationNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class PaginationNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
return ($data instanceof SlidingPagination) || ($data instanceof Paginator);
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object $object Object to normalize.
* @param string $format Format the normalization result
* will be encoded as.
* @param array $context Context options for the normalizer.
*
* @return array
*/
public function normalize($object, $format = null, array $context = [])
{
if ($object instanceof Paginator) {
$data = iterator_to_array($object);
$normalizedData = [
'data' => $this->normalizer->normalize($data, $format, $context),
'count' => count($data),
'totalCount' => count($object),
'limit' => $object->getQuery()->getMaxResults(),
];
} elseif ($object instanceof SlidingPagination) {
$normalizedData = [
'data' => $this->normalizer
->normalize(iterator_to_array($object), $format, $context),
'count' => count($object),
'totalCount' => $object->getTotalItemCount(),
'page' => $object->getCurrentPageNumber(),
'limit' => $object->getItemNumberPerPage(),
];
} else {
throw new \InvalidArgumentException('Expect one of paginator.');
}
return $normalizedData;
}
}
@@ -0,0 +1,57 @@
<?php
namespace ApiBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use UserBundle\Entity\Notification\ThemeOption\ThemeOptionFont;
/**
* Class ThemeOptionFontNormalizer
* @package ApiBundle\Serializer\Normalizer
*/
class ThemeOptionFontNormalizer implements
NormalizerInterface,
NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* Checks whether the given class is supportedClass for normalization by this
* normalizer.
*
* @param mixed $data Data to normalize.
* @param string $format The format being (de-)serialized from or into.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof ThemeOptionFont;
}
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object|ThemeOptionFont $object Object to normalize.
* @param string $format Format the normalization result will
* be encoded as.
* @param array $context Context options for the normalizer.
*
* @return array
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function normalize($object, $format = null, array $context = [])
{
return [
'family' => $object->getFamily(),
'size' => $object->getSize(),
'style' => $this->normalizer->normalize($object->getStyle()),
];
}
}