backports murph-skeleton

This commit is contained in:
Simon Vieille 2021-05-12 12:04:03 +02:00
parent ced59844c4
commit 7698277368
47 changed files with 989 additions and 95 deletions

View File

@ -3,8 +3,8 @@
namespace App\Core\Controller\Admin;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Response;
abstract class AdminController extends AbstractController
{

View File

@ -0,0 +1,172 @@
<?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 App\Core\Repository\RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
/**
* class CrudController.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class CrudController extends AdminController
{
protected array $filters = [];
abstract protected function getConfiguration(): CrudConfiguration;
protected function doIndex(int $page = 1, RepositoryQuery $query, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
$this->updateFilters($request, $session);
$pager = $query
->useFilters($this->filters)
->paginate($page, $configuration->getMaxPerPage('index'))
;
return $this->render($this->getConfiguration()->getView('index'), [
'configuration' => $configuration,
'pager' => $pager,
'filters' => [
'show' => null !== $configuration->getForm('filter'),
'isEmpty' => empty($this->filters),
],
]);
}
protected function doNew(EntityInterface $entity, EntityManager $entityManager, Request $request): Response
{
$configuration = $this->getConfiguration();
$form = $this->createForm($configuration->getForm('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(),
'configuration' => $configuration,
'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
{
$configuration = $this->getConfiguration();
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
{
$configuration = $this->getConfiguration();
$type = $configuration->getForm('filter');
if (null === $type) {
throw $this->createNotFoundException();
}
$form = $this->createForm($type);
$form->submit($session->get($form->getName(), []));
return $this->render($configuration->getView('filter'), [
'form' => $form->createView(),
'configuration' => $configuration,
]);
}
protected function updateFilters(Request $request, Session $session)
{
$configuration = $this->getConfiguration();
$type = $configuration->getForm('filter');
if (null === $type) {
return;
}
$form = $this->createForm($type);
if ($request->query->has($form->getName())) {
$filters = $request->query->get($form->getName());
if ('0' === $filters) {
$filters = [];
}
} elseif ($session->has($form->getName())) {
$filters = $session->get($form->getName());
} else {
$filters = [];
}
$form->submit($filters);
if (empty($filters)) {
$this->filters = $filters;
$session->set($form->getName(), $filters);
} elseif ($form->isValid()) {
$this->filters = $form->getData();
$session->set($form->getName(), $filters);
}
}
}

View File

@ -6,6 +6,7 @@ use App\Core\Event\Account\PasswordRequestEvent;
use App\Core\Manager\EntityManager;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -14,7 +15,6 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use ZxcvbnPhp\Zxcvbn;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class AuthController extends AbstractController
{

View File

@ -5,7 +5,6 @@ namespace App\Core\Controller\Setting;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Setting as Entity;
use App\Core\Event\Setting\SettingEvent;
use App\Core\Factory\SettingFactory as EntityFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\SettingRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -47,8 +46,7 @@ class SettingAdminController extends AdminController
EntityManager $entityManager,
EventDispatcherInterface $eventDispatcher,
Request $request
): Response
{
): Response {
$builder = $this->createFormBuilder($entity);
$eventDispatcher->dispatch(new SettingEvent([

View File

@ -24,7 +24,8 @@ class NavigationAdminController extends AdminController
{
$pager = $query
->orderBy('.label, .domain')
->paginate($page);
->paginate($page)
;
return $this->render('@Core/site/navigation_admin/index.html.twig', [
'pager' => $pager,

View File

@ -5,17 +5,15 @@ namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Site\Page\Page as Entity;
use App\Core\Factory\Site\Page\PageFactory as EntityFactory;
use App\Core\Form\Site\Page\PageType as EntityType;
use App\Core\Form\Site\Page\Filter\PageFilterType as FilterType;
use App\Core\Form\Site\Page\PageType as EntityType;
use App\Core\Manager\EntityManager;
use App\Core\Page\FooPage;
use App\Core\Page\SimplePage;
use App\Core\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery;
use App\Core\Site\PageLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/page")
@ -31,7 +29,8 @@ class PageAdminController extends AdminController
$pager = $query
->useFilters($this->filters)
->paginate($page);
->paginate($page)
;
return $this->render('@Core/site/page_admin/index.html.twig', [
'pager' => $pager,
@ -39,24 +38,6 @@ class PageAdminController extends AdminController
]);
}
/**
* @Route("/new", name="admin_site_page_new")
*/
public function new(EntityFactory $factory, EntityManager $entityManager): Response
{
// $entity = $factory->create(FooPage::class);
$entity = $factory->create(SimplePage::class);
$entity->setName('Page de test '.mt_rand());
$entityManager->create($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute('admin_site_page_edit', [
'entity' => $entity->getId(),
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_page_edit")
*/
@ -122,6 +103,11 @@ class PageAdminController extends AdminController
]);
}
public function getSection(): string
{
return 'site_page';
}
protected function updateFilters(Request $request, Session $session)
{
if ($request->query->has('page_filter')) {
@ -147,9 +133,4 @@ class PageAdminController extends AdminController
$session->set('page_filter', $filters);
}
}
public function getSection(): string
{
return 'site_page';
}
}

View File

@ -5,7 +5,6 @@ namespace App\Core\Controller\Site;
use App\Core\Site\SiteRequest;
use App\Core\Site\SiteStore;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PageController extends AbstractController

View File

@ -56,7 +56,8 @@ class TreeAdminController extends AdminController
): Response {
$navigations = $navigationQuery->create()
->orderBy('.label, .domain')
->find();
->find()
;
$session->set('site_tree_last_navigation', $navigation->getId());

View File

@ -13,7 +13,6 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* @Route("/admin/user")
@ -38,7 +37,6 @@ class UserAdminController extends AdminController
public function new(
EntityFactory $factory,
EntityManager $entityManager,
UserPasswordEncoderInterface $encoder,
Request $request
): Response {
$entity = $factory->create($this->getUser());

View File

@ -0,0 +1,164 @@
<?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 = [];
protected array $maxPerPage = [];
/* -- */
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] ?? null;
}
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] ?? [];
}
/* -- */
public function setMaxPerPage(string $page, int $max)
{
$this->maxPerPage[$page] = $max;
return $this;
}
public function getMaxPerPage(string $page, int $default = 20)
{
return $this->maxPerPage[$page] ?? $default;
}
}

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

