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 %}
+ {{ c|trans }} |
+ {% endfor %}
+ |
+
+
+
+ {% for entity in entities %}
+
+ {% for c in columns %}
+ {{ entity|chill_crud_display(c) }} |
+ {% endfor %}
+
+
+
+ {% for action in actions %}
+ - {{ action }}
+ {% endfor %}
+
+ |
+ {% endfor %}
+
+
+
+ {% endif %}
+
+
+{% endblock content %}
\ No newline at end of file