From e2bd1a7c3b12cb512e3fd4fd463e4b91fd8c7cbe Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 10 May 2025 20:06:16 +0200 Subject: [PATCH] Implement snip tags with very elegant tags form --- migrations/Version20250510180413.php | 59 +++++++++++++++++ src/Entity/Snip.php | 34 ++++++++++ src/Entity/Tag.php | 94 ++++++++++++++++++++++++++++ src/Form/SnipType.php | 1 + src/Form/TagsType.php | 79 +++++++++++++++++++++++ src/Repository/TagRepository.php | 49 +++++++++++++++ templates/snip/single.html.twig | 17 ++--- 7 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 migrations/Version20250510180413.php create mode 100644 src/Entity/Tag.php create mode 100644 src/Form/TagsType.php create mode 100644 src/Repository/TagRepository.php diff --git a/migrations/Version20250510180413.php b/migrations/Version20250510180413.php new file mode 100644 index 0000000..43f9565 --- /dev/null +++ b/migrations/Version20250510180413.php @@ -0,0 +1,59 @@ +addSql(<<<'SQL' + CREATE TABLE tag (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, name VARCHAR(255) NOT NULL, INDEX IDX_389B783A76ED395 (user_id), UNIQUE INDEX user_tag_unique (name, user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE tag_snip (tag_id INT NOT NULL, snip_id INT NOT NULL, INDEX IDX_10B22820BAD26311 (tag_id), INDEX IDX_10B22820140FD260 (snip_id), PRIMARY KEY(tag_id, snip_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE tag ADD CONSTRAINT FK_389B783A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE tag_snip ADD CONSTRAINT FK_10B22820BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE tag_snip ADD CONSTRAINT FK_10B22820140FD260 FOREIGN KEY (snip_id) REFERENCES snip (id) ON DELETE CASCADE + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE tag DROP FOREIGN KEY FK_389B783A76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE tag_snip DROP FOREIGN KEY FK_10B22820BAD26311 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE tag_snip DROP FOREIGN KEY FK_10B22820140FD260 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE tag + SQL); + $this->addSql(<<<'SQL' + DROP TABLE tag_snip + SQL); + } +} diff --git a/src/Entity/Snip.php b/src/Entity/Snip.php index 4a559d7..87a847d 100644 --- a/src/Entity/Snip.php +++ b/src/Entity/Snip.php @@ -39,9 +39,16 @@ class Snip #[ORM\Column] private bool $archived = false; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Tag::class, mappedBy: 'snips')] + private Collection $tags; + public function __construct() { $this->snipContents = new ArrayCollection(); + $this->tags = new ArrayCollection(); } public function __toString(): string @@ -160,4 +167,31 @@ class Snip return $this; } + + /** + * @return Collection + */ + public function getTags(): Collection + { + return $this->tags; + } + + public function addTag(Tag $tag): static + { + if (!$this->tags->contains($tag)) { + $this->tags->add($tag); + $tag->addSnip($this); + } + + return $this; + } + + public function removeTag(Tag $tag): static + { + if ($this->tags->removeElement($tag)) { + $tag->removeSnip($this); + } + + return $this; + } } diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php new file mode 100644 index 0000000..5a430e3 --- /dev/null +++ b/src/Entity/Tag.php @@ -0,0 +1,94 @@ + + */ + #[ORM\ManyToMany(targetEntity: Snip::class, inversedBy: 'tags')] + private Collection $snips; + + public function __construct() + { + $this->snips = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->name ?? ''; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): static + { + $this->user = $user; + + return $this; + } + + /** + * @return Collection + */ + public function getSnips(): Collection + { + return $this->snips; + } + + public function addSnip(Snip $snip): static + { + if (!$this->snips->contains($snip)) { + $this->snips->add($snip); + } + + return $this; + } + + public function removeSnip(Snip $snip): static + { + $this->snips->removeElement($snip); + + return $this; + } +} diff --git a/src/Form/SnipType.php b/src/Form/SnipType.php index b28cee1..bbfc000 100644 --- a/src/Form/SnipType.php +++ b/src/Form/SnipType.php @@ -28,6 +28,7 @@ class SnipType extends AbstractType 'attr' => ['rows' => 20], 'mapped' => false, ]) + ->add('tags', TagsType::class) ->add('public', SwitchType::class) ->add('visible', SwitchType::class) ; diff --git a/src/Form/TagsType.php b/src/Form/TagsType.php new file mode 100644 index 0000000..d5135d2 --- /dev/null +++ b/src/Form/TagsType.php @@ -0,0 +1,79 @@ +toArray(); + } + + if (is_array($value)) { + $tags = array_map(fn(Tag $tag) => $tag->getName(), $value); + } else { + return ''; + } + return implode(', ', $tags); + } + + public function reverseTransform($value): array + { + $tags = array_filter(array_map('trim', explode(',', $value))); + $user = $this->security->getUser(); + + $tagEntities = []; + foreach ($tags as $tag) { + $tagEntity = $this->repository->findOneBy(['name' => $tag, 'user' => $user]); + if ($tagEntity === null) { + $tagEntity = new Tag() + ->setName($tag) + ->setUser($user) + ; + $this->repository->save($tagEntity); + } + $tagEntities[] = $tagEntity; + } + + return $tagEntities; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->addModelTransformer($this); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => null, // No specific entity class + 'label' => 'Tags (comma-separated)', + 'attr' => ['class' => 'tags-input'], // Optional: Add custom attributes + ]); + } + + public function getParent(): string + { + return TextType::class; + } +} \ No newline at end of file diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php new file mode 100644 index 0000000..5a30f64 --- /dev/null +++ b/src/Repository/TagRepository.php @@ -0,0 +1,49 @@ + + */ +class TagRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Tag::class); + } + + public function save(Tag $tag): void + { + $this->getEntityManager()->persist($tag); + $this->getEntityManager()->flush(); + } + + // /** + // * @return Tag[] Returns an array of Tag objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Tag + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/templates/snip/single.html.twig b/templates/snip/single.html.twig index 3fbd137..4e71f4c 100644 --- a/templates/snip/single.html.twig +++ b/templates/snip/single.html.twig @@ -19,15 +19,13 @@ Edit - {% if snip.archived %} - + + {% if snip.archived %} Unarchive - - {% else %} - + {% else %} Archive - - {% endif %} + {% endif %} + Delete @@ -39,7 +37,10 @@

{{ include('snip/badge.html.twig', {snip: snip}) }} - {{ snip }} #{{ snip.id }} + {{ snip }} #{{ snip.id }}
+ {% for tag in snip.tags %} + {{ tag }} + {% endfor %}

{{ content|raw }}