sonata-project/article-bundle
Sonata Article Bundle adds article management to Symfony apps, offering SonataAdmin CRUD screens, publishing controls, categories, and Doctrine-backed content types for building news or blog sections.
The bundle is archived (last release 2021-07-17), so treat this as: pin exact versions, vendor the source, and plan to fork. It is a Symfony 3.4/4.x-era bundle built on the Sonata stack — it will not install cleanly on Symfony 5+/6/7 without patching.
sonata-project/article-bundle is not a standalone CMS. It supplies abstract models and admin/block plumbing that sits on top of SonataAdminBundle, SonataMediaBundle (images), SonataFormatterBundle (rich-text/Markdown), SonataBlockBundle (frontend rendering) and optionally SonataClassificationBundle (tags/categories/collections). You must wire the concrete classes yourself.composer require sonata-project/article-bundle:^2.x --with-all-dependencies
SonataArticleBundle in AppKernel::registerBundles() or config/bundles.php for the 4.x layout) and make sure SonataMediaBundle, SonataFormatterBundle, SonataBlockBundle and SonataAdminBundle are registered first.vendor/sonata-project/article-bundle/src/Entity/AbstractArticle.php first — it is a Doctrine MappedSuperclass carrying title, slug, abstract, content, image, publication dates, enabled and author. In src/AppBundle/Entity/Article.php:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sonata\ArticleBundle\Entity\AbstractArticle;
/**
* @ORM\Entity(repositoryClass="AppBundle\Repository\ArticleRepository")
* @ORM\Table(name="app_article")
*/
class Article extends AbstractArticle
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
}
Sonata\ArticleBundle\Admin\ArticleAdmin and register it with the sonata.admin tag (manager_type="orm", label, code, group, pager_type).src/Entity/ (the abstract models), src/Admin/ (what's pre-wired in the forms/lists), src/Block/ (block services and their execute() query logic), and Resources/views/ (the block templates you will almost certainly override).Extend-and-configure, never edit vendor. The bundle's own docs and code assume you subclass AbstractArticle, AbstractFragment, AbstractFragmentContainer and their admins. Your app/config/config.yml then points Sonata's admin/block config at your classes. Keep this one-to-one: AppBundle\Entity\Article ↔ AppBundle\Admin\ArticleAdmin.
Admin registration. Everything else in Sonata keys off the admin code, so choose it deliberately (e.g. app.admin.article) and reference it in sonata_admin.templates, dashboard groups and block admin_code settings:
app.admin.article:
class: AppBundle\Admin\ArticleAdmin
arguments: [~, AppBundle\Entity\Article, SonataAdminBundle:CRUD]
tags:
- { name: sonata.admin, manager_type: orm, label: Articles, group: Content, pager_type: sonata.admin.pager.doctrine.orm }
Publishing is your job, not the bundle's. enabled plus publicationDateStart/publicationDateEnd are just columns. The base admin list will happily show drafts and future items. Put the canonical filter in a repository method and use it everywhere (controller, block, sitemap):
public function findPublished(\DateTime $now = null): array
{
$now = $now ?: new \DateTime();
return $this->createQueryBuilder('a')
->where('a.enabled = :enabled')
->andWhere('a.publicationDateStart <= :now')
->andWhere('a.publicationDateEnd IS NULL OR a.publicationDateEnd >= :now')
->setParameters(['enabled' => true, 'now' => $now])
->orderBy('a.publicationDateStart', 'DESC')
->getQuery()
->getResult();
}
Blocks are the rendering layer. Use the shipped block services (sonata.article.block.* — check Resources/config/ for the exact ids in your version) for "latest articles", article detail and fragment rendering rather than hand-rolling Twig queries. Configure them via sonata_block.blocks.<service>.settings and, critically, override templates there rather than copying vendor templates into your app.
Formatters over raw HTML. Store the author's raw source in content and render through the formatter pool (sonata.formatter.pool) so Markdown/richhtml both work. Do this in a Twig extension or your controller, not in the entity — it keeps the model renderer-agnostic and avoids calling services from entities.
Media wiring. The image association is a ManyToOne to SonataMediaBundle's Media. Configure at least one media context (e.g. default with a thumbnail format) and reference the same context in the admin form field; mismatched contexts are the usual cause of "image doesn't render" in block templates.
Frontend controller. Write your own ArticleController with /blog/{slug} routing and a findOneBySlug() lookup that also enforces the publication window (return 404 for drafts — do not rely on the admin enabled flag alone). Overriding the bundle's shipped frontend controller/routes is possible but ties you to its URL assumptions.
Fragments for reusable content. Fragment/FragmentContainer are for shared page chunks (promos, footers, CTA strips) that editors manage centrally. Model them as their own admin group and render via the fragment block, not by duplicating the text into every article.
Programmatic import. Because everything goes through Doctrine entities, bulk migration from an old CMS is straightforward: instantiate Article, set slug/dates/enabled, attach a Media (or null), persist+flush in batches. Run it as a console command with --env=prod --no-debug so you don't blow memory on the admin's form services.
Localisation. The shipped model has no translation layer. If you need multi-locale content, either add a Gedmo Translatable/Translation entity in your subclass or run one article per locale with a locale column — decide this before content exists.
ReturnTypeWillChange-class errors, and Doctrine DBAL 3 breakage in the admin's createQuery overrides. Budget for a fork; keep composer.lock committed and constrain sonata-project/* exactly.bin/console debug:container | grep sonata.article, debug:router, and sonata:admin:list.@ORM\Entity, forgetting the $id property, or placing the subclass in a directory outside Doctrine's mapping config produces "No identifier/No mapping" errors that look unrelated to the bundle.slug is populated by Gedmo Sluggable; if the listener isn't registered (stof/doctrine-extensions-bundle or manual gedmo.listener.sluggable tag) it stays null, your route 404s, and nothing warns you. Regenerating a slug after a title edit silently breaks inbound links — freeze the slug on first persist.publicationDateEnd IS NULL means "never expires". Any query written as publicationDateEnd >= :now alone will hide every evergreen article. This is the classic silent-content-disappears bug.DATETIMEs. Editors scheduling "9am" get UTC. Normalise at write time in the admin or store an explicit timezone-aware policy.SonataBlockBundle caches block output; editors publish and see nothing change. Set cache: false (or a short TTL) in the block settings during authoring, and remember the block's cache key includes settings — changing templates in config may not invalidate it.sonata_admin.security / ROLE_APP_ADMIN_ARTICLE_*. New admins silently widen your attack surface.Media leaves a dangling reference and block templates throw on media_thumbnail. Add a Doctrine listener or a soft-delete policy on media.AbstractArticle subclasses will work in a controller and explode in a console command or fixture. Keep rendering in Twig extensions/services.getTemplate() in your admin for CRUD views, override block templates via sonata_block config, add AdminExtensions rather than editing ArticleAdmin, and decorate the manager/repository for custom publication rules. Those four cover nearly every customisation need without forking.Article yourself (a slug, content, publishedAt, enabled) and skip the bundle — you'd be reimplementing about 200 lines of entity/admin/block code in exchange for maintained dependencies. Adopt the bundle only where an existing Sonata installation already carries it.How can I help you explore Laravel packages today?