first impl for api

This commit is contained in:
Julien Fastré 2021-05-05 20:33:34 +02:00
parent 19fdf2a503
commit f02e33fda7
16 changed files with 849 additions and 87 deletions

View File

@ -0,0 +1,117 @@
<?php
namespace Chill\MainBundle\CRUD\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Chill\MainBundle\Pagination\PaginatorFactory;
class AbstractCRUDController extends AbstractController
{
/**
* The crud configuration
*
* This configuration si defined by `chill_main['crud']` or `chill_main['apis']`
*
* @var array
*/
protected array $crudConfig = [];
/**
* get the instance of the entity with the given id
*
* @param string $id
* @return object
*/
protected function getEntity($action, $id, Request $request): ?object
{
return $this->getDoctrine()
->getRepository($this->getEntityClass())
->find($id);
}
/**
*
* @return string the complete fqdn of the class
*/
protected function getEntityClass(): string
{
return $this->crudConfig['class'];
}
/**
*
*/
protected function onPostFetchEntity(string $action, Request $request, $entity, $_format): ?Response
{
return null;
}
/**
*
*/
protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response
{
return null;
}
/**
* check the acl. Called by every action.
*
* By default, check the role given by `getRoleFor` for the value given in
* $entity.
*
* Throw an \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException
* if not accessible.
*
* @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException
*/
protected function checkACL(string $action, Request $request, $entity, $_format)
{
$this->denyAccessUnlessGranted($this->getRoleFor($action, $request, $entity, $_format), $entity);
}
protected function getActionConfig(string $action)
{
return $this->crudConfig['actions'][$action];
}
/**
* Set the crud configuration
*
* Used by the container to inject configuration for this crud.
*/
public function setCrudConfig(array $config): void
{
dump($config);
$this->crudConfig = $config;
}
/**
* @return PaginatorFactory
*/
protected function getPaginatorFactory(): PaginatorFactory
{
return $this->container->get(PaginatorFactory::class);
}
/**
* Defined the services necessary for this controller
*
* @return array
*/
public static function getSubscribedServices(): array
{
return \array_merge(
parent::getSubscribedServices(),
[
PaginatorFactory::class => PaginatorFactory::class,
]
);
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace Chill\MainBundle\CRUD\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ApiController extends AbstractCRUDController
{
/**
* The view action.
*
* Some steps may be overriden during this process of rendering:
*
* This method:
*
* 1. fetch the entity, using `getEntity`
* 2. launch `onPostFetchEntity`. If postfetch is an instance of Response,
* this response is returned.
* 2. throw an HttpNotFoundException if entity is null
* 3. check ACL using `checkACL` ;
* 4. launch `onPostCheckACL`. If the result is an instance of Response,
* this response is returned ;
* 5. Serialize the entity and return the result. The serialization context is given by `getSerializationContext`
*
*/
protected function entityGet(string $action, Request $request, $id, $_format = 'html'): Response
{
$entity = $this->getEntity($action, $id, $request, $_format);
$postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format);
if ($postFetch instanceof Response) {
return $postFetch;
}
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found", $this->getCrudName(), $id));
}
$response = $this->checkACL($action, $request, $entity, $_format);
if ($response instanceof Response) {
return $response;
}
$response = $this->onPostCheckACL($action, $request, $entity, $_format);
if ($response instanceof Response) {
return $response;
}
$response = $this->onBeforeSerialize($action, $request, $entity, $_format);
if ($response instanceof Response) {
return $response;
}
if ($_format === 'json') {
$context = $this->getContextForSerialization($action, $request, $entity, $_format);
return $this->json($entity, Response::HTTP_OK, [], $context);
} else {
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented");
}
}
public function onBeforeSerialize(string $action, Request $request, $entity, string $_format): ?Response
{
return null;
}
/**
* Base method for handling api action
*
* @return void
*/
public function entityApi(Request $request, $id, $_format): Response
{
switch ($request->getMethod()) {
case Request::METHOD_GET:
case REQUEST::METHOD_HEAD:
return $this->entityGet('_entity', $request, $id, $_format);
default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented");
}
}
protected function getContextForSerialization(string $action, Request $request, $entity, $_format): array
{
return [];
}
/**
* get the role given from the config.
*/
protected function getRoleFor(string $action, Request $request, $entity, $_format): string
{
$actionConfig = $this->getActionConfig($action);
if (NULL !== $actionConfig['roles'][$request->getMethod()]) {
return $actionConfig['roles'][$request->getMethod()];
}
if ($this->crudConfig['role']) {
return $this->crudConfig['role'];
}
throw new \RuntimeException(sprintf("the config does not have any role for the ".
"method %s nor a global role for the whole action. Add those to your ".
"configuration or override the required method", $request->getMethod()));
}
protected function getSerializer(): SerializerInterface
{
return $this->get(SerializerInterface::class);
}
/**
* Defined the services necessary for this controller
*
* @return array
*/
public static function getSubscribedServices(): array
{
return \array_merge(
parent::getSubscribedServices(),
[
SerializerInterface::class => SerializerInterface::class,
]
);
}
}

View File

@ -34,6 +34,7 @@ use Chill\MainBundle\CRUD\Form\CRUDDeleteEntityForm;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Class CRUDController
@ -484,7 +485,7 @@ class CRUDController extends AbstractController
* @param mixed $id
* @return Response
*/
protected function viewAction(string $action, Request $request, $id)
protected function viewAction(string $action, Request $request, $id, $_format = 'html'): Response
{
$entity = $this->getEntity($action, $id, $request);
@ -496,7 +497,7 @@ class CRUDController extends AbstractController
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found"), $this->getCrudName(), $id);
. "is not found", $this->getCrudName(), $id));
}
$response = $this->checkACL($action, $entity);
@ -509,17 +510,36 @@ class CRUDController extends AbstractController
return $response;
}
$defaultTemplateParameters = [
'entity' => $entity,
'crud_name' => $this->getCrudName()
];
if ($_format === 'html') {
$defaultTemplateParameters = [
'entity' => $entity,
'crud_name' => $this->getCrudName()
];
return $this->render(
$this->getTemplateFor($action, $entity, $request),
$this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
return $this->render(
$this->getTemplateFor($action, $entity, $request),
$this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
);
} elseif ($_format === 'json') {
$context = $this->getContextForSerialization($action, $request, $entity, $_format);
return $this->json($entity, Response::HTTP_OK, [], $context);
} else {
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented");
}
}
/**
* Get the context for the serialization
*/
public function getContextForSerialization(string $action, Request $request, $entity, string $_format): array
{
return [];
}
/**
* The edit action.
*
@ -799,7 +819,7 @@ class CRUDController extends AbstractController
*/
protected function getRoleFor($action)
{
if (NULL !== ($this->getActionConfig($action)['role'])) {
if (\array_key_exists('role', $this->getActionConfig($action))) {
return $this->getActionConfig($action)['role'];
}
@ -1181,6 +1201,7 @@ class CRUDController extends AbstractController
AuthorizationHelper::class => AuthorizationHelper::class,
EventDispatcherInterface::class => EventDispatcherInterface::class,
Resolver::class => Resolver::class,
SerializerInterface::class => SerializerInterface::class,
]
);
}

View File

@ -23,6 +23,9 @@ namespace Chill\MainBundle\CRUD\Routing;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\CRUD\Controller\CRUDController;
/**
* Class CRUDRoutesLoader
@ -32,24 +35,34 @@ use Symfony\Component\Routing\RouteCollection;
*/
class CRUDRoutesLoader extends Loader
{
/**
* @var array
*/
protected $config = [];
protected array $crudConfig = [];
protected array $apiCrudConfig = [];
/**
* @var bool
*/
private $isLoaded = false;
private const ALL_SINGLE_METHODS = [
Request::METHOD_GET,
Request::METHOD_POST,
Request::METHOD_PUT,
Request::METHOD_DELETE
];
private const ALL_INDEX_METHODS = [ Request::METHOD_GET, Request::METHOD_HEAD ];
/**
* CRUDRoutesLoader constructor.
*
* @param $config
* @param $crudConfig the config from cruds
* @param $apicrudConfig the config from api_crud
*/
public function __construct($config)
public function __construct(array $crudConfig, array $apiConfig)
{
$this->config = $config;
$this->crudConfig = $crudConfig;
$this->apiConfig = $apiConfig;
}
/**
@ -63,51 +76,132 @@ class CRUDRoutesLoader extends Loader
}
/**
* @return RouteCollection
* Load routes for CRUD and CRUD Api
*/
public function load($resource, $type = null)
public function load($resource, $type = null): RouteCollection
{
if (true === $this->isLoaded) {
throw new \RuntimeException('Do not add the "CRUD" loader twice');
}
$collection = new RouteCollection();
foreach ($this->config as $config) {
$collection->addCollection($this->loadConfig($config));
foreach ($this->crudConfig as $crudConfig) {
$collection->addCollection($this->loadCrudConfig($crudConfig));
}
foreach ($this->apiConfig as $crudConfig) {
$collection->addCollection($this->loadApiSingle($crudConfig));
//$collection->addCollection($this->loadApiMulti($crudConfig));
}
return $collection;
}
/**
* @param $config
* Load routes for CRUD (without api)
*
* @param $crudConfig
* @return RouteCollection
*/
protected function loadConfig($config): RouteCollection
protected function loadCrudConfig($crudConfig): RouteCollection
{
$collection = new RouteCollection();
foreach ($config['actions'] as $name => $action) {
$controller = $crudConfig['controller'] === CrudController::class ?
'cscrud_'.$crudConfig['name'].'_controller' : $crudConfig['controller'];
foreach ($crudConfig['actions'] as $name => $action) {
// defaults (controller name)
$defaults = [
'_controller' => 'cscrud_'.$config['name'].'_controller'.':'.($action['controller_action'] ?? $name)
'_controller' => $controller.':'.($action['controller_action'] ?? $name)
];
if ($name === 'index') {
$path = "{_locale}".$config['base_path'];
$path = "{_locale}".$crudConfig['base_path'];
$route = new Route($path, $defaults);
} elseif ($name === 'new') {
$path = "{_locale}".$config['base_path'].'/'.$name;
$path = "{_locale}".$crudConfig['base_path'].'/'.$name;
$route = new Route($path, $defaults);
} else {
$path = "{_locale}".$config['base_path'].($action['path'] ?? '/{id}/'.$name);
$path = "{_locale}".$crudConfig['base_path'].($action['path'] ?? '/{id}/'.$name);
$requirements = $action['requirements'] ?? [
'{id}' => '\d+'
];
$route = new Route($path, $defaults, $requirements);
}
$collection->add('chill_crud_'.$config['name'].'_'.$name, $route);
$collection->add('chill_crud_'.$crudConfig['name'].'_'.$name, $route);
}
return $collection;
}
/**
* Load routes for api single
*
* @param $crudConfig
* @return RouteCollection
*/
protected function loadApiSingle(array $crudConfig): RouteCollection
{
$collection = new RouteCollection();
$controller = $crudConfig['controller'] === ApiController::class ?
'cscrud_'.$crudConfig['name'].'_controller' : $crudConfig['controller'];
foreach ($crudConfig['actions'] as $name => $action) {
// filter only on single actions
$singleCollection = $action['single-collection'] ?? $name === '_entity' ? 'single' : NULL;
if ('collection' === $singleCollection) {
continue;
}
$defaults = [
'_controller' => $controller.':'.($action['controller_action'] ?? '_entity' === $name ? 'entityApi' : $name.'Api')
];
// path are rewritten
// if name === 'default', we rewrite it to nothing :-)
$localName = '_entity' === $name ? '' : $name;
$localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}';
$path = $crudConfig['base_path'].$localPath;
$requirements = $action['requirements'] ?? [ '{id}' => '\d+' ];
$methods = \array_keys(\array_filter($action['methods'], function($value, $key) { return $value; },
ARRAY_FILTER_USE_BOTH));
$route = new Route($path, $defaults, $requirements);
$route->setMethods($methods);
$collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route);
}
return $collection;
}
/**
* Load routes for api multi
*
* @param $crudConfig
* @return RouteCollection
*/
protected function loadApiMultiConfig(array $crudConfig): RouteCollection
{
$collection = new RouteCollection();
foreach ($crudConfig['actions_multi'] as $name => $action) {
// we compute the data from configuration to a local form
$defaults = [
'_controller' => 'cscrud_'.$crudConfig['name'].'_controller'.':'.($action['controller_action'] ?? $name.'Api')
];
// path are rewritten
// if name === 'index', we rewrite it to nothing :-)
$localName = 'index' === $name ? '' : $name;
$localPath = $action['path'] ?? '.{_format}';
$path = $crudConfig['base_path'].$localPath.$name;
$requirements = $action['requirements'] ?? [ '{id}' => '\d+' ];
$methods = $name === 'default' ? self::ALL_MULTI_METHODS: [];
$route = new Route($path, $defaults, $requirements);
$collection->add('chill_api_multi'.$crudConfig['name'].'_'.$name, $route);
}
return $collection;

View File

@ -14,6 +14,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompile
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\GroupingCenterCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\CRUDControllerCompilerPass;
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
@ -33,5 +34,6 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new ACLFlagsCompilerPass());
$container->addCompilerPass(new GroupingCenterCompilerPass());
$container->addCompilerPass(new RenderEntityCompilerPass());
$container->addCompilerPass(new CRUDControllerCompilerPass());
}
}

View File

@ -133,7 +133,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services/search.yaml');
$loader->load('services/serializer.yaml');
$this->configureCruds($container, $config['cruds'], $loader);
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
}
/**
@ -214,47 +214,91 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
* @param array $config the config under 'cruds' key
* @return null
*/
protected function configureCruds(ContainerBuilder $container, $config, Loader\YamlFileLoader $loader)
protected function configureCruds(ContainerBuilder $container, $crudConfig, $apiConfig, Loader\YamlFileLoader $loader)
{
if (count($config) === 0) {
if ((count($crudConfig) + count($apiConfig)) === 0) {
return;
}
// dump(array_keys($container->getDefinitions()));
$loader->load('services/crud.yaml');
$container->setParameter('chill_main_crud_route_loader_config', $config);
$container->setParameter('chill_main_crud_route_loader_config', $crudConfig);
$container->setParameter('chill_main_api_route_loader_config', $apiConfig);
return;
/*
$definition = new Definition();
$definition
->setClass(\Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader::class)
->addArgument('%chill_main_crud_route_loader_config%')
->addArgument('%chill_main_api_route_loader_config%')
;
$container->setDefinition('chill_main_crud_route_loader', $definition);
*/
$alreadyExistingNames = [];
foreach ($config as $crudEntry) {
$controller = $crudEntry['controller'];
$controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller';
$name = $crudEntry['name'];
foreach ($crudConfig as $crudEntry) {
$this->configureCrudController($container, $crudEntry, 'crud');
}
// check for existing crud names
if (\in_array($name, $alreadyExistingNames)) {
throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name));
}
if (!$container->has($controllerServiceName)) {
$controllerDefinition = new Definition($controller);
$controllerDefinition->addTag('controller.service_arguments');
$controllerDefinition->setAutoconfigured(true);
$controllerDefinition->setClass($crudEntry['controller']);
$container->setDefinition($controllerServiceName, $controllerDefinition);
}
$container->setParameter('chill_main_crud_config_'.$name, $crudEntry);
$container->getDefinition($controllerServiceName)
->addMethodCall('setCrudConfig', ['%chill_main_crud_config_'.$name.'%']);
foreach ($apiConfig as $crudEntry) {
$this->configureCrudController($container, $crudEntry, 'crud');
}
}
/**
* Add a controller for each definition, and add a methodCall to inject crud configuration to controller
*/
private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void
{
$controller = $crudEntry['controller'];
$controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller';
$name = $crudEntry['name'];
// check for existing crud names
/*if (\in_array($name, $alreadyExistingNames)) {
throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name));
}*/
if (!$container->has($controllerServiceName)) {
$controllerDefinition = new Definition($controller);
$controllerDefinition->addTag('controller.service_arguments');
$controllerDefinition->setAutoconfigured(true);
$controllerDefinition->setClass($crudEntry['controller']);
$container->setDefinition($controllerServiceName, $controllerDefinition);
}
$container->setParameter('chill_main_'.$apiOrCrud.'_config_'.$name, $crudEntry);
$container->getDefinition($controllerServiceName)
->addMethodCall('setCrudConfig', ['%chill_main_'.$apiOrCrud.'_config_'.$name.'%']);
/*
dump($controllerClass);
if ($container->hasDefinition($controllerClass)) {
dump('container has controller class');
$controllerServiceName = $controllerClass;
$controller = $container->getDefinition($controllerServiceName);
$param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name'];
$container->setParameter($param, $crudEntry);
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
dump(__LINE__, $controller);
$controller->setDefinition($controllerServiceName, $controller);
} else {
$controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller';
$controller = new Definition($controllerClass);
$controller->addTag('controller.service_arguments');
$controller->setAutoconfigured(true);
$controller->setClass($controllerClass);
$param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name'];
$container->setParameter($param, $crudEntry);
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
dump(__LINE__, $controller);
$container->setDefinition($controllerServiceName, $controller);
}*/
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\MainBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use Chill\MainBundle\Routing\MenuComposer;
use Symfony\Component\DependencyInjection\Definition;
/**
*
*
*/
class CRUDControllerCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$crudConfig = $container->getParameter('chill_main_crud_route_loader_config');
$apiConfig = $container->getParameter('chill_main_api_route_loader_config');
foreach ($crudConfig as $crudEntry) {
$this->configureCrudController($container, $crudEntry, 'crud');
}
foreach ($apiConfig as $crudEntry) {
$this->configureCrudController($container, $crudEntry, 'crud');
}
}
/**
* Add a controller for each definition, and add a methodCall to inject crud configuration to controller
*/
private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void
{
$controller = $crudEntry['controller'];
$controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller';
$name = $crudEntry['name'];
// check for existing crud names
/*if (\in_array($name, $alreadyExistingNames)) {
throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name));
}
if (!$container->has($controllerServiceName)) {
$controllerDefinition = new Definition($controller);
$controllerDefinition->addTag('controller.service_arguments');
$controllerDefinition->setAutoconfigured(true);
$controllerDefinition->setClass($crudEntry['controller']);
$container->setDefinition($controllerServiceName, $controllerDefinition);
}
$container->setParameter('chill_main_'.$apiOrCrud.'_config_'.$name, $crudEntry);
$container->getDefinition($controllerServiceName)
->addMethodCall('setCrudConfig', ['%chill_main_'.$apiOrCrud.'_config_'.$name.'%']);
/*
dump($controllerClass);
*/
$controllerClass = $crudEntry['controller'];
dump('in_array', $controllerClass, \in_array($controllerClass, \array_keys($container->getDefinitions())));
if ($container->hasDefinition($controllerClass)) {
dump('container has controller class');
$controllerServiceName = $controllerClass;
$controller = $container->getDefinition($controllerServiceName);
$param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name'];
$container->setParameter($param, $crudEntry);
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
dump(__LINE__, $controller);
$controller->setDefinition($controllerServiceName, $controller);
} else {
$controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller';
$controller = new Definition($controllerClass);
$controller->addTag('controller.service_arguments');
$controller->setAutoconfigured(true);
$controller->setClass($controllerClass);
$param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name'];
$container->setParameter($param, $crudEntry);
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
dump(__LINE__, $controller);
$container->setDefinition($controllerServiceName, $controller);
}
}
}

View File

@ -8,6 +8,7 @@ use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\Request;
/**
@ -140,7 +141,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('controller_action')
->defaultNull()
->info('the method name to call in the route. Will be set to the action name if left empty.')
->example("'action'")
->example("action")
->end()
->scalarNode('path')
->defaultNull()
@ -168,6 +169,72 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->arrayNode('apis')
->defaultValue([])
->arrayPrototype()
->children()
->scalarNode('class')->cannotBeEmpty()->isRequired()->end()
->scalarNode('controller')
->cannotBeEmpty()
->defaultValue(\Chill\MainBundle\CRUD\Controller\ApiController::class)
->end()
->scalarNode('name')->cannotBeEmpty()->isRequired()->end()
->scalarNode('base_path')->cannotBeEmpty()->isRequired()->end()
->scalarNode('base_role')->defaultNull()->end()
->arrayNode('actions')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('controller_action')
->defaultNull()
->info('the method name to call in the route. Will be set to the concatenation of action name + \'Api\' if left empty.')
->example("showApi")
->end()
->scalarNode('path')
->defaultNull()
->info('the path that will be **appended** after the base path. Do not forget to add '
. 'arguments for the method. By default, will set to the action name, including an `{id}` '
. 'parameter. A suffix of action name will be appended, except if the action name is "entity".')
->example('/{id}/my-action')
->end()
->arrayNode('requirements')
->ignoreExtraKeys(false)
->info('the requirements for the route. Will be set to `[ \'id\' => \'\d+\' ]` if left empty.')
->end()
->enumNode('single-collection')
->values(['single', 'collection'])
->info('indicates if the returned object is a single element or a collection')
->end()
->arrayNode('methods')
->addDefaultsIfNotSet()
->info('the allowed methods')
->children()
->booleanNode(Request::METHOD_GET)->defaultTrue()->end()
->booleanNode(Request::METHOD_HEAD)->defaultTrue()->end()
->booleanNode(Request::METHOD_POST)->defaultFalse()->end()
->booleanNode(Request::METHOD_DELETE)->defaultFalse()->end()
->booleanNode(Request::METHOD_PUT)->defaultFalse()->end()
->end()
->end()
->arrayNode('roles')
->info("The role require for each http method")
->children()
->scalarNode(Request::METHOD_GET)->defaultNull()->end()
->scalarNode(Request::METHOD_HEAD)->defaultNull()->end()
->scalarNode(Request::METHOD_POST)->defaultNull()->end()
->scalarNode(Request::METHOD_DELETE)->defaultNull()->end()
->scalarNode(Request::METHOD_PUT)->defaultNull()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end() // end of root/children
->end() // end of root
;

View File

@ -1,7 +1,8 @@
services:
Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader:
arguments:
$config: '%chill_main_crud_route_loader_config%'
$crudConfig: '%chill_main_crud_route_loader_config%'
$apiConfig: '%chill_main_api_route_loader_config%'
tags: [ routing.loader ]
Chill\MainBundle\CRUD\Resolver\Resolver:

View File

@ -0,0 +1,48 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
class AccompanyingCourseApiController extends ApiController
{
public function participationApi($accompanyingPeriodId, Request $request)
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = $this->getEntity($accompanyingPeriodId);
$person = $this->serializer->deserialize($request->getContent(), Person::class, $_format, []);
if (NULL === $person) {
throw new BadRequestException('person id not found');
}
// TODO add acl
switch ($request->getMethod()) {
case Request::METHOD_POST:
$participation = $accompanyingCours->addPerson($person);
break;
case Request::METHOD_DELETE:
$participation = $accompanyingCours->removePerson($person);
break;
default:
throw new BadRequestException("This method is not supported");
}
$errors = $this->validator->validate($accompanyingCourse);
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($participation);
}
}

View File

@ -28,6 +28,7 @@ use Chill\MainBundle\DependencyInjection\MissingBundleException;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\PersonBundle\Doctrine\DQL\AddressPart;
use Symfony\Component\HttpFoundation\Request;
/**
* Class ChillPersonExtension
@ -76,6 +77,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/templating.yaml');
$loader->load('services/alt_names.yaml');
$loader->load('services/serializer.yaml');
$loader->load('services/security.yaml');
// load service advanced search only if configure
if ($config['search']['search_by_phone'] != 'never') {
@ -307,6 +309,34 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'template' => '@ChillPerson/MaritalStatus/edit.html.twig',
]
]
],
],
'apis' => [
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod::class,
'name' => 'accompanying_course',
'base_path' => '/api/1.0/accompanying_course',
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
'actions' => [
'_entity' => [
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'/participation' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
]
]
]
]
]);

View File

@ -344,50 +344,68 @@ class AccompanyingPeriod
}
/**
* This private function scan Participations Collection,
* searching for a given Person
* Get the participation containing a person
*/
private function participationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation
public function getParticipationsContainsPerson(Person $person): Collection
{
foreach ($this->participations as $participation) {
/** @var AccompanyingPeriodParticipation $participation */
if ($person === $participation->getPerson()) {
return $participation;
}}
return null;
return $this->getParticipations()->filter(
function(AccompanyingPeriodParticipation $participation) use ($person) {
if ($person === $participation->getPerson()) {
return $participation;
}
});
}
/**
* This public function is the same but return only true or false
* Get the opened participation containing a person
*
* "Open" means that the closed date is NULL
*/
public function getOpenParticipationsContainsPerson(Person $person): ?AccompanyingPeriodParticipation
{
$collection = $this->getParticipationsContainsPerson()->filter(
function(AccompanyingPeriodParticipation $participation) use ($person) {
if (NULL === $participation->getClosingDate()) {
return $participation;
}
});
return $collection->count() > 0 ? $collection->first() : NULL;
}
/**
* Return true if the accompanying period contains a person.
*
* **Note**: this participation can be opened or not.
*/
public function containsPerson(Person $person): bool
{
return ($this->participationsContainsPerson($person) === null) ? false : true;
return $this->participationsContainsPerson($person)->count() > 0;
}
/**
* Add Person
*/
public function addPerson(Person $person = null): self
public function addPerson(Person $person = null): AccompanyingPeriodParticipation
{
$participation = new AccompanyingPeriodParticipation($this, $person);
$this->participations[] = $participation;
return $this;
return $participation;
}
/**
* Remove Person
*/
public function removePerson(Person $person): void
public function removePerson(Person $person): ?AccompanyingPeriodParticipation
{
$participation = $this->participationsContainsPerson($person);
$participation = $this->getOpenParticipationContainsPerson($person);
if (! null === $participation) {
$participation->setEndDate(new \DateTimeImmutable('now'));
$this->participations->removeElement($participation);
}
return $participation;
}

View File

@ -0,0 +1,73 @@
<?php
namespace Chill\PersonBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Entity\Center;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Role\Role;
class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
protected AuthorizationHelper $helper;
public const SEE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE';
/**
* @param AuthorizationHelper $helper
*/
public function __construct(AuthorizationHelper $helper)
{
$this->helper = $helper;
}
protected function supports($attribute, $subject)
{
return $subject instanceof AccompanyingPeriod;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
if (!$token->getUser() instanceof User) {
return false;
}
// TODO take scopes into account
foreach ($subject->getPersons() as $person) {
// give access as soon as on center is reachable
if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) {
return true;
}
return false;
}
}
private function getAttributes()
{
return [
self::SEE
];
}
public function getRoles()
{
return $this->getAttributes();
}
public function getRolesWithoutScope()
{
return [];
}
public function getRolesWithHierarchy()
{
return [ 'Person' => $this->getRoles() ];
}
}

View File

@ -28,14 +28,6 @@ services:
tags:
- { name: chill.timeline, context: 'person' }
chill.person.security.authorization.person:
class: Chill\PersonBundle\Security\Authorization\PersonVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
chill.person.birthdate_validation:
class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator
arguments:

View File

@ -0,0 +1,16 @@
services:
chill.person.security.authorization.person:
class: Chill\PersonBundle\Security\Authorization\PersonVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter:
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }

View File

@ -190,6 +190,7 @@ CHILL_PERSON_CREATE: Ajouter des personnes
CHILL_PERSON_STATS: Statistiques sur les personnes
CHILL_PERSON_LISTS: Liste des personnes
CHILL_PERSON_DUPLICATE: Gérer les doublons de personnes
CHILL_PERSON_ACCOMPANYING_PERIOD_SEE: Voir les périodes d'accompagnement
#period
Period closed!: Période clôturée!