Browse Source

add navigation CRUD; add menu CRUD; add base of node CRUD

develop
Simon Vieille 2 months ago
parent
commit
8a7e323501
  1. 71
      assets/css/admin.scss
  2. 17
      assets/js/addons/modal.js
  3. 1
      composer.json
  4. 1
      config/bundles.php
  5. 6
      config/packages/doctrine.yaml
  6. 4
      config/packages/stof_doctrine_extensions.yaml
  7. 7
      config/services.yaml
  8. 2
      src/Controller/Blog/CategoryAdminController.php
  9. 3
      src/Controller/Blog/PostAdminController.php
  10. 82
      src/Controller/Site/MenuAdminController.php
  11. 116
      src/Controller/Site/NavigationAdminController.php
  12. 178
      src/Controller/Site/NodeAdminController.php
  13. 72
      src/Controller/Site/TreeAdminController.php
  14. 2
      src/Controller/User/UserAdminController.php
  15. 140
      src/Entity/Site/Menu.php
  16. 122
      src/Entity/Site/Navigation.php
  17. 263
      src/Entity/Site/Node.php
  18. 4
      src/EventSuscriber/Account/PasswordRequestEventSubscriber.php
  19. 8
      src/EventSuscriber/Blog/PostEventSubscriber.php
  20. 72
      src/EventSuscriber/Site/MenuEventSubscriber.php
  21. 61
      src/EventSuscriber/Site/NodeEventSubscriber.php
  22. 25
      src/Factory/Site/MenuFactory.php
  23. 18
      src/Factory/Site/NavigationFactory.php
  24. 25
      src/Factory/Site/NodeFactory.php
  25. 51
      src/Form/Site/MenuType.php
  26. 65
      src/Form/Site/NavigationType.php
  27. 62
      src/Form/Site/NodeMoveType.php
  28. 72
      src/Form/Site/NodeType.php
  29. 35
      src/Manager/EntityManager.php
  30. 6
      src/Repository/Blog/CategoryRepository.php
  31. 6
      src/Repository/Blog/PostRepository.php
  32. 10
      src/Repository/RepositoryQuery.php
  33. 15
      src/Repository/Site/MenuRepository.php
  34. 19
      src/Repository/Site/MenuRepositoryQuery.php
  35. 15
      src/Repository/Site/NavigationRepository.php
  36. 19
      src/Repository/Site/NavigationRepositoryQuery.php
  37. 15
      src/Repository/Site/NodeRepository.php
  38. 9
      src/Repository/UserRepository.php
  39. 18
      symfony.lock
  40. 10
      templates/admin/layout.html.twig
  41. 12
      templates/site/navigation_admin/_form.html.twig
  42. 57
      templates/site/navigation_admin/edit.html.twig
  43. 77
      templates/site/navigation_admin/index.html.twig
  44. 39
      templates/site/navigation_admin/new.html.twig
  45. 48
      templates/site/navigation_admin/show.html.twig
  46. 19
      templates/site/node_admin/move.html.twig
  47. 19
      templates/site/node_admin/new.html.twig
  48. 177
      templates/site/tree_admin/navigation.html.twig

71
assets/css/admin.scss

@ -352,3 +352,74 @@ 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;
}
}
}
}

17
assets/js/addons/modal.js

@ -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');
});
});
}

1
composer.json

@ -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.*",

1
config/bundles.php

@ -16,4 +16,5 @@ 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],
];

6
config/packages/doctrine.yaml

@ -16,3 +16,9 @@ doctrine:
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
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

4
config/packages/stof_doctrine_extensions.yaml

@ -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

7
config/services.yaml

@ -27,5 +27,12 @@ services:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
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

2
src/Controller/Blog/CategoryAdminController.php

@ -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');

3
src/Controller/Blog/PostAdminController.php

@ -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;
@ -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');

82
src/Controller/Site/MenuAdminController.php

@ -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 '';
}
}

116
src/Controller/Site/NavigationAdminController.php

@ -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';
}
}

178
src/Controller/Site/NodeAdminController.php

@ -0,0 +1,178 @@
<?php
namespace App\Controller\Site;
use App\Controller\Admin\AdminController;
use App\Entity\Site\Node;
use App\Entity\Site\Node as Entity;
use App\Event\EntityManager\EntityManagerEvent;
use App\Factory\Site\NodeFactory as EntityFactory;
use App\Form\Site\NodeMoveType;
use App\Form\Site\NodeType as EntityType;
use App\Manager\EntityManager;
use App\Repository\Site\NodeRepository;
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,
EntityManager $entityManager,
NodeRepository $nodeRepository,
Request $request
): Response {
$entity = $factory->create($node->getMenu());
$form = $this->createForm(EntityType::class, $entity);
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);
$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' => $node->getMenu()->getNavigation()->getId(),
]);
}
return $this->render('site/node_admin/new.html.twig', [
'form' => $form->createView(),
'node' => $node,
'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 '';
}
}

72
src/Controller/Site/TreeAdminController.php

@ -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';
}
}

2
src/Controller/User/UserAdminController.php

@ -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');

140
src/Entity/Site/Menu.php

@ -0,0 +1,140 @@
<?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)
*/
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;
}
}

122
src/Entity/Site/Navigation.php

@ -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;
}
}

263
src/Entity/Site/Node.php

@ -0,0 +1,263 @@
<?php
namespace App\Entity\Site;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
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;
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());
}
}

4
src/EventSuscriber/AccountPasswordRequestEventSubscriber.php → src/EventSuscriber/Account/PasswordRequestEventSubscriber.php

@ -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;

8
src/EventSuscriber/BlogPostEventSubscriber.php → src/EventSuscriber/Blog/PostEventSubscriber.php

@ -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 extends EntityManagerEventSubscriber
class PostEventSubscriber extends EntityManagerEventSubscriber
{
protected Filesystem $filesystem;
protected PostRepositoryQuery $query;

72
src/EventSuscriber/Site/MenuEventSubscriber.php

@ -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);
}
}

61
src/EventSuscriber/Site/NodeEventSubscriber.php

@ -0,0 +1,61 @@
<?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;
/**
* class NodeEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NodeEventSubscriber 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 Node;
}
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);
}
}

25
src/Factory/Site/MenuFactory.php

@ -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;
}
}

18
src/Factory/Site/NavigationFactory.php

@ -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();
}
}

25
src/Factory/Site/NodeFactory.php

@ -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;
}
}

51
src/Form/Site/MenuType.php

@ -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,
]);
}
}

65
src/Form/Site/NavigationType.php

@ -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,
]);
}
}

62
src/Form/Site/NodeMoveType.php

@ -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,
]);
}
}

72
src/Form/Site/NodeType.php

@ -0,0 +1,72 @@
<?php
namespace App\Form\Site;