@ -12,7 +12,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
abstract class EntityManagerEventSubscriber implements EventSubscriberInterface
{
static protected int $priority = 0;
protected static int $priority = 0;
public static function getSubscribedEvents()
{

View File

@ -2,6 +2,7 @@
namespace App\Core\EventSuscriber\Site;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Menu;
use App\Core\Event\EntityManager\EntityManagerEvent;
@ -10,7 +11,6 @@ use App\Core\Factory\Site\NodeFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Slugify\CodeSlugify;
use App\Core\Cache\SymfonyCacheManager;
/**
* class MenuEventSubscriber.

View File

@ -2,15 +2,14 @@
namespace App\Core\EventSuscriber\Site;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Navigation;
use App\Core\Entity\Site\Node;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\KernelInterface;
use App\Core\Cache\SymfonyCacheManager;
/**
* class SiteEventSubscriber.

View File

@ -0,0 +1,12 @@
<?php
namespace App\Core\Factory;
/**
* interface FactoryInterface.
*
* @author Simon Vieille <simon@deblan.fr>
*/
interface FactoryInterface
{
}

View File

@ -9,7 +9,7 @@ use App\Core\Entity\Setting;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SettingFactory
class SettingFactory implements FactoryInterface
{
public function create(string $code): Setting
{

View File

@ -4,13 +4,14 @@ namespace App\Core\Factory\Site;
use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Navigation;
use App\Core\Factory\FactoryInterface;
/**
* class MenuFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MenuFactory
class MenuFactory implements FactoryInterface
{
public function create(?Navigation $navigation = null): Menu
{

View File

@ -3,13 +3,14 @@
namespace App\Core\Factory\Site;
use App\Core\Entity\Site\Navigation;
use App\Core\Factory\FactoryInterface;
/**
* class NavigationFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationFactory
class NavigationFactory implements FactoryInterface
{
public function create(): Navigation
{

View File

@ -4,13 +4,14 @@ namespace App\Core\Factory\Site;
use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Node;
use App\Core\Factory\FactoryInterface;
/**
* class NodeFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NodeFactory
class NodeFactory implements FactoryInterface
{
public function create(?Menu $menu = null, string $url = null): Node
{

View File

@ -3,13 +3,14 @@
namespace App\Core\Factory\Site\Page;
use App\Core\Entity\Site\Page\Page;
use App\Core\Factory\FactoryInterface;
/**
* class PageFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageFactory
class PageFactory implements FactoryInterface
{
public function create(string $className, string $name): Page
{

View File

@ -11,7 +11,7 @@ use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class UserFactory
class UserFactory implements FactoryInterface
{
protected TokenGeneratorInterface $tokenGenerator;
protected UserPasswordEncoderInterface $encoder;

View File

@ -7,8 +7,8 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
class NavigationType extends AbstractType
{

View File

@ -2,18 +2,13 @@
namespace App\Core\Form\Site\Page\Filter;
use App\Core\Entity\Site\Page\Page;
use App\Core\Entity\Site\Navigation;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use App\Core\Entity\Site\Navigation;
use Doctrine\ORM\EntityRepository;
class PageFilterType extends AbstractType
{

View File

@ -74,6 +74,27 @@ abstract class RepositoryQuery
return $this->repository;
}
public function useFilters(array $filters)
{
foreach ($filters as $name => $value) {
if (null === $value) {
continue;
}
if (is_int($value) || is_bool($value)) {
$this->andWhere('.'.$name.' = :'.$name);
$this->setParameter(':'.$name, $value);
} elseif (is_string($value)) {
$this->andWhere('.'.$name.' LIKE :'.$name);
$this->setParameter(':'.$name, '%'.$value.'%');
} else {
$this->filterHandler($name, $value);
}
}
return $this;
}
protected function populateDqlId(&$data)
{
if (is_string($data)) {
@ -98,25 +119,4 @@ abstract class RepositoryQuery
protected function filterHandler(string $name, $value)
{
}
public function useFilters(array $filters)
{
foreach ($filters as $name => $value) {
if (null === $value) {
continue;
}
if (is_int($value) || is_bool($value)) {
$this->andWhere('.'.$name.' = :'.$name);
$this->setParameter(':'.$name, $value);
} elseif (is_string($value)) {
$this->andWhere('.'.$name.' LIKE :'.$name);
$this->setParameter(':'.$name, '%'.$value.'%');
} else {
$this->filterHandler($name, $value);
}
}
return $this;
}
}

View File

@ -2,9 +2,9 @@
namespace App\Core\Repository\Site\Page;
use App\Core\Entity\Site\Navigation;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
use App\Core\Entity\Site\Navigation;
/**
* class PageRepositoryQuery.
@ -18,15 +18,6 @@ class PageRepositoryQuery extends RepositoryQuery
parent::__construct($repository, 'p', $paginator);
}
protected function filterHandler(string $name, $value)
{
if ($name === 'navigation') {
return $this->filterByNavigation($value);
} else {
return parent::filterHandler($name, $value);
}
}
public function filterByNavigation(Navigation $navigation)
{
return $this
@ -34,7 +25,8 @@ class PageRepositoryQuery extends RepositoryQuery
->leftJoin('node.menu', 'menu')
->leftJoin('menu.navigation', 'navigation')
->where('navigation.id = :navigationId')
->setParameter(':navigationId', $navigation->getId());
->setParameter(':navigationId', $navigation->getId())
;
}
public function filterById($id)
@ -46,4 +38,13 @@ class PageRepositoryQuery extends RepositoryQuery
return $this;
}
protected function filterHandler(string $name, $value)
{
if ('navigation' === $name) {
return $this->filterByNavigation($value);
}
return parent::filterHandler($name, $value);
}
}

View File

@ -86,6 +86,7 @@
"Add a menu": "Ajouter un menu"
"Actions": "Actions"
"Remove": "Supprimer"
"Delete": "Supprimer"
"Move": "Déplacer"
"Hidden": "Caché"
"Show": "Voir"
@ -126,9 +127,13 @@
"Name": "Nom"
"Authentication": "Authentification"
"Anyway": "Peu importe"
"Reset": "Réinitialiser"
"Yes": "Oui"
"No": "Non"
"yes": "oui"
"no": "non"
"Locale": "Langue"
"Settings": "Paramètres"
"Setting": "Paramètre"
"Section": "Section"
"Filter": "Filtrer"

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,21 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ 'Filter'|trans }}
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form action="{{ path(configuration.pageRoute('index')) }}" id="form-filters" method="GET">
{{ form_widget(form) }}
</form>
</div>
<div class="modal-footer">
<a href="{{ path(configuration.pageRoute('index'), {(form.vars.name): 0}) }}" class="btn btn-secondary">{{ 'Reset'|trans }}</a>
<button type="submit" form="form-filters" class="btn btn-primary">{{ 'Filter'|trans }}</button>
</div>
</div>
</div>

View File

@ -0,0 +1,127 @@
{% 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 %}{% if filters.show %}pb-3{% else %}pb-5{% endif %}{% 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>
{% block header_filter_pager %}
{% if filters.show %}
<div class="row pb-3">
<div class="col-auto ml-auto {% if pager.getPaginationData.pageCount > 1 %}mr-3{% endif %}">
<button data-modal="{{ path(configuration.pageRoute('filter')) }}" class="btn btn-sm btn-secondary">
{{ 'Filter'|trans }} {% if not filters.isEmpty %}({{ 'yes'|trans }}){% endif %}
</button>
</div>
<div class="col-auto">
{{ knp_pagination_render(pager) }}
</div>
</div>
{% else %}
{{ knp_pagination_render(pager) }}
{% endif %}
{% endblock %}
</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" colspan="{{ configuration.fields('index')|length + 1 }}">
<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

@ -2,10 +2,10 @@
namespace App\Core\Setting;
use App\Core\Entity\Setting;
use App\Core\Factory\SettingFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\SettingRepositoryQuery;
use App\Core\Entity\Setting;
/**
* class SettingManager.

View File

@ -18,6 +18,7 @@ class StringBuilder
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->disableExceptionOnInvalidPropertyPath()
->getPropertyAccessor()
;
}
@ -33,12 +34,12 @@ class StringBuilder
return $format;
}
preg_match_all('/\{([a-zA-Z0-9\.]+)\}/i', $format, $matches, PREG_SET_ORDER);
preg_match_all('/\{([a-zA-Z0-9\._]+)\}/i', $format, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$propertyValue = $this->propertyAccessor->getValue($object, $match[1]);
$format = u($format)->replace($match[0], $propertyValue);
$format = u($format)->replace($match[0], (string) $propertyValue);
}
return $format;

View File

@ -0,0 +1,44 @@
<?php
namespace App\Core\Twig\Extension;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
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']));
}
}

View File

@ -2,6 +2,7 @@
namespace App\Factory\Blog;
use App\Core\Factory\FactoryInterface;
use App\Entity\Blog\Category;
/**
@ -9,7 +10,7 @@ use App\Entity\Blog\Category;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CategoryFactory
class CategoryFactory implements FactoryInterface
{
public function create(): Category
{

View File

@ -2,6 +2,7 @@
namespace App\Factory\Blog;
use App\Core\Factory\FactoryInterface;
use App\Entity\Blog\Comment;
use App\Entity\Blog\Post;
@ -10,7 +11,7 @@ use App\Entity\Blog\Post;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CommentFactory
class CommentFactory implements FactoryInterface
{
public function create(Post $post): Comment
{

View File

@ -2,6 +2,7 @@
namespace App\Factory\Blog;
use App\Core\Factory\FactoryInterface;
use App\Entity\Blog\Post;
/**
@ -9,7 +10,7 @@ use App\Entity\Blog\Post;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PostFactory
class PostFactory implements FactoryInterface
{
public function create(): Post
{