diff --git a/cms/contents/article/admin/_tags_field.html.twig b/cms/contents/article/admin/_tags_field.html.twig
new file mode 100644
index 0000000..3bbbcdb
--- /dev/null
+++ b/cms/contents/article/admin/_tags_field.html.twig
@@ -0,0 +1,130 @@
+{% trans_default_domain 'sfs_cms_contents' %}
+
+
+ {{ form_label(form.tags) }}
+ {{ form_errors(form.tags) }}
+ {{ form_widget(form.tags) }}
+
+
+
+
+
+
{{ 'admin_article.form.tags.help'|trans }}
+
+
+
diff --git a/cms/contents/article/admin/create.html.twig b/cms/contents/article/admin/create.html.twig
index 9d21fcd..b763aae 100644
--- a/cms/contents/article/admin/create.html.twig
+++ b/cms/contents/article/admin/create.html.twig
@@ -20,4 +20,4 @@
{{ form_rest(form) }}
-{% endblock form_fields %}
\ No newline at end of file
+{% endblock form_fields %}
diff --git a/cms/contents/article/admin/update.html.twig b/cms/contents/article/admin/update.html.twig
index b4e3d98..4583612 100644
--- a/cms/contents/article/admin/update.html.twig
+++ b/cms/contents/article/admin/update.html.twig
@@ -4,6 +4,7 @@
{{ form_row(form.name) }}
{% if form.extraData is defined %}{{ form_widget(form.extraData) }}{% endif %}
{{ form_row(form.publishedAt) }}
+ {% if form.tags is defined %}{% include '@content/article/admin/_tags_field.html.twig' with {'form': form} %}{% endif %}
{% if form.author is defined %}{{ form_row(form.author) }}{% endif %}
{{ form_rest(form) }}
-{% endblock form_fields %}
\ No newline at end of file
+{% endblock form_fields %}
diff --git a/cms/contents/article/translations/sfs_cms_contents.en.yaml b/cms/contents/article/translations/sfs_cms_contents.en.yaml
index f90205c..8c2f874 100644
--- a/cms/contents/article/translations/sfs_cms_contents.en.yaml
+++ b/cms/contents/article/translations/sfs_cms_contents.en.yaml
@@ -89,6 +89,7 @@ admin_article:
status: "Status"
publishDate: "Publish date"
author: "Author"
+ tags: "Tags"
indexing:
title: "Indexing"
noIndex:
@@ -318,6 +319,9 @@ admin_article:
description.label: "Excerpt"
publishedAt.label: "Visible published date time"
author.label: "Author"
+ tags.label: "Tags"
+ tags.help: "Press Enter or comma to add each tag. Existing similar tags are reused."
+ tags.placeholder: "Write a tag"
version_form:
note.label: "Note"
diff --git a/cms/contents/article/translations/sfs_cms_contents.es.yaml b/cms/contents/article/translations/sfs_cms_contents.es.yaml
index 7562eae..231e668 100644
--- a/cms/contents/article/translations/sfs_cms_contents.es.yaml
+++ b/cms/contents/article/translations/sfs_cms_contents.es.yaml
@@ -88,6 +88,7 @@ admin_article:
status: "Estado"
publishDate: "Fecha de publicación"
author: "Autor"
+ tags: "Etiquetas"
indexing:
title: "Indexado"
noIndex:
@@ -317,6 +318,9 @@ admin_article:
description.label: "Excerpt"
publishedAt.label: "Fecha de publicación visible"
author.label: "Autor"
+ tags.label: "Etiquetas"
+ tags.help: "Pulsa Enter o coma para añadir cada etiqueta. Si ya existe una parecida, se reutiliza la misma."
+ tags.placeholder: "Escribe una etiqueta"
version_form:
note.label: "Nota"
diff --git a/config/doctrine-mapping/entities/ArticleContent.orm.xml b/config/doctrine-mapping/entities/ArticleContent.orm.xml
index a5651fc..cec183c 100644
--- a/config/doctrine-mapping/entities/ArticleContent.orm.xml
+++ b/config/doctrine-mapping/entities/ArticleContent.orm.xml
@@ -13,6 +13,8 @@
+
+
diff --git a/config/services/services.yaml b/config/services/services.yaml
index ffdc333..a8ece70 100644
--- a/config/services/services.yaml
+++ b/config/services/services.yaml
@@ -16,6 +16,16 @@ services:
resource: '../../src/Form/*'
tags: ['form.type']
+ Softspring\CmsBlogPlugin\Form\Admin\Article\ArticleCreateForm:
+ arguments:
+ $translatableContext: '@Softspring\CmsBundle\Translator\TranslatableContext'
+ tags: ['form.type']
+
+ Softspring\CmsBlogPlugin\Form\Admin\Article\ArticleUpdateForm:
+ arguments:
+ $translatableContext: '@Softspring\CmsBundle\Translator\TranslatableContext'
+ tags: ['form.type']
+
Softspring\CmsBlogPlugin\Manager\:
resource: '../../src/Manager/*'
diff --git a/src/Data/EntityTransformer/ArticleEntityTransformer.php b/src/Data/EntityTransformer/ArticleEntityTransformer.php
index 98d97dd..c14ad2a 100644
--- a/src/Data/EntityTransformer/ArticleEntityTransformer.php
+++ b/src/Data/EntityTransformer/ArticleEntityTransformer.php
@@ -4,10 +4,10 @@
use DateTime;
use Softspring\CmsBlogPlugin\Model\ArticleContentInterface;
+use Softspring\CmsBundle\Model\ContentInterface;
use Softspring\CmsDataPlugin\Data\EntityTransformer\ContentEntityTransformer;
use Softspring\CmsDataPlugin\Data\Exception\InvalidElementException;
use Softspring\CmsDataPlugin\Data\ReferencesRepository;
-use Softspring\CmsBundle\Model\ContentInterface;
class ArticleEntityTransformer extends ContentEntityTransformer
{
diff --git a/src/Form/Admin/Article/ArticleCreateForm.php b/src/Form/Admin/Article/ArticleCreateForm.php
index 0e9b927..594a8c9 100644
--- a/src/Form/Admin/Article/ArticleCreateForm.php
+++ b/src/Form/Admin/Article/ArticleCreateForm.php
@@ -2,26 +2,23 @@
namespace Softspring\CmsBlogPlugin\Form\Admin\Article;
-use ReflectionClass;
+use Softspring\CmsBlogPlugin\Form\Type\ArticleTagsType;
use Softspring\CmsBlogPlugin\Model\ArticleAuthorInterface;
use Softspring\CmsBundle\Form\Admin\Content\ContentCreateForm;
use Softspring\CmsBundle\Form\Type\UserType;
-use Softspring\CmsBundle\Manager\ContentManagerInterface;
use Softspring\CmsBundle\Translator\TranslatableContext;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormBuilderInterface;
class ArticleCreateForm extends ContentCreateForm
{
- public function __construct(protected ContentManagerInterface $contentManager, TranslatableContext $translatableContext)
+ public function __construct(TranslatableContext $translatableContext)
{
parent::__construct($translatableContext);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
- $entityClass = new ReflectionClass($this->contentManager->getTypeClass($options['content_config']['_id']));
-
parent::buildForm($builder, $options);
$builder->add('publishedAt', DateTimeType::class, [
@@ -29,7 +26,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'widget' => 'single_text',
]);
- if ($entityClass->implementsInterface(ArticleAuthorInterface::class)) {
+ $builder->add('tags', ArticleTagsType::class);
+
+ $dataClass = $options['data_class'] ?? null;
+
+ if (is_string($dataClass) && is_a($dataClass, ArticleAuthorInterface::class, true)) {
$builder->add('author', UserType::class, [
'required' => true,
]);
diff --git a/src/Form/Admin/Article/ArticleUpdateForm.php b/src/Form/Admin/Article/ArticleUpdateForm.php
index 7d44a85..ff8ea51 100644
--- a/src/Form/Admin/Article/ArticleUpdateForm.php
+++ b/src/Form/Admin/Article/ArticleUpdateForm.php
@@ -2,26 +2,23 @@
namespace Softspring\CmsBlogPlugin\Form\Admin\Article;
-use ReflectionClass;
+use Softspring\CmsBlogPlugin\Form\Type\ArticleTagsType;
use Softspring\CmsBlogPlugin\Model\ArticleAuthorInterface;
use Softspring\CmsBundle\Form\Admin\Content\ContentUpdateForm;
use Softspring\CmsBundle\Form\Type\UserType;
-use Softspring\CmsBundle\Manager\ContentManagerInterface;
use Softspring\CmsBundle\Translator\TranslatableContext;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormBuilderInterface;
class ArticleUpdateForm extends ContentUpdateForm
{
- public function __construct(protected ContentManagerInterface $contentManager, TranslatableContext $translatableContext)
+ public function __construct(TranslatableContext $translatableContext)
{
parent::__construct($translatableContext);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
- $entityClass = new ReflectionClass($this->contentManager->getTypeClass($options['content_config']['_id']));
-
parent::buildForm($builder, $options);
$builder->add('publishedAt', DateTimeType::class, [
@@ -29,7 +26,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'widget' => 'single_text',
]);
- if ($entityClass->implementsInterface(ArticleAuthorInterface::class)) {
+ $builder->add('tags', ArticleTagsType::class);
+
+ $dataClass = $options['data_class'] ?? null;
+
+ if (is_string($dataClass) && is_a($dataClass, ArticleAuthorInterface::class, true)) {
$builder->add('author', UserType::class, [
'required' => true,
]);
diff --git a/src/Form/Type/ArticleTagsType.php b/src/Form/Type/ArticleTagsType.php
new file mode 100644
index 0000000..7408842
--- /dev/null
+++ b/src/Form/Type/ArticleTagsType.php
@@ -0,0 +1,56 @@
+addModelTransformer(new CallbackTransformer(
+ fn (?array $tags): string => json_encode($tags ?? [], JSON_THROW_ON_ERROR),
+ function (?string $tags): array {
+ if (empty($tags)) {
+ return [];
+ }
+
+ $decodedTags = json_decode($tags, true);
+
+ return is_array($decodedTags) ? $this->articleTagManager->canonicalizeTags($decodedTags) : [];
+ }
+ ));
+ }
+
+ public function buildView(FormView $view, FormInterface $form, array $options): void
+ {
+ $view->vars['tag_suggestions'] = $options['tag_suggestions'];
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'required' => false,
+ 'empty_data' => '[]',
+ 'tag_suggestions' => fn (Options $options): array => $this->articleTagManager->getExistingTags(),
+ ]);
+ $resolver->setAllowedTypes('tag_suggestions', 'array');
+ }
+
+ public function getParent(): string
+ {
+ return HiddenType::class;
+ }
+}
diff --git a/src/Manager/ArticleEntityDuplicator.php b/src/Manager/ArticleEntityDuplicator.php
index 447f3d7..510a812 100644
--- a/src/Manager/ArticleEntityDuplicator.php
+++ b/src/Manager/ArticleEntityDuplicator.php
@@ -21,5 +21,6 @@ public function duplicateData(ContentInterface $oldContent, ContentInterface $ne
{
$newContent->setAuthor($oldContent->getAuthor());
$newContent->setPublishedAt($oldContent->getPublishedAt());
+ $newContent->setTags($oldContent->getTags());
}
}
diff --git a/src/Manager/ArticleTagManager.php b/src/Manager/ArticleTagManager.php
new file mode 100644
index 0000000..fdb7ff0
--- /dev/null
+++ b/src/Manager/ArticleTagManager.php
@@ -0,0 +1,91 @@
+contentManager->getRepository('article')->createQueryBuilder('a')
+ ->select('a.tags')
+ ->andWhere('a.tags IS NOT NULL')
+ ->getQuery()
+ ->getScalarResult();
+
+ $tags = [];
+ $seenTags = [];
+
+ foreach ($rows as $row) {
+ $rowTags = $row['tags'] ?? null;
+
+ if (is_string($rowTags)) {
+ $decodedTags = json_decode($rowTags, true);
+ $rowTags = is_array($decodedTags) ? $decodedTags : [];
+ }
+
+ if (!is_array($rowTags)) {
+ continue;
+ }
+
+ foreach ($rowTags as $tag) {
+ if (!is_scalar($tag)) {
+ continue;
+ }
+
+ $normalizedTag = preg_replace('/\s+/', ' ', trim((string) $tag)) ?: '';
+
+ if ('' === $normalizedTag) {
+ continue;
+ }
+
+ $normalizedKey = mb_strtolower($normalizedTag);
+
+ if (isset($seenTags[$normalizedKey])) {
+ continue;
+ }
+
+ $seenTags[$normalizedKey] = $normalizedTag;
+ $tags[] = $normalizedTag;
+ }
+ }
+
+ natcasesort($tags);
+
+ return array_values($tags);
+ }
+
+ public function canonicalizeTags(array $tags): array
+ {
+ $existingTags = [];
+
+ foreach ($this->getExistingTags() as $existingTag) {
+ $existingTags[mb_strtolower($existingTag)] = $existingTag;
+ }
+
+ $canonicalTags = [];
+
+ foreach ($tags as $tag) {
+ if (!is_scalar($tag)) {
+ continue;
+ }
+
+ $normalizedTag = preg_replace('/\s+/', ' ', trim((string) $tag)) ?: '';
+
+ if ('' === $normalizedTag) {
+ continue;
+ }
+
+ $normalizedKey = mb_strtolower($normalizedTag);
+ $canonicalTags[] = $existingTags[$normalizedKey] ?? $normalizedTag;
+ $existingTags[$normalizedKey] = $existingTags[$normalizedKey] ?? $normalizedTag;
+ }
+
+ return $canonicalTags;
+ }
+}
diff --git a/src/Migrations/Version20230325161610.php b/src/Migrations/Version20230325161610.php
index b2682a9..4178b6f 100644
--- a/src/Migrations/Version20230325161610.php
+++ b/src/Migrations/Version20230325161610.php
@@ -4,8 +4,8 @@
namespace Softspring\CmsBlogPlugin\Migrations;
-use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230325161610 extends AbstractMigration
diff --git a/src/Migrations/Version20230325161836.php b/src/Migrations/Version20230325161836.php
index cedff64..523921f 100644
--- a/src/Migrations/Version20230325161836.php
+++ b/src/Migrations/Version20230325161836.php
@@ -4,8 +4,8 @@
namespace Softspring\CmsBlogPlugin\Migrations;
-use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230325161836 extends AbstractMigration
diff --git a/src/Migrations/Version20260525090000.php b/src/Migrations/Version20260525090000.php
new file mode 100644
index 0000000..fa494eb
--- /dev/null
+++ b/src/Migrations/Version20260525090000.php
@@ -0,0 +1,35 @@
+connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
+ $this->addSql('ALTER TABLE cms_content_blog_article ADD tags JSON DEFAULT NULL');
+ $this->addSql("UPDATE cms_content_blog_article SET tags = '[]' WHERE tags IS NULL");
+
+ return;
+ }
+
+ $this->addSql("ALTER TABLE cms_content_blog_article ADD tags LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)'");
+ $this->addSql("UPDATE cms_content_blog_article SET tags = '[]' WHERE tags IS NULL");
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE cms_content_blog_article DROP tags');
+ }
+}
diff --git a/src/Model/ArticleContentInterface.php b/src/Model/ArticleContentInterface.php
index f22f0cb..ea73446 100644
--- a/src/Model/ArticleContentInterface.php
+++ b/src/Model/ArticleContentInterface.php
@@ -10,4 +10,8 @@ interface ArticleContentInterface extends ContentInterface
public function getPublishedAt(): ?DateTime;
public function setPublishedAt(?DateTime $publishedAt = null): void;
+
+ public function getTags(): array;
+
+ public function setTags(array $tags): void;
}
diff --git a/src/Model/ArticleContentTrait.php b/src/Model/ArticleContentTrait.php
index 2e0da59..e04622f 100644
--- a/src/Model/ArticleContentTrait.php
+++ b/src/Model/ArticleContentTrait.php
@@ -8,6 +8,8 @@ trait ArticleContentTrait
{
protected ?int $publishedAt = null;
+ protected ?array $tags = [];
+
public function getPublishedAt(): ?DateTime
{
return $this->publishedAt ? DateTime::createFromFormat('U', "{$this->publishedAt}") : null;
@@ -17,4 +19,38 @@ public function setPublishedAt(?DateTime $publishedAt = null): void
{
$this->publishedAt = $publishedAt instanceof DateTime ? (int) $publishedAt->format('U') : null;
}
+
+ public function getTags(): array
+ {
+ return $this->tags ?? [];
+ }
+
+ public function setTags(array $tags): void
+ {
+ $normalizedTags = [];
+ $seenTags = [];
+
+ foreach ($tags as $tag) {
+ if (!is_scalar($tag)) {
+ continue;
+ }
+
+ $normalizedTag = preg_replace('/\s+/', ' ', trim((string) $tag)) ?: '';
+
+ if ('' === $normalizedTag) {
+ continue;
+ }
+
+ $normalizedKey = mb_strtolower($normalizedTag);
+
+ if (isset($seenTags[$normalizedKey])) {
+ continue;
+ }
+
+ $seenTags[$normalizedKey] = true;
+ $normalizedTags[] = $normalizedTag;
+ }
+
+ $this->tags = $normalizedTags;
+ }
}