Merge branch 'feature/cms' into develop

This commit is contained in:
Simon Vieille 2021-03-19 15:15:28 +01:00
commit 169bd4ecf3
84 changed files with 3953 additions and 66 deletions

3
.gitignore vendored
View file

@ -22,3 +22,6 @@
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
/public/uploads/
!/public/uploads/.gitkeep

View file

@ -114,7 +114,7 @@ tr.table-primary-light {
.fa {
font-size: 1.2rem;
margin-right: 5px;
min-width: 20px;
min-width: 30px;
color: #fff;
}
@ -352,3 +352,78 @@ table.table-fixed, .table-fixed > table {
.login-image {
width: 50%;
}
.tree {
position: relative;
background: white;
color: #212529;
span {
font-style: italic;
letter-spacing: .4px;
color: #a8a8a8;
}
.fa-folder-open, .fa-folder {
color: #007bff;
}
.fa-html5 {
color: #f21f10;
}
ul {
padding-left: 5px;
list-style: none;
margin: 0;
padding-bottom: 0;
li {
position: relative;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 15px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
&:before {
position: absolute;
top: 15px;
left: 0;
width: 10px;
height: 1px;
margin: auto;
content: '';
background-color: #666;
}
&:after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
height: 100%;
content: '';
background-color: #666;
}
&:last-child:after {
height: 15px;
}
}
a {
cursor: pointer;
&:hover {
text-decoration: none;
}
}
}
}
fieldset.form-group {
margin-bottom: 0;
}

View file

@ -5,9 +5,20 @@ module.exports = function() {
e.preventDefault();
e.stopPropagation();
let id = $(e.target).attr('data-modal');
let modal = $(id);
let container = $('#modal-container');
modal.modal('toggle');
if (!container.length) {
container = $('<div id="modal-container" class="modal">');
$('body').append(container);
}
container.html('');
const url = $(e.target).attr('data-modal');
container.load(url, function() {
$(container).modal('show');
});
});
}

View file

@ -19,6 +19,7 @@
"scheb/2fa-google-authenticator": "^5.7",
"scheb/2fa-qr-code": "^5.7",
"sensio/framework-extra-bundle": "^6.1",
"stof/doctrine-extensions-bundle": "^1.6",
"symfony/apache-pack": "^1.0",
"symfony/asset": "5.2.*",
"symfony/console": "5.2.*",

View file

@ -16,4 +16,6 @@ return [
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
App\Bundle\AppBundle::class => ['all' => true],
];

8
config/packages/app.yaml Normal file
View file

@ -0,0 +1,8 @@
app:
site:
pages:
App\Site\Page\SimplePage:
name: 'Page simple'
templates:
- {name: "Template 1", file: "site/page/simple/page.html.twig"}
- {name: "Template 2", file: "site/page/simple/page2.html.twig"}

View file

@ -10,9 +10,20 @@ doctrine:
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
App\Entity:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
alias: App\Entity
App\Site\Page:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Site/Page'
prefix: 'App\Site\Page'
gedmo_tree:
type: annotation
prefix: Gedmo\Tree\Entity
dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Tree/Entity"
alias: GedmoTree # (optional) it will default to the name set for the mapping
is_bundle: false

View file

@ -0,0 +1,4 @@
# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
# See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/
stof_doctrine_extensions:
default_locale: en_US

View file

@ -2,11 +2,10 @@
# path: /
# controller: App\Controller\DefaultController::index
#https://symfony.com/doc/current/routing/custom_route_loader.html
#admin_routes:
# resource: 'admin_route_loader::loadRoutes'
# type: service
#
site_route:
resource: 'site.route_loader::loadRoutes'
type: extra
2fa_login:
path: /2fa
defaults:

View file

@ -27,5 +27,16 @@ services:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
site.route_loader:
class: App\Router\SiteRouteLoader
tags: [routing.loader]
gedmo.listener.tree:
class: Gedmo\Tree\TreeListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

23
src/Bundle/AppBundle.php Normal file
View file

@ -0,0 +1,23 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Bundle;
use App\DependencyInjection\AppExtension;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function getContainerExtension()
{
return new AppExtension();
}
}

View file

@ -111,7 +111,7 @@ class CategoryAdminController extends AdminController
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimées.');
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_blog_category_index');

View file

