— 12 min de lectura
Cuando un proyecto Symfony maduro necesita una capa de contenido, la propuesta habitual del mercado es la misma: monta un CMS aparte, expón una API y sincroniza. A partir de ahí tienes 2 fuentes de verdad, un proceso de sincronización que mantener y un modelo de datos que ya no vive donde tú lo escribiste.
Armonic parte de la posición contraria. Es un bundle Symfony, no una aplicación externa, y se apoya en el Doctrine que ya tienes. Tus entidades no se copian ni se envuelven: se convierten en recursos editoriales o se relacionan con ellos. El negocio conserva su modelo y el CMS aporta lo que le corresponde, que es edición, publicación y composición de páginas.
Vamos a verlo con código real de proyectos en producción.
Tipos de contenido respaldados por tus propias entidades
Un tipo de contenido en Armonic puede estar respaldado por una entidad específica que tú defines. Es el enfoque que usamos, por ejemplo, para tecnologías y proyectos en nuestra propia web.
La configuración del tipo de contenido vive en un YAML:
# cms/contents/technology/config.yaml
content:
revision: 1
entity_class: 'App\Entity\Cms\TechnologyContent'
extra_fields:
description:
type: translation
type_options:
type: textarea
logo:
type: media
Y la entidad se declara como cualquier otra entidad Symfony:
#[ORM\Table(name: 'cms_content_technology')]
#[ORM\Entity]
class TechnologyContent extends Content
{
}
Con eso, una tecnología hereda las capacidades editoriales de Armonic: traducciones, imágenes, administración, layouts, publicación y SEO. No has renunciado a nada del lado Doctrine y no has escrito un panel de administración.
El mismo patrón se aplica a ProjectContent, donde añadimos campos como cliente, descripción e imagen.
Relacionar contenido editorial con entidades que ya existen
Una entidad editorial puede tener relaciones Doctrine normales con entidades que ya forman parte de tu aplicación. Sin adaptadores ni tablas puente inventadas por el CMS.
En Librio, una página CMS de producto se relaciona con un producto real del catálogo:
#[ORM\Entity]
class ProductContent extends Content
{
#[ORM\ManyToOne(targetEntity: Product::class)]
#[ORM\JoinColumn(name: 'product_id', onDelete: 'CASCADE')]
private ?Product $product = null;
public function getProduct(): ?Product
{
return $this->product;
}
public function setProduct(?Product $product): void
{
$this->product = $product;
}
}
La página mantiene sus capacidades editoriales y los datos comerciales siguen perteneciendo al producto. Si mañana cambia el precio, cambia en un sitio.
La relación entre artículos y usuarios funciona igual:
<many-to-one
field="author"
target-entity="Softspring\CmsBlogPlugin\Model\AuthorInterface">
<join-column name="author_id" on-delete="SET NULL"/>
</many-to-one>
En tu aplicación solo tienes que indicar qué entidad implementa el concepto de autor:
sfs_cms_blog:
author:
class: 'App\Entity\User'
Reutilizas la tabla de usuarios que ya existe. No creas una segunda base de autores dentro del CMS que alguien tendrá que mantener sincronizada durante los próximos 5 años.
Los formularios de administración son formularios Symfony
Esto suena obvio y casi nunca lo es. Los formularios de Armonic son formularios Symfony extensibles: puedes añadir campos EntityType, validaciones, filtros o tipos personalizados con las mismas herramientas que usas en el resto del proyecto.
Librio, por ejemplo, añade un selector de productos al formulario CMS:
class ProductContentCreateForm extends ContentCreateForm
{
public function buildForm(
FormBuilderInterface $builder,
array $options,
): void {
parent::buildForm($builder, $options);
$builder->add('product', EntityType::class, [
'class' => Product::class,
'choice_label' => fn (Product $product) => $product->getName(),
]);
}
}
El tipo de contenido declara ese formulario personalizado:
content:
entity_class: 'App\Entity\Cms\ProductContent'
admin:
create:
type: 'App\Form\Type\Cms\ProductContentCreateForm'
update:
type: 'App\Form\Type\Cms\ProductContentUpdateForm'
En el plugin de blog aplicamos el mismo concepto para seleccionar autor:
$builder->add('author', UserType::class, [
'required' => true,
]);
Y también en los filtros del listado:
$builder->add('author', EntityType::class, [
'class' => AuthorInterface::class,
'choice_label' => 'displayName',
'required' => false,
]);
Entidades dentro de módulos y bloques
Los módulos pueden incorporar campos que seleccionan entidades existentes. El módulo de tarjeta tecnológica usa un tipo de formulario específico:
module:
module_options:
form_fields:
technology:
type: technologyCard
type_options:
constraints:
- notBlank
Ese tipo extiende EntityType y apunta a la entidad correspondiente:
class TechnologyCardType extends AbstractType
{
public function getParent(): string
{
return EntityType::class;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'class' => TechnologyContent::class,
'choice_label' => 'name',
'required' => false,
]);
}
}
El patrón se repite para seleccionar proyectos o artículos relacionados:
module:
module_options:
form_fields:
project:
type: projectCard
article:
type: blogArticleCard
En Librio, una tarjeta puede seleccionar directamente un producto:
module:
module_options:
form_fields:
product:
type: productCard
A partir de ahí, la plantilla recibe la entidad y usa sus datos:
<h3>{{ product.translation(app.request.locale).name }}</h3>
<p>
{{ product.translation(app.request.locale).shortDescription }}
</p>
{% set image = product.getImagesByType('card_en').first %}
Aquí es donde se ve el beneficio para quien edita: puede combinar datos vivos del negocio con campos editoriales propios, como títulos alternativos, llamadas a la acción o imágenes específicas para esa pieza.
Extender componentes internos, como los enlaces
La integración no se limita a contenidos completos. También puedes extender tipos de formulario internos del CMS.
En Librio, el selector de enlaces incorpora una opción nueva para enlazar con el configurador de un producto:
class LinkType extends BaseLinkType
{
public function buildForm(
FormBuilderInterface $builder,
array $options,
): void {
parent::buildForm($builder, $options);
$builder->add('product', EntityType::class, [
'class' => Product::class,
'required' => false,
]);
}
}
Un botón editorial puede apuntar a una ruta, una URL, un ancla o una entidad de negocio, sin que el editor tenga que pegar identificadores a mano.
Consultar entidades desde Twig
Cuando no necesitas guardar una relación editorial, puedes exponer repositorios o servicios mediante funciones y filtros Twig:
final class ProductExtension extends AbstractExtension
{
public function __construct(
private ProductRepository $products,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction(
'get_product_by_id',
[$this, 'getProductById'],
),
];
}
public function getProductById(int $id): ?Product
{
return $this->products->find($id);
}
}
Cualquier plantilla o bloque CMS accede al producto:
{% set product = get_product_by_id(productId) %}
{% if product %}
<h2>{{ product.name }}</h2>
{% endif %}
Es la vía razonable para datos que cambian solos: precios, stock, disponibilidad, resultados de búsquedas o listados calculados. Guardar una relación fija ahí solo te daría contenido desactualizado con mejor aspecto.
Referencias estables al importar y exportar contenido
Si los módulos contienen entidades, hace falta decidir qué se escribe en el fichero de exportación. Armonic permite personalizar cómo se exportan e importan esas referencias.
Librio identifica los productos por su código estable:
public function export(mixed $product): array
{
return [
'_product' => $product->getCode(),
];
}
public function import(mixed $data): ?Product
{
return $this->productRepository->findOneByCode(
$data['_product'],
);
}
Así puedes mover contenido entre entornos sin depender de que los identificadores internos de la base de datos coincidan. Quien haya sincronizado alguna vez un staging con producción sabe por qué esto importa.
Qué ganas con este enfoque
- Integración nativa con Symfony. Doctrine, Symfony Forms, servicios, repositorios y Twig. Las herramientas que tu equipo ya domina.
- Una única fuente de verdad. Usuarios, productos y demás datos siguen en sus tablas originales.
- Sin sincronización. No hay que copiar datos a un CMS externo ni vigilar que la copia siga siendo fiel.
- Flexibilidad editorial. Las entidades pueden aparecer en contenidos, tarjetas, carruseles, enlaces o bloques dinámicos.
- Adopción gradual. Puedes empezar con un simple selector
EntityTypey llegar hasta tipos de contenido y formularios completamente personalizados. No hay migración grande al principio.
Conviene decir también dónde no encaja: si tu aplicación no es Symfony, o si el contenido no tiene relación con tu modelo de negocio, un CMS headless convencional te resolverá el problema igual de bien y con menos código propio. Armonic gana cuando el contenido y el negocio se tocan.
Armonic es una capa editorial dentro de tu aplicación Symfony, no un sistema paralelo. Tu modelo de datos se queda donde está.
Revisa el código en GitHub o escríbenos y lo miramos sobre tu proyecto.
