add base of crud controller

This commit is contained in:
Simon Vieille 2021-05-12 10:18:34 +02:00
parent b0990c0adf
commit b556432f3e
18 changed files with 829 additions and 0 deletions

View file

@ -0,0 +1,156 @@
<?php
namespace App\Core\Controller\Admin\Crud;
use App\Core\Controller\Admin\AdminController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Entity\EntityInterface;
use App\Core\Manager\EntityManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use App\Core\Repository\RepositoryQuery;
/**
* class CrudController.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class CrudController extends AdminController
{
abstract protected function getConfiguration(): CrudConfiguration;
protected function doIndex(int $page = 1, RepositoryQuery $query, Request $request, Session $session): Response
{
/*$this->updateFilters($options['request'], $options['session']);*/
$pager = $query
//->useFilters($this->filters)
->paginate($page)
;
/*$viewOptions = array_merge([
'pager' => $pager,
'hasFilters' => !empty($this->filters),
], $options['viewOptions']);*/
$configuration = $this->getConfiguration();
return $this->render($this->getConfiguration()->getView('index'), [
'configuration' => $configuration,
'pager' => $pager,
]);
}
protected function doNew(EntityInterface $entity, EntityManager $entityManager, Request $request): Response
{
$form = $this->createForm($this->forms['new'], $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->create($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute($configuration->getPageRoute('edit'), [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'The form is not valid.');
}
return $this->render($configuration->getView('new'), [
'form' => $form->createView(),
'entity' => $entity,
]);
}
protected function doShow(EntityInterface $entity): Response
{
$configuration = $this->getConfiguration();
return $this->render($configuration->getView('show'), [
'entity' => $entity,
'configuration' => $configuration,
]);
}
protected function doEdit(EntityInterface $entity, EntityManager $entityManager, Request $request): Response
{
$configuration = $this->getConfiguration();
$form = $this->createForm($configuration->getForm('edit'), $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute($configuration->getPageRoute('edit'), [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'The form is not valid.');
}
return $this->render($configuration->getView('edit'), [
'form' => $form->createView(),
'configuration' => $configuration,
'entity' => $entity,
]);
}
protected function doDelete(EntityInterface $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'The data has been removed.');
}
return $this->redirectToRoute($configuration->getPageRoute('index'));
}
protected function doFilter(Session $session): Response
{
$form = $this->createForm($this->forms['filters']);
$form->submit($session->get($this->filterRequestId, []));
return $this->render($this->getView('filters'), [
'form' => $form->createView(),
]);
}
protected function updateFilters(Request $request, Session $session)
{
if ($request->query->has($this->filterRequestId)) {
$filters = $request->query->get($this->filterRequestId);
if ('0' === $filters) {
$filters = [];
}
} elseif ($session->has($this->filterRequestId)) {
$filters = $session->get($this->filterRequestId);
} else {
$filters = [];
}
if (isset($this->forms['filters'])) {
$form = $this->createForm($this->forms['filters']);
$form->submit($filters);
} else {
$form = null;
}
if (empty($filters)) {
$this->filters = $filters;
$session->set($this->filterRequestId, $filters);
} elseif (null !== $form && $form->isValid()) {
$this->filters = $form->getData();
$session->set($this->filterRequestId, $filters);
}
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace App\Core\Crud;
/**
* class CrudConfiguration.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CrudConfiguration
{
protected array $pageTitles = [];
protected array $pageRoutes = [];
protected array $actions = [];
protected array $actionTitles = [];
protected array $forms = [];
protected array $formOptions = [];
protected array $views = [];
protected array $fields = [];
/* -- */
public function setPageTitle(string $page, string $title): self
{
$this->pageTitles[$page] = $title;
return $this;
}
public function getPageTitle(string $page, ?string $default = null): ?string
{
return $this->pageTitles[$page] ?? $default;
}
/* -- */
public function setPageRoute(string $page, string $route): self
{
$this->pageRoutes[$page] = $route;
return $this;
}
public function getPageRoute(string $page): ?string
{
return $this->pageRoutes[$page];
}
/* -- */
public function setForm(string $context, string $form, array $options = []): self
{
$this->forms[$context] = $form;
return $this;
}
public function getForm(string $context): string
{
return $this->forms[$context];
}
public function setFormOptions(string $context, array $options = []): self
{
$this->formOptions[$context] = $options;
return $this;
}
public function getFormOptions(string $context): array
{
return $this->formOptions[$context] ?? [];
}
/* -- */
public function setAction(string $page, string $action, bool $enabled): self
{
if (!isset($this->actions[$page])) {
$this->actions[$page] = [];
}
$this->actions[$page][$action] = $enabled;
return $this;
}
public function getAction(string $page, string $action, bool $default = true)
{
return $this->actions[$page][$action] ?? $default;
}
/* -- */
public function setActionTitle(string $page, string $action, string $title): self
{
if (!isset($this->actionTitles[$page])) {
$this->actionTitles[$page] = [];
}
$this->actions[$page][$action] = $title;
return $this;
}
public function getActionTitle(string $page, string $action, ?string $default = null): ?string
{
return $this->actionTitles[$page][$action] ?? $default;
}
/* -- */
public function setView(string $context, string $view): self
{
$this->views[$context] = $view;
return $this;
}
public function getView(string $context, ?string $default = null)
{
if (null === $default) {
$default = sprintf('@Core/admin/crud/%s.html.twig', $context);
}
return $this->views[$context] ?? $default;
}
/* -- */
public function setField(string $context, string $label, string $field, array $options): self
{
if (!isset($this->fields[$context])) {
$this->fields[$context] = [];
}
$this->fields[$context][$label] = [
'field' => $field,
'options' => $options,
];
return $this;
}
public function getFields(string $context): array
{
return $this->fields[$context] ?? [];
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace App\Core\Crud\Exception;
/**
* class CrudConfigurationException.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CrudConfigurationException extends \Exception
{
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class ButtonField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class ButtonField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/button.html.twig',
'button_attr' => [],
'button_tag' => 'button',
]);
$resolver->setAllowedTypes('button_attr', ['array']);
$resolver->setAllowedTypes('button_tag', ['string']);
return $resolver;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class DateField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class DateField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/date.html.twig',
'format' => 'Y-m-d',
]);
return $resolver;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class DatetimeField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class DatetimeField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/date.html.twig',
'format' => 'Y-m-d H:i:s',
]);
return $resolver;
}
}

64
core/Crud/Field/Field.php Normal file
View file

@ -0,0 +1,64 @@
<?php
namespace App\Core\Crud\Field;
use App\Core\Crud\Exception\CrudConfigurationException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Twig\Environment;
/**
* class Field.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class Field
{
public function buildView(Environment $twig, $entity, array $options)
{
return $twig->render($this->getView($options), [
'value' => $this->getValue($entity, $options),
'options' => $options,
]);
}
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
$resolver->setDefaults([
'property' => null,
'property_builder' => null,
'view' => null,
'attr' => [],
]);
$resolver->setRequired('view');
$resolver->setAllowedTypes('property', ['null', 'string']);
$resolver->setAllowedTypes('view', 'string');
$resolver->setAllowedTypes('attr', 'array');
$resolver->setAllowedTypes('property_builder', ['null', 'callable']);
return $resolver;
}
protected function getValue($entity, array $options)
{
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->getPropertyAccessor()
;
if (null !== $options['property']) {
$value = $propertyAccessor->getValue($entity, $options['property']);
} elseif (null !== $options['property_builder']) {
$value = call_user_func($options['property_builder'], $entity, $options);
} else {
throw new CrudConfigurationException('Unable to get the value. One of "property" and "property_builder" is required.');
}
return $value;
}
protected function getView(array $options)
{
return $options['view'];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class TextField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class TextField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/text.html.twig',
]);
return $resolver;
}
}

View file

@ -0,0 +1,6 @@
<div class="row">
<div class="col-md-12">
{{ form_widget(form) }}
</div>
</div>

View file

@ -0,0 +1 @@
<p>{{ '{__toString}'|build_string(entity) }}</p>

View file

@ -0,0 +1,88 @@
{% extends '@Core/admin/layout.html.twig' %}
{% block title %}{{ configuration.pageTitle('edit')|trans|build_string(entity) }} - {{ parent() }}{% endblock %}
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle('edit')|trans|build_string(entity) }}</h1>
{% endblock %}
{% block header_actions %}
<div class="crud-header-actions">
<div class="btn-group">
{% if configuration.action('edit', 'back', true) %}
<a href="{{ path(configuration.pageRoute('index')) }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
{{ configuration.actionTitle('edit', 'back', 'Back to the list')|trans }}
</a>
{% endif %}
{% if configuration.action('edit', 'show', true) %}
<a href="{{ path(configuration.pageRoute('show'), {entity: entity.id}) }}" class="btn btn-secondary">
<span class="fa fa-eye pr-1"></span>
{{ configuration.actionTitle('edit', 'show', 'Show')|trans|build_string(entity) }}
</a>
{% endif %}
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
{{ configuration.actionTitle('edit', 'save', 'Save')|trans|build_string(entity) }}
</button>
{% block dropdownMenu %}
{% set menu = '' %}
{% if configuration.action('edit', 'delete', true) %}
{% set item %}
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" form="form-delete" class="dropdown-item">
{{ configuration.actionTitle('edit', 'delete', 'Delete')|trans|build_string(entity) }}
</button>
</div>
{% endset %}
{% set menu = menu ~ item %}
{% endif %}
{% if menu %}
<button type="button" class="btn btn-white dropdown-toggle dropdown-toggle-hide-after" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="font-weight-bold">
⋅⋅⋅
</span>
</button>
{{ menu|raw }}
{% endif %}
{% endblock %}
</div>
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block form %}
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
<div class="tab-content">
<div class="tab-pane active">
<div class="tab-form">
{{ include(configuration.view('editForm', '@Core/admin/crud/_form.html.twig')) }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
{% endblock %}
{% if configuration.action('edit', 'delete', true) %}
<form method="post" action="{{ path(configuration.pageRoute('delete'), {entity: entity.id}) }}" id="form-delete" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">
</form>
{% endif %}
{% endblock %}

View file

@ -0,0 +1 @@
<{{ options.button_tag }} {% for k, v in options.button_attr %}{{ k }}="{{ v }}"{% endfor %}>{{ value }}</{{ options.button_tag }}>

View file

@ -0,0 +1 @@
{{ value|date(options.format) }}

View file

@ -0,0 +1 @@
{{ value }}

View file

@ -0,0 +1,112 @@
{% extends '@Core/admin/layout.html.twig' %}
{% block title %}{{ configuration.pageTitle('index')|trans }} - {{ parent() }}{% endblock %}
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.paginationData.pageCount < 2 %}pb-5{% endif %}">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle('index')|trans }}</h1>
{% endblock %}
{% block header_actions %}
<div class="crud-header-actions">
<div class="btn-group">
{% if configuration.action('index', 'new', true) %}
<a href="{{ path(configuration.pageRoute('new')) }}" class="btn btn-primary">
<span class="fa fa-plus pr-1"></span>
{{ configuration.actionTitle('index', 'new', 'New')|trans }}
</a>
{% endif %}
</div>
</div>
{% endblock %}
</div>
{{ knp_pagination_render(pager) }}
</div>
{% endblock %}
{% block list %}
<div class="table-responsive">
<table class="table">
{% block list_header %}
<thead class="thead-light">
<tr>
{% for label, config in configuration.fields('index') %}
{% set attr = config.options.attr is defined ? config.options.attr : [] %}
<th {% for key, value in attr %}{{ key }}="{{ value }}"{% endfor %}>
{{ label|trans }}
</th>
{% endfor %}
<th class="col-2 miw-100 text-right">Actions</th>
</tr>
</thead>
{% endblock %}
{% block list_items %}
<tbody>
{% for item in pager %}
{% block list_item %}
<tr data-dblclick="">
{% for config in configuration.fields('index') %}
{% set attr = config.options.attr is defined ? config.options.attr : [] %}
{% set action = config.options.action is defined ? config.options.action : null %}
<td {% for key, value in attr %}{{ key }}="{{ value }}"{% endfor %}>
{% if action == 'show' %}
<a href="{{ path(configuration.pageRoute('show'), {entity: item.id}) }}">
{{ render_field(item, config) }}
</a>
{% elseif action == 'edit' %}
<a href="{{ path(configuration.pageRoute('edit'), {entity: item.id}) }}">
{{ render_field(item, config) }}
</a>
{% else %}
{{ render_field(item, config) }}
{% endif %}
</td>
{% endfor %}
{% if configuration.action('index', 'edit', true) or configuration.action('index', 'delete', true) %}
<td class="col-2 miw-100 text-right">
{% if configuration.action('index', 'edit', true) %}
<a href="{{ path(configuration.pageRoute('edit'), {entity: item.id}) }}" class="btn btn-sm btn-primary mr-1">
<span class="fa fa-edit"></span>
</a>
{% endif %}
{% if configuration.action('index', 'delete', true) %}
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
<span class="fa fa-trash"></span>
</button>
<form method="post" action="{{ path(configuration.pageRoute('delete'), {entity: item.id}) }}" id="form-delete-{{ item.id }}" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ item.id) }}">
</form>
{% endif %}
</td>
{% endif %}
</tr>
{% endblock %}
{% else %}
<tr>
<td class="col-12 text-center p-4 text-black-50">
<div class="display-1">
<span class="fa fa-search"></span>
</div>
<div class="display-5 mt-3">
Aucun résultat
</div>
</td>
</tr>
{% endfor %}
</tbody>
{% endblock %}
</table>
</div>
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends '@Core/admin/layout.html.twig' %}
{% block title %}{{ configuration.pageTitle('new')|trans|build_string(entity) }} - {{ parent() }}{% endblock %}
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle('new')|trans|build_string(entity) }}</h1>
{% endblock %}
{% block header_actions %}
<div class="crud-header-actions">
<div class="btn-group">
{% if configuration.action('new', 'back', true) %}
<a href="{{ path(configuration.pageRoute('index')) }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
{{ configuration.actionTitle('new', 'back', 'Back to the list')|trans }}
</a>
{% endif %}
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
{{ configuration.actionTitle('new', 'save', 'Save')|trans|build_string(entity) }}
</button>
</div>
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block form %}
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
<div class="tab-content">
<div class="tab-pane active">
<div class="tab-form">
{{ include(configuration.view('newForm', '@Core/admin/crud/_form.html.twig')) }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,44 @@
{% extends '@Core/admin/layout.html.twig' %}
{% block title %}{{ configuration.pageTitle('show')|trans|build_string(entity) }} - {{ parent() }}{% endblock %}
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle('show')|trans|build_string(entity) }}</h1>
{% endblock %}
{% block header_actions %}
<div class="crud-header-actions">
<div class="btn-group">
{% if configuration.action('show', 'back', true) %}
<a href="{{ path(configuration.pageRoute('index')) }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
{{ configuration.actionTitle('show', 'back', 'Back to the list')|trans }}
</a>
{% endif %}
{% if configuration.action('show', 'edit', true) %}
<a href="{{ path(configuration.pageRoute('edit'), {entity: entity.id}) }}" class="btn btn-primary">
<span class="fa fa-edit pr-1"></span>
{{ configuration.actionTitle('show', 'edit', 'Edit')|trans|build_string(entity) }}
</a>
{% endif %}
</div>
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block show %}
<div class="row">
<div class="col-md-12 p-3">
{{ include(configuration.view('showEntity', '@Core/admin/crud/_show.html.twig')) }}
</div>
</div>
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Core\Twig\Extension;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CrudExtension extends AbstractExtension
{
protected PropertyAccessor $propertyAccessor;
protected Environment $twig;
public function __construct(Environment $twig)
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->getPropertyAccessor()
;
$this->twig = $twig;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('render_field', [$this, 'renderField'], ['is_safe' => ['html']]),
];
}
public function renderField($entity, array $config): string
{
$field = $config['field'];
$instance = new $field;
$resolver = $instance->configureOptions(new OptionsResolver());
return $instance->buildView($this->twig, $entity, $resolver->resolve($config['options']));
}
}