From 40782e56ab4a506b1a8cc90cc4e8f7e5e6b6447e Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 16 Mar 2021 16:46:39 +0100 Subject: [PATCH 1/7] add blog entities; add repository query logic --- .../Blog/CategoryAdminController.php | 27 +++ src/Controller/Blog/PostAdminController.php | 27 +++ src/Doctrine/Timestampable.php | 57 +++++ src/Entity/Blog/Category.php | 92 ++++++++ src/Entity/Blog/Post.php | 198 ++++++++++++++++++ src/Entity/User.php | 46 ++++ src/Repository/Blog/CategoryRepository.php | 50 +++++ .../Blog/CategoryRepositoryQuery.php | 20 ++ src/Repository/Blog/PostRepository.php | 50 +++++ src/Repository/Blog/PostRepositoryQuery.php | 20 ++ src/Repository/RepositoryQuery.php | 90 ++++++++ src/Repository/UserRepositoryQuery.php | 18 ++ templates/admin/layout.html.twig | 4 +- templates/blog/category_admin/index.html.twig | 1 + templates/blog/post_admin/index.html.twig | 1 + 15 files changed, 699 insertions(+), 2 deletions(-) create mode 100644 src/Controller/Blog/CategoryAdminController.php create mode 100644 src/Controller/Blog/PostAdminController.php create mode 100644 src/Doctrine/Timestampable.php create mode 100644 src/Entity/Blog/Category.php create mode 100644 src/Entity/Blog/Post.php create mode 100644 src/Repository/Blog/CategoryRepository.php create mode 100644 src/Repository/Blog/CategoryRepositoryQuery.php create mode 100644 src/Repository/Blog/PostRepository.php create mode 100644 src/Repository/Blog/PostRepositoryQuery.php create mode 100644 src/Repository/RepositoryQuery.php create mode 100644 src/Repository/UserRepositoryQuery.php create mode 100644 templates/blog/category_admin/index.html.twig create mode 100644 templates/blog/post_admin/index.html.twig diff --git a/src/Controller/Blog/CategoryAdminController.php b/src/Controller/Blog/CategoryAdminController.php new file mode 100644 index 0000000..9403182 --- /dev/null +++ b/src/Controller/Blog/CategoryAdminController.php @@ -0,0 +1,27 @@ +render('blog/category_admin/index.html.twig', [ + ]); + } + + public function getSection(): string + { + return 'blog_category'; + } +} diff --git a/src/Controller/Blog/PostAdminController.php b/src/Controller/Blog/PostAdminController.php new file mode 100644 index 0000000..c07e7bf --- /dev/null +++ b/src/Controller/Blog/PostAdminController.php @@ -0,0 +1,27 @@ +render('blog/post_admin/index.html.twig', [ + ]); + } + + public function getSection(): string + { + return 'blog_post'; + } +} diff --git a/src/Doctrine/Timestampable.php b/src/Doctrine/Timestampable.php new file mode 100644 index 0000000..4109114 --- /dev/null +++ b/src/Doctrine/Timestampable.php @@ -0,0 +1,57 @@ +createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + + /** + * @ORM\PreUpdate + */ + public function onPreUpdate(): void + { + $this->updatedAt = new \DateTime(); + } + + public function setCreatedAt(?\DateTime $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getCreatedAt():?\DateTime + { + return $this->createdAt; + } + + public function setUpdatedAt(?\DateTime $updatedAt): self + { + $this->updatedAt = $updatedAt; + + return $this; + } + + public function getUpdatedAt():?\DateTime + { + return $this->updatedAt; + } +} diff --git a/src/Entity/Blog/Category.php b/src/Entity/Blog/Category.php new file mode 100644 index 0000000..efdde93 --- /dev/null +++ b/src/Entity/Blog/Category.php @@ -0,0 +1,92 @@ +id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getSubTitle(): ?string + { + return $this->subTitle; + } + + public function setSubTitle(?string $subTitle): self + { + $this->subTitle = $subTitle; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setSlug(?string $slug): self + { + $this->slug = $slug; + + return $this; + } +} diff --git a/src/Entity/Blog/Post.php b/src/Entity/Blog/Post.php new file mode 100644 index 0000000..d89c9cb --- /dev/null +++ b/src/Entity/Blog/Post.php @@ -0,0 +1,198 @@ +id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getSubTitle(): ?string + { + return $this->subTitle; + } + + public function setSubTitle(?string $subTitle): self + { + $this->subTitle = $subTitle; + + return $this; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(?string $content): self + { + $this->content = $content; + + return $this; + } + + public function getMetaDescription(): ?string + { + return $this->metaDescription; + } + + public function setMetaDescription(?string $metaDescription): self + { + $this->metaDescription = $metaDescription; + + return $this; + } + + public function getStatus(): ?int + { + return $this->status; + } + + public function setStatus(int $status): self + { + $this->status = $status; + + return $this; + } + + public function getImage(): ?string + { + return $this->image; + } + + public function setImage(?string $image): self + { + $this->image = $image; + + return $this; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setSlug(?string $slug): self + { + $this->slug = $slug; + + return $this; + } + + public function getPublishedAt(): ?\DateTimeInterface + { + return $this->publishedAt; + } + + public function setPublishedAt(?\DateTimeInterface $publishedAt): self + { + $this->publishedAt = $publishedAt; + + return $this; + } + + public function getImageCaption(): ?string + { + return $this->imageCaption; + } + + public function setImageCaption(?string $imageCaption): self + { + $this->imageCaption = $imageCaption; + + return $this; + } + + public function getAuthor(): ?User + { + return $this->author; + } + + public function setAuthor(?User $author): self + { + $this->author = $author; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index cb6af78..b56c63d 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,11 +2,15 @@ namespace App\Entity; +use App\Entity\Blog\Post; use App\Repository\UserRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration; use Symfony\Component\Security\Core\User\UserInterface; +use App\Doctrine\Timestampable; /** * @ORM\Entity(repositoryClass=UserRepository::class) @@ -62,6 +66,18 @@ class User implements UserInterface, TwoFactorInterface, Entity */ private $isAdmin; + /** + * @ORM\OneToMany(targetEntity=Post::class, mappedBy="author") + */ + private $posts; + + public function __construct() + { + $this->posts = new ArrayCollection(); + } + + use Timestampable; + public function getId(): ?int { return $this->id; @@ -241,4 +257,34 @@ class User implements UserInterface, TwoFactorInterface, Entity return $this; } + + /** + * @return Collection|Post[] + */ + public function getPosts(): Collection + { + return $this->posts; + } + + public function addPost(Post $post): self + { + if (!$this->posts->contains($post)) { + $this->posts[] = $post; + $post->setAuthor($this); + } + + return $this; + } + + public function removePost(Post $post): self + { + if ($this->posts->removeElement($post)) { + // set the owning side to null (unless already changed) + if ($post->getAuthor() === $this) { + $post->setAuthor(null); + } + } + + return $this; + } } diff --git a/src/Repository/Blog/CategoryRepository.php b/src/Repository/Blog/CategoryRepository.php new file mode 100644 index 0000000..8928b3b --- /dev/null +++ b/src/Repository/Blog/CategoryRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('c.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Category + { + return $this->createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Blog/CategoryRepositoryQuery.php b/src/Repository/Blog/CategoryRepositoryQuery.php new file mode 100644 index 0000000..8cdc3c3 --- /dev/null +++ b/src/Repository/Blog/CategoryRepositoryQuery.php @@ -0,0 +1,20 @@ + + */ +class CategoryRepositoryQuery extends RepositoryQuery +{ + public function __construct(CategoryRepositoryQuery $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'c', $paginator); + } +} diff --git a/src/Repository/Blog/PostRepository.php b/src/Repository/Blog/PostRepository.php new file mode 100644 index 0000000..e1382d1 --- /dev/null +++ b/src/Repository/Blog/PostRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('p.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Post + { + return $this->createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/src/Repository/Blog/PostRepositoryQuery.php b/src/Repository/Blog/PostRepositoryQuery.php new file mode 100644 index 0000000..4c386b5 --- /dev/null +++ b/src/Repository/Blog/PostRepositoryQuery.php @@ -0,0 +1,20 @@ + + */ +class PostRepositoryQuery extends RepositoryQuery +{ + public function __construct(PostRepository $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'p', $paginator); + } +} diff --git a/src/Repository/RepositoryQuery.php b/src/Repository/RepositoryQuery.php new file mode 100644 index 0000000..88b1c8f --- /dev/null +++ b/src/Repository/RepositoryQuery.php @@ -0,0 +1,90 @@ + + */ +abstract class RepositoryQuery +{ + protected ServiceEntityRepository $repository; + protected QueryBuilder $query; + protected PaginatorInterface $paginator; + protected string $id; + + public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null) + { + $this->repository = $repository; + $this->query = $repository->createQueryBuilder($id); + $this->paginator = $paginator; + $this->id = $id; + } + + public function create() + { + $class = get_called_class(); + + return new $class($this->repository, $this->paginator); + } + + public function __call(string $name, $params): self + { + $fn = function(&$data) { + if (is_string($data)) { + $words = explode(' ', $data); + + foreach ($words as $k => $v) { + if (isset($v[0]) && $v[0] === '.') { + $words[$k] = $this->id.$v; + } + } + + $data = implode(' ', $words); + } else { + foreach ($data as $k => $v) { + $fn($data[$k]); + } + } + + return $data; + }; + + foreach ($params as $key => $value) + { + $fn($params[$key]); + } + + call_user_func_array([$this->query, $name], $params); + + return $this; + } + + public function call(callable $fn): self + { + $fn($this->query, $this); + + return $this; + } + + public function findOne() + { + return $this->query->getQuery()->getOneOrNullResult(); + } + + public function find() + { + return $this->query->getQuery()->getResult(); + } + + public function paginate(int $page = 1, int $limit = 20) + { + return $this->paginator->paginate($this->query->getQuery(), $page, $limit); + } +} diff --git a/src/Repository/UserRepositoryQuery.php b/src/Repository/UserRepositoryQuery.php new file mode 100644 index 0000000..a4ca016 --- /dev/null +++ b/src/Repository/UserRepositoryQuery.php @@ -0,0 +1,18 @@ + + */ +class UserRepositoryQuery extends RepositoryQuery +{ + public function __construct(UserRepository $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'u', $paginator); + } +} diff --git a/templates/admin/layout.html.twig b/templates/admin/layout.html.twig index 5ec8beb..61dd2ef 100644 --- a/templates/admin/layout.html.twig +++ b/templates/admin/layout.html.twig @@ -64,7 +64,7 @@ - -
- - - - - + + + + {% for item in range(1, 20) %} - - - {% endfor %} From f952403f83d778144ad9a5c1199ff4d93ce11dc0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 17 Mar 2021 12:43:54 +0100 Subject: [PATCH 4/7] add blog category CRUD --- src/Command/TestCommand.php | 43 ------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 src/Command/TestCommand.php diff --git a/src/Command/TestCommand.php b/src/Command/TestCommand.php deleted file mode 100644 index ca239e2..0000000 --- a/src/Command/TestCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -userRepositoryQuery = $userRepositoryQuery; - } - - protected function configure() - { - $this - ->setDescription(self::$defaultDescription) - ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') - ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $query = $this->userRepositoryQuery->create(); - - return Command::SUCCESS; - } -} From e0d58349de2d91d79c29321e9430383960a16d7f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 17 Mar 2021 12:44:02 +0100 Subject: [PATCH 5/7] add blog category CRUD --- .gitignore | 1 + assets/js/addons/editor.js | 2 +- config/packages/twig.yaml | 1 + package.json | 2 +- public/vendor/tinymce | 1 + .../Blog/CategoryAdminController.php | 99 ++++++++++++++++++- src/Doctrine/Timestampable.php | 4 +- src/Entity/Blog/Category.php | 42 +++++++- src/Entity/Blog/Post.php | 42 +++++++- .../{Entity.php => EntityInterface.php} | 2 +- src/Entity/User.php | 7 +- .../EntityManager/EntityManagerEvent.php | 8 +- src/Factory/Blog/CategoryFactory.php | 18 ++++ src/Form/Blog/CategoryType.php | 79 +++++++++++++++ src/Manager/EntityManager.php | 12 +-- .../Blog/CategoryRepositoryQuery.php | 3 +- src/Repository/Blog/PostRepositoryQuery.php | 17 +++- src/Repository/RepositoryQuery.php | 24 +++-- templates/admin/layout.html.twig | 2 +- templates/blog/category_admin/_form.html.twig | 17 ++++ templates/blog/category_admin/edit.html.twig | 57 +++++++++++ templates/blog/category_admin/index.html.twig | 78 ++++++++++----- templates/blog/category_admin/new.html.twig | 39 ++++++++ templates/blog/category_admin/show.html.twig | 83 ++++++++++++++++ yarn.lock | 8 +- 25 files changed, 577 insertions(+), 71 deletions(-) create mode 120000 public/vendor/tinymce rename src/Entity/{Entity.php => EntityInterface.php} (56%) create mode 100644 src/Factory/Blog/CategoryFactory.php create mode 100644 src/Form/Blog/CategoryType.php create mode 100644 templates/blog/category_admin/_form.html.twig create mode 100644 templates/blog/category_admin/edit.html.twig create mode 100644 templates/blog/category_admin/new.html.twig create mode 100644 templates/blog/category_admin/show.html.twig diff --git a/.gitignore b/.gitignore index ba27aaf..281da24 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /.env.*.local /config/secrets/prod/prod.decrypt.private.php /public/bundles/ +/src/Command/TestCommand.php /var/ /vendor/ ###< symfony/framework-bundle ### diff --git a/assets/js/addons/editor.js b/assets/js/addons/editor.js index 53c5997..75c06f0 100644 --- a/assets/js/addons/editor.js +++ b/assets/js/addons/editor.js @@ -5,7 +5,7 @@ module.exports = function() { tinymce.init({ selector: '*[data-tinymce]', - base_url: '/nm/tinymce/', + base_url: '/vendor/tinymce/', cache_suffix: '?v=4.1.6', language: 'fr_FR', plugins: 'print preview importcss searchreplace visualblocks visualchars fullscreen template table charmap hr pagebreak nonbreaking toc insertdatetime advlist lists wordcount textpattern noneditable help charmap quickbars', diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index b3cdf30..90c5321 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,2 +1,3 @@ twig: default_path: '%kernel.project_dir%/templates' + form_themes: ['bootstrap_4_layout.html.twig'] diff --git a/package.json b/package.json index 3537b19..c154a13 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "jquery": "^3.6.0", "popper.js": "^1.16.0", "qrcodejs": "^1.0.0", - "tinymce": "^5.2.0", + "tinymce": "^5.7.1", "vanillajs-datepicker": "^1.1.2", "zxcvbn": "^4.4.2" } diff --git a/public/vendor/tinymce b/public/vendor/tinymce new file mode 120000 index 0000000..3da1341 --- /dev/null +++ b/public/vendor/tinymce @@ -0,0 +1 @@ +../../node_modules/tinymce \ No newline at end of file diff --git a/src/Controller/Blog/CategoryAdminController.php b/src/Controller/Blog/CategoryAdminController.php index 9403182..d9fc9bd 100644 --- a/src/Controller/Blog/CategoryAdminController.php +++ b/src/Controller/Blog/CategoryAdminController.php @@ -3,6 +3,13 @@ namespace App\Controller\Blog; use App\Controller\Admin\AdminController; +use App\Entity\Blog\Category as Entity; +use App\Factory\Blog\CategoryFactory as EntityFactory; +use App\Form\Blog\CategoryType as EntityType; +use App\Manager\EntityManager; +use App\Repository\Blog\CategoryRepositoryQuery as RepositoryQuery; +use App\Repository\Blog\PostRepositoryQuery; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -12,14 +19,102 @@ use Symfony\Component\Routing\Annotation\Route; class CategoryAdminController extends AdminController { /** - * @Route("/", name="admin_blog_category_index") + * @Route("/{page}", name="admin_blog_category_index", requirements={"page": "\d+"}) */ - public function index(): Response + public function index(int $page = 1, RepositoryQuery $query, Request $request): Response { + $pager = $query->paginate($page); + return $this->render('blog/category_admin/index.html.twig', [ + 'pager' => $pager, ]); } + /** + * @Route("/new", name="admin_blog_category_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)->flush()->clear(); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_blog_category_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('blog/category_admin/new.html.twig', [ + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_blog_category_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)->flush()->clear(); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_blog_category_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('blog/category_admin/edit.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/show/{entity}", name="admin_blog_category_show") + */ + public function show(Entity $entity, PostRepositoryQuery $postQuery): Response + { + $posts = $postQuery->create() + ->orderBy('.publishedAt', 'DESC') + ->inCategory($entity) + ->paginate(1, 10) + ; + + return $this->render('blog/category_admin/show.html.twig', [ + 'entity' => $entity, + 'posts' => $posts, + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_blog_category_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)->flush()->clear(); + + $this->addFlash('success', 'Données supprimées.'); + } + + return $this->redirectToRoute('admin_blog_category_index'); + } + public function getSection(): string { return 'blog_category'; diff --git a/src/Doctrine/Timestampable.php b/src/Doctrine/Timestampable.php index 4109114..6fafc6e 100644 --- a/src/Doctrine/Timestampable.php +++ b/src/Doctrine/Timestampable.php @@ -38,7 +38,7 @@ trait Timestampable return $this; } - public function getCreatedAt():?\DateTime + public function getCreatedAt(): ?\DateTime { return $this->createdAt; } @@ -50,7 +50,7 @@ trait Timestampable return $this; } - public function getUpdatedAt():?\DateTime + public function getUpdatedAt(): ?\DateTime { return $this->updatedAt; } diff --git a/src/Entity/Blog/Category.php b/src/Entity/Blog/Category.php index efdde93..4324f31 100644 --- a/src/Entity/Blog/Category.php +++ b/src/Entity/Blog/Category.php @@ -2,13 +2,16 @@ namespace App\Entity\Blog; +use App\Entity\EntityInterface; use App\Repository\Blog\CategoryRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass=CategoryRepository::class) */ -class Category +class Category implements EntityInterface { /** * @ORM\Id @@ -37,6 +40,16 @@ class Category */ private $slug; + /** + * @ORM\ManyToMany(targetEntity=Post::class, mappedBy="categories") + */ + private $posts; + + public function __construct() + { + $this->posts = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -89,4 +102,31 @@ class Category return $this; } + + /** + * @return Collection|Post[] + */ + public function getPosts(): Collection + { + return $this->posts; + } + + public function addPost(Post $post): self + { + if (!$this->posts->contains($post)) { + $this->posts[] = $post; + $post->addCategory($this); + } + + return $this; + } + + public function removePost(Post $post): self + { + if ($this->posts->removeElement($post)) { + $post->removeCategory($this); + } + + return $this; + } } diff --git a/src/Entity/Blog/Post.php b/src/Entity/Blog/Post.php index d89c9cb..27d463d 100644 --- a/src/Entity/Blog/Post.php +++ b/src/Entity/Blog/Post.php @@ -2,16 +2,20 @@ namespace App\Entity\Blog; +use App\Doctrine\Timestampable; +use App\Entity\EntityInterface; use App\Entity\User; use App\Repository\Blog\PostRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use App\Doctrine\Timestampable; /** * @ORM\Entity(repositoryClass=PostRepository::class) */ -class Post +class Post implements EntityInterface { + use Timestampable; /** * @ORM\Id * @ORM\GeneratedValue @@ -69,7 +73,15 @@ class Post */ private $author; - use Timestampable; + /** + * @ORM\ManyToMany(targetEntity=Category::class, inversedBy="posts") + */ + private $categories; + + public function __construct() + { + $this->categories = new ArrayCollection(); + } public function getId(): ?int { @@ -195,4 +207,28 @@ class Post return $this; } + + /** + * @return Collection|Category[] + */ + public function getCategories(): Collection + { + return $this->categories; + } + + public function addCategory(Category $category): self + { + if (!$this->categories->contains($category)) { + $this->categories[] = $category; + } + + return $this; + } + + public function removeCategory(Category $category): self + { + $this->categories->removeElement($category); + + return $this; + } } diff --git a/src/Entity/Entity.php b/src/Entity/EntityInterface.php similarity index 56% rename from src/Entity/Entity.php rename to src/Entity/EntityInterface.php index f6ad59b..72e60b8 100644 --- a/src/Entity/Entity.php +++ b/src/Entity/EntityInterface.php @@ -2,6 +2,6 @@ namespace App\Entity; -interface Entity +interface EntityInterface { } diff --git a/src/Entity/User.php b/src/Entity/User.php index b56c63d..8d03289 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,6 +2,7 @@ namespace App\Entity; +use App\Doctrine\Timestampable; use App\Entity\Blog\Post; use App\Repository\UserRepository; use Doctrine\Common\Collections\ArrayCollection; @@ -10,14 +11,14 @@ use Doctrine\ORM\Mapping as ORM; use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration; use Symfony\Component\Security\Core\User\UserInterface; -use App\Doctrine\Timestampable; /** * @ORM\Entity(repositoryClass=UserRepository::class) * @ORM\Table(name="`user`") */ -class User implements UserInterface, TwoFactorInterface, Entity +class User implements UserInterface, TwoFactorInterface, EntityInterface { + use Timestampable; /** * @ORM\Id * @ORM\GeneratedValue @@ -76,8 +77,6 @@ class User implements UserInterface, TwoFactorInterface, Entity $this->posts = new ArrayCollection(); } - use Timestampable; - public function getId(): ?int { return $this->id; diff --git a/src/Event/EntityManager/EntityManagerEvent.php b/src/Event/EntityManager/EntityManagerEvent.php index f858324..29cf9e3 100644 --- a/src/Event/EntityManager/EntityManagerEvent.php +++ b/src/Event/EntityManager/EntityManagerEvent.php @@ -2,7 +2,7 @@ namespace App\Event\EntityManager; -use App\Entity\Entity; +use App\Entity\EntityInterface; use Symfony\Contracts\EventDispatcher\Event; /** @@ -16,14 +16,14 @@ class EntityManagerEvent extends Event const UPDATE_EVENT = 'entity_manager_event.update'; const DELETE_EVENT = 'entity_manager_event.delete'; - protected Entity $entity; + protected EntityInterface $entity; - public function __construct(Entity $entity) + public function __construct(EntityInterface $entity) { $this->entity = $entity; } - public function getEntity(): Entity + public function getEntity(): EntityInterface { return $this->entity; } diff --git a/src/Factory/Blog/CategoryFactory.php b/src/Factory/Blog/CategoryFactory.php new file mode 100644 index 0000000..eb21c80 --- /dev/null +++ b/src/Factory/Blog/CategoryFactory.php @@ -0,0 +1,18 @@ + + */ +class CategoryFactory +{ + public function create(): Category + { + return new Category(); + } +} diff --git a/src/Form/Blog/CategoryType.php b/src/Form/Blog/CategoryType.php new file mode 100644 index 0000000..48e17d2 --- /dev/null +++ b/src/Form/Blog/CategoryType.php @@ -0,0 +1,79 @@ +add( + 'title', + TextType::class, + [ + 'label' => 'Titre', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'subTitle', + TextareaType::class, + [ + 'label' => 'Sous-titre', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'description', + TextareaType::class, + [ + 'label' => 'Description', + 'required' => false, + 'attr' => [ + 'data-tinymce' => '', + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'slug', + TextType::class, + [ + 'label' => 'Slug', + 'required' => false, + 'help' => 'Laisser vide pour une génération automatique', + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Category::class, + ]); + } +} diff --git a/src/Manager/EntityManager.php b/src/Manager/EntityManager.php index 19aba0d..a841dbd 100644 --- a/src/Manager/EntityManager.php +++ b/src/Manager/EntityManager.php @@ -2,7 +2,7 @@ namespace App\Manager; -use App\Entity\Entity; +use App\Entity\EntityInterface; use App\Event\EntityManager\EntityManagerEvent; use Doctrine\ORM\EntityManager as DoctrineEntityManager; use Doctrine\ORM\EntityManagerInterface; @@ -25,7 +25,7 @@ class EntityManager $this->entityManager = $entityManager; } - public function create(Entity $entity): self + public function create(EntityInterface $entity): self { $this->persist($entity); $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::CREATE_EVENT); @@ -33,7 +33,7 @@ class EntityManager return $this; } - public function update(Entity $entity): self + public function update(EntityInterface $entity): self { $this->persist($entity); $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::UPDATE_EVENT); @@ -41,9 +41,9 @@ class EntityManager return $this; } - public function delete(Entity $entity): self + public function delete(EntityInterface $entity): self { - $this->remove($entity); + $this->entityManager->remove($entity); $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT); return $this; @@ -63,7 +63,7 @@ class EntityManager return $this; } - protected function persist(Entity $entity) + protected function persist(EntityInterface $entity) { $this->entityManager->persist($entity); } diff --git a/src/Repository/Blog/CategoryRepositoryQuery.php b/src/Repository/Blog/CategoryRepositoryQuery.php index 2292122..2b86f71 100644 --- a/src/Repository/Blog/CategoryRepositoryQuery.php +++ b/src/Repository/Blog/CategoryRepositoryQuery.php @@ -2,9 +2,8 @@ namespace App\Repository\Blog; -use Knp\Component\Pager\PaginatorInterface; -use App\Repository\Blog\PostRepository; use App\Repository\RepositoryQuery; +use Knp\Component\Pager\PaginatorInterface; /** * class CategoryRepositoryQuery. diff --git a/src/Repository/Blog/PostRepositoryQuery.php b/src/Repository/Blog/PostRepositoryQuery.php index 4c386b5..4d45843 100644 --- a/src/Repository/Blog/PostRepositoryQuery.php +++ b/src/Repository/Blog/PostRepositoryQuery.php @@ -2,9 +2,9 @@ namespace App\Repository\Blog; -use Knp\Component\Pager\PaginatorInterface; -use App\Repository\Blog\PostRepository; +use App\Entity\Blog\Category; use App\Repository\RepositoryQuery; +use Knp\Component\Pager\PaginatorInterface; /** * class PostRepositoryQuery. @@ -17,4 +17,17 @@ class PostRepositoryQuery extends RepositoryQuery { parent::__construct($repository, 'p', $paginator); } + + public function inCategory(Category $category) + { + $c = 'c'.mt_rand(); + + $this + ->innerJoin('p.categories', $c) + ->andWhere($c.'.id = :category') + ->setParameter(':category', $category->getId()) + ; + + return $this; + } } diff --git a/src/Repository/RepositoryQuery.php b/src/Repository/RepositoryQuery.php index 88b1c8f..6324c32 100644 --- a/src/Repository/RepositoryQuery.php +++ b/src/Repository/RepositoryQuery.php @@ -2,7 +2,6 @@ namespace App\Repository; -use App\Criteria\CriteriaMap; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\QueryBuilder; use Knp\Component\Pager\PaginatorInterface; @@ -27,27 +26,20 @@ abstract class RepositoryQuery $this->id = $id; } - public function create() - { - $class = get_called_class(); - - return new $class($this->repository, $this->paginator); - } - public function __call(string $name, $params): self { - $fn = function(&$data) { + $fn = function (&$data) { if (is_string($data)) { $words = explode(' ', $data); foreach ($words as $k => $v) { - if (isset($v[0]) && $v[0] === '.') { + if (isset($v[0]) && '.' === $v[0]) { $words[$k] = $this->id.$v; } } $data = implode(' ', $words); - } else { + } elseif (is_array($data)) { foreach ($data as $k => $v) { $fn($data[$k]); } @@ -56,8 +48,7 @@ abstract class RepositoryQuery return $data; }; - foreach ($params as $key => $value) - { + foreach ($params as $key => $value) { $fn($params[$key]); } @@ -66,6 +57,13 @@ abstract class RepositoryQuery return $this; } + public function create() + { + $class = get_called_class(); + + return new $class($this->repository, $this->paginator); + } + public function call(callable $fn): self { $fn($this->query, $this); diff --git a/templates/admin/layout.html.twig b/templates/admin/layout.html.twig index bf82df5..d83e108 100644 --- a/templates/admin/layout.html.twig +++ b/templates/admin/layout.html.twig @@ -104,7 +104,7 @@ {% endblock %} {% block js %} - + {{ encore_entry_script_tags('admin') }} {% endblock %} diff --git a/templates/blog/category_admin/_form.html.twig b/templates/blog/category_admin/_form.html.twig new file mode 100644 index 0000000..b8d77b0 --- /dev/null +++ b/templates/blog/category_admin/_form.html.twig @@ -0,0 +1,17 @@ +
+
+
+ {% for item in ['title', 'subTitle', 'slug'] %} +
+ {{ form_row(form[item]) }} +
+ {% endfor %} +
+
+
+ {% for item in ['description'] %} + {{ form_row(form[item]) }} + {% endfor %} +
+
+ diff --git a/templates/blog/category_admin/edit.html.twig b/templates/blog/category_admin/edit.html.twig new file mode 100644 index 0000000..503b8ea --- /dev/null +++ b/templates/blog/category_admin/edit.html.twig @@ -0,0 +1,57 @@ +{% extends 'admin/layout.html.twig' %} + +{% block body %} +
+
+
+

{{ entity.title }}

+
+ +
+
+ + + Retour à la liste + + + + Voir + + + + + + +
+
+
+
+ +
+
+
+
+ {{ include('blog/category_admin/_form.html.twig') }} +
+
+
+ + {{ form_rest(form) }} + + +
+ + + +{% endblock %} diff --git a/templates/blog/category_admin/index.html.twig b/templates/blog/category_admin/index.html.twig index fc8b6d0..27b9c49 100644 --- a/templates/blog/category_admin/index.html.twig +++ b/templates/blog/category_admin/index.html.twig @@ -1,16 +1,23 @@ {% extends 'admin/layout.html.twig' %} {% block body %} -
+

Catégories

- +
+ + {{ knp_pagination_render(pager) }}
TitreAuteurCatégorieStatut TitreMise à jourStatutActions
- Titre de l'article {{ item }}
+
+ + + Titre de l'article {{ item }}
+ Dans Nom de la catégorie par Mark
- Mark + + + 27/03/2021 09:10 + - Nom de la catégorie - - En ligne + + - - - + +
@@ -20,28 +27,51 @@ - - - {% for item in range(1, 4) %} - - - - - - {% endfor %} + + + {% for item in pager %} + {% set edit = path('admin_blog_category_edit', {entity: item.id}) %} + {% set show = path('admin_blog_category_show', {entity: item.id}) %} + + + + + + + {% else %} + + + + {% endfor %}
Articles Actions
- Titre de la catégories
-
- - 5 articles - - - - -
+ + {{ item.title }} + + + + {% set postsCount = item.posts|length %} + + {{ postsCount }} {{ postsCount < 2 ? 'article' : 'articles' }} + + + + + + + +
+ + +
+
+
+ +
+
+ Aucun résultat +
+
{% endblock %} diff --git a/templates/blog/category_admin/new.html.twig b/templates/blog/category_admin/new.html.twig new file mode 100644 index 0000000..c8eb21b --- /dev/null +++ b/templates/blog/category_admin/new.html.twig @@ -0,0 +1,39 @@ +{% extends 'admin/layout.html.twig' %} + +{% block body %} +
+
+
+

Nouvelle catégorie

+
+ +
+
+ + + + Retour à la liste + + + +
+
+
+
+ +
+
+
+
+ {{ include('blog/category_admin/_form.html.twig') }} +
+
+
+ + {{ form_rest(form) }} +
+{% endblock %} diff --git a/templates/blog/category_admin/show.html.twig b/templates/blog/category_admin/show.html.twig new file mode 100644 index 0000000..c7e3779 --- /dev/null +++ b/templates/blog/category_admin/show.html.twig @@ -0,0 +1,83 @@ +{% extends 'admin/layout.html.twig' %} + +{% block body %} +
+
+
+

{{ entity.title }}

+
+ + +
+
+ +
+
+
    +
  • + Titre
    + + {{ entity.title }} +
  • +
  • + Sous-titre
    + + {{ entity.subTitle }} +
  • +
  • + URL
    + + {{ absolute_url('/' ~ entity.slug) }} +
  • +
+
+
+
Description
+ + {{ entity.description|raw|nl2br }} +
+ +
+ + + + + + + + {% for item in posts %} + + + + {% else %} + + + + {% endfor %} + +
Derniers articles
+ {{ item.post }} +
+
+ +
+
+ Aucun résultat +
+
+
+
+{% endblock %} diff --git a/yarn.lock b/yarn.lock index 714d8a3..188c1d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5771,10 +5771,10 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tinymce@^5.2.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.7.0.tgz#bc565877e4041db83848a330dfa7916993110cba" - integrity sha512-WikgMpJbqYPaucV3lfstCj+Y4NquZlK61gyuJ2eqDUBlSU+4fFh6rpwnelVTxuvtEyJsAVOl8HZmbzBfDJiLsQ== +tinymce@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.7.1.tgz#658a6fb4c7d53a8496cc00f8da33f4b8290da06d" + integrity sha512-1gY8RClc734srSlkYwY0MQzmkS1j73PuPC+nYtNtrrQVPY9VNcZ4bOiRwzTbdjPPD8GOtv6BAk8Ww/H2RiqKpA== tmp@^0.2.1: version "0.2.1" From 427360759471f0fe6412dfd4c637f93d7fe18510 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 17 Mar 2021 15:57:07 +0100 Subject: [PATCH 6/7] add blog post CRUD --- assets/img/blank.png | Bin 0 -> 163 bytes assets/img/no-image.png | Bin 0 -> 1536 bytes composer.json | 1 + .../post/2021/20210317-035213ovh_feu1.jpg | Bin 0 -> 15109 bytes .../Account/AccountAdminController.php | 12 +- src/Controller/Auth/AuthController.php | 4 +- .../Blog/CategoryAdminController.php | 7 +- src/Controller/Blog/PostAdminController.php | 102 ++++++++- src/Entity/Blog/Category.php | 1 + src/Entity/Blog/Post.php | 7 +- src/Entity/User.php | 12 +- .../BlogPostEventSubscriber.php | 78 +++++++ .../EntityManagerEventSubscriber.php | 2 +- src/Factory/Blog/PostFactory.php | 25 +++ src/Form/Blog/PostType.php | 208 ++++++++++++++++++ src/Form/FileUploadHandler.php | 28 +++ src/Manager/EntityManager.php | 3 + templates/blog/category_admin/edit.html.twig | 6 +- templates/blog/category_admin/index.html.twig | 8 +- templates/blog/category_admin/new.html.twig | 6 +- templates/blog/category_admin/show.html.twig | 14 +- templates/blog/post_admin/_form.html.twig | 36 +++ templates/blog/post_admin/edit.html.twig | 57 +++++ templates/blog/post_admin/index.html.twig | 128 ++++++----- templates/blog/post_admin/new.html.twig | 39 ++++ templates/blog/post_admin/show.html.twig | 106 +++++++++ 26 files changed, 798 insertions(+), 92 deletions(-) create mode 100644 assets/img/blank.png create mode 100644 assets/img/no-image.png create mode 100644 public/uploads/post/2021/20210317-035213ovh_feu1.jpg create mode 100644 src/EventSuscriber/BlogPostEventSubscriber.php create mode 100644 src/Factory/Blog/PostFactory.php create mode 100644 src/Form/Blog/PostType.php create mode 100644 src/Form/FileUploadHandler.php create mode 100644 templates/blog/post_admin/_form.html.twig create mode 100644 templates/blog/post_admin/edit.html.twig create mode 100644 templates/blog/post_admin/new.html.twig create mode 100644 templates/blog/post_admin/show.html.twig diff --git a/assets/img/blank.png b/assets/img/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..5e21c8a9d64b6b89586c0cd1e2a0ced98ee35c98 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-mSQK*5Dp-y;YjHK@;M7UB8wRq zIOIW?@rCpD)j&bX64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1SPJY5_^ sG8*3=WMl;LniT%8&+~8qF~Q)#Vn%f*2F8>=Lpu=P)78&qol`;+02@6fmjD0& literal 0 HcmV?d00001 diff --git a/assets/img/no-image.png b/assets/img/no-image.png new file mode 100644 index 0000000000000000000000000000000000000000..7957221866d0ebf40bc0649eb2b32f0eb5fe8c2b GIT binary patch literal 1536 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 h@|a9YE%!NHS%addFlash('success', 'Double authentification activée.'); - $entityManager->update($account)->flush()->clear(); + $entityManager->update($account); return $this->redirectToRoute('admin_account'); } @@ -78,7 +78,7 @@ class AccountAdminController extends AdminController if (!$enable && $account->isTotpAuthenticationEnabled()) { $account->setTotpSecret(null); - $entityManager->update($account)->flush()->clear(); + $entityManager->update($account); $this->addFlash('success', 'Double authentification désactivée.'); @@ -100,7 +100,8 @@ class AccountAdminController extends AdminController Request $request, UserRepository $repository, TokenGeneratorInterface $tokenGenerator, - UserPasswordEncoderInterface $encoder + UserPasswordEncoderInterface $encoder, + EntityManager $entityManager ): Response { $account = $this->getUser(); $csrfToken = $request->request->get('_csrf_token'); @@ -129,10 +130,7 @@ class AccountAdminController extends AdminController ->setConfirmationToken($tokenGenerator->generateToken()) ; - $entityManager = $this->getDoctrine()->getManager(); - $entityManager->persist($account); - $entityManager->flush(); - $entityManager->clear(); + $entityManager->update($account); $this->addFlash('success', 'Mot de passe modifié !'); diff --git a/src/Controller/Auth/AuthController.php b/src/Controller/Auth/AuthController.php index 8d98b91..ce4ec79 100644 --- a/src/Controller/Auth/AuthController.php +++ b/src/Controller/Auth/AuthController.php @@ -71,7 +71,7 @@ class AuthController extends AbstractController $account->setConfirmationToken($tokenGenerator->generateToken()); $account->setPasswordRequestedAt(new \DateTime('now')); - $entityManager->update($account)->flush()->clear(); + $entityManager->update($account); $eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT); $emailSent = true; @@ -135,7 +135,7 @@ class AuthController extends AbstractController ->setPasswordRequestedAt(new \DateTime('now')) ; - $entityManager->update($account)->flush()->clear(); + $entityManager->update($account); $passwordUpdated = true; } diff --git a/src/Controller/Blog/CategoryAdminController.php b/src/Controller/Blog/CategoryAdminController.php index d9fc9bd..4d81499 100644 --- a/src/Controller/Blog/CategoryAdminController.php +++ b/src/Controller/Blog/CategoryAdminController.php @@ -42,7 +42,7 @@ class CategoryAdminController extends AdminController $form->handleRequest($request); if ($form->isValid()) { - $entityManager->create($entity)->flush()->clear(); + $entityManager->create($entity); $this->addFlash('success', 'Donnée enregistrée.'); return $this->redirectToRoute('admin_blog_category_edit', [ @@ -54,6 +54,7 @@ class CategoryAdminController extends AdminController return $this->render('blog/category_admin/new.html.twig', [ 'form' => $form->createView(), + 'entity' => $entity, ]); } @@ -68,7 +69,7 @@ class CategoryAdminController extends AdminController $form->handleRequest($request); if ($form->isValid()) { - $entityManager->update($entity)->flush()->clear(); + $entityManager->update($entity); $this->addFlash('success', 'Donnée enregistrée.'); return $this->redirectToRoute('admin_blog_category_edit', [ @@ -107,7 +108,7 @@ class CategoryAdminController extends AdminController public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response { if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { - $entityManager->delete($entity)->flush()->clear(); + $entityManager->delete($entity); $this->addFlash('success', 'Données supprimées.'); } diff --git a/src/Controller/Blog/PostAdminController.php b/src/Controller/Blog/PostAdminController.php index c07e7bf..677be20 100644 --- a/src/Controller/Blog/PostAdminController.php +++ b/src/Controller/Blog/PostAdminController.php @@ -3,6 +3,14 @@ namespace App\Controller\Blog; use App\Controller\Admin\AdminController; +use App\Entity\Blog\Post as Entity; +use App\Factory\Blog\PostFactory as EntityFactory; +use App\Form\Blog\PostType as EntityType; +use App\Manager\EntityManager; +use App\Repository\Blog\PostRepositoryQuery; +use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery; +use App\Form\FileUploadHandler; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -12,14 +20,104 @@ use Symfony\Component\Routing\Annotation\Route; class PostAdminController extends AdminController { /** - * @Route("/", name="admin_blog_post_index") + * @Route("/{page}", name="admin_blog_post_index", requirements={"page": "\d+"}) */ - public function index(): Response + public function index(int $page = 1, RepositoryQuery $query, Request $request): Response { + $pager = $query->paginate($page); + return $this->render('blog/post_admin/index.html.twig', [ + 'pager' => $pager, ]); } + /** + * @Route("/new", name="admin_blog_post_new") + */ + public function new(EntityFactory $factory, EntityManager $entityManager, Request $request): Response + { + $entity = $factory->create($this->getUser()); + $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_blog_post_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('blog/post_admin/new.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_blog_post_edit") + */ + public function edit(Entity $entity, EntityManager $entityManager, FileUploadHandler $fileUpload, Request $request): Response + { + $form = $this->createForm(EntityType::class, $entity); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $fileUpload->handleForm( + $form->get('image')->getData(), + 'uploads/post/'.date('Y'), + function ($filename) use ($entity) { + $entity->setImage('post/'.date('Y').'/'.$filename); + } + ); + + $entityManager->update($entity); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_blog_post_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('blog/post_admin/edit.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/show/{entity}", name="admin_blog_post_show") + */ + public function show(Entity $entity, PostRepositoryQuery $postQuery): Response + { + return $this->render('blog/post_admin/show.html.twig', [ + 'entity' => $entity, + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_blog_post_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ées.'); + } + + return $this->redirectToRoute('admin_blog_post_index'); + } + public function getSection(): string { return 'blog_post'; diff --git a/src/Entity/Blog/Category.php b/src/Entity/Blog/Category.php index 4324f31..a2ab1ff 100644 --- a/src/Entity/Blog/Category.php +++ b/src/Entity/Blog/Category.php @@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass=CategoryRepository::class) + * @ORM\HasLifecycleCallbacks */ class Category implements EntityInterface { diff --git a/src/Entity/Blog/Post.php b/src/Entity/Blog/Post.php index 27d463d..8daafd2 100644 --- a/src/Entity/Blog/Post.php +++ b/src/Entity/Blog/Post.php @@ -12,6 +12,7 @@ use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass=PostRepository::class) + * @ORM\HasLifecycleCallbacks */ class Post implements EntityInterface { @@ -153,8 +154,12 @@ class Post implements EntityInterface return $this->image; } - public function setImage(?string $image): self + public function setImage(?string $image, bool $force = false): self { + if (false === $force && null === $image) { + return $this; + } + $this->image = $image; return $this; diff --git a/src/Entity/User.php b/src/Entity/User.php index 8d03289..1afabed 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -9,11 +9,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; -use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity(repositoryClass=UserRepository::class) + * @ORM\HasLifecycleCallbacks * @ORM\Table(name="`user`") */ class User implements UserInterface, TwoFactorInterface, EntityInterface @@ -191,16 +191,6 @@ class User implements UserInterface, TwoFactorInterface, EntityInterface return null !== $this->getTotpSecret(); } - public function getTotpAuthenticationUsername(): string - { - return $this->getEmail(); - } - - public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface - { - return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6); - } - public function isGoogleAuthenticatorEnabled(): bool { return $this->isTotpAuthenticationEnabled(); diff --git a/src/EventSuscriber/BlogPostEventSubscriber.php b/src/EventSuscriber/BlogPostEventSubscriber.php new file mode 100644 index 0000000..297f433 --- /dev/null +++ b/src/EventSuscriber/BlogPostEventSubscriber.php @@ -0,0 +1,78 @@ + + */ +class BlogPostEventSubscriber implements EventSubscriberInterface +{ + protected Filesystem $filesystem; + protected PostRepositoryQuery $query; + + public function __construct(Filesystem $filesystem, PostRepositoryQuery $query) + { + $this->filesystem = $filesystem; + $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; + } + + public function onUpdate(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $this->removeOrphanUploads(); + } + + public function onDelete(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $this->removeOrphanUploads(); + } + + protected function removeOrphanUploads() + { + $finder = new Finder(); + $finder->files()->in('uploads/post'); + + foreach ($finder as $file) { + $image = str_replace('uploads/', '', $file->getPathname()); + + $post = $this->query->create() + ->where('.image = :image') + ->setParameter(':image', $image) + ->findOne(); + + if (null === $post) { + $this->filesystem->remove($file->getRealPath()); + } + } + } +} diff --git a/src/EventSuscriber/EntityManagerEventSubscriber.php b/src/EventSuscriber/EntityManagerEventSubscriber.php index 6044a30..ef819f1 100644 --- a/src/EventSuscriber/EntityManagerEventSubscriber.php +++ b/src/EventSuscriber/EntityManagerEventSubscriber.php @@ -6,7 +6,7 @@ use App\Event\EntityManager\EntityManagerEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** - * class EventListener. + * class EntityManagerEventSubscriber. * * @author Simon Vieille */ diff --git a/src/Factory/Blog/PostFactory.php b/src/Factory/Blog/PostFactory.php new file mode 100644 index 0000000..d1e31ca --- /dev/null +++ b/src/Factory/Blog/PostFactory.php @@ -0,0 +1,25 @@ + + */ +class PostFactory +{ + public function create(?User $author = null): Post + { + $entity = new Post(); + + $entity + ->setAuthor($author) + ->setStatus(0); + + return $entity; + } +} diff --git a/src/Form/Blog/PostType.php b/src/Form/Blog/PostType.php new file mode 100644 index 0000000..a788d7e --- /dev/null +++ b/src/Form/Blog/PostType.php @@ -0,0 +1,208 @@ +add( + 'title', + TextType::class, + [ + 'label' => 'Titre', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'subTitle', + TextareaType::class, + [ + 'label' => 'Sous-titre', + 'required' => false, + 'attr' => [ + 'rows' => 5, + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'metaDescription', + TextType::class, + [ + 'label' => 'Meta description', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'content', + TextareaType::class, + [ + 'label' => 'Contenu', + 'required' => false, + 'attr' => [ + 'data-tinymce' => '', + 'rows' => 20, + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'slug', + TextType::class, + [ + 'label' => 'Slug', + 'required' => false, + 'help' => 'Laisser vide pour une génération automatique', + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'categories', + EntityType::class, + [ + 'label' => 'Catégories', + 'class' => Category::class, + 'choice_label' => 'title', + 'required' => false, + 'multiple' => true, + 'attr' => [ + 'data-jschoice' => '', + ], + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('a') + ->orderBy('a.title', 'ASC') + ; + }, + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'status', + ChoiceType::class, + [ + 'label' => 'Statut', + 'required' => true, + 'choices' => [ + 'Brouillon' => 0, + 'Publié' => 1, + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'imageCaption', + TextType::class, + [ + 'label' => 'Titre de l\'image', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'publishedAt', + DateType::class, + [ + 'label' => 'Date de publication', + 'required' => false, + 'html5' => true, + 'widget' => 'single_text', + 'attr' => [ + ], + 'constraints' => [ + new Date(), + ], + ] + ); + + $builder->add( + 'author', + EntityType::class, + [ + 'label' => 'Auteur', + 'class' => User::class, + 'choice_label' => 'displayName', + 'required' => true, + 'attr' => [ + ], + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('u') + ->orderBy('u.displayName', 'ASC') + ; + }, + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'image', + FileType::class, + [ + 'label' => 'Image', + 'required' => false, + 'data_class' => null, + 'attr' => [ + ], + 'constraints' => [ + new Image(), + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Post::class, + ]); + } +} diff --git a/src/Form/FileUploadHandler.php b/src/Form/FileUploadHandler.php new file mode 100644 index 0000000..940ea28 --- /dev/null +++ b/src/Form/FileUploadHandler.php @@ -0,0 +1,28 @@ + + */ +class FileUploadHandler +{ + public function handleForm(?UploadedFile $uploadedFile, string $path, callable $afterUploadCallback): void + { + if (null === $uploadedFile) { + return; + } + + $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension(); + + $uploadedFile->move($path, $filename); + + $afterUploadCallback($filename); + } +} diff --git a/src/Manager/EntityManager.php b/src/Manager/EntityManager.php index a841dbd..8216aab 100644 --- a/src/Manager/EntityManager.php +++ b/src/Manager/EntityManager.php @@ -44,6 +44,8 @@ class EntityManager public function delete(EntityInterface $entity): self { $this->entityManager->remove($entity); + $this->flush(); + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT); return $this; @@ -66,5 +68,6 @@ class EntityManager protected function persist(EntityInterface $entity) { $this->entityManager->persist($entity); + $this->flush(); } } diff --git a/templates/blog/category_admin/edit.html.twig b/templates/blog/category_admin/edit.html.twig index 503b8ea..a469cfa 100644 --- a/templates/blog/category_admin/edit.html.twig +++ b/templates/blog/category_admin/edit.html.twig @@ -3,8 +3,8 @@ {% block body %}
-
-

{{ entity.title }}

+
+

{{ entity.title }}

@@ -38,7 +38,7 @@
-
+
diff --git a/templates/blog/category_admin/index.html.twig b/templates/blog/category_admin/index.html.twig index 27b9c49..1f6b0a3 100644 --- a/templates/blog/category_admin/index.html.twig +++ b/templates/blog/category_admin/index.html.twig @@ -1,10 +1,10 @@ {% extends 'admin/layout.html.twig' %} {% block body %} -
+
-
-

Catégories

+
+

Catégories

@@ -35,7 +35,7 @@ - + {{ item.title }} diff --git a/templates/blog/category_admin/new.html.twig b/templates/blog/category_admin/new.html.twig index c8eb21b..682b25b 100644 --- a/templates/blog/category_admin/new.html.twig +++ b/templates/blog/category_admin/new.html.twig @@ -3,8 +3,8 @@ {% block body %}
-
-

Nouvelle catégorie

+
+

Nouvelle catégorie

@@ -25,7 +25,7 @@
- +
diff --git a/templates/blog/category_admin/show.html.twig b/templates/blog/category_admin/show.html.twig index c7e3779..c80ad38 100644 --- a/templates/blog/category_admin/show.html.twig +++ b/templates/blog/category_admin/show.html.twig @@ -3,8 +3,8 @@ {% block body %}
-
-

{{ entity.title }}

+
+

{{ entity.title }}

@@ -28,17 +28,17 @@
  • - Titre
    + Titre {{ entity.title }}
  • - Sous-titre
    + Sous-titre {{ entity.subTitle }}
  • - URL
    + URL {{ absolute_url('/' ~ entity.slug) }}
  • @@ -61,7 +61,9 @@ {% for item in posts %} - {{ item.post }} + + {{ item.title }} + {% else %} diff --git a/templates/blog/post_admin/_form.html.twig b/templates/blog/post_admin/_form.html.twig new file mode 100644 index 0000000..684bf52 --- /dev/null +++ b/templates/blog/post_admin/_form.html.twig @@ -0,0 +1,36 @@ +
    +
    +
    + {% for item in ['title', 'subTitle', 'categories', 'metaDescription', 'slug'] %} +
    + {{ form_row(form[item]) }} +
    + {% endfor %} +
    +
    +
    +
    + {% for item in ['content'] %} +
    + {{ form_row(form[item]) }} +
    + {% endfor %} +
    +
    +
    +
    + {% for item in ['image', 'imageCaption', 'status', 'publishedAt', 'author'] %} +
    + {{ form_row(form[item]) }} + + {% if item == 'image' %} + {% if entity.image %} + + {% endif %} + {% endif %} +
    + {% endfor %} +
    +
    +
    + diff --git a/templates/blog/post_admin/edit.html.twig b/templates/blog/post_admin/edit.html.twig new file mode 100644 index 0000000..d2d4d4d --- /dev/null +++ b/templates/blog/post_admin/edit.html.twig @@ -0,0 +1,57 @@ +{% extends 'admin/layout.html.twig' %} + +{% block body %} +
    +
    +
    +

    {{ entity.title }}

    +
    + +
    +
    + + + Retour à la liste + + + + Voir + + + + + + +
    +
    +
    +
    + + +
    +
    +
    + {{ include('blog/post_admin/_form.html.twig') }} +
    +
    +
    + + {{ form_rest(form) }} + + +
    + + +
    +{% endblock %} diff --git a/templates/blog/post_admin/index.html.twig b/templates/blog/post_admin/index.html.twig index 10f369e..23e0506 100644 --- a/templates/blog/post_admin/index.html.twig +++ b/templates/blog/post_admin/index.html.twig @@ -1,32 +1,23 @@ {% extends 'admin/layout.html.twig' %} {% block body %} -
    +
    -
    -

    Articles

    +
    +

    Articles

    - +
    -
    - -
    + {{ knp_pagination_render(pager) }}
    @@ -37,37 +28,76 @@ - - - {% for item in range(1, 20) %} - - + {% for item in pager %} + {% set edit = path('admin_blog_post_edit', {entity: item.id}) %} + {% set show = path('admin_blog_post_show', {entity: item.id}) %} - Titre de l'article {{ item }}
    - Dans Nom de la catégorie par Mark - - - - - - {% endfor %} + + + + + + + {% else %} + + + + {% endfor %}
    Statut Actions
    - + +
    - - - 27/03/2021 09:10 - - - - - - -
    + {% if item.image %} + {% set image = asset('uploads/' ~ item.image) %} + {% else %} + {% set image = asset('build/images/no-image.png') %} + {% endif %} + + + + + {{ item.title }} + + + {% set categories = [] %} + + {% for category in item.categories %} + {% set url = path('admin_blog_category_show', {entity: category.id}) %} + {% set categories = categories|merge(['' ~ category.title ~ '']) %} + {% endfor %} + + Dans {{ categories|join(', ')|raw }} par {{ item.author.displayName }} + + + + {{ item.updatedAt|date('d/m/Y H:i') }} + + + {% set map = { + 0: ['warning', 'Brouillon'], + 1: ['success', 'Publié'], + } %} + + + + + + + +
    + + +
    +
    +
    + +
    +
    + Aucun résultat +
    +
    {% endblock %} diff --git a/templates/blog/post_admin/new.html.twig b/templates/blog/post_admin/new.html.twig new file mode 100644 index 0000000..dd684d1 --- /dev/null +++ b/templates/blog/post_admin/new.html.twig @@ -0,0 +1,39 @@ +{% extends 'admin/layout.html.twig' %} + +{% block body %} +
    +
    +
    +

    Nouvel article

    +
    + +
    +
    + + + + Retour à la liste + + + +
    +
    +
    +
    + +
    +
    +
    +
    + {{ include('blog/post_admin/_form.html.twig') }} +
    +
    +
    + + {{ form_rest(form) }} +
    +{% endblock %} diff --git a/templates/blog/post_admin/show.html.twig b/templates/blog/post_admin/show.html.twig new file mode 100644 index 0000000..733e00f --- /dev/null +++ b/templates/blog/post_admin/show.html.twig @@ -0,0 +1,106 @@ +{% extends 'admin/layout.html.twig' %} + +{% block body %} +
    +
    +
    +

    {{ entity.title }}

    +
    + + +
    +
    + +
    +
    +
      +
    • + Titre + + {{ entity.title }} +
    • +
    • + Sous-titre + + {{ entity.subTitle }} +
    • +
    • + Catégories + + {% for category in entity.categories %} + {{ category.title }} + {% endfor %} +
    • +
    • + URL + + {{ absolute_url('/' ~ entity.slug) }} +
    • +
    • + Meta description + + {{ entity.metaDescription }} +
    • +
    +
    +
    +
    Contenu
    + + {{ entity.content|raw|nl2br }} +
    +
    +
      +
    • + Image + + {% if entity.image %} +
      + {{ entity.imageCaption }} + +
      + {{ entity.imageCaption }} +
      +
      + {% else %} + - + {% endif %} +
    • + +
    • + Statut + + {% if entity.status == 0 %} + Brouillon + {% else %} + Publié + {% endif %} +
    • +
    • + Date de publication + + {{ entity.publishedAt ? entity.publishedAt|date('d/m/Y H:i') : '-' }} +
    • +
    • + Auteur + + + {{ entity.author.displayName }} + +
    • +
    +
    +
    +{% endblock %} From 7781298d3f9287abfd93160cc20a35dce720314c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 17 Mar 2021 15:57:20 +0100 Subject: [PATCH 7/7] PSR 2 compliance --- src/Controller/Blog/PostAdminController.php | 2 +- .../BlogPostEventSubscriber.php | 11 +++++----- src/Factory/Blog/PostFactory.php | 3 ++- src/Form/Blog/PostType.php | 20 +++++++++---------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Controller/Blog/PostAdminController.php b/src/Controller/Blog/PostAdminController.php index 677be20..93ffdbf 100644 --- a/src/Controller/Blog/PostAdminController.php +++ b/src/Controller/Blog/PostAdminController.php @@ -6,10 +6,10 @@ use App\Controller\Admin\AdminController; use App\Entity\Blog\Post as Entity; 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 App\Form\FileUploadHandler; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; diff --git a/src/EventSuscriber/BlogPostEventSubscriber.php b/src/EventSuscriber/BlogPostEventSubscriber.php index 297f433..f653f83 100644 --- a/src/EventSuscriber/BlogPostEventSubscriber.php +++ b/src/EventSuscriber/BlogPostEventSubscriber.php @@ -2,12 +2,12 @@ namespace App\EventSuscriber; -use App\Event\EntityManager\EntityManagerEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use App\Entity\EntityInterface; use App\Entity\Blog\Post; -use Symfony\Component\Filesystem\Filesystem; +use App\Entity\EntityInterface; +use App\Event\EntityManager\EntityManagerEvent; use App\Repository\Blog\PostRepositoryQuery; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; /** @@ -68,7 +68,8 @@ class BlogPostEventSubscriber implements EventSubscriberInterface $post = $this->query->create() ->where('.image = :image') ->setParameter(':image', $image) - ->findOne(); + ->findOne() + ; if (null === $post) { $this->filesystem->remove($file->getRealPath()); diff --git a/src/Factory/Blog/PostFactory.php b/src/Factory/Blog/PostFactory.php index d1e31ca..b7a7df4 100644 --- a/src/Factory/Blog/PostFactory.php +++ b/src/Factory/Blog/PostFactory.php @@ -18,7 +18,8 @@ class PostFactory $entity ->setAuthor($author) - ->setStatus(0); + ->setStatus(0) + ; return $entity; } diff --git a/src/Form/Blog/PostType.php b/src/Form/Blog/PostType.php index a788d7e..bef759e 100644 --- a/src/Form/Blog/PostType.php +++ b/src/Form/Blog/PostType.php @@ -2,22 +2,22 @@ namespace App\Form\Blog; -use App\Entity\Blog\Post; -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; use App\Entity\Blog\Category; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use App\Entity\Blog\Post; +use App\Entity\User; use Doctrine\ORM\EntityRepository; -use Symfony\Component\Form\Extension\Core\Type\TextType; -use Symfony\Component\Form\Extension\Core\Type\TextareaType; -use Symfony\Component\Validator\Constraints\NotBlank; +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\DateType; -use Symfony\Component\Validator\Constraints\Date; -use App\Entity\User; use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Date; use Symfony\Component\Validator\Constraints\Image; +use Symfony\Component\Validator\Constraints\NotBlank; class PostType extends AbstractType {