juststeveking/signal
Signal is a lightweight Laravel package by Steve King that provides a clean, expressive API for defining and dispatching application signals, helping you decouple components and react to events with simple, testable PHP code.
What this actually is. There is a material discrepancy between the two documented surfaces, and it must be resolved before any adoption decision. The package description sells a runtime event library ("emitting and handling application signals," "dispatch helpers," "listener registration via a service provider"). The shipped code, the bundled SKILL.md, and the engineer assessment all describe a pure PHP 8.1 attribute/annotation library with no service provider, no config, no dispatcher, and no runtime behaviour. The Attributes\ namespace and the absence of any registration surface point to the annotation library being the real product. Treat the "signals" copy as stale or aspirational until proven otherwise by reading composer.json and the source tree. This is the single highest-priority open question — it determines whether the value proposition is "replace illuminate/events with something lighter" (weak, and unlikely to survive scrutiny against a mature framework primitive) or "make architecture machine-readable" (genuinely interesting, and unserved by anything in the Laravel core).
Architecture fit. Assuming the annotation library is the real product, the fit is unusual and worth articulating precisely: this is not a library that does anything. It ships a vocabulary — one type discriminant per class (Module, Service, Repository, Action, Controller, Event, Listener, Middleware, Job, Command, Query, Aggregate, ValueObject) plus metadata (DependsOn, ListensTo, Emits, Route, Authorize, Validates, Cached, SideEffect, Throws, Deprecated, Internal). Value only materialises when we build the reflection consumer: an inventory command that emits JSON, a generated architecture doc, and CI rules that fail the build on dependency violations. That means the package is a schema, and the actual product is internal engineering work we own. A TPM should frame this honestly — we are not buying a capability, we are buying a shared vocabulary and saving perhaps a day of designing one.
Integration feasibility. Low technical friction, high organisational friction. Composer-only, additive-only, zero runtime surface, zero boot cost, no route or queue coupling. Removal is find-and-delete across annotated files — an unusually clean exit. The organisational friction is that adoption requires annotating real code and enforcing a style guide in review, which needs a named owner and a pilot module, not a mandate. The bundled SKILL.md is effectively the authoring standard and is written for a coding agent; using an agent for a first annotation draft with human review of the diff is the pragmatic path, but fabricated annotations (invented events, invented validation rules, invented abilities) are the expected failure mode.
Technical risk.
composer require into a real Laravel app. Every bump needs a five-minute smoke test.#[Authorize] and #[Validates] describe checks; they do not perform them. #[Route] does not register routes. A new engineer reading an annotated controller can reasonably infer framework enforcement. This must be stated loudly in the style guide, or we create a compliance illusion around authorisation.#[Emits] will not fail if the method stops dispatching. #[Throws] will not fail if the exception changed. Stale annotations are worse than none because they are trusted. #[Cached(ttl: 3600)] sitting next to cache()->remember($key, 300, ...) is a concrete divergence with no arbiter.Emits/ListensTo use short names, so matching emitters to listeners requires resolving short name → FQCN ourselves, and App\Orders\Events\OrderPlaced vs App\Billing\Events\OrderPlaced will conflate.Key questions.
SKILL.md and the source? Confirm by reading the source tree, not the description.Stack fit. Laravel 9/10/11 on PHP 8.1+, native PHP attributes. No service provider means no boot-time cost and no config file to version, which keeps the operational footprint near zero. It also means there is no framework integration to test — the only integration is Composer autoloading plus our own reflection code.
Pin ^1.0.1, never ^1.0. v1.0.0 shipped a broken autoloader for the dependency install case. After install, verify the attributes resolve under both autoload modes before writing a single annotation:
composer dump-autoload -o
php -r "var_dump(class_exists(\JustSteveKing\Signal\Attributes\Controller::class));"
Add this to the PR checklist for any future bump, and re-run it once inside a Docker build with a cached vendor layer — that is where the original defect would have surfaced.
Migration path is additive, not a refactor. Annotating a class changes only use statements and attribute lines; method bodies are untouched. There is no BC surface for application code, no dual-write period, no data migration. Rollback is git revert on the annotation commits, independently of the Composer remove. This is the strongest argument for a timeboxed pilot: the cost of reversal is near zero.
Compatibility decisions to lock before the pilot.
Service/Action becomes an Action. A class with two type attributes silently breaks every consumer.DependsOn, and pick one namespace strategy for event names. Either store FQCNs in Emits/ListensTo and accept verbosity, or commit to a single event namespace and document that our matcher resolves short names against it. Decide now, not after the graph is half-built.#[Cached], the TTL in the attribute must be the TTL in code. If we annotate #[Deprecated], retire the @deprecated docblock. Pick per concern in the style guide or reviewers will arbitrate ad hoc.#[Job(description: '...')] with no queue: is indistinguishable from "forgot to annotate." If a CI rule needs that distinction, check raw attribute arguments, not hydrated instances.Sequencing.
ReflectionClass::getAttributes(). If the dump does not look useful, stop here — the exit cost is an afternoon.SKILL.md verbatim so the pilot measures the real authoring cost, then human-review the agent's diff for fabricated attributes.app/, emit JSON to a build artifact or render docs/architecture.md nightly. This is the deliverable that converts annotation effort into something decision-useful.Controller may DependsOn a Repository directly" or "every Listener must declare at least one ListensTo." One rule, enforced, proves the model. Fail the build on violation.What to explicitly not do. Do not attempt a big-bang annotation pass over a monolith — it burns the goodwill the pilot requires. Do not bolt DDD vocabulary (Aggregate, ValueObject, Query) onto a CRUD codebase; a vocabulary nobody believes is worse than none. Do not try to make Signal emit OpenAPI; #[Route] is metadata, not a spec — feed it to a generator.
Maintenance. The dependency itself is close to zero-maintenance: one Composer entry, no runtime, MIT, no boot hook. The maintenance burden sits entirely with us — the inventory command is ours, the CI rules are ours, and the style guide is ours. That is the honest resourcing picture: the package is a rounding error, the programme is a small ongoing engineering commitment. A single maintainer with ~46 stars and no dependents offers no release-cadence guarantee, so treat upgrades as discretionary and re-smoke-test each one. If the pilot succeeds and CI starts blocking merges, fork.
Support. Effectively none. No dependents means no battlefield archaeology on Stack Overflow, no second opinion on edge cases, no ecosystem of integration patterns to crib from. The effective support corpus is SKILL.md plus the source, which is small enough to read in an afternoon — a mitigating factor. Internal support therefore means one named engineer who has read it and can answer questions, and a style guide committed to the repo. Ramp-up for an additional senior engineer is 1–2 days to fluency, most of which is reading SKILL.md and the attribute constructors rather than learning a runtime.
Scaling. No request-path risk whatsoever — the package does nothing at runtime, so there is no queue, no middleware, no boot cost, no per-request reflection. The only scaling concern is the reflection scan: getAttributes() on one class is negligible; iterating every class in app/ and calling newInstance() on every attribute is not, and gets worse linearly as the codebase grows. Run it in CI or a nightly job, cache the output to a JSON artifact, memoise in the dev command. Never put a full-codebase scan on a hot path. If a future need pushes this into runtime, that is the moment to reassess the architecture, not to optimise the loop.
Failure modes to instrument.
| Failure | Symptom | Mitigation |
|---|---|---|
| Autoload break on install/bump | Attributes silently resolve to nothing; inventory returns empty | Smoke test per bump, both autoload modes; assert non-empty attribute count in CI |
| Stale annotation trusted | #[Emits] claims an event never dispatched; #[Throws] claims a retired exception |
PR checklist ("behaviour changed → attributes changed?"); flag commits touching dispatch(/event( without attributes; scheduled sweep ticket |
| Compliance illusion | Reader assumes #[Authorize]/#[Validates] are enforced |
State it in the style guide; keep enforcement in policies/form requests, visibly |
| Duplicate type attribute | Consumer breaks on single-discriminant assumption | CI rule asserting exactly one type attribute per class |
| Short-name event collision | Emitter/listener graph conflates two OrderPlaced classes |
One event namespace, documented, or FQCNs throughout |
| Silent defaults | #[Job] with no queue: indistinguishable from intentional default |
Check raw arguments in the rule; annotate explicitly |
| Maintainer abandonment | No releases; CI pinned to a package nobody owns | Fork/vendor before CI rules become merge blockers |
Ramp-up and decision framing. Approve a timeboxed pilot, not a platform commitment: one engineer, one module, 2–3 weeks, exit criteria defined as "the inventory command produced something a real decision was made from" and "one CI rule caught a real violation." The cost of trying is low and the cost of reversing is lower, which is exactly what justifies the spend against a 46-star, zero-dependent, single-maintainer package. What should not be approved is a roadmap line item called "architecture observability" justified by this dependency — the capability is ours to build; Signal is only the vocabulary, and the vocabulary is replaceable in a day if it turns out to be wrong.
How can I help you explore Laravel packages today?