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

Activity Logger Bundle Laravel Package

modera/activity-logger-bundle

A Laravel bundle for recording user and system activity, giving you an audit trail of events across your application. Log actions, review histories, and track what changed and who changed it. Useful for compliance, debugging, and monitoring workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. modera/activity-logger-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no publishable migrations, and no config/activity-logger.php. It expects Symfony's DI container, Doctrine ORM, and the Symfony event dispatcher. Last release was 2016-06-10 (Symfony 2.8/3.x, PHP 5.6/7.0 era), 4 stars, 0 dependents. Do not plan to composer require it into a Laravel app and "just wire it up" — it will not boot.

Two legitimate ways to start:

  1. You are actually on a legacy Symfony app (or maintaining one). Then this is a genuine fit.
  2. You are on Laravel and evaluating it as a design reference for an audit/activity trail. In that case read the patterns below and implement them with spatie/laravel-activitylog + your own type registry — the concepts map almost 1:1.

Minimal Symfony setup:

composer require modera/activity-logger-bundle
// app/AppKernel.php
new Modera\ActivityLoggerBundle\ModeraActivityLoggerBundle(),
# app/config/config.yml
modera_activity_logger:
    enabled: true
    activities:
        acme.invoice.sent:   { label: "Invoice sent" }
        acme.user.invited:   { label: "User invited" }

Then update the schema for the bundle's Activity entity (doctrine:schema:update --force in dev, a generated migration elsewhere) and import the bundle's routing if you want its listing controller.

Where to look first, in order:

  1. DependencyInjection/Configuration.php — the authoritative list of config keys for this version. Ignore blog posts.
  2. Manager/ActivityManagerInterface — the whole public surface: essentially log() plus query/count methods.
  3. Manager/DelegatingActivityManager — the indirection layer; it decides which concrete manager actually handles your call.
  4. Entity/Activity (or the Model/ActivityInterface it maps) — the storage shape: type, author, description, target, meta, createdAt.

First use case: log a domain event with who/what/where, then render a per-record timeline. Something like "Alice sent invoice #4411" — type = acme.invoice.sent, author = Alice, target = Invoice#4411, meta = ['total' => ..., 'request_id' => ...].

Implementation Patterns

Inject the interface, never the concrete class. Autowire Modera\ActivityLoggerBundle\Manager\ActivityManagerInterface (or alias it explicitly in services.yml). This is what keeps you swappable when the delegating manager resolves to a different backend.

final class SendInvoice
{
    public function __construct(private ActivityManagerInterface $activityManager) {}

    public function __invoke(Invoice $invoice, User $actor): void
    {
        // ... send ...

        $this->activityManager->log(
            'acme.invoice.sent',
            sprintf('Invoice %s sent to %s', $invoice->getNumber(), $invoice->getRecipientEmail()),
            ['total' => $invoice->getTotal(), 'request_id' => $this->requestId()],
            $actor,
            $invoice
        );
    }
}

Treat the configured type registry as an enum. Define types once in modera_activity_logger.activities, then reference them through PHP constants. The registry is what gives you human-readable labels, filtering, icons/colors in the UI, and (often) priority. A typo'd string silently produces an unlabeled, unfilterable entry — no exception, no warning.

Use target for entity timelines, meta for context. The target is effectively a polymorphic (class, id) pair, so WHERE target = :invoice gives you "everything that ever happened to this invoice." Keep meta to scalars — request ID, IP, a small diff of changed fields. It's serialized into a single column; stuffing entities or large arrays in there is how you get 2 MB rows and unserialize failures after a refactor.

Pass author = null for system/console actions and log them through the same manager rather than a separate channel. The bundle ships console-oriented handling for exactly this; keeping one write path means one query path and one retention policy.

Query with the manager, not the repository. Filter by type/author/target, always pass limit/offset, always order newest-first. If you go around the manager into the Doctrine repository you lose the delegating layer and any filtering it applies — including, potentially, visibility scoping.

Decorate, don't fork, for fan-out. The delegating manager pattern exists so other bundles (or your app) can register additional managers. Use that hook, or a Doctrine postPersist listener on the activity entity, to mirror events to Slack, an audit SIEM, or a Laravel app via HTTP. Do not put the fan-out in the caller — you'll forget it in half the call sites.

Decide sync vs. post-commit early. Logging inside the same transaction as the business write means a rolled-back transaction can still leave (or lose) an activity, depending on ordering. For anything financially or legally relevant, dispatch to a queue or kernel.terminate and log after commit.

Mapping to Laravel, if you're porting:

Bundle concept Laravel equivalent
type + config registry log_name + a PHP enum
description description
meta properties (JSON)
target subject() morph
author causer()
DelegatingActivityManager a custom ActivityLogger binding

Write the port as a small ActivityLogger service with an enum of types and a ->to($subject) builder; you'll get 90% of the bundle's value in an afternoon and none of the abandonment risk.

Gotchas and Tips

It will not install on a modern stack. Composer constraints pin Symfony 2.x/3.x and PHP 5.x/7.0-era Doctrine. On PHP 8.1+ / Symfony 5–6 you'll be fighting composer why-not and, if you force it, untyped-property and deprecated-API errors. Fork and patch, or don't. Budget for "we now own this package."

Zero community. 4 stars, 0 dependents, no releases since 2016, no security advisories and nobody to issue them. There is no upgrade path and no one to ask. If it's load-bearing, put a named owner and a pinned commit hash on it.

Schema comes from annotations, not migrations. There are no Laravel-style migration files to publish. doctrine:schema:update in dev will "work" and then silently diverge from production; generate a real migration and diff it.

enabled: false makes logging a no-op, silently. If activities stop appearing, check the config flag and the container before you check your call site:

bin/console debug:container modera_activity_logger --show-private

Confirm which implementation ActivityManagerInterface resolves to. If a decorator is misregistered you'll get a manager that accepts log() and discards it.

Orphaned targets. No foreign key on the target — delete an invoice and its activities survive pointing at a nonexistent ID, and any timeline renderer that lazy-loads the target will 500. Either null the target in a preRemove listener or render defensively (target ?? 'deleted').

Type typos are invisible. No validation against the registry at log time. Centralize type strings as constants and add a dev-environment assertion that every logged type exists in config.

Check the listing controller's access control. The bundle ships a UI/endpoint for browsing activities; whether it's behind your firewall and scoped to the current user is your configuration problem. An unscoped activity list is a data-leak: it exposes other users' actions, IPs, and metadata. Verify with a second, low-privilege account before shipping.

No retention policy. The table grows forever with serialized meta. Write a purge command on day one (e.g. keep 90 days of acme.*, 7 years of anything flagged audit-critical) — retrofitting one onto 50M rows is a migration project.

Debugging reads: query the table directly first (type, created_at, target_id) to separate "not logged" from "not displayed". The two failure modes look identical in the UI and have completely different causes.

Extension points worth knowing: the model/interface indirection for the activity entity (map your own subclass if you need extra columns), a custom manager registered with the delegating manager (swap Doctrine for something else), and Doctrine lifecycle listeners on the activity entity (fan-out, enrichment, retention). Anything beyond those three means forking — and at that point, seriously reconsider whether a maintained alternative or a 200-line in-house implementation is the better bet.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle