aiarmada/signals
A Laravel package for working with signals: lightweight, typed events that let you broadcast and observe changes across your app. Clean API, framework-native integration, and easy to wire into existing services and Eloquent models.
Read the source before you install it. With 0 stars and 0 dependents this is an unvetted package; budget 20 minutes to skim composer.json, the service provider, and the config file on GitHub before committing it to a project.
composer.json in the repo and confirm require.php and require.laravel/framework cover your version. For an unmaintained-looking package this is the single most likely reason install fails.composer require aiarmada/signals
php artisan about # confirm the provider was auto-discovered
php artisan vendor:publish --provider="AIArmada\Signals\SignalsServiceProvider"
Publish tags vary — run php artisan vendor:publish with no arguments and read the interactive list rather than guessing --tag=signals-config.php artisan migrate --pretend # see the DDL before it touches your DB
Where to look first, in order: the config file → the migrations (they define the data model, which is the real API) → the facade or entry-point class → any Console commands (retention/pruning is usually there).
First use case: instrument a single conversion — e.g. signup_completed or checkout_completed — with three properties (plan, source, value). Once you can see that row and query it back, you understand the package's whole model. Everything else is repetition.
With a package this young, the highest-value pattern is a one-file abstraction so a future swap is a config change, not a refactor.
// app/Analytics/Tracker.php
interface Tracker
{
public function track(string $event, array $properties = [], ?string $subject = null): void;
}
Bind SignalsTracker to it in a service provider. Controllers and jobs type-hint Tracker, never the vendor facade. If the package is abandoned in six months you rewrite one class.
Two clean insertion points, and you want both:
web group only; API traffic will otherwise flood your table.OrderPlaced, UserRegistered events to track() calls. Your domain stays unaware analytics exists.Event::listen(OrderPlaced::class, fn ($e) => app(Tracker::class)->track(
'order_placed',
['value' => $e->order->total_cents, 'currency' => $e->order->currency],
$e->order->user,
));
Analytics writes are the classic case for ShouldQueue. If the package's config exposes a queue connection, point it at a low-priority queue — never default — so a slow analytics insert can't back up mail or webhooks. If it doesn't expose one, wrap your Tracker implementation in a queued job yourself.
// config/queue.php or config/signals.php
'queue' => env('SIGNALS_QUEUE', 'analytics'),
Pick snake_case, flat keys, and a fixed vocabulary for the handful of properties you'll filter on (plan, source, value, currency). Enforce it in the Tracker implementation with a small allowlist. This is the difference between usable aggregates in six months and a table full of userId, user_id, and uid meaning the same thing.
Prefer passing the subject model (user, tenant, order) into track() over relying on auth()->id() inside the package. Background jobs and webhooks run without a session, and silent anonymous events are a common source of "why are my numbers low". If you're multi-tenant, put tenant_id in every event's properties — retrofitting it later means a backfill.
If the package ships query helpers or a dashboard, treat them as convenience only. For anything you'd put in front of a stakeholder, write your own query against the underlying table/view and wrap it in a read-only repository. That way a package upgrade can't change your KPI definitions, and you control indexes and caching.
Wrap the tracker in a fake in your test suite:
$this->instance(Tracker::class, $fake = new FakeTracker());
// ... act ...
$fake->assertTracked('order_placed', fn ($p) => $p['value'] === 4200);
If the package provides its own fake/assertions, use them for the integration test and your fake everywhere else — you don't want 300 tests coupled to vendor assertion APIs.
The package is unproven. 0 stars and 0 dependents is not a verdict, but it is a risk profile. Pin it exactly ("aiarmada/signals": "1.0.3", not ^1.0) and note it in your dependency-review doc. Have an exit plan: the Tracker interface above is the exit plan.
Config caching bites on install. After editing config/signals.php, php artisan config:clear — otherwise your local changes appear to do nothing while config:cache is active in staging. Classic first-hour confusion.
Migrations need indexes before data, not after. Adding an index to a 10M-row analytics table in production is a different task than adding it on an empty table. Read the migration, and if the query columns (event, subject_id, created_at) aren't indexed, add a follow-up migration immediately — while the table is still empty.
Timezone ambiguity will corrupt your reports. Determine whether created_at is stored UTC and whether the package buckets by app timezone. A "yesterday's signups" number that disagrees with your DB by a few hours is almost always this. Decide once, document it, and always convert in the query, not in the view.
PII and consent are your problem, not the package's. Analytics tables quietly accumulate IPs, emails in properties, and URLs with query strings. Before the first deploy: decide on IP anonymization/hashing, add a retention/prune command to the scheduler, and make sure the tracker respects your cookie-consent state (return early from middleware when consent is absent). A package can't know your jurisdiction.
Cardinality will kill query performance. Properties are usually stored as JSON or a key-value table. Once a high-cardinality key (session ID, email, UUID) lands in properties and someone adds a GROUP BY, reporting slows to a crawl. Keep high-cardinality identifiers in dedicated columns and properties for low-cardinality dimensions only.
Watch for N+1 in any built-in reporting. Dashboards that hydrate models per event row are common in small analytics packages. DB::listen() while loading the first dashboard will tell you in 30 seconds.
Queue failures are silent. A queued tracker that throws on a malformed property looks identical to "no traffic". Add a failed-job alert for the analytics queue, or at minimum log inside your Tracker implementation before dispatch.
Extension points to look for, in descending value: a config-selectable storage driver, an event fired before/after a track call (for enrichment or filtering), and a swappable middleware. If none exist, your Tracker wrapper becomes the extension point — which is fine, and further justification for building it first.
How can I help you explore Laravel packages today?