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/css/admin.scss b/assets/css/admin.scss index 5284dfb..9186bef 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -181,7 +181,7 @@ tr.table-primary-light { } .body { - padding-top: 71px; + padding-top: 60px; .nav { padding-left: 10px; @@ -213,12 +213,12 @@ table.table-fixed, .table-fixed > table { height: 500px; } - thead, tbody, tr, td, th{ + thead, tbody, tr, td, th { display: block; } tbody { - td { + td, th { float: left; min-height: 60px; } diff --git a/assets/img/blank.png b/assets/img/blank.png new file mode 100644 index 0000000..5e21c8a Binary files /dev/null and b/assets/img/blank.png differ diff --git a/assets/img/no-image.png b/assets/img/no-image.png new file mode 100644 index 0000000..7957221 Binary files /dev/null and b/assets/img/no-image.png differ 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/assets/js/admin.js b/assets/js/admin.js index 9e5cc71..4d1b469 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -9,7 +9,7 @@ import '../css/admin.scss'; require('../../node_modules/bootstrap/dist/js/bootstrap.min.js'); // require('./addons/table-selectable.js')(); -// require('./addons/table-fixed.js')(); +require('./addons/table-fixed.js')(); // require('./addons/document-selector.js')(); require('./addons/form-confirm.js')(); require('./addons/form.js')(); diff --git a/composer.json b/composer.json index 950f40f..d98caa8 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "symfony/dotenv": "5.2.*", "symfony/event-dispatcher": "5.2.*", "symfony/expression-language": "5.2.*", + "symfony/finder": "5.2.*", "symfony/flex": "^1.3.1", "symfony/form": "5.2.*", "symfony/framework-bundle": "5.2.*", 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/uploads/post/2021/20210317-035213ovh_feu1.jpg b/public/uploads/post/2021/20210317-035213ovh_feu1.jpg new file mode 100644 index 0000000..0e4e0e8 Binary files /dev/null and b/public/uploads/post/2021/20210317-035213ovh_feu1.jpg differ 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/Account/AccountAdminController.php b/src/Controller/Account/AccountAdminController.php index bc02a75..03b69b3 100644 --- a/src/Controller/Account/AccountAdminController.php +++ b/src/Controller/Account/AccountAdminController.php @@ -68,7 +68,7 @@ class AccountAdminController extends AdminController } else { $this->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 new file mode 100644 index 0000000..4d81499 --- /dev/null +++ b/src/Controller/Blog/CategoryAdminController.php @@ -0,0 +1,123 @@ +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); + $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(), + 'entity' => $entity, + ]); + } + + /** + * @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); + $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); + + $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/Controller/Blog/PostAdminController.php b/src/Controller/Blog/PostAdminController.php new file mode 100644 index 0000000..93ffdbf --- /dev/null +++ b/src/Controller/Blog/PostAdminController.php @@ -0,0 +1,125 @@ +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/Doctrine/Timestampable.php b/src/Doctrine/Timestampable.php new file mode 100644 index 0000000..6fafc6e --- /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..a2ab1ff --- /dev/null +++ b/src/Entity/Blog/Category.php @@ -0,0 +1,133 @@ +posts = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->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; + } + + /** + * @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 new file mode 100644 index 0000000..8daafd2 --- /dev/null +++ b/src/Entity/Blog/Post.php @@ -0,0 +1,239 @@ +categories = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->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, bool $force = false): self + { + if (false === $force && null === $image) { + return $this; + } + + $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; + } + + /** + * @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 cb6af78..1afabed 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,18 +2,23 @@ namespace App\Entity; +use App\Doctrine\Timestampable; +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; /** * @ORM\Entity(repositoryClass=UserRepository::class) + * @ORM\HasLifecycleCallbacks * @ORM\Table(name="`user`") */ -class User implements UserInterface, TwoFactorInterface, Entity +class User implements UserInterface, TwoFactorInterface, EntityInterface { + use Timestampable; /** * @ORM\Id * @ORM\GeneratedValue @@ -62,6 +67,16 @@ class User implements UserInterface, TwoFactorInterface, Entity */ private $isAdmin; + /** + * @ORM\OneToMany(targetEntity=Post::class, mappedBy="author") + */ + private $posts; + + public function __construct() + { + $this->posts = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -176,16 +191,6 @@ class User implements UserInterface, TwoFactorInterface, Entity 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(); @@ -241,4 +246,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/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/EventSuscriber/BlogPostEventSubscriber.php b/src/EventSuscriber/BlogPostEventSubscriber.php new file mode 100644 index 0000000..f653f83 --- /dev/null +++ b/src/EventSuscriber/BlogPostEventSubscriber.php @@ -0,0 +1,79 @@ + + */ +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/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/Factory/Blog/PostFactory.php b/src/Factory/Blog/PostFactory.php new file mode 100644 index 0000000..b7a7df4 --- /dev/null +++ b/src/Factory/Blog/PostFactory.php @@ -0,0 +1,26 @@ + + */ +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/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/Form/Blog/PostType.php b/src/Form/Blog/PostType.php new file mode 100644 index 0000000..bef759e --- /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 19aba0d..8216aab 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,11 @@ class EntityManager return $this; } - public function delete(Entity $entity): self + public function delete(EntityInterface $entity): self { - $this->remove($entity); + $this->entityManager->remove($entity); + $this->flush(); + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT); return $this; @@ -63,8 +65,9 @@ class EntityManager return $this; } - protected function persist(Entity $entity) + protected function persist(EntityInterface $entity) { $this->entityManager->persist($entity); + $this->flush(); } } diff --git a/src/Repository/Blog/CategoryRepository.php b/src/Repository/Blog/CategoryRepository.php new file mode 100644 index 0000000..b69b076 --- /dev/null +++ b/src/Repository/Blog/CategoryRepository.php @@ -0,0 +1,21 @@ + + */ +class CategoryRepositoryQuery extends RepositoryQuery +{ + public function __construct(CategoryRepository $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..1882005 --- /dev/null +++ b/src/Repository/Blog/PostRepository.php @@ -0,0 +1,21 @@ + + */ +class PostRepositoryQuery extends RepositoryQuery +{ + public function __construct(PostRepository $repository, PaginatorInterface $paginator) + { + 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 new file mode 100644 index 0000000..6324c32 --- /dev/null +++ b/src/Repository/RepositoryQuery.php @@ -0,0 +1,88 @@ + + */ +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 __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); + } elseif (is_array($data)) { + 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 create() + { + $class = get_called_class(); + + return new $class($this->repository, $this->paginator); + } + + 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/UserRepository.php b/src/Repository/UserRepository.php index 1a38975..21b3759 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -35,33 +35,4 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader $this->_em->persist($user); $this->_em->flush(); } - - // /** - // * @return User[] Returns an array of User objects - // */ - /* - public function findByExampleField($value) - { - return $this->createQueryBuilder('u') - ->andWhere('u.exampleField = :val') - ->setParameter('val', $value) - ->orderBy('u.id', 'ASC') - ->setMaxResults(10) - ->getQuery() - ->getResult() - ; - } - */ - - /* - public function findOneBySomeField($value): ?User - { - return $this->createQueryBuilder('u') - ->andWhere('u.exampleField = :val') - ->setParameter('val', $value) - ->getQuery() - ->getOneOrNullResult() - ; - } - */ } 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..d83e108 100644 --- a/templates/admin/layout.html.twig +++ b/templates/admin/layout.html.twig @@ -17,7 +17,7 @@ {{ include('admin/module/flashes.html.twig') }} -