kitpages/activity-bundle
Kitpages ActivityBundle is a Symfony bundle for tracking and displaying user activity streams. It provides Doctrine-backed activity entities, services, and templates to log actions and render feeds, making it easier to add timeline or notification-style activity lists to an application.
Read this first: kitpages/activity-bundle is a Symfony bundle, not a Laravel package. It depends on Symfony DI, Doctrine ORM, and Twig. In a Laravel codebase you cannot use it idiomatically — you would need doctrine/orm plus the Symfony container, which is a bad trade. The Laravel equivalent is spatie/laravel-activitylog, or a hand-rolled activities table with a model observer. The rest of this assessment assumes you are working in a Symfony 3.4/4.x app (the era of the last release, September 2018) and treating the bundle as legacy-but-workable infrastructure.
Minimal steps for a Symfony app:
composer require kitpages/activity-bundle — pin the exact tag that resolves. Expect the constraints to be php: ^7.x / symfony: ^3.x|^4.x; check with composer why-not php 8.1 before you commit.app/AppKernel.php (or config/bundles.php on Symfony 4 Flex):
new Kitpages\ActivityBundle\KitpagesActivityBundle(),
bin/console doctrine:schema:update --force (or generate a migration and inspect the SQL — the bundle ships its own mapping, so let Doctrine diff it).kitpages_activity:
activity_definition:
comment_created:
class: AppBundle\Activity\CommentCreatedActivity
template: activity/comment_created.html.twig
bin/console debug:container | grep kitpages_activity to find the manager service id, then call it once from a controller to see a row land in the table.Where to look first: the bundle's Activity/ directory (base class / interface you extend for each activity type), the ActivityManager service (the only public API you should call), and Resources/config/services.yml to see what is actually registered. The README covers the config shape; it does not cover upgrades, which is where you will spend your time.
First use case: "user commented on a page." Write a domain service method that persists the comment and calls the activity manager with the type comment_created and a payload of ['comment_id' => ..., 'page_id' => ..., 'user_id' => ...]. Render it on the profile page as a one-line feed item. That end-to-end slice exercises the config, the manager, the Doctrine mapping, and the template — everything that can break.
One class + one Twig template per activity type. The bundle's model is a registry of named activity definitions, each pointing at a class and a template. Treat that config block as your activity taxonomy: comment_created, page_published, user_followed. Resist the urge to overload a single generic type with a subtype field — the template lookup is the whole point.
Record from a single choke point. Two good patterns:
CommentService::post() writes the comment and the activity in the same transaction. Best for correctness: if the comment rolls back, so does the activity.kernel.event_subscriber (or Doctrine postPersist listener) that reacts to your domain events and converts them into activities. Best when activities are cross-cutting and you don't want domain services depending on the activity manager.Do the first one when the activity is a product feature; do the second when it's an audit trail.
public function post(Page $page, User $user, string $body): Comment
{
$comment = new Comment($page, $user, $body);
$this->em->persist($comment);
$this->activityManager->createActivity('comment_created', [
'comment_id' => $comment->getId(),
'page_id' => $page->getId(),
'user_id' => $user->getId(),
]);
$this->em->flush();
return $comment;
}
(Confirm the exact createActivity signature against the installed tag — it is the one thing the README does document, and it changed between minor versions.)
Payload = ids and scalars only, not entities. The activity row is a historical record. If you serialize whole entities into the data column, changing a Comment property later breaks every old row. Store ids, resolve at render time through a repository, and accept that a deleted entity renders as "[deleted]".
Render in the feed loop. Each activity's configured template gets the decoded payload, so the item template stays tiny and type-specific:
{% for activity in feed %}
<li class="activity-item">
{{ include(activity.template, { activity: activity, payload: activity.data }) }}
</li>
{% endfor %}
Feed queries should be your own. Use the manager to write, but write a repository method to read: findByFilters(array $types, ?User $user, \DateTime $since, $limit, $offset). The bundle's list helpers are thin; once you need "only my follows, excluding my own actions, last 30 days," you are writing the DQL anyway.
Same table, two audiences. A user-facing feed and a back-office audit log can share the table and differ only by filter and template. That is the cheapest way to get audit value out of this bundle.
composer.json, and expect to patch the package in place (or fork it) the first time you move to PHP 8 or Symfony 5+.composer why-not, then either --ignore-platform-reqs (risky), fork with widened constraints, or vendor the ~10 classes you actually use. Vendoring is usually the right call.spatie/laravel-activitylog and save a week.serialized you inherit a class-rename landmine; convert to JSON in your own manager wrapper if the bundle allows it, or write a migration to normalize old rows.{% if activity.user is not null %} or you get 500s on deleted accounts — this is the single most common production failure for activity feeds.|raw. Activity payloads originate from user input (comment excerpts, page titles). Escape on write or strictly escape in the template; do not |raw a payload field.createActivity flushes on its own in some versions, which means an activity can survive a rolled-back parent write. Wrap parent + activity in one beginTransaction/commit, or accept eventual inconsistency deliberately.app/Resources/KitpagesActivityBundle/views/... (Symfony 3) or templates/bundles/KitpagesActivityBundle/... (Symfony 4), rather than editing the vendor directory.bin/console debug:container kitpages_activity for the service id, debug:config kitpages_activity to confirm your definitions are actually loaded after a cache clear. A missing definition usually fails at write time with an unhelpful manager-level exception — put a try/catch with the activity type name in the message.How can I help you explore Laravel packages today?