@ -8,7 +8,6 @@ use App\Factory\Blog\PostFactory as EntityFactory;
use App\Form\Blog\PostType as EntityType;
use App\Form\FileUploadHandler;
use App\Manager\EntityManager;
use App\Repository\Blog\PostRepositoryQuery;
use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -97,7 +96,7 @@ class PostAdminController extends AdminController
/**
* @Route("/show/{entity}", name="admin_blog_post_show")
*/
public function show(Entity $entity, PostRepositoryQuery $postQuery): Response
public function show(Entity $entity): Response
{
return $this->render('blog/post_admin/show.html.twig', [
'entity' => $entity,
@ -112,7 +111,7 @@ class PostAdminController extends AdminController
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimées.');
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_blog_post_index');

View file

@ -0,0 +1,82 @@
<?php
namespace App\Controller\Site;
use App\Controller\Admin\AdminController;
use App\Entity\Site\Menu as Entity;
use App\Entity\Site\Navigation;
use App\Factory\Site\MenuFactory as EntityFactory;
use App\Form\Site\MenuType as EntityType;
use App\Manager\EntityManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/menu")
*/
class MenuAdminController extends AdminController
{
/**
* @Route("/new/{navigation}", name="admin_site_menu_new", methods={"POST"})
*/
public function new(Navigation $navigation, EntityFactory $factory, EntityManager $entityManager, Request $request): Response
{
$entity = $factory->create($navigation);
$form = $this->createForm(EntityType::class, $entity);
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->create($entity);
$this->addFlash('success', 'Donnée enregistrée.');
} else {
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $navigation->getId(),
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_menu_edit", methods={"POST"})
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
$form = $this->createForm(EntityType::class, $entity);
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
} else {
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getNavigation()->getId(),
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_menu_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getNavigation()->getId(),
]);
}
public function getSection(): string
{
return '';
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Controller\Site;
use App\Controller\Admin\AdminController;
use App\Entity\Site\Navigation as Entity;
use App\Factory\Site\NavigationFactory as EntityFactory;
use App\Form\Site\NavigationType as EntityType;
use App\Manager\EntityManager;
use App\Repository\Site\NavigationRepositoryQuery as RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/navigation")
*/
class NavigationAdminController extends AdminController
{
/**
* @Route("/{page}", name="admin_site_navigation_index", requirements={"page": "\d+"})
*/
public function index(int $page = 1, RepositoryQuery $query, Request $request): Response
{
$pager = $query->paginate($page);
return $this->render('site/navigation_admin/index.html.twig', [
'pager' => $pager,
]);
}
/**
* @Route("/new", name="admin_site_navigation_new")
*/
public function new(EntityFactory $factory, EntityManager $entityManager, Request $request): Response
{
$entity = $factory->create();
$form = $this->createForm(EntityType::class, $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->create($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_site_navigation_edit', [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->render('site/navigation_admin/new.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_navigation_edit")
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
$form = $this->createForm(EntityType::class, $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_site_navigation_edit', [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->render('site/navigation_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/show/{entity}", name="admin_site_navigation_show")
*/
public function show(Entity $entity): Response
{
return $this->render('site/navigation_admin/show.html.twig', [
'entity' => $entity,
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_navigation_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_site_navigation_index');
}
public function getSection(): string
{
return 'site_navigation';
}
}

View file

@ -0,0 +1,263 @@
<?php
namespace App\Controller\Site;
use App\Controller\Admin\AdminController;
use App\Entity\Site\Node;
use App\Entity\Site\Node as Entity;
use App\Entity\Site\Page\Page;
use App\Event\EntityManager\EntityManagerEvent;
use App\Factory\Site\NodeFactory as EntityFactory;
use App\Factory\Site\Page\PageFactory;
use App\Form\Site\NodeMoveType;
use App\Form\Site\NodeType as EntityType;
use App\Manager\EntityManager;
use App\Repository\Site\NodeRepository;
use App\Site\PageLocator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/node")
*/
class NodeAdminController extends AdminController
{
/**
* @Route("/new/{node}", name="admin_site_node_new")
*/
public function new(
Node $node,
EntityFactory $factory,
PageFactory $pageFactory,
EntityManager $entityManager,
NodeRepository $nodeRepository,
PageLocator $pageLocator,
Request $request
): Response {
$entity = $factory->create($node->getMenu());
$form = $this->createForm(EntityType::class, $entity, [
'pages' => $pageLocator->getPages(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$position = $form->get('position')->getData();
$parent = 'above' === $position ? $node : $node->getParent();
$entity->setParent($parent);
if ('above' === $position) {
$nodeRepository->persistAsLastChild($entity, $node);
} else {
if ('after' === $position) {
$nodeRepository->persistAsNextSiblingOf($entity, $node);
} elseif ('before' === $position) {
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
}
}
$this->handlePageAssociation(
$form->get('pageAction')->getData(),
$form->get('pageEntity')->getData(),
$form->get('pageType')->getData(),
$entity,
$pageFactory,
$pageLocator
);
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
} else {
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $node->getMenu()->getNavigation()->getId(),
]);
}
return $this->render('site/node_admin/new.html.twig', [
'form' => $form->createView(),
'node' => $node,
'entity' => $entity,
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_node_edit")
*/
public function edit(
Entity $entity,
EntityManager $entityManager,
PageFactory $pageFactory,
PageLocator $pageLocator,
Request $request
): Response {
$form = $this->createForm(EntityType::class, $entity, [
'pages' => $pageLocator->getPages(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->handlePageAssociation(
$form->get('pageAction')->getData(),
$form->get('pageEntity')->getData(),
$form->get('pageType')->getData(),
$entity,
$pageFactory,
$pageLocator
);
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
} else {
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]);
}
return $this->render('site/node_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/move/{entity}", name="admin_site_node_move")
*/
public function move(
Entity $entity,
EntityManager $entityManager,
NodeRepository $nodeRepository,
Request $request
): Response {
$form = $this->createForm(NodeMoveType::class, null, [
'menu' => $entity->getMenu(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->get('node')->getData()->getId() === $entity->getId()) {
$form->get('node')->addError(new FormError('Élement de référence invalide.'));
}
if ($form->isValid()) {
$position = $form->get('position')->getData();
$node = $form->get('node')->getData();
$parent = 'above' === $position ? $node : $node->getParent();
$entity->setParent($parent);
if ('above' === $position) {
$nodeRepository->persistAsLastChild($entity, $node);
$entityManager->flush();
} else {
if ('after' === $position) {
$nodeRepository->persistAsNextSiblingOf($entity, $node);
} elseif ('before' === $position) {
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
}
$entityManager->flush();
}
$this->addFlash('success', 'Donnée enregistrée.');
} else {
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]);
}
return $this->render('site/node_admin/move.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/toggle/visibility/{entity}", name="admin_site_node_toggle_visibility", methods={"POST"})
*/
public function toggleVisibility(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('toggle_visibility'.$entity->getId(), $request->request->get('_token'))) {
$entity->setIsVisible(!$entity->getIsVisible());
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_node_delete", methods={"DELETE"})
*/
public function delete(
Entity $entity,
NodeRepository $nodeRepository,
EventDispatcherInterface $eventDispatcher,
Request $request
): Response {
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_DELETE_EVENT);
$nodeRepository->removeFromTree($entity);
$nodeRepository->reorder($entity->getMenu()->getRootNode());
$eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
$this->addFlash('success', 'Donnée supprimée.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]);
}
public function getSection(): string
{
return '';
}
protected function handlePageAssociation(
string $pageAction,
?Page $pageEntity,
string $pageType,
Entity $entity,
PageFactory $pageFactory,
PageLocator $pageLocator
) {
if ('new' === $pageAction) {
$pageConfiguration = $pageLocator->getPage($pageType);
$page = $pageFactory->create($pageType, $entity->getLabel());
$page->setTemplate($pageConfiguration->getTemplates()[0]['file']);
$entity->setPage($page);
} elseif ('existing' === $pageAction) {
if ($pageEntity) {
$entity->setPage($pageEntity);
} else {
$this->addFlash('info', 'Aucun changement de page effectué.');
}
} elseif ('none' === $pageAction) {
$entity->setPage(null);
}
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace App\Controller\Site;
use App\Controller\Admin\AdminController;
use App\Entity\Site\Page\Page as Entity;
use App\Factory\Site\Page\PageFactory as EntityFactory;
use App\Form\Site\Page\PageType as EntityType;
use App\Manager\EntityManager;
use App\Page\FooPage;
use App\Page\SimplePage;
use App\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery;
use App\Site\PageLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/page")
*/
class PageAdminController extends AdminController
{
/**
* @Route("/{page}", name="admin_site_page_index", requirements={"page": "\d+"})
*/
public function index(int $page = 1, RepositoryQuery $query, Request $request): Response
{
$pager = $query->paginate($page);
return $this->render('site/page_admin/index.html.twig', [
'pager' => $pager,
]);
}
/**
* @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', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_site_page_edit', [
'entity' => $entity->getId(),
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_page_edit")
*/
public function edit(
int $entity,
EntityFactory $factory,
EntityManager $entityManager,
RepositoryQuery $repositoryQuery,
PageLocator $pageLocator,
Request $request
): Response {
$entity = $repositoryQuery->filterById($entity)->findOne();
$form = $this->createForm(EntityType::class, $entity, [
'pageConfiguration' => $pageLocator->getPage(get_class($entity)),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_site_page_edit', [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->render('site/page_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_page_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_site_page_index');
}
public function getSection(): string
{
return 'site_page';
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Controller\Site;
use App\Site\SiteRequest;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PageController extends AbstractController
{
public function show(Request $request, SiteRequest $siteRequest): Response
{
if (!$siteRequest->getPage()) {
throw $this->createNotFoundException();
}
return $this->render($siteRequest->getPage()->getTemplate(), [
'_node' => $siteRequest->getNode(),
'_page' => $siteRequest->getPage(),
'_menu' => $siteRequest->getMenu(),
'_navigation' => $siteRequest->getNavigation(),
]);
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Controller\Site;
use App\Controller\Admin\AdminController;
use App\Entity\Site\Navigation;
use App\Factory\Site\MenuFactory;
use App\Form\Site\MenuType;
use App\Repository\Site\NavigationRepositoryQuery;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/tree")
*/
class TreeAdminController extends AdminController
{
/**
* @Route("/", name="admin_site_tree_index")
*/
public function index(NavigationRepositoryQuery $navigationQuery): Response
{
$navigation = $navigationQuery->create()
->orderBy('.label')
->findOne()
;
if (null === $navigation) {
$this->addFlash('warning', 'Vous devez ajouter une navigation.');
return $this->redirectToRoute('admin_site_navigation_new');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $navigation->getId(),
]);
}
/**
* @Route("/navigation/{navigation}", name="admin_site_tree_navigation")
*/
public function navigation(
Navigation $navigation,
NavigationRepositoryQuery $navigationQuery,
MenuFactory $menuFactory
): Response {
$navigations = $navigationQuery->create()
->orderBy('.label')
->find()
;
$forms = [
'menu' => $this->createForm(MenuType::class, $menuFactory->create())->createView(),
'menus' => [],
];
foreach ($navigation->getMenus() as $menu) {
$forms['menus'][$menu->getId()] = $this->createForm(MenuType::class, $menu)->createView();
}
return $this->render('site/tree_admin/navigation.html.twig', [
'navigation' => $navigation,
'navigations' => $navigations,
'forms' => $forms,
]);
}
public function getSection(): string
{
return 'site_tree';
}
}

View file

@ -144,7 +144,7 @@ class UserAdminController extends AdminController
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimées.');
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_user_index');

View file

@ -0,0 +1,28 @@
<?php
namespace App\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
class AppExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('app', $config);
}
/**
* {@inheritdoc}
*/
public function getConfiguration(array $configs, ContainerBuilder $container)
{
return new Configuration();
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('app');
$treeBuilder->getRootNode()
->children()
->arrayNode('site')
->children()
->arrayNode('pages')
->prototype('array')
->children()
->scalarNode('name')
->isRequired()
->cannotBeEmpty()
->end()
->arrayNode('templates')
->prototype('array')
->children()
->scalarNode('name')
->cannotBeEmpty()
->end()
->scalarNode('file')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
}
}

View file

@ -2,6 +2,7 @@
namespace App\Entity\Blog;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Repository\Blog\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
@ -14,6 +15,8 @@ use Doctrine\ORM\Mapping as ORM;
*/
class Category implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue

141
src/Entity/Site/Menu.php Normal file
View file

@ -0,0 +1,141 @@
<?php
namespace App\Entity\Site;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Repository\Site\MenuRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=MenuRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Menu implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $label;
/**
* @ORM\Column(type="string", length=255)
*/
private $code;
/**
* @ORM\ManyToOne(targetEntity=Navigation::class, inversedBy="menus")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private $navigation;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="menu", orphanRemoval=true)
*/
private $nodes;
/**
* @ORM\OneToOne(targetEntity=Node::class)
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $rootNode;
public function __construct()
{
$this->nodes = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getNavigation(): ?Navigation
{
return $this->navigation;
}
public function setNavigation(?Navigation $navigation): self
{
$this->navigation = $navigation;
return $this;
}
/**
* @return Collection|Node[]
*/
public function getNodes(): Collection
{
return $this->nodes;
}
public function addNode(Node $node): self
{
if (!$this->nodes->contains($node)) {
$this->nodes[] = $node;
$node->setMenu($this);
}
return $this;
}
public function removeNode(Node $node): self
{
if ($this->nodes->removeElement($node)) {
// set the owning side to null (unless already changed)
if ($node->getMenu() === $this) {
$node->setMenu(null);
}
}
return $this;
}
public function getRootNode(): ?Node
{
return $this->rootNode;
}
public function setRootNode(?Node $rootNode): self
{
$this->rootNode = $rootNode;
return $this;
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace App\Entity\Site;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Repository\Site\NavigationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=NavigationRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Navigation implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $label;
/**
* @ORM\Column(type="string", length=255)
*/
private $code;
/**
* @ORM\Column(type="string", length=255)
*/
private $domain;
/**
* @ORM\OneToMany(targetEntity=Menu::class, mappedBy="navigation")
*/
private $menus;
public function __construct()
{
$this->menus = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function setDomain(string $domain): self
{
$this->domain = $domain;
return $this;
}
/**
* @return Collection|Menu[]
*/
public function getMenus(): Collection
{
return $this->menus;
}
public function addMenu(Menu $menu): self
{
if (!$this->menus->contains($menu)) {
$this->menus[] = $menu;
$menu->setNavigation($this);
}
return $this;
}
public function removeMenu(Menu $menu): self
{
if ($this->menus->removeElement($menu)) {
// set the owning side to null (unless already changed)
if ($menu->getNavigation() === $this) {
$menu->setNavigation(null);
}
}
return $this;
}
}

282
src/Entity/Site/Node.php Normal file
View file

@ -0,0 +1,282 @@
<?php
namespace App\Entity\Site;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Entity\Site\Page\Page;
use App\Repository\Site\NodeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @Gedmo\Tree(type="nested")
* @ORM\HasLifecycleCallbacks
* @ORM\Entity(repositoryClass=NodeRepository::class)
*/
class Node implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Menu::class, inversedBy="nodes", cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private $menu;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $label;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $url;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
private $isVisible = false;
/**
* @Gedmo\TreeLeft
* @ORM\Column(type="integer")
*/
private $treeLeft;
/**
* @Gedmo\TreeLevel
* @ORM\Column(type="integer")
*/
private $treeLevel;
/**
* @Gedmo\TreeRight
* @ORM\Column(type="integer")
*/
private $treeRight;
/**
* @Gedmo\TreeRoot
* @ORM\ManyToOne(targetEntity="Node")
* @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
*/
private $treeRoot;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Node", inversedBy="children")
* @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
*/
private $parent;
/**
* @ORM\OneToMany(targetEntity="Node", mappedBy="parent")
* @ORM\OrderBy({"treeLeft"="ASC"})
*/
private $children;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="nodes", cascade={"persist"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $page;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getMenu(): ?Menu
{
return $this->menu;
}
public function setMenu(?Menu $menu): self
{
$this->menu = $menu;
return $this;
}
public function getTreeLeft(): ?int
{
return $this->treeLeft;
}
public function setTreeLeft(int $treeLeft): self
{
$this->treeLeft = $treeLeft;
return $this;
}
public function getTreeLevel(): ?int
{
return $this->treeLevel;
}
public function setTreeLevel(int $treeLevel): self
{
$this->treeLevel = $treeLevel;
return $this;
}
public function getTreeRight(): ?int
{
return $this->treeRight;
}
public function setTreeRight(int $treeRight): self
{
$this->treeRight = $treeRight;
return $this;
}
public function getTreeRoot(): ?self
{
return $this->treeRoot;
}
public function setTreeRoot(?self $treeRoot): self
{
$this->treeRoot = $treeRoot;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
return $this;
}
/**
* @return Collection|Node[]
*/
public function getChildren(): Collection
{
return $this->children;
}
public function addChild(Node $child): self
{
if (!$this->children->contains($child)) {
$this->children[] = $child;
$child->setParent($this);
}
return $this;
}
public function removeChild(Node $child): self
{
if ($this->children->removeElement($child)) {
// set the owning side to null (unless already changed)
if ($child->getParent() === $this) {
$child->setParent(null);
}
}
return $this;
}
public function getAllChildren(): ArrayCollection
{
$children = [];
$getChildren = function (Node $node) use (&$children, &$getChildren) {
foreach ($node->getChildren() as $nodeChildren) {
$children[] = $nodeChildren;
$getChildren($nodeChildren);
}
};
$getChildren($this);
usort($children, function ($a, $b) {
return $a->getTreeLeft() < $b->getTreeLeft() ? -1 : 1;
});
return new ArrayCollection($children);
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(?string $label): self
{
$this->label = $label;
return $this;
}
public function getUrl(): ?string
{
return $this->url;
}
public function setUrl(?string $url): self
{
$this->url = $url;
return $this;
}
public function getIsVisible(): ?bool
{
return $this->isVisible;
}
public function setIsVisible(bool $isVisible): self
{
$this->isVisible = $isVisible;
return $this;
}
public function getTreeLabel()
{
$prefix = str_repeat('-', ($this->getTreeLevel() - 1) * 5);
return trim($prefix.' '.$this->getLabel());
}
public function getPage(): ?Page
{
return $this->page;
}
public function setPage(?Page $page): self
{
$this->page = $page;
return $this;
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Entity\Site\Page;
use App\Doctrine\Timestampable;
use App\Repository\Site\Page\BlockRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=BlockRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Block
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="text", nullable=true)
*/
private $value;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="blocks")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $page;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getValue(): ?string
{
return $this->value;
}
public function setValue(string $value): self
{
$this->value = $value;
return $this;
}
public function getPage(): ?Page
{
return $this->page;
}
public function setPage(?Page $page): self
{
$this->page = $page;
return $this;
}
}

View file

@ -0,0 +1,248 @@
<?php
namespace App\Entity\Site\Page;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Entity\Site\Node;
use App\Repository\Site\Page\PageRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @ORM\Entity(repositoryClass=PageRepository::class)
* @ORM\DiscriminatorColumn(name="class_key", type="string")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\HasLifecycleCallbacks
*/
class Page implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $template;
/**
* @ORM\OneToMany(targetEntity=Block::class, mappedBy="page", cascade={"persist"})
*/
private $blocks;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $metaTitle;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $metaDescrition;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $ogTitle;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $ogDescription;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="page")
*/
private $nodes;
public function __construct()
{
$this->blocks = new ArrayCollection();
$this->nodes = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getTemplate(): ?string
{
return $this->template;
}
public function setTemplate(?string $template): self
{
$this->template = $template;
return $this;
}
/**
* @return Collection|Block[]
*/
public function getBlocks(): Collection
{
return $this->blocks;
}
public function addBlock(Block $block): self
{
if (!$this->blocks->contains($block)) {
$this->blocks[] = $block;
$block->setPage($this);
}
return $this;
}
public function removeBlock(Block $block): self
{
if ($this->blocks->removeElement($block)) {
// set the owning side to null (unless already changed)
if ($block->getPage() === $this) {
$block->setPage(null);
}
}
return $this;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function getBlock($name)
{
foreach ($this->getBlocks() as $block) {
if ($block->getName() === $name) {
return $block;
}
}
$block = new Block();
$block->setName($name);
$block->setPage($this);
return $block;
}
public function setBlock(Block $block): self
{
foreach ($this->blocks->toArray() as $key => $value) {
if ($value->getName() === $block->getName()) {
$this->blocks->remove($key);
$this->blocks->add($block);
return $this;
}
}
$this->blocks->add($block);
return $this;
}
public function getMetaTitle(): ?string
{
return $this->metaTitle;
}
public function setMetaTitle(?string $metaTitle): self
{
$this->metaTitle = $metaTitle;
return $this;
}
public function getMetaDescrition(): ?string
{
return $this->metaDescrition;
}
public function setMetaDescrition(?string $metaDescrition): self
{
$this->metaDescrition = $metaDescrition;
return $this;
}
public function getOgTitle(): ?string
{
return $this->ogTitle;
}
public function setOgTitle(?string $ogTitle): self
{
$this->ogTitle = $ogTitle;
return $this;
}
public function getOgDescription(): ?string
{
return $this->ogDescription;
}
public function setOgDescription(?string $ogDescription): self
{
$this->ogDescription = $ogDescription;
return $this;
}
/**
* @return Collection|Node[]
*/
public function getNodes(): Collection
{
return $this->nodes;
}
public function addNode(Node $node): self
{
if (!$this->nodes->contains($node)) {
$this->nodes[] = $node;
$node->setPage($this);
}
return $this;
}
public function removeNode(Node $node): self
{
if ($this->nodes->removeElement($node)) {
// set the owning side to null (unless already changed)
if ($node->getPage() === $this) {
$node->setPage(null);
}
}
return $this;
}
}

View file

@ -15,6 +15,9 @@ class EntityManagerEvent extends Event
const CREATE_EVENT = 'entity_manager_event.create';
const UPDATE_EVENT = 'entity_manager_event.update';
const DELETE_EVENT = 'entity_manager_event.delete';
const PRE_CREATE_EVENT = 'entity_manager_event.pre_create';
const PRE_UPDATE_EVENT = 'entity_manager_event.pre_update';
const PRE_DELETE_EVENT = 'entity_manager_event.pre_delete';
protected EntityInterface $entity;

View file

@ -1,6 +1,6 @@
<?php
namespace App\EventSuscriber;
namespace App\EventSuscriber\Account;
use App\Event\Account\PasswordRequestEvent;
use App\Notification\MailNotifier;
@ -12,7 +12,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class AccountPasswordRequestEventSubscriber implements EventSubscriberInterface
class PasswordRequestEventSubscriber implements EventSubscriberInterface
{
protected MailNotifier $notifier;
protected UrlGeneratorInterface $urlGenerator;

View file

@ -1,21 +1,21 @@
<?php
namespace App\EventSuscriber;
namespace App\EventSuscriber\Blog;
use App\Entity\Blog\Post;
use App\Entity\EntityInterface;
use App\Event\EntityManager\EntityManagerEvent;
use App\EventSuscriber\EntityManagerEventSubscriber;
use App\Repository\Blog\PostRepositoryQuery;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
/**
* class BlogPostEventSubscriber.
* class PostEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class BlogPostEventSubscriber implements EventSubscriberInterface
class PostEventSubscriber extends EntityManagerEventSubscriber
{
protected Filesystem $filesystem;
protected PostRepositoryQuery $query;
@ -26,14 +26,6 @@ class BlogPostEventSubscriber implements EventSubscriberInterface
$this->query = $query;
}
public static function getSubscribedEvents()
{
return [
EntityManagerEvent::UPDATE_EVENT => 'onUpdate',
EntityManagerEvent::DELETE_EVENT => 'onDelete',
];
}
public function support(EntityInterface $entity)
{
return $entity instanceof Post;

View file

@ -10,18 +10,17 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class EntityManagerEventSubscriber implements EventSubscriberInterface
abstract class EntityManagerEventSubscriber implements EventSubscriberInterface
{
public function __construct()
{
}
public static function getSubscribedEvents()
{
return [
EntityManagerEvent::CREATE_EVENT => 'onCreate',
EntityManagerEvent::UPDATE_EVENT => 'onUpdate',
EntityManagerEvent::DELETE_EVENT => 'onDelete',
EntityManagerEvent::PRE_CREATE_EVENT => 'onPreCreate',
EntityManagerEvent::PRE_UPDATE_EVENT => 'onPreUpdate',
EntityManagerEvent::PRE_DELETE_EVENT => 'onPreDelete',
];
}
@ -36,4 +35,16 @@ class EntityManagerEventSubscriber implements EventSubscriberInterface
public function onDelete(EntityManagerEvent $event)
{
}
public function onPreCreate(EntityManagerEvent $event)
{
}
public function onPreUpdate(EntityManagerEvent $event)
{
}
public function onPreDelete(EntityManagerEvent $event)
{
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\EventSuscriber\Site;
use App\Entity\EntityInterface;
use App\Entity\Site\Menu;
use App\Event\EntityManager\EntityManagerEvent;
use App\EventSuscriber\EntityManagerEventSubscriber;
use App\Factory\Site\NodeFactory;
use App\Manager\EntityManager;
use App\Repository\Site\NodeRepository;
/**
* class MenuEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MenuEventSubscriber extends EntityManagerEventSubscriber
{
protected NodeFactory $nodeFactory;
protected EntityManager $entityManager;
public function __construct(
NodeFactory $nodeFactory,
NodeRepository $nodeRepository,
EntityManager $entityManager
) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
$this->entityManager = $entityManager;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Menu;
}
public function onCreate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$menu = $event->getEntity();
if (0 !== count($menu->getNodes())) {
return;
}
$rootNode = $this->nodeFactory->create($menu);
$childNode = $this->nodeFactory->create($menu);
$childNode
->setParent($rootNode)
->setLabel('Premier élément')
;
$menu->setRootNode($rootNode);
$this->entityManager->create($rootNode);
$this->entityManager->create($childNode);
$this->entityManager->getEntityManager()->persist($menu);
$this->entityManager->flush();
$this->nodeRepository->persistAsFirstChild($childNode, $rootNode);
}
public function onUpdate(EntityManagerEvent $event)
{
return $this->onCreate($event);
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace App\EventSuscriber\Site;
use App\Entity\EntityInterface;
use App\Entity\Site\Node;
use App\Event\EntityManager\EntityManagerEvent;
use App\EventSuscriber\EntityManagerEventSubscriber;
use App\Factory\Site\NodeFactory;
use App\Manager\EntityManager;
use App\Repository\Site\NodeRepository;
use App\Slugify\Slugify;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* class NodeEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NodeEventSubscriber extends EntityManagerEventSubscriber
{
protected NodeFactory $nodeFactory;
protected EntityManager $entityManager;
protected KernelInterface $kernel;
protected Slugify $slugify;
public function __construct(
NodeFactory $nodeFactory,
NodeRepository $nodeRepository,
EntityManager $entityManager,
KernelInterface $kernel,
Slugify $slugify
) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
$this->entityManager = $entityManager;
$this->kernel = $kernel;
$this->slugify = $slugify;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Node;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$node = $event->getEntity();
if ($node->getUrl()) {
$generatedUrl = $node->getUrl();
} else {
$path = [];
$parent = $node->getParent();
if ($parent && $parent->getUrl()) {
$pPath = trim($parent->getUrl(), '/');
if ($pPath) {
$path[] = $pPath;
}
}
$path[] = $this->slugify->slugify($node->getLabel());
$generatedUrl = '/'.implode('/', $path);
}
$urlExists = $this->nodeRepository->urlExists($generatedUrl, $node);
if ($urlExists) {
$number = 1;
while ($this->nodeRepository->urlExists($generatedUrl.'-'.$number, $node)) {
++$number;
}
$generatedUrl = $generatedUrl.'-'.$number;
}
$node->setUrl($generatedUrl);
}
public function onUpdate(EntityManagerEvent $event)
{
$application = new Application($this->kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'cache:clear',
]);
$output = new BufferedOutput();
$application->run($input, $output);
}
public function onDelete(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$menu = $event->getEntity()->getMenu();
$rootNode = $menu->getRootNode();
if (0 !== count($rootNode->getChildren())) {
return;
}
$childNode = $this->nodeFactory->create($menu);
$childNode
->setParent($rootNode)
->setLabel('Premier élément')
;
$this->entityManager->update($rootNode, false);
$this->entityManager->create($childNode, false);
$this->nodeRepository->persistAsFirstChild($childNode, $rootNode);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Factory\Site;
use App\Entity\Site\Menu;
use App\Entity\Site\Navigation;
/**
* class MenuFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MenuFactory
{
public function create(?Navigation $navigation = null): Menu
{
$entity = new Menu();
if (null !== $navigation) {
$entity->setNavigation($navigation);
}
return $entity;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Factory\Site;
use App\Entity\Site\Navigation;
/**
* class NavigationFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationFactory
{
public function create(): Navigation
{
return new Navigation();
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Factory\Site;
use App\Entity\Site\Menu;
use App\Entity\Site\Node;
/**
* class NodeFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NodeFactory
{
public function create(?Menu $menu = null): Node
{
$entity = new Node();
if (null !== $menu) {
$entity->setMenu($menu);
}
return $entity;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Factory\Site\Page;
use App\Entity\Site\Page\Page;
/**
* class PageFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageFactory
{
public function create(string $className, string $name): Page
{
$entity = new $className();
$entity->setName($name);
return $entity;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Form\Site;
use App\Entity\Site\Menu;
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;
class MenuType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'label',
TextType::class,
[
'label' => 'Libellé',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'code',
TextType::class,
[
'label' => 'Code',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Menu::class,
]);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Form\Site;
use App\Entity\Site\Navigation;
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;
class NavigationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'label',
TextType::class,
[
'label' => 'Libellé',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'code',
TextType::class,
[
'label' => 'Code',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'domain',
TextType::class,
[
'label' => 'Nom de domaine',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Navigation::class,
]);
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace App\Form\Site;
use App\Entity\Site\Node;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class NodeMoveType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'position',
ChoiceType::class,
[
'label' => 'Position',
'required' => true,
'choices' => [
'Après' => 'after',
'Avant' => 'before',
'En dessous' => 'above',
],
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'node',
EntityType::class,
[
'label' => 'Élement de référence',
'class' => Node::class,
'choices' => call_user_func(function () use ($options) {
return $options['menu']->getRootNode()->getAllChildren();
}),
'choice_label' => 'treeLabel',
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
'menu' => null,
]);
}
}

144
src/Form/Site/NodeType.php Normal file
View file

@ -0,0 +1,144 @@
<?php
namespace App\Form\Site;
use App\Entity\Site\Node;
use App\Entity\Site\Page\Page;
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\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class NodeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'label',
TextType::class,
[
'label' => 'Libellé',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'url',
TextType::class,
[
'label' => 'URL',
'required' => false,
'help' => 'Laisser vide pour une génération automatique',
'attr' => [
],
'constraints' => [
],
]
);
$actions = [
'Nouvelle page' => 'new',
'Associer à une page existante' => 'existing',
'Aucune page' => 'none',
];
if ($builder->getData()->getId()) {
$actions['Garder la configuration actuelle'] = 'keep';
}
$builder->add(
'pageAction',
ChoiceType::class,
[
'label' => false,
'required' => true,
'expanded' => true,
'mapped' => false,
'choices' => $actions,
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'pageType',
ChoiceType::class,
[
'label' => false,
'required' => true,
'mapped' => false,
'choices' => call_user_func(function () use ($options) {
$choices = [];
foreach ($options['pages'] as $page) {
$choices[$page->getName()] = $page->getClassName();
}
return $choices;
}),
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'pageEntity',
EntityType::class,
[
'label' => false,
'required' => true,
'mapped' => false,
'class' => Page::class,
'choice_label' => 'name',
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('p')
->orderBy('p.name', 'ASC')
;
},
'constraints' => [
],
]
);
if (null === $builder->getData()->getId()) {
$builder->add(
'position',
ChoiceType::class,
[
'label' => 'Position',
'required' => true,
'mapped' => false,
'choices' => [
'Après' => 'after',
'Avant' => 'before',
'En dessous' => 'above',
],
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Node::class,
'pages' => [],
]);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Form\Site\Page;
use App\Entity\Site\Page\Page;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'name',
TextType::class,
[
'label' => 'Nom',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'metaTitle',
TextType::class,
[
'label' => 'Titre',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'metaDescrition',
TextType::class,
[
'label' => 'Description',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'ogTitle',
TextType::class,
[
'label' => 'Titre',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'ogDescription',
TextType::class,
[
'label' => 'Description',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'template',
ChoiceType::class,
[
'label' => 'Rendu',
'required' => true,
'choices' => call_user_func(function () use ($options) {
$choices = [];
foreach ($options['pageConfiguration']->getTemplates() as $template) {
$choices[$template['name']] = $template['file'];
}
return $choices;
}),
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->getData()->buildForm($builder);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Page::class,
'pageConfiguration' => null,
]);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Form\Site\Page;
use App\Entity\Site\Page\Block;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextBlockType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
TextType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Block::class,
'options' => [],
]);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Form\Site\Page;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
class TextareaBlockType extends TextBlockType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
TextareaType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
}

View file

@ -25,28 +25,48 @@ class EntityManager
$this->entityManager = $entityManager;
}
public function create(EntityInterface $entity): self
public function create(EntityInterface $entity, bool $dispatchEvent = true): self
{
if ($dispatchEvent) {
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_CREATE_EVENT);
}
$this->persist($entity);
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::CREATE_EVENT);
if ($dispatchEvent) {
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::CREATE_EVENT);
}
return $this;
}
public function update(EntityInterface $entity): self
public function update(EntityInterface $entity, bool $dispatchEvent = true): self
{
if ($dispatchEvent) {
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_UPDATE_EVENT);
}
$this->persist($entity);
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::UPDATE_EVENT);
if ($dispatchEvent) {
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::UPDATE_EVENT);
}
return $this;
}
public function delete(EntityInterface $entity): self
public function delete(EntityInterface $entity, bool $dispatchEvent = true): self
{
if ($dispatchEvent) {
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_DELETE_EVENT);
}
$this->entityManager->remove($entity);
$this->flush();
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
if ($dispatchEvent) {
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
}
return $this;
}
@ -65,6 +85,11 @@ class EntityManager
return $this;
}
public function getEntityManager(): EntityManagerInterface
{
return $this->entityManager;
}
protected function persist(EntityInterface $entity)
{
$this->entityManager->persist($entity);

View file

@ -6,12 +6,6 @@ use App\Entity\Blog\Category;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Category|null find($id, $lockMode = null, $lockVersion = null)
* @method Category|null findOneBy(array $criteria, array $orderBy = null)
* @method Category[] findAll()
* @method Category[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CategoryRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View file

@ -6,12 +6,6 @@ use App\Entity\Blog\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
* @method Post|null findOneBy(array $criteria, array $orderBy = null)
* @method Post[] findAll()
* @method Post[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View file

@ -73,7 +73,10 @@ abstract class RepositoryQuery
public function findOne()
{
return $this->query->getQuery()->getOneOrNullResult();
return $this->query->getQuery()
->setMaxResults(1)
->getOneOrNullResult()
;
}
public function find()
@ -85,4 +88,9 @@ abstract class RepositoryQuery
{
return $this->paginator->paginate($this->query->getQuery(), $page, $limit);
}
public function getRepository(): ServiceEntityRepository
{
return $this->repository;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository\Site;
use App\Entity\Site\Menu;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class MenuRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Menu::class);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Repository\Site;
use App\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
/**
* class MenuRepositoryQuery.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MenuRepositoryQuery extends RepositoryQuery
{
public function __construct(MenuRepository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'm', $paginator);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository\Site;
use App\Entity\Site\Navigation;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class NavigationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Navigation::class);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Repository\Site;
use App\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
/**
* class NavigationRepositoryQuery.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationRepositoryQuery extends RepositoryQuery
{
public function __construct(NavigationRepository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'n', $paginator);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Repository\Site;
use App\Entity\Site\Node;
use Doctrine\ORM\EntityManagerInterface;
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
class NodeRepository extends NestedTreeRepository
{
public function __construct(EntityManagerInterface $manager)
{
parent::__construct($manager, $manager->getClassMetadata(Node::class));
}
public function urlExists($url, Node $node)
{
$query = $this->createQueryBuilder('n')
->where('n.url = :url')
->setParameter(':url', $url)
;
if ($node->getId()) {
$query
->andWhere('n.id != :id')
->setParameter(':id', $node->getId())
;
}
return $query->getQuery()
->setMaxResults(1)
->getOneOrNullResult()
;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository\Site\Page;
use App\Entity\Site\Page\Block;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class BlockRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Block::class);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Repository\Site\Page;
use App\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
/**
* class BlockRepositoryQuery.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class BlockRepositoryQuery extends RepositoryQuery
{
public function __construct(BlockRepository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'b', $paginator);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository\Site\Page;
use App\Entity\Site\Page\Page;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Page::class);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Repository\Site\Page;
use App\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
/**
* class PageRepositoryQuery.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageRepositoryQuery extends RepositoryQuery
{
public function __construct(PageRepository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'p', $paginator);
}
public function filterById($id)
{
$this
->where('.id = :id')
->setParameter(':id', $id)
;
return $this;
}
}

View file

@ -9,12 +9,6 @@ use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
@ -22,9 +16,6 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
parent::__construct($registry, User::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
{
if (!$user instanceof User) {

View file

@ -0,0 +1,65 @@
<?php
namespace App\Router;
use App\Controller\Site\PageController;
use App\Repository\Site\NavigationRepositoryQuery;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* class SiteRouteLoader.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SiteRouteLoader extends Loader
{
protected NavigationRepositoryQuery $navigationQuery;
protected $isLoaded = false;
public function __construct(NavigationRepositoryQuery $navigationQuery)
{
$this->navigationQuery = $navigationQuery;
}
public function load($resource, ?string $type = null)
{
if (true === $this->isLoaded) {
throw new \RuntimeException('Do not add the "extra" loader twice');
}
$routes = new RouteCollection();
$navigations = $this->navigationQuery->find();
foreach ($navigations as $navigation) {
foreach ($navigation->getMenus() as $menu) {
foreach ($menu->getRootNode()->getAllChildren() as $node) {
$requirements = [];
$defaults = [
'_controller' => PageController::class.'::show',
'_node' => $node->getId(),
'_menu' => $menu->getId(),
'_page' => $node->getPage() ? $node->getPage()->getId() : null,
'_navigation' => $navigation->getId(),
];
$route = new Route($node->getUrl(), $defaults, $requirements);
$route->setHost($navigation->getDomain());
$routes->add('site_page_'.$node->getId(), $route);
}
}
}
$this->isLoaded = true;
return $routes;
}
public function supports($resource, string $type = null)
{
return 'extra' === $type;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Site\Page;
use App\Entity\Site\Page\Block;
use App\Entity\Site\Page\Page;
use App\Form\Site\Page\TextareaBlockType;
use App\Form\Site\Page\TextBlockType;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @ORM\Entity
*/
class SimplePage extends Page
{
public function buildForm(FormBuilderInterface $builder)
{
$builder->add(
'title',
TextBlockType::class,
[
'label' => 'Titre',
'required' => true,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'content',
TextareaBlockType::class,
[
'label' => 'Content',
'options' => [
'attr' => [
'data-tinymce' => '',
'rows' => '18',
],
'constraints' => [
],
],
]
);
}
public function setTitle(Block $block)
{
return $this->setBlock($block);
}
public function getTitle()
{
return $this->getBlock('title');
}
public function setContent(Block $block)
{
return $this->setBlock($block);
}
public function getContent()
{
return $this->getBlock('content');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Site;
/**
* class PageConfiguration.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageConfiguration
{
protected string $className;
protected string $name;
protected array $templates;
public function setClassName(string $className): self
{
$this->className = $className;
return $this;
}
public function getClassName(): string
{
return $this->className;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setTemplates(array $templates): self
{
$this->templates = $templates;
return $this;
}
public function getTemplates(): array
{
return $this->templates;
}
}

48
src/Site/PageLocator.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace App\Site;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* class PageLocator.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageLocator
{
protected array $params;
protected array $pages;
public function __construct(ParameterBagInterface $bag)
{
$this->params = $bag->get('app');
$this->loadPages();
}
public function getPages(): array
{
return $this->pages;
}
public function getPage($className)
{
return $this->pages[$className] ?? null;
}
protected function loadPages(): void
{
$params = $this->params['site']['pages'] ?? [];
foreach ($params as $className => $conf) {
$pageConfiguration = new PageConfiguration();
$pageConfiguration
->setClassName($className)
->setName($conf['name'])
->setTemplates($conf['templates'])
;
$this->pages[$className] = $pageConfiguration;
}
}
}

69
src/Site/SiteRequest.php Normal file
View file

@ -0,0 +1,69 @@
<?php
namespace App\Site;
use App\Entity\Site\Menu;
use App\Entity\Site\Navigation;
use App\Entity\Site\Node;
use App\Entity\Site\Page\Page;
use App\Repository\Site\NavigationRepositoryQuery;
use App\Repository\Site\NodeRepository;
use App\Repository\Site\Page\PageRepositoryQuery;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* class SiteRequest.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SiteRequest
{
protected RequestStack $requestStack;
protected NodeRepository $nodeRepository;
protected NavigationRepositoryQuery $navigationRepositoryQuery;
protected PageRepositoryQuery $pageRepositoryQuery;
public function __construct(RequestStack $requestStack, NodeRepository $nodeRepository)
{
$this->requestStack = $requestStack;
$this->nodeRepository = $nodeRepository;
}
public function getNode(): ?Node
{
$request = $this->requestStack->getCurrentRequest();
if ($request->attributes->has('_node')) {
return $this->nodeRepository->findOneBy([
'id' => $request->attributes->get('_node'),
]);
}
return null;
}
public function getPage(): ?Page
{
$node = $this->getNode();
if ($node && $node->getPage()) {
return $node->getPage();
}
return null;
}
public function getMenu(): ?Menu
{
$node = $this->getNode();
return null !== $node ? $node->getMenu() : null;
}
public function getNavigation(): ?Navigation
{
$menu = $this->getMenu();
return null !== $menu ? $menu->getNavigation() : null;
}
}

31
src/Slugify/Slugify.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace App\Slugify;
use Cocur\Slugify\Slugify as BaseSlugify;
/**
* class Slugify.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class Slugify
{
public function slugify($data)
{
return $this->create()->slugify($data);
}
protected function create(): BaseSlugify
{
$slugify = new BaseSlugify([
'separator' => '-',
'lowercase' => false,
]);
$slugify->activateRuleSet('french');
$slugify->addRule("'", '');
return $slugify;
}
}

View file

@ -5,6 +5,9 @@
"beberlei/assert": {
"version": "v3.3.0"
},
"behat/transliterator": {
"version": "v1.3.0"
},
"bjeavons/zxcvbn-php": {
"version": "1.2.0"
},
@ -102,6 +105,9 @@
"friendsofphp/proxy-manager-lts": {
"version": "v1.0.3"
},
"gedmo/doctrine-extensions": {
"version": "v3.0.3"
},
"khanamiryan/qrcode-detector-decoder": {
"version": "1.0.4"
},
@ -190,6 +196,18 @@
"spomky-labs/otphp": {
"version": "v10.0.1"
},
"stof/doctrine-extensions-bundle": {
"version": "1.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "master",
"version": "1.2",
"ref": "6c1ceb662f8997085f739cd089bfbef67f245983"
},
"files": [
"config/packages/stof_doctrine_extensions.yaml"
]
},
"swiftmailer/swiftmailer": {
"version": "v6.2.7"
},

View file

@ -50,20 +50,44 @@
</li>
</ul>
{% if is_granted('ROLE_WRITER') %}
{% if is_granted('ROLE_ADMIN') %}
<h6 class="sidebar-heading justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Contenu</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ macros.active_class('page', section) }}" href="{{ path('admin_dashboard_index') }}">
<a class="nav-link {{ macros.active_class('site_navigation', section) }}" href="{{ path('admin_site_navigation_index') }}">
<span class="fa fa-globe-europe"></span>
<span class="nav-item-label">Navigations</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ macros.active_class('site_tree', section) }}" href="{{ path('admin_site_tree_index') }}">
<span class="fa fa-sitemap"></span>
<span class="nav-item-label">Arborescence</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ macros.active_class('site_page', section) }}" href="{{ path('admin_site_page_index') }}">
<span class="fa fa-file-alt"></span>
<span class="nav-item-label">Pages</span>
</a>
</li>
</ul>
{% endif %}
{% if is_granted('ROLE_WRITER') %}
<h6 class="sidebar-heading justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Blog</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ macros.active_class('blog_post', section) }}" href="{{ path('admin_blog_post_index') }}">
<span class="fa fa-pen"></span>

View file

@ -0,0 +1,12 @@
<div class="row">
<div class="col-12 p-3">
<div class="row">
{% for item in ['label', 'code', 'domain'] %}
<div class="col-12">
{{ form_row(form[item]) }}
</div>
{% endfor %}
</div>
</div>
</div>

View file

@ -0,0 +1,57 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.label }}</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_site_navigation_index') }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
Retour à la liste
</a>
<a href="{{ path('admin_site_navigation_show', {entity: entity.id}) }}" class="btn btn-secondary">
<span class="fa fa-eye pr-1"></span>
Voir
</a>
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
Enregistrer
</button>
<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>
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" form="form-delete" class="dropdown-item">
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
<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('site/navigation_admin/_form.html.twig') }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
<form method="post" action="{{ path('admin_site_navigation_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>
{% endblock %}

View file

@ -0,0 +1,77 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.getPaginationData.pageCount < 2 %}pb-5{% endif %}">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">Navigations</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_site_navigation_new') }}" class="btn btn-primary">
<span class="fa fa-plus pr-1"></span>
Nouveau
</a>
</div>
</div>
</div>
{{ knp_pagination_render(pager) }}
</div>
<table class="table" data-table-fixed>
<thead class="thead-light">
<tr>
<th class="col-5">Libellé</th>
<th class="col-5">Domaine</th>
<th class="col-2 text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for item in pager %}
{% set edit = path('admin_site_navigation_edit', {entity: item.id}) %}
{% set show = path('admin_site_navigation_show', {entity: item.id}) %}
<tr data-dblclick="{{ edit }}">
<td class="col-6">
<a href="{{ show }}" class="font-weight-bold text-body d-block">
{{ item.label }}
</a>
{{ item.code }}
</td>
<td class="col-4">
<a href="" class="btn btn-sm btn-light">
{{ item.domain }}
</a>
</td>
<td class="col-2 text-right">
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
<span class="fa fa-edit"></span>
</a>
<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('admin_site_navigation_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>
</td>
</tr>
{% 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>
</table>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">Nouvelle navigation</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_site_navigation_index') }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
Retour à la liste
</a>
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
Enregistrer
</button>
</div>
</div>
</div>
</div>
<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('site/navigation_admin/_form.html.twig') }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
{% endblock %}

View file

@ -0,0 +1,48 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.label }}</h1>
</div>:
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_site_navigation_index') }}" class="btn btn-secondary">
<span class="fa fa-list pr-1"></span>
Retour à la liste
</a>
<a href="{{ path('admin_site_navigation_edit', {entity: entity.id}) }}" class="btn btn-primary">
<span class="fa fa-edit pr-1"></span>
Éditer
</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 p-3">
<ul class="list-group">
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Libellé</span>
{{ entity.label }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Code</span>
{{ entity.code }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Nom de domaine</span>
{{ entity.domain }}
</li>
</ul>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,108 @@
{{ form_row(form.label) }}
{{ form_row(form.url) }}
{% if form.position is defined %}
{{ form_row(form.position) }}
{% endif %}
<div class="accordion" id="node-page-action">
<div class="card">
{% set action = form.pageAction[0] %}
{% set options = not entity.id ? {'attr': {'checked': 'checked'}} : {} %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-new">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action, options) }}
</div>
</h2>
</div>
<div id="form-node-page-action-new" class="collapse {% if not entity.id %}show{% endif %}" data-parent="#node-page-action">
<div class="card-body">
{{ form_row(form.pageType) }}
</div>
</div>
</div>
<div class="card">
{% set action = form.pageAction[1] %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-existing">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action) }}
</div>
</h2>
</div>
<div id="form-node-page-action-existing" class="collapse" data-parent="#node-page-action">
<div class="card-body">
{{ form_row(form.pageEntity) }}
</div>
</div>
</div>
<div class="card">
{% set action = form.pageAction[2] %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-none">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action) }}
</div>
</h2>
</div>
<div id="form-node-page-action-none" class="collapse" data-parent="#node-page-action">
<div class="card-body">
Aucune action
</div>
</div>
</div>
{% if entity.id %}
<div class="card">
{% set action = form.pageAction[3] %}
{% set options = {'attr': {'checked': 'checked'}} %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-keep">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action, options) }}
</div>
</h2>
</div>
<div id="form-node-page-action-keep" class="collapse show" data-parent="#node-page-action">
<div class="card-body">
Aucune action
</div>
</div>
</div>
{% endif %}
</div>
{{ form_rest(form) }}

View file

@ -0,0 +1,20 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Édition de « {{ entity.label }} »</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('admin_site_node_edit', {entity: entity.id}) }}" id="form-node-edit" method="POST">
{{ include('site/node_admin/_form.html.twig') }}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="submit" form="form-node-edit" class="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>

View file

@ -0,0 +1,19 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Déplacement de « {{ entity.label }} »</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('admin_site_node_move', {entity: entity.id}) }}" id="form-node-move" method="POST">
{{ form_widget(form) }}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="submit" form="form-node-move" class="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>

View file

@ -0,0 +1,19 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Nouvel élément</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('admin_site_node_new', {node: node.id}) }}" id="form-node-new" method="POST">
{{ include('site/node_admin/_form.html.twig') }}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="submit" form="form-node-new" class="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>

View file

@ -0,0 +1,8 @@
template 1<br>
Node : {{ _node.label }}<br>
Menu : {{ _menu.label }}<br>
Navigation : {{ _navigation.label }}<br>
Page : {{ _page.title.value }}<br>
Content : {{ _page.content.value|raw }}<br>

View file

@ -0,0 +1,8 @@
template 2<br>
Node : {{ _node.label }}<br>
Menu : {{ _menu.label }}<br>
Navigation : {{ _navigation.label }}<br>
Page : {{ _page.title.value }}<br>
Content : {{ _page.content.value|raw }}<br>

View file

@ -0,0 +1,51 @@
{% set formMetas %}
{% for item in ['metaTitle', 'metaDescrition'] %}
{{ form_row(form[item]) }}
{% endfor %}
{% endset %}
{% set formOpenGraph %}
{% for item in ['ogTitle', 'ogDescription'] %}
{{ form_row(form[item]) }}
{% endfor %}
{% endset %}
{% set formOthers %}
{% for item in ['name', 'template'] %}
{{ form_row(form[item]) }}
{% endfor %}
{% endset %}
{% set formSitemap %}
{% endset %}
<div class="row">
<div class="col-8 p-2">
{{ form_widget(form) }}
</div>
<div class="col-4 p-3">
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#form-page-metas">Métas</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#form-page-og">OpenGraph</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#form-page-others">Autres</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active p-3" id="form-page-metas">
{{ formMetas|raw }}
</div>
<div class="tab-pane p-3" id="form-page-og">
{{ formOpenGraph|raw }}
</div>
<div class="tab-pane p-3" id="form-page-others">
{{ formOthers|raw }}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,48 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.name }}</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
Enregistrer
</button>
<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>
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" form="form-delete" class="dropdown-item">
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
<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('site/page_admin/_form.html.twig') }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
<form method="post" action="{{ path('admin_site_page_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>
{% endblock %}

View file

@ -0,0 +1,59 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.getPaginationData.pageCount < 2 %}pb-5{% endif %}">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">Pages</h1>
</div>
</div>
{{ knp_pagination_render(pager) }}
</div>
<table class="table" data-table-fixed>
<thead class="thead-light">
<tr>
<th class="col-10">Nom</th>
<th class="col-2 text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for item in pager %}
{% set edit = path('admin_site_page_edit', {entity: item.id}) %}
<tr data-dblclick="{{ edit }}">
<td class="col-10">
<a href="{{ edit }}" class="font-weight-bold text-body d-block">
{{ item.name }}
</a>
</td>
<td class="col-2 text-right">
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
<span class="fa fa-edit"></span>
</a>
<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('admin_site_page_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>
</td>
</tr>
{% 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>
</table>
{% endblock %}

View file

@ -0,0 +1,200 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<button type="button" class="btn btn-outline-dark dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="font-weight-bold">
{{ navigation.label }}
</span>
</button>
<div class="dropdown-menu">
{% for item in navigations %}
<a href="{{ path('admin_site_tree_navigation', {navigation: item.id}) }}" class="dropdown-item">
{{ item.label }}
</a>
{% endfor %}
</div>
</div>
<div class="ml-auto">
<div class="btn-group">
<button class="btn btn-primary" data-toggle="modal" data-target="#modal-menu-new">
<span class="fa fa-plus pr-1"></span>
Ajouter un menu
</button>
</div>
</div>
</div>
</div>
<div class="row">
{% for menu in navigation.menus %}
<div class="col-12 p-3 m-0">
<div class="d-flex">
<div class="mr-auto">
<div class="h4">
{{ menu.label }}
</div>
</div>
<div class="ml-auto">
<div class="btn-group">
<span class="btn btn-light">
{{ menu.code }}
</span>
<button type="button" class="btn btn-secondary dropdown-toggle " data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" class="dropdown-item" data-toggle="modal" data-target="#modal-menu-edit-{{ menu.id }}">
Modifier
</button>
<button type="submit" form="form-menu-delete-{{ menu.id }}" class="dropdown-item">
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 pl-3 mb-4">
<div class="list-group mr-3">
{% set rootNode = menu.rootNode %}
{% if rootNode %}
{% for node in rootNode.allChildren %}
{% set move = path('admin_site_node_move', {entity: node.id}) %}
{% set edit = path('admin_site_node_edit', {entity: node.id}) %}
{% set new = path('admin_site_node_new', {node: node.id}) %}
<div class="list-group-item">
<div class="float-right">
{% if node.page %}
<a href="{{ path('admin_site_page_edit', {entity: node.page.id}) }}" class="btn btn-sm btn-warning text-white mr-1">
<span class="fa fa-file-alt"></span>
Page
</a>
{% endif %}
<button data-modal="{{ edit }}" type="submit" class="btn btn-sm btn-success mr-1">
<span data-modal="{{ edit }}" class="fa fa-pen"></span>
Éditer
</button>
<button type="submit" form="form-node-visibility-{{ node.id }}" class="btn btn-sm btn-light border-dark mr-1">
{% if node.isVisible %}
<span class="fa fa-eye"></span>
Visible
{% else %}
<span class="fa fa-eye-slash"></span>
Caché
{% endif %}
</button>
<button data-modal="{{ move }}" type="submit" class="btn btn-sm btn-dark mr-1">
<span data-modal="{{ move }}" class="fa fa-arrows-alt"></span>
</button>
<button data-modal="{{ new }}" type="submit" class="btn btn-sm btn-primary mr-1">
<span data-modal="{{ new }}" class="fa fa-plus"></span>
</button>
<button type="submit" form="form-node-delete-{{ node.id }}" class="btn btn-sm btn-danger">
<span class="fa fa-trash"></span>
Supprimer
</button>
</div>
<div class="d block" style="padding-left: {{ (node.treeLevel - 1) * 30 }}px">
{{ node.label }}
{% if node.url %}
<a href="{{ node.url }}" target="_blank" class="ml-3 btn btn-sm btn-light">
{{ node.url }}
</a>
{% endif %}
</div>
<form method="post" action="{{ path('admin_site_node_toggle_visibility', {entity: node.id}) }}" id="form-node-visibility-{{ node.id }}">
<input type="hidden" name="_token" value="{{ csrf_token('toggle_visibility' ~ node.id) }}">
</form>
<form method="post" action="{{ path('admin_site_node_delete', {entity: node.id}) }}" id="form-node-delete-{{ node.id }}" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ node.id) }}">
</form>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% else %}
<div 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-4">
<button class="btn btn-primary" data-toggle="modal" data-target="#modal-menu-new">
<span class="fa fa-plus pr-1"></span>
Ajouter un menu
</button>
</div>
</div>
{% endfor %}
</div>
<div class="modal" id="modal-menu-new">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Nouveau menu</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('admin_site_menu_new', {navigation: navigation.id}) }}" id="form-menu-new" method="POST">
{{ form_widget(forms.menu) }}
</form>
</div>
<div class="modal-footer">
<button type="button" form="form-menu-new" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="submit" form="form-menu-new" class="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>
</div>
{% for menuId, form in forms.menus %}
<div class="modal" id="modal-menu-edit-{{ menuId }}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ form.vars.data.label }}</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('admin_site_menu_edit', {entity: menuId}) }}" id="form-menu-edit-{{ menuId }}" method="POST">
{{ form_widget(form) }}
</form>
</div>
<div class="modal-footer">
<button type="button" form="form-menu-edit-{{ menuId }}" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="submit" form="form-menu-edit-{{ menuId }}" class="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>
</div>
<form method="post" action="{{ path('admin_site_menu_delete', {entity: menuId}) }}" id="form-menu-delete-{{ menuId }}" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ menuId) }}">
</form>
{% endfor %}
{% endblock %}

View file

@ -30,7 +30,7 @@
</button>
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" form="form-resetting-request" class="dropdown-item">
Envoye un mail de récupération
Envoyer un mail de récupération
</button>
<button type="submit" form="form-delete" class="dropdown-item">
Supprimer