Run from terminal:
$ composer require beelab/tag-bundle
If you still don't use Flex, you'll need to enable bundle in the kernel:
<?php
// app/AppKernel.php
public function registerBundles()
{
$bundles = [
// ...
new Beelab\TagBundle\BeelabTagBundle(),
];
}
See also Other bundles for a note about registering order.
Create a Tag
entity class.
Using Flex, a contrib recipe is creating such entity for you.
Example:
<?php
// src/Entity/Tag.php
namespace App\Entity;
use Beelab\TagBundle\Tag\TagInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table()
* @ORM\Entity()
*/
class Tag implements TagInterface
{
/**
* @var int
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column()
*/
protected $name;
public function __toString(): string
{
return $this->name;
}
public function getId(): int
{
return $this->id;
}
public function setName(?string $name): void
{
$this->name = $name;
}
public function getName(): ?string
{
return $this->name;
}
}
Following configuration is added by recipe:
# config/packages/beelab_tag.yaml
beelab_tag:
tag_class: App\Entity\Tag
purge: false
Warning: the
purge
option is not mandatory and defaults tofalse
. You should use this option (withtrue
value) only if you want to delete a tag when a taggable entity is deleted. You should avoid purging tags if you configured more than a taggable entity, since this could lead to constraint violations.
Then you can create some entities that implement TaggableInteface
.
Suppose you want to use tags on an Article
entity. You have two options: implementing TaggableInterface
(more flexible, showed here), or extending AbstractTaggable
(simpler, showed later).
<?php
// src/Entity/Article.php
namespace App\Entity;
use Beelab\TagBundle\Tag\TaggableInterface;
use Beelab\TagBundle\Tag\TagInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table()
* @ORM\Entity()
*/
class Article implements TaggableInterface
{
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="Tag")
*/
private $tags;
// note: if you generated code, you need to
// replace "Tag" with "TagInterface" where appropriate
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function addTag(TagInterface $tag): void
{
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
}
}
public function removeTag(TagInterface $tag): void
{
$this->tags->removeElement($tag);
}
public function hasTag(TagInterface $tag): bool
{
return $this->tags->contains($tag);
}
public function getTags(): iterable
{
return $this->tags;
}
public function getTagNames(): array
{
return empty($this->tagsText) ? [] : \array_map('trim', explode(',', $this->tagsText));
}
}
Most simple usage is in a Form like this one:
<?php
// src/Form/Type/ArticleFormType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
// ...
class ArticleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('tagsText', TextType::class, ['required' => false, 'label' => 'Tags'])
// other fields...
;
}
}
Then, add a $tagsText
property to your entity:
<?php
// src/Entity/Article.php
// use...
class Article implements TaggableInterface
{
// ...
private $tagsText;
/**
* @var \DateTimeInterface
*
* @ORM\Column(type="datetime_immutable", nullable=true)
*/
private $updated;
// ...
public function setTagsText(?string $tagsText): void
{
$this->tagsText = $tagsText;
$this->updated = new \DateTimeImmutable();
}
public function getTagsText(): ?string
{
$this->tagsText = \implode(', ', $this->tags->toArray());
return $this->tagsText;
}
// ...
}
Note that you need to change something in your Entity when $tagsText
is updated,
otherwise flush is not triggered and tags won't work. In example above, we're using
an $updated
DateTimeImmutable property.
Instead of implementing TaggableInterface
, you can extend AbstractTaggable
, like in this example:
<?php
// src/Entity/Article.php
namespace App\Entity;
use Beelab\TagBundle\Entity\AbstractTaggable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table()
* @ORM\Entity()
*/
class Article extends AbstractTaggable
{
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="Tag")
*/
private $tags;
/**
* @var \DateTimeInterface
*
* @ORM\Column(type="datetime", nullable=true)
*/
private $updated;
public function setTagsText(?string $tagsText): void
{
$this->updated = new \DateTimeImmutable();
parent::setTagsText($tagsText);
}
}
This is much simpler, but of course also less flexible.
Please note that if your entity needs a constructor, you need to call parent::__construct()
inside it.
This bundle register a Doctrine subscriber that listens to onFlush
event with priority 10.
If you use this bundle together with other bundles that register subscribers on the same
event, you could experience some issues in case of higher priority.
If this case even occurs, feel free to open a Pull Request.
If you want to enhance user experience with a bit of Javascript/AJAX, read this small cookbook.
Here is a sneak preview of what you'll get.