Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fork Sonata Project Classification Bundle Laravel Package

paysera/fork-sonata-project-classification-bundle

A Paysera-maintained fork of the Sonata ClassificationBundle for Symfony, providing taxonomy and classification management for categories, tags, collections, and contexts, with Sonata Admin integration for organizing and reusing content classifications.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal path to a working taxonomy:

  1. Install (note: this is the Paysera fork, not sonata-project/classification-bundle—pull it explicitly):

    composer require paysera/fork-sonata-project-classification-bundle
    
  2. Register the bundle in config/bundles.php (or AppKernel.php):

    Sonata\ClassificationBundle\SonataClassificationBundle::class => ['all' => true],
    
  3. Provide concrete entities. Extend the bundle's base classes and point the config at them. Media is the only one you must extend (it's a mapped superclass); extending Category, Tag, Collection, and Context is strongly recommended so you can add fields like image or SEO metadata without forking:

    sonata_classification:
        class:
            category:   App\Application\Sonata\ClassificationBundle\Entity\Category
            tag:        App\Application\Sonata\ClassificationBundle\Entity\Tag
            collection: App\Application\Sonata\ClassificationBundle\Entity\Collection
            context:    App\Application\Sonata\ClassificationBundle\Entity\Context
            media:      App\Application\Sonata\ClassificationBundle\Entity\Media
    
  4. Create the schema with migrations (bin/console make:migration / doctrine:migrations:migrate), not schema:update, because the tree columns (tree_left, tree_right, tree_level, …) come from the Gedmo tree listener and need to be baked into a real migration.

  5. First use case: create a Context (e.g. default) via admin or code, then a root Category inside it, then fetch both from a controller:

    $context = $this->get('sonata.classification.manager.context')->findOneBy(['slug' => 'default']);
    $categories = $this->get('sonata.classification.manager.category')
        ->findBy(['context' => $context, 'enabled' => true]);
    

Where to look first: Sonata\ClassificationBundle\Model\ for the entity interface (shows every field and association), then the Entity/ folder for concrete defaults, then Admin/ to see how the Sonata admin is wired. The Manager/ folder is thin—most real work happens in the Doctrine repositories, especially CategoryRepository wrapping the Gedmo nested-set repository.

Implementation Patterns

Contexts are the outer boundary—use them deliberately. A Context namespaces your whole taxonomy; categories, tags, and collections are each scoped to exactly one. The clean pattern is one context per content domain (e.g. blog, products, events) so that a "News" category in the blog context never collides with a "News" category in products. In queries, always constrain by context first, or you'll get cross-domain leakage in autocompletes and tag clouds.

Tree operations on categories. Categories use Gedmo's nested-set tree. Fetch a subtree, not a flat list, when you're rendering navigation:

$categoryManager = $this->get('sonata.classification.manager.category');
$root = $categoryManager->findOneBy(['slug' => 'root', 'context' => $context]);
$children = $categoryManager->findBy(['parent' => $root]);

Or drop to the repository to use getChildren($node, true) for a hydrated tree, getPath($node) for breadcrumbs, and getRootNodes($context) for a sidebar. If you need a whole tree in one query, use getNodesHierarchy() and cache the result.

Tags for freeform labelling. Tags are flat (no tree) and many-to-many with whatever you attach them to. The pattern that works: expose a Sonata admin for tags filtered by context, let editors curate them, and attach via a ManyToMany on your own content entity. Don't reuse the classification tag entity as your content's owner—join it rather than nesting.

Collections for curated groupings. Collection is the classification bundle's answer to "featured sets" or "sections" — flat, context-scoped, and typically rendered as a list. It's the least opinionated of the four; treat it as an ordered bucket of categories or as a label for a hand-picked set of content.

Media as a shared image store. BaseMedia gives you a blob column (content) plus contentType, width, height, and a provider so you can stream bytes:

$media = $this->get('sonata.classification.manager.media')->find($id);
$response = $media->getContentAsResponse($media->getContentType());

This is fine for small images but is not a CDN replacement—if you're serving real media, integrate SonataMediaBundle instead and keep classification media as thumbnails only.

Integration with Sonata admin/other Sonata bundles. Once enabled, the admin generates routes under /admin/sonata/classification/{category,tag,collection,context} (or whatever your admin prefix is). SonataNewsBundle, SonataPageBundle, and SonataBlockBundle expect the classification entities and their managers to be present, so enabling classification before those is the usual order. If you're building a custom admin for your own content, add a sonata_type_model_list field pointing at the tag/category manager to get the tree picker for free.

Programmatic creation. For seeding (fixtures, imports, multi-tenant setup), create entities through the managers so the tree listener fires:

$context = $contextManager->create();
$context->setName('Blog')->setSlug('blog')->setEnabled(true);
$contextManager->save($context);

$cat = $categoryManager->create();
$cat->setContext($context)->setName('News')->setSlug('news')->setEnabled(true);
$categoryManager->save($cat); // Gedmo assigns tree position here

Do not persist with raw EntityManager::persist() in a batch loop for tree entities—Gedmo's nested-set listener needs to run per insert, otherwise tree_left/tree_right are wrong and the tree silently corrupts.

Gotchas and Tips

  • It's archived and forked. Zero stars, no dependents, MIT, archived. Treat it as a frozen dependency: pin the exact version in composer.json, and accept that security updates or Symfony/PHP-version fixes will not arrive from upstream. The Paysera fork exists mainly to keep the bundle installable on newer Symfony/PHP; it is not a maintained successor. If you're starting greenfield, use sonata-project/classification-bundle directly, and plan an exit strategy (a thin in-house taxonomy) if this bundle is load-bearing.

  • The tree is the #1 source of "weird" bugs. If categories disappear from admin, render in the wrong order, or getParent() loops, it's almost always a tree_left/tree_right inconsistency. Rebuild with bin/console doctrine:tree:recover on the category repository (or call $repo->recover() in a command). Never hand-edit rows or run schema:update against the tree columns.

  • Slugs are unique per context, not globally. The unique constraint is (slug, context_id). If your fixtures set the same slug in two contexts and your app assumes global uniqueness, you'll get silent collisions in findOneBy(['slug' => …]) — always add context to that findOneBy.

  • Context is required. Queries missing a context filter will return everything across contexts. Wrap it in a small service (TaxonomyFactory::categoriesForContext($slug)) so the filter is enforced in one place instead of 40 controllers.

  • enabled = false is not a filter. The bundle doesn't hide disabled entities from repository methods; you have to filter on enabled yourself. The admin does it for you, which makes the discrepancy easy to miss.

  • Config quirks. The class: map must be fully resolved before doctrine.orm.mappings runs; if you see "The class '…' was not found in the chain configured namespaces," your concrete entity is either in an excluded namespace or missing @ORM\Entity. Media is the one you cannot skip—the base class is a mapped superclass, so the bundle will fatally error if you don't supply a concrete class.

  • Migrations need the tree listener. If you generate the schema by diff and then run it on a fresh DB, the tree columns will be NOT NULL with no defaults; the initial insert must go through the manager so Gedmo populates them. A common failure is a fixture that bulk-inserts categories with raw SQL and leaves tree_left = 0 on every row.

  • Extensions points. If you need a new field, extend the base entity (preferred). If you need new tree behavior, listen on postPersist/preUpdate around $categoryManager->save(). If you need a new admin action, decorate the bundle's admin service via a compiler pass or simply subclass the admin class and register your own service pointing at the same model manager.

  • Caching. The category tree is read-heavy and tiny in absolute terms. Cache getNodesHierarchy() output keyed by context slug and invalidate on postPersist/postUpdate of any classification entity. Without this, menu rendering will hammer the DB on every page view.

  • Debugging checklist for "categories don't show up": (1) context slug matches, (2) enabled = true, (3) parent is null or points at an existing node, (4) tree columns are non-zero, (5) you fetched via the manager and not a raw repository call. In that order, because that's roughly the frequency each causes the symptom.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource