tourze/counter-bundle
Tourze Counter Bundle provides reusable counter services for Symfony applications, making it easy to increment, decrement, and read numeric values. A lightweight building block for tracking counts, metrics, or usage data, with straightforward integration into existing services.
What you're actually installing. tourze/counter-bundle is a Symfony bundle (the -bundle suffix and tourze/* namespace are the giveaways). It wraps the underlying tourze/counter abstraction with DI wiring, config, and a Doctrine-backed store for named counter records. Laravel does not auto-discover bundles — there is no CounterServiceProvider, no facade, no publishable config in the Laravel sense. Budget your first hour for an adapter, not for composer require and go.
Minimal steps.
composer require tourze/counter-bundle and immediately run composer show tourze/counter-bundle to see its real dependency set — the transitive doctrine/orm/symfony/* requirements are what will actually shape your integration decision.src/DependencyInjection/*Extension.php first. That file gives you the config alias (e.g. tourze_counter) and the exact service IDs/aliases the bundle registers. Everything else follows from that.src/Entity/) to learn the counter record shape — you need the table name, the key column, and its length limit before you write a migration.php artisan make:migration and hand-write the schema from the mapping, or run doctrine:schema:update once to capture the DDL and port it.CounterServiceProvider ($this->app->bind(CounterInterface::class, ...) → a thin wrapper that constructs the Doctrine-backed implementation against a Laravel connection).First use case: page-view counter. Pick the simplest thing that exercises the read/write path end to end — increment a key on a route hit, read it back on a dashboard. Resist starting with time-series or per-user counters; those need key-scheme design the package won't do for you.
Also note: 1.0.1 shipped with nothing but a compare link. Treat the whole 1.0.x line as an unstable API surface and pin an exact version in composer.json.
Wrap, don't scatter. Put the bundle behind one app-level service (App\Services\Counter) that exposes increment(), decrement(), get(), reset(). Every call site depends on your class, not on tourze/*. This is the single highest-leverage decision — it's what makes the eventual version bump or replacement a one-file change.
Increment-on-event via listener. The idiomatic Laravel shape is an event + listener: PageViewed dispatched from the controller, IncrementPageCounter listener does counter->increment("page.views.{$pageId}"). Queued listeners (ShouldQueue) work well here and get the write off the request path — but only if the underlying store is atomic (see Gotchas).
Denormalized counter + source of truth. Use the counter for display and sorting; keep the real rows (views table, likes table) as the audit trail so you can recompute with a backfill command when a counter drifts. A counter:rebuild artisan command that truncates and re-derives from the primary table is cheap insurance and worth writing on day one.
Key naming discipline. Establish a namespaced key convention up front (post.{id}.views, user.{id}.notifications.unread) and centralize it in one CounterKeys class. The package treats keys as opaque strings; collisions and "what is this key?" archaeology in six months are entirely on you.
Time-windowed counters. The base counter almost certainly tracks a single running total per key. For daily/weekly/trending counts, encode the window in the key (post.{id}.views.2025-01-15) and let a scheduled command prune old buckets — or maintain a second derived counter. Don't try to make the package's storage do windowing.
Transactions and batching. If you're incrementing N counters for one logical action, wrap them in a single DB::transaction() against the same connection the counter uses, so a partial failure doesn't leave half-counted state.
Testing. Point the bundle at an in-memory SQLite connection in phpunit.xml, run your migration in setUp/RefreshDatabase, and assert on your own wrapper's contract rather than the package's internals — that keeps tests alive through version bumps.
Caching layer. Counters are read far more than written. A short-TTL Cache::remember (30–60s) around reads in front of your wrapper absorbs the hot-key traffic and is the cheapest scaling win available.
The bundle/package gap. No auto-discovery means no config/app.php entry and no php artisan vendor:publish. If you expect Symfony-style YAML config, that config is loaded by the bundle's extension inside Symfony's kernel — in Laravel you'll be constructing the services manually. Read the extension class and translate it, don't guess at config keys.
Check whether increment is atomic. This is the one thing to verify before going to production. If the implementation reads the row, adds one, and writes it back, you have a lost-update race under concurrency. Either confirm the generated SQL is UPDATE ... SET value = value + 1 (or the ORM's atomic variant), or serialize access with a transaction and row lock, or route hot counters through Cache::increment / DB::table()->increment() instead. Test it with a parallel-hit script, not by reading the source and hoping.
Transitive Doctrine. Pulling doctrine/orm into a Laravel app is heavy and can collide with an existing doctrine/dbal version. Check composer why doctrine/orm after install. If it's only there for a single table, the pragmatic answer may be to use tourze/counter (the abstraction) with your own Eloquent-backed implementation and drop the bundle entirely.
Key length and indexing. Find the column length in the mapping before you design your key scheme; a truncated key silently merges two counters into one. Also confirm the key column is uniquely indexed — without it, "increment" may create duplicate rows and reads become nondeterministic.
Negative and reset semantics. Verify what decrement does at zero (clamps or goes negative) and whether there's a reset/delete. A counter that drifts negative is usually a bug in your call site, so add an assertion or log when the post-decrement value is below zero.
Debugging. Set DB::listen() to log the actual counter SQL during development — it answers the atomicity, table-name, and duplicate-row questions faster than reading bundle internals. If a counter looks wrong, query the table directly before suspecting the package.
Where to extend. The intended extension points are the manager interface and (in Symfony terms) a custom storage/model implementation. Custom storage behind the interface is the clean way to swap in Redis, Eloquent, or an atomic SQL path. Don't subclass the Doctrine entity — migrations and mapping inheritance will fight you.
Release hygiene. 1.0.1 is a patch with an empty note body, so you have no signal about what changed. Pin "tourze/counter-bundle": "1.0.1" exactly, and read the GitHub compare diff yourself before bumping. Given the bus-factor and documentation profile of the tourze/* ecosystem, the wrapper service from Implementation Patterns is not optional — it's the whole mitigation strategy.
How can I help you explore Laravel packages today?