nti/log-bundle
NTI Log Bundle provides a centralized logging layer for PHP applications, with configurable handlers and straightforward framework integration. It helps teams standardize log output, route messages consistently, and keep logging configuration simple and maintainable across projects.
This is a Symfony bundle, not a Laravel package — there is no service provider, no config/log-bundle.php, and no facade. If you are coming from Laravel, budget an hour to get oriented in Symfony's bundle/config/DI model before touching the code. Nothing about v3.0.0 changes that; the release is Symfony/Doctrine housekeeping, not a Laravel bridge.
Install and register:
composer require nti/log-bundle:^3.0
Then enable it in config/bundles.php:
Nti\LogBundle\NtiLogBundle::class => ['all' => true],
If the bundle requires manual wiring (common for small bundles), check the README for the exact class name — 0-star bundles frequently ship with stale READMEs, and v3.0.0 renamed things.
v3.0.0 is a major bump — treat it as breaking even if the notes read like bugfixes. The four bullets are all compatibility work: annotations → PHP 8 attributes, EntityManager injection, event subscriber relocation, and entity-to-table mapping. Practically, adopting v3 means:
composer.json).doctrine.yaml only enables type: annotation, the bundle's own log entity may not map at all.^3.0 explicitly; an unconstrained require can silently pull a major.Look here first, in order:
composer.json — PHP, Symfony, and Doctrine version constraints. v3 needs a modern Symfony and attribute-capable Doctrine; verify against your majors.Resources/config/services.yaml (or config/services.yaml in newer layouts) — the real service IDs. Every class name in the README is a guess until you read this file, and v3.0.0 moved at least one subscriber between folders.EventListener/.DependencyInjection/Configuration.php — the actual config tree with defaults.Configure:
# config/packages/nti_log.yaml
nti_log:
doctrine:
enabled: true
# entity/event filters if supported
exception:
enabled: true
First use case: turn it on for exception logging only, in staging, before enabling Doctrine event logging. Exceptions are low-volume and easy to validate in var/log/. Doctrine logging on a write-heavy app can generate thousands of lines per request if misconfigured — and v3's mapping changes mean the first Doctrine-enabled run is also the first time you find out whether the bundle's entity maps cleanly against your schema.
For on-demand use, inject the bundle's logger service (find the ID in services.yaml, likely nti_log.logger) rather than reaching for LoggerInterface — the whole point is that it applies the bundle's formatting/channel conventions. If the service ID moved in v3, bin/console debug:container nti_log will tell you.
Exception logging as a safety net. The kernel exception listener gives you a consistent shape for uncaught exceptions without writing your own subscriber. Use it as a floor, not a replacement for monolog's fingers_crossed handler. If you already have Sentry or a Monolog exception handler, check whether you're now double-logging (see Gotchas).
Doctrine event capture for audit trails. The bundle subscribes to Doctrine lifecycle events (typically postPersist, postUpdate, postRemove, sometimes onFlush). Narrow what it sees as early as possible:
nti_log:
doctrine:
enabled: true
entities:
- App\Entity\Invoice
- App\Entity\Payment
If per-entity filtering isn't in the config tree, don't fight it — implement your own Doctrine subscriber that delegates to the bundle's logger service only when the entity is in an allowlist. Wrap, don't fork. This matters more in v3, because the subscriber you'd be decorating just moved; write your wrapper against the injected logger service, not the bundle's subscriber class, and it will survive the next release.
Verify the entity/table relationship before you rely on it. One of v3's four fixes is specifically "make relation between entity and database table" — which means that relationship was broken. Diff your schema before and after installing:
bin/console doctrine:schema:update --dump-sql
# or, if you use migrations:
bin/console doctrine:migrations:diff
If v3 changed the log table's name or its FK shape, you get a migration, not a surprise in production. Do this in the same PR as the composer bump.
On-demand logging inside domain services. The most useful and least risky part of the package:
final class PaymentReconciler
{
public function __construct(private NtiLogger $logger) {}
public function reconcile(Invoice $invoice): void
{
$this->logger->info('Reconciling invoice', ['id' => $invoice->getId()]);
// ...
}
}
Keep payloads scalar and small — IDs, statuses, amounts. Not entities. See Gotchas.
Pair with a Messenger worker. In async command handlers, on-demand logging is where this bundle earns its keep: one business event produces one log line with a consistent structure. That's far more useful than the Doctrine listener's automatic output during a bulk flush — and it sidesteps the multi-EntityManager ambiguity introduced by the "include entity manager" fix.
Environment gating. Wrap production config in when@prod / when@dev blocks. In dev, enable everything; in prod, exceptions only, or Doctrine events restricted to a short allowlist. Run bin/console cache:clear after every change — the container will tell you at warm-up time if keys are wrong.
It is not Laravel. No facade, no Log::channel(), no config/logging.php. The bundle talks to Symfony's LoggerInterface/Monolog under the hood, so channel routing happens in config/packages/monolog.yaml. If you maintain a Laravel codebase, this package cannot be dropped in at all — you'd port the concept, and there's no Doctrine-equivalent for free (use Eloquent observers).
The annotation → attribute change is the one that will bite you. v3 replaced the AnnotationReader with the attribute loader. Consequences:
#[ORM\Entity] / #[ORM\Table] attributes. Under Doctrine ORM 3 the annotation driver is gone entirely, so 2.x is effectively out of support.doctrine.orm.mappings config only enables type: annotation for the bundle's namespace, the entity will not map and you'll get "table not found" or a metadata exception at first flush, not at boot.Expect a table rename or mapping change; migrate deliberately. The "relation between entity and database table" fix is a schema-level change to someone's model. Run the schema diff (above) before deploy. If the table moved, backfill with a migration rather than letting --force drop and recreate — you'd lose your audit history in exactly the release where you most want it.
Multi-EntityManager apps: check which EM got injected. The "fix issue including entity manager" bullet means EM wiring changed. If you run more than one manager (doctrine.orm.entity_manager vs. named ones), confirm the listener is registered against the manager that owns the entities you care about. bin/console debug:event-dispatcher doctrine.event_manager and compare against your named managers. A listener attached to the wrong EM fails silently — no errors, just no log lines.
Subscriber relocation breaks class-name overrides. The "eventSubscriber on eventListener folder deprecation" fix moved the class. If you previously overrode the listener by FQCN in your own services.yaml, that override now points at nothing — Symfony won't complain; the bundle's version simply wins again. Re-check your overrides after the bump and prefer decorating the logger service, whose ID is more stable.
Zero stars, zero dependents, four-bullet release notes. Treat this as vendored code you happen to install via Composer. Pin the exact version, and be prepared to fork. Before adopting, diff the released tag against main — small bundles often have unreleased fixes that never got a tag, and v3.0.0's notes are terse enough that the diff is the real changelog.
Circular references when serializing entities. If the Doctrine listener logs the entity itself rather than an ID, you will hit CircularReferenceException on any bidirectional relationship, and you will trigger lazy loading inside a flush, producing queries mid-transaction. Verify the listener extracts identity/class metadata only. If it doesn't, override the listener service.
Infinite recursion if your log handler writes to the database. Doctrine event → logger → Monolog DB handler → Doctrine write → Doctrine event. This is doubly likely here because the bundle has its own log entity and table. If you route its channel to a database handler, ensure the listener is guarded (re-entrancy flag or SplObjectStorage). This is the single most common way people take down production with a logging bundle.
Listener ordering vs. onFlush. onFlush fires before SQL executes; postFlush after. If the bundle hooks onFlush, you may log changes that get rolled back or never produce an UPDATE. For audit purposes prefer postPersist/postUpdate/postRemove — but those only fire for entities with actual changes, so "no log line" doesn't mean "no attempt."
Double-logged exceptions. Symfony's ErrorListener and Monolog's FingersCrossedHandler already log uncaught exceptions; the bundle's listener adds a third. Either set nti_log.exception.enabled: false in prod, or give the bundle's channel level: error and let fingers_crossed own deduplication.
Config cache is real. Unknown keys under nti_log: hard-fail on newer Symfony, and are silently ignored if the bundle's Configuration is malformed. Always bin/console cache:clear, and run bin/console config:dump-reference nti_log — if it returns nothing, your YAML is doing nothing.
Extension points, in order of usefulness: decorate the logger service; override the listener service definition; decorate the listener. Do not edit files under vendor/ — this package will not be updated for you, and v3 just proved how much can move in a single release.
Verifying it's actually on. bin/console debug:event-dispatcher and grep for the bundle's listener (expect a different class name than in 2.x), plus bin/console debug:container nti_log. If neither shows up, the bundle didn't register — check bundles.php and run composer dump-autoload.
How can I help you explore Laravel packages today?