ezsystems/comment-bundle
A Symfony bundle that adds a full comment system to your application, with threaded comment trees, configurable sorting, a REST API, event extensibility, optional ACL protection, Akismet spam filtering, and Doctrine ORM or MongoDB persistence.
composer require ezsystems/comment-bundle. Expect the package to be flagged abandoned on Packagist and to carry Symfony 3.4|4.4 constraints — check composer why-not and your PHP platform before assuming it resolves. On Symfony 5+/PHP 8 you will likely need a fork or replace in composer.json.config/bundles.php as FOS\CommentBundle\FOSCommentBundle::class => ['all' => true]. The package name is misleading: the namespace is still FOS\CommentBundle and the config root is fos_comment, not ezsystems_comment. Verify with bin/console debug:container | grep fos_comment before debugging anything else.Resources/doc/index.md (plus integration, acl, spam_detection, markup_parser, rest pages) in the vendor directory. It's the only reliable reference; there is no maintained docs site.config/packages/fos_comment.yml: set db_driver: orm (or mongodb), and map class.model.comment / class.model.thread to your own entity FQCNs. Without the class block you'll get the "child config must be configured" error immediately.FOS\CommentBundle\Model\Comment and Thread as mapped superclasses (the doc shows a MappedSuperclass base + @ORM\Entity subclass), then bin/console make:migration / doctrine:schema:validate. You must implement the abstract bits and add a parent/children self-association for the tree.{% include '@FOSComment/Thread/async.html.twig' with {'id': 'article-' ~ article.id} %}
The thread is created lazily on first request from the identifier. Check Resources/views/ in vendor for the exact template path in your version.Model/Thread.php and Model/Comment.php (what's denormalized), Entity/CommentManager.php (the write path), Controller/ (the route surface), and Resources/doc/. If debug:router | grep fos_comment shows nothing, the bundle wasn't registered or routing wasn't imported.Thread-per-content-entity with namespaced identifiers. The Thread.identifier is your join key to the host entity. Always prefix by content type (article-42, product-7), never bare IDs — one thread table shared across content types will collide. Keep a helper (ThreadLocator::forArticle($article)) so every entry point resolves the same string.
Async rendering for cacheable pages. The async template loads the tree over JSON and keeps your page fragments cacheable; the server-rendered variant couples comment state to page caching and is usually a mistake. Pass id, and if you customize routes also pass the thread URL.
Write through the manager, never the EntityManager. The manager owns tree maintenance (path/level/depth), parent/child wiring, and the thread's numComments / lastCommentAt counters. From your own controller or messaging consumer:
$comment = $commentManager->createComment($thread);
$comment->setBody($body);
$comment->setAuthor($user); // or your own author relation
$commentManager->saveComment($comment);
If you need custom moderation, set state (STATE_VALID / STATE_PENDING / STATE_SPAM / STATE_DELETED) rather than deleting rows — the tree and counters assume comments still exist.
Events are the business-logic seam. Register listeners for the lifecycle events (FOSCommentEvents::COMMENT_CREATE, COMMENT_PRE_PERSIST, COMMENT_POST_PERSIST, THREAD_CREATE, etc.) for: notification emails, moderation queueing, search indexing, auto-closing comment threads past N days, and rejecting comments from banned users. Prefer PRE_PERSIST for validation, POST_PERSIST for side effects that need an ID.
Authorization: pick one layer. Either use the bundle's Symfony ACL integration (fos_comment.acl.* — configure permissions per thread) or, more often, a small Voter that checks thread->getIdentifier() against the underlying entity (canComment(Article $article)). Don't enable both; double-denial is a painful hour to debug.
Persistence customization. For a hot comments query, override the repository via your entity's repositoryClass (eager-join authors to avoid N+1 on the tree endpoint) or override class.model.comment. Add DB indexes on thread_id, parent_id, createdAt.
API-first consuming. The FOSRestBundle endpoints give you a JSON tree. Configure serialization exclusion strategies/groups so author emails and internal fields don't leak. If you're building a SPA, skip the bundled JS client entirely and call the REST routes with your own frontend; if you don't need JSON beyond a thin wrapper, you can drop FOSRestBundle from the dependency graph and write two small controllers around the managers.
getSubscribedServices, controller signatures, FOSRest API changes).ezsystems/comment-bundle installs FOS\CommentBundle with root config fos_comment and Twig namespace @FOSComment. Searching config for "ezsystems" gives a false negative.Model/ classes are plain PHP. Miss a mapping field (state, depth, numComments) and failures surface as cryptic null counters or broken tree ordering, not schema errors.parent or children directly through Doctrine (bulk DQL, fixtures, imports) leaves path/depth stale. Always go through fos_comment.manager.comment.saveComment(). If you deleted rows in bulk, write a console command that recomputes thread counters.fos_comment.acl demands ACEs for every thread; threads without ACEs deny everyone. Add it only after threads exist and seed permissions in a command.UserInterface, write an event listener that maps your user to the comment's author; don't call fos_user.user_manager.test/dev or mock fos_comment.spam_detector; otherwise test suites become slow and flaky.Comment.body is stored raw. Configure a markup parser (html_purifier or sundown) or sanitize at render time — never |raw. A purifier service is also where you enforce a whitelist of allowed HTML.fos_comment_comment_tree, fos_comment_comment_new_head, etc.). Change the prefix/host and the async thread renders empty with a 404 in devtools — nothing in PHP logs.Comment.parent ↔ children self-reference loops through JMS/json_encode when you serialize a thread. Set @MaxDepth / groups, or serialize a flattened tree assembled in a listener.debug:router | grep fos_comment (routes imported?), debug:container fos_comment.manager.comment (services wired?), step through Entity/CommentManager::saveComment (which events fired, in what order?), then check var/log for event-listener exceptions swallowed by the controller.How can I help you explore Laravel packages today?