diff --git a/.gitignore b/.gitignore index 4daae382..97157a30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ ###> symfony/framework-bundle ### +/public/uploads /.env.local /.env.local.php /.env.*.local diff --git a/.idea/guestbook.iml b/.idea/guestbook.iml index 303cbcd8..79c42166 100644 --- a/.idea/guestbook.iml +++ b/.idea/guestbook.iml @@ -3,7 +3,6 @@ - diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f9..805892e4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,7 +4,7 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: - + photo_dir: "%kernel.project_dir%/public/uploads/photos" services: # default configuration for services in *this* file _defaults: diff --git a/migrations/Version20240703092113.php b/migrations/Version20240703092113.php new file mode 100644 index 00000000..1f6af09f --- /dev/null +++ b/migrations/Version20240703092113.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE conference ADD slug VARCHAR(255)'); + $this->addSql("UPDATE conference SET slug=CONCAT(LOWER(city), '-', year)"); + $this->addSql('ALTER TABLE conference ALTER COLUMN slug SET NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE conference DROP slug'); + } +} diff --git a/migrations/Version20240703092601.php b/migrations/Version20240703092601.php new file mode 100644 index 00000000..89ebd68f --- /dev/null +++ b/migrations/Version20240703092601.php @@ -0,0 +1,32 @@ +addSql('CREATE UNIQUE INDEX UNIQ_911533C8989D9B62 ON conference (slug)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP INDEX UNIQ_911533C8989D9B62'); + } +} diff --git a/src/Controller/Admin/CommentCrudController.php b/src/Controller/Admin/CommentCrudController.php index 3e3d0061..46264166 100644 --- a/src/Controller/Admin/CommentCrudController.php +++ b/src/Controller/Admin/CommentCrudController.php @@ -10,6 +10,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField; use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; +use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; @@ -46,7 +47,9 @@ class CommentCrudController extends AbstractCrudController yield TextareaField::new('text') ->hideOnIndex() ; - yield TextField::new('photoFilename') + yield ImageField::new('photoFilename') + ->setBasePath('/uploads/photos') + ->setLabel('Photo') ->onlyOnIndex() ; @@ -56,9 +59,8 @@ class CommentCrudController extends AbstractCrudController ]); if (Crud::PAGE_EDIT === $pageName) { yield $createdAt->setFormTypeOption('disabled', true); - } else { - yield $createdAt; } + } } diff --git a/src/Controller/ConferenceController.php b/src/Controller/ConferenceController.php index 14432ba1..2bd13c9b 100644 --- a/src/Controller/ConferenceController.php +++ b/src/Controller/ConferenceController.php @@ -2,10 +2,15 @@ namespace App\Controller; + +use App\Entity\Comment; use App\Entity\Conference; +use App\Form\CommentType; use App\Repository\CommentRepository; use App\Repository\ConferenceRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -13,6 +18,10 @@ use Symfony\Component\Routing\Attribute\Route; class ConferenceController extends AbstractController { + public function __construct( + private EntityManagerInterface $entityManager, + ) { + } #[Route('/', name: 'homepage')] public function index(ConferenceRepository $conferenceRepository): Response { @@ -21,17 +30,41 @@ class ConferenceController extends AbstractController ]); } - #[Route('/conference/{id}', name: 'conference')] - public function show(Request $request, Conference $conference, CommentRepository $commentRepository): Response - { + #[Route('/conference/{slug}', name: 'conference')] + public function show( + Request $request, + Conference $conference, + CommentRepository $commentRepository, + ConferenceRepository $conferenceRepository, + #[Autowire('%photo_dir%')] string $photoDir, + ): Response { + $comment = new Comment(); + $form = $this->createForm(CommentType::class, $comment); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $comment->setConference($conference); + if ($photo = $form['photo']->getData()) { + $filename = bin2hex(random_bytes(6)).'.'.$photo->guessExtension(); + $photo->move($photoDir, $filename); + $comment->setPhotoFilename($filename); + } + $this->entityManager->persist($comment); + $this->entityManager->flush(); + + return $this->redirectToRoute('conference', ['slug' => $conference->getSlug()]); + } + $offset = max(0, $request->query->getInt('offset', 0)); $paginator = $commentRepository->getCommentPaginator($conference, $offset); return $this->render('conference/show.html.twig', [ + 'conferences' => $conferenceRepository->findAll(), 'conference' => $conference, 'comments' => $paginator, 'previous' => $offset - CommentRepository::COMMENTS_PER_PAGE, 'next' => min(count($paginator), $offset + CommentRepository::COMMENTS_PER_PAGE), + 'comment_form' => $form, ]); } } diff --git a/src/Entity/Comment.php b/src/Entity/Comment.php index 58e62b5b..76ab9367 100644 --- a/src/Entity/Comment.php +++ b/src/Entity/Comment.php @@ -5,8 +5,10 @@ namespace App\Entity; use App\Repository\CommentRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: CommentRepository::class)] +#[ORM\HasLifecycleCallbacks] class Comment { #[ORM\Id] @@ -15,12 +17,16 @@ class Comment private ?int $id = null; #[ORM\Column(length: 255)] + #[Assert\NotBlank] private ?string $author = null; #[ORM\Column(type: Types::TEXT)] + #[Assert\NotBlank] private ?string $text = null; #[ORM\Column(length: 255)] + #[Assert\NotBlank] + #[Assert\Email] private ?string $email = null; #[ORM\Column] @@ -86,6 +92,11 @@ class Comment return $this; } + #[ORM\PrePersist] + public function setCreatedAtValue() + { + $this->createdAt = new \DateTimeImmutable(); + } public function getConference(): ?Conference { return $this->conference; diff --git a/src/Entity/Conference.php b/src/Entity/Conference.php index 21c8251c..a72ac79d 100644 --- a/src/Entity/Conference.php +++ b/src/Entity/Conference.php @@ -6,8 +6,11 @@ use App\Repository\ConferenceRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\String\Slugger\SluggerInterface; #[ORM\Entity(repositoryClass: ConferenceRepository::class)] +#[UniqueEntity('slug')] class Conference { #[ORM\Id] @@ -30,6 +33,9 @@ class Conference #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'conference', orphanRemoval: true)] private Collection $comments; + #[ORM\Column(length: 255, unique: true)] + private ?string $slug = null; + public function __construct() { $this->comments = new ArrayCollection(); @@ -44,6 +50,13 @@ class Conference return $this->id; } + public function computeSlug(SluggerInterface $slugger) + { + if (!$this->slug || '-' === $this->slug) { + $this->slug = (string) $slugger->slug((string) $this)->lower(); + } + } + public function getCity(): ?string { return $this->city; @@ -109,4 +122,16 @@ class Conference return $this; } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setSlug(string $slug): static + { + $this->slug = $slug; + + return $this; + } } diff --git a/src/EntityListener/ConferenceEntityListener.php b/src/EntityListener/ConferenceEntityListener.php new file mode 100644 index 00000000..2e57be98 --- /dev/null +++ b/src/EntityListener/ConferenceEntityListener.php @@ -0,0 +1,29 @@ +computeSlug($this->slugger); + } + + public function preUpdate(Conference $conference, LifecycleEventArgs $event) + { + $conference->computeSlug($this->slugger); + } +} \ No newline at end of file diff --git a/src/EventSubscriber/TwigEventSubscriber.php b/src/EventSubscriber/TwigEventSubscriber.php new file mode 100644 index 00000000..296ebb6b --- /dev/null +++ b/src/EventSubscriber/TwigEventSubscriber.php @@ -0,0 +1,33 @@ +twig = $twig; + $this->conferenceRepository = $conferenceRepository; + } + + public function onControllerEvent(ControllerEvent $event): void + { + $this->twig->addGlobal('conferences', $this->conferenceRepository->findAll()); + } + + public static function getSubscribedEvents(): array + { + return [ + ControllerEvent::class => 'onControllerEvent', + ]; + } +} diff --git a/src/Form/CommentType.php b/src/Form/CommentType.php new file mode 100644 index 00000000..9b18bea2 --- /dev/null +++ b/src/Form/CommentType.php @@ -0,0 +1,43 @@ +add('author', null, [ + 'label' => 'Your name', + ]) + ->add('text') + ->add('email', EmailType::class) + ->add('photo', FileType::class, [ + 'required' => false, + 'mapped' => false, + 'constraints' => [ + new Image(['maxSize' => '1024k']) + ], + ]) + ->add('submit', SubmitType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Comment::class, + ]); + } +} diff --git a/src/Repository/ConferenceRepository.php b/src/Repository/ConferenceRepository.php index 23fb8e15..cbf52c63 100644 --- a/src/Repository/ConferenceRepository.php +++ b/src/Repository/ConferenceRepository.php @@ -16,6 +16,11 @@ class ConferenceRepository extends ServiceEntityRepository parent::__construct($registry, Conference::class); } + public function findAll(): array + { + return $this->findBy([], ['year' => 'ASC', 'city' => 'ASC']); + } + // /** // * @return Conference[] Returns an array of Conference objects // */ diff --git a/templates/base.html.twig b/templates/base.html.twig index 3cda30fb..85b76116 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -12,6 +12,15 @@ {% endblock %} +
+

Guestbook

+ +
+
{% block body %}{% endblock %} diff --git a/templates/conference/index.html.twig b/templates/conference/index.html.twig index 485501b3..6ecddb10 100644 --- a/templates/conference/index.html.twig +++ b/templates/conference/index.html.twig @@ -11,7 +11,7 @@ {% for conference in conferences %}

{{ conference }}

- View + View

{% endfor %} {% endblock %} diff --git a/templates/conference/show.html.twig b/templates/conference/show.html.twig index 2b31cacb..f49b3d36 100644 --- a/templates/conference/show.html.twig +++ b/templates/conference/show.html.twig @@ -20,12 +20,15 @@

{{ comment.text }}

{% endfor %} {% if previous >= 0 %} - Previous + Previous {% endif %} {% if next < comments|length %} - Next + Next {% endif %} {% else %}
No comments have been posted yet for this conference.
{% endif %} +

Add your own feedback

+ + {{ form(comment_form) }} {% endblock %} \ No newline at end of file