diff --git a/CRUD/Controller/CRUDController.php b/CRUD/Controller/CRUDController.php new file mode 100644 index 000000000..9153833e8 --- /dev/null +++ b/CRUD/Controller/CRUDController.php @@ -0,0 +1,128 @@ + + * + * 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 . + */ + +namespace Chill\MainBundle\CRUD\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Pagination\PaginatorFactory; + +/** + * + * + */ +abstract class CRUDController extends Controller +{ + /** + * + * @var PaginatorFactory + */ + protected $paginatorFactory; + + abstract protected function getEntity(): string; + + abstract protected function orderingOptions(): array; + + protected function getDefaultOrdering(): array + { + return $this->orderingOptions(); + } + + protected function getTemplate($action): string + { + switch($action) { + case 'index': + return '@ChillMain\CRUD\index.html.twig'; + default: + throw new LogicException("action not supported: $action"); + } + } + + protected function getTemplateParameters($action): array + { + return []; + } + + protected function processTemplateParameters($action): array + { + $configured = $this->getTemplateParameters($action); + + switch($action) { + case 'index': + $default = [ + 'columns' => $this->getDoctrine()->getManager() + ->getClassMetadata($this->getEntity()) + ->getIdentifierFieldNames(), + 'actions' => ['edit', 'delete'] + ]; + break; + default: + throw new \LogicException("this action is not supported: $action"); + } + + $result = \array_merge($default, $configured); + + // add constants + $result['class'] = $this->getEntity(); + + return $result; + } + + protected function orderQuery(QueryBuilder $query, Request $request): QueryBuilder + { + $defaultOrdering = $this->getDefaultOrdering(); + + foreach ($defaultOrdering as $sort => $order) { + $query->addOrderBy('e.'.$sort, $order); + } + + return $query; + } + + public function index(Request $request) + { + $totalItems = $this->getDoctrine()->getManager() + ->createQuery("SELECT COUNT(e) FROM ".$this->getEntity()." e") + ->getSingleScalarResult() + ; + + $query = $this->getDoctrine()->getManager() + ->createQueryBuilder() + ->select('e') + ->from($this->getEntity(), 'e'); + + $this->orderQuery($query, $request); + + $paginator = $this->paginatorFactory->create($totalItems); + + $query->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber()) + ->setMaxResults($paginator->getItemsPerPage()) + ; + + $entities = $query->getQuery()->getResult(); + + return $this->render($this->getTemplate('index'), \array_merge([ + 'entities' => $entities, + ], $this->processTemplateParameters('index')) + ); + } +} diff --git a/CRUD/Resolver/Resolver.php b/CRUD/Resolver/Resolver.php new file mode 100644 index 000000000..0eac94d01 --- /dev/null +++ b/CRUD/Resolver/Resolver.php @@ -0,0 +1,110 @@ + + * + * 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 . + */ + +namespace Chill\MainBundle\CRUD\Resolver; + +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; + +/** + * + * + */ +class Resolver +{ + /** + * + * @var EntityManagerInterface + */ + protected $em; + + /** + * + * @var \Symfony\Component\PropertyAccess\PropertyAccessor + */ + protected $propertyAccess; + + function __construct(EntityManagerInterface $em) + { + $this->em = $em; + + $this->buildPropertyAccess(); + } + + + private function buildPropertyAccess() + { + $this->propertyAccess = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + } + + /** + * Return the data at given path. + * + * Path are given to + * + * @param object $entity + * @param string $path + */ + public function getData($entity, $path) + { + return $this->propertyAccess->getValue($entity, $path); + } + + public function getTwigTemplate($entity, $path): string + { + list($focusEntity, $subPath) = $this->getFocusedEntity($entity, $path); + + $classMetadata = $this->em->getClassMetadata(\get_class($focusEntity)); + $type = $classMetadata->getTypeOfField($subPath); + dump($type); + switch ($type) { + + default: + return '@ChillMain/CRUD/_inc/default.html.twig'; + } + } + + /** + * Get the object on which the path apply + * + * This methods recursively parse the path and entity and return the entity + * which will deliver the info, and the last path. + * + * @param object $entity + * @param string $path + * @return array [$focusedEntity, $lastPath] + */ + private function getFocusedEntity($entity, $path) + { + if (\strpos($path, '.') === FALSE) { + return [$entity, $path]; + } + + $exploded = \explode('.', $path); + + $subEntity = $this->propertyAccess + ->getValue($entity, $exploded[0]); + + return $this->getFocusedEntity($subEntity, + \implode('.', \array_slice($exploded, 1))); + } +} diff --git a/CRUD/Routing/CRUDRoutesLoader.php b/CRUD/Routing/CRUDRoutesLoader.php new file mode 100644 index 000000000..669401ce9 --- /dev/null +++ b/CRUD/Routing/CRUDRoutesLoader.php @@ -0,0 +1,87 @@ + + * + * 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 . + */ + +namespace Chill\MainBundle\CRUD\Routing; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + + +/** + * + * + */ +class CRUDRoutesLoader +{ + protected $config = []; + + public function __construct($config) + { + $this->config = $config; + } + + protected function addDummyConfig() + { + $this->config[] = [ + 'name' => 'country', + 'actions' => ['index'],//, 'new', 'edit', 'delete'], + 'base_path' => '/admin/country', + 'controller' => \Chill\MainBundle\Controller\AdminCountryCRUDController::class + ]; + } + + + public function load() + { + $collection = new RouteCollection(); + + foreach ($this->config as $config) { + $collection->addCollection($this->loadConfig($config)); + } + + return $collection; + } + + protected function loadConfig($config): RouteCollection + { + $collection = new RouteCollection(); + + foreach ($config['actions'] as $action) { + $defaults = [ + '_controller' => $config['controller'].'::'.$action + ]; + + if ($action === 'index') { + $path = "{_locale}".$config['base_path']; + $route = new Route($path, $defaults); + } else { + $path = "{_locale}".$config['base_path'].'/{id}/'.$action; + $requirements = [ + '{id}' => '\d+' + ]; + $route = new Route($path, $defaults, $requirements); + } + + $collection->add('chill_crud_'.$config['name'].'_'.$action, $route); + } + + return $collection; + } +} diff --git a/CRUD/Templating/TwigCRUDResolver.php b/CRUD/Templating/TwigCRUDResolver.php new file mode 100644 index 000000000..ca398814c --- /dev/null +++ b/CRUD/Templating/TwigCRUDResolver.php @@ -0,0 +1,61 @@ + + * + * 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 . + */ + +namespace Chill\MainBundle\CRUD\Templating; + +use Chill\MainBundle\CRUD\Resolver\Resolver; +use Twig\TwigFilter; +use Twig\Extension\AbstractExtension; +use Twig\Environment; + +/** + * Twig filters to display data in crud template + * + */ +class TwigCRUDResolver extends AbstractExtension +{ + /** + * + * @var Resolver + */ + protected $resolver; + + function __construct(Resolver $resolver) + { + $this->resolver = $resolver; + } + + public function getFilters() + { + return [ + new TwigFilter('chill_crud_display', [$this, 'display'], + ['needs_environment' => true, 'is_safe' => ['html']]) + ]; + } + + public function display(Environment $env, $entity, $path): string + { + $data = $this->resolver->getData($entity, $path); + $template = $this->resolver->getTwigTemplate($entity, $path); + + return $env->render($template, ['data' => $data, 'entity' => $entity, ]); + } + +} diff --git a/Controller/AdminCountryCRUDController.php b/Controller/AdminCountryCRUDController.php new file mode 100644 index 000000000..00ce5c9d8 --- /dev/null +++ b/Controller/AdminCountryCRUDController.php @@ -0,0 +1,43 @@ +paginatorFactory = $paginator; + } + + protected function getEntity(): string + { + return Country::class; + } + + protected function orderingOptions(): array + { + return [ + 'countryCode' => 'ASC' + ]; + } + + protected function getTemplateParameters($action): array + { + switch ($action) { + case 'index': + return [ + 'columns' => [ 'countryCode', 'name' ], + 'title' => 'Liste des pays' + ]; + } + } +} diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php index 7b4b0243a..6e7c22581 100644 --- a/DependencyInjection/ChillMainExtension.php +++ b/DependencyInjection/ChillMainExtension.php @@ -32,6 +32,8 @@ use Chill\MainBundle\Doctrine\DQL\JsonAggregate; use Chill\MainBundle\Doctrine\DQL\JsonbExistsInArray; use Chill\MainBundle\Doctrine\DQL\Similarity; use Chill\MainBundle\Doctrine\DQL\OverlapsI; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; /** * This class load config for chillMainExtension. @@ -96,6 +98,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, array('homepage' => $config['widgets']['homepage']): array() ); + + $this->configureCruds($container, $config['cruds']); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); @@ -117,6 +121,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $loader->load('services/phonenumber.yml'); $loader->load('services/cache.yml'); $loader->load('services/templating.yml'); + $loader->load('services/crud.yml'); } public function getConfiguration(array $config, ContainerBuilder $container) @@ -194,4 +199,27 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, 'channels' => array('chill') )); } + + /** + * + * @param ContainerBuilder $container + * @param array $config the config under 'cruds' key + * @return null + */ + protected function configureCruds(ContainerBuilder $container, $config) + { + if (count($config) === 0) { + return; + } + + $container->setParameter('chill_main_crud_route_loader_config', $config); + + $definition = new Definition(); + $definition + ->setClass(\Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader::class) + ->addArgument('%chill_main_crud_route_loader_config%') + ; + + $container->setDefinition('chill_main_crud_route_loader', $definition); + } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 7909bbd01..c901fca40 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -116,6 +116,23 @@ class Configuration implements ConfigurationInterface ->append($this->addWidgetsConfiguration('homepage', $this->containerBuilder)) ->end() // end of widgets/children ->end() // end of widgets + ->arrayNode('cruds') + ->defaultValue([]) + ->arrayPrototype() + ->children() + ->scalarNode('class')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('controller')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('name')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('base_path')->cannotBeEmpty()->isRequired()->end() + ->arrayNode('actions') + ->scalarPrototype()->end() + //->defaultValue(['index', 'new', 'edit', 'show', 'delete']) + //->ifEmpty()->thenInvalid() + ->end() + ->end() + ->end() + + ->end() ->end() // end of root/children ->end() // end of root ; diff --git a/Resources/config/crud/country.yml b/Resources/config/crud/country.yml new file mode 100644 index 000000000..751529b12 --- /dev/null +++ b/Resources/config/crud/country.yml @@ -0,0 +1,4 @@ +name: 'country' +actions: ['index', 'new', 'edit', 'delete'] +base_path: '/admin/country' +controller: 'Chill\MainBundle\CRUD\Controller\CRUDController' \ No newline at end of file diff --git a/Resources/config/services/crud.yml b/Resources/config/services/crud.yml new file mode 100644 index 000000000..ab686d34e --- /dev/null +++ b/Resources/config/services/crud.yml @@ -0,0 +1,15 @@ +services: +# Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader: +# +# tags: +# - routing.loader + + Chill\MainBundle\CRUD\Resolver\Resolver: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + + Chill\MainBundle\CRUD\Templating\TwigCRUDResolver: + arguments: + $resolver: '@Chill\MainBundle\CRUD\Resolver\Resolver' + tags: + - { name: twig.extension } \ No newline at end of file diff --git a/Resources/views/CRUD/_inc/default.html.twig b/Resources/views/CRUD/_inc/default.html.twig new file mode 100644 index 000000000..0e9d6dd4c --- /dev/null +++ b/Resources/views/CRUD/_inc/default.html.twig @@ -0,0 +1 @@ +{{ data }} \ No newline at end of file diff --git a/Resources/views/CRUD/index.html.twig b/Resources/views/CRUD/index.html.twig new file mode 100644 index 000000000..83c8683c4 --- /dev/null +++ b/Resources/views/CRUD/index.html.twig @@ -0,0 +1,39 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block content %} +

{{ title|default('List of %class%')|trans({'%class%': class}) }}

+ + {% if entities|length == 0 %} +

{{ no_existing_entities_sentences|default('No entities')|trans }}

+ {% else %} + + + + {% for c in columns %} + + {% endfor %} + + + + + {% for entity in entities %} + + {% for c in columns %} + + {% endfor %} + + + {% endfor %} + +
{{ c|trans }} 
{{ entity|chill_crud_display(c) }} +
    + {% for action in actions %} +
  • {{ action }}
  • + {% endfor %} +
+
+ + {% endif %} + + +{% endblock content %} \ No newline at end of file