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

Signal Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Signal is, in practice, a PHP 8.1+ attribute library — it ships no service provider, no config file, and no runtime behaviour. Installing it does nothing on its own; the value only appears when something reflects the attributes back out. So getting started is two moves: adopt the vocabulary, then build the smallest possible reader.

Install, and pin past the 1.0.0 autoload bug:

composer require juststeveking/signal:^1.0.1

Do not use ^1.0. v1.0.0 shipped a broken Composer autoloader specifically for the "installed as a dependency" case — which is exactly how you'll consume it. Then confirm the attributes resolve under both autoload modes before you annotate a single file:

composer dump-autoload -o
php -r "var_dump(class_exists(\JustSteveKing\Signal\Attributes\Controller::class));"

Where to look first: the bundled skills/signal/SKILL.md. It is effectively the authoring standard — the full taxonomy of type attributes (Module, Service, Repository, Action, Controller, Event, Listener, Middleware, Job, Command, Query, Aggregate, ValueObject), the metadata attributes (DependsOn, ListensTo, Deprecated, Internal), the method-level attributes (Route, Authorize, Validates, Cached, Emits, SideEffect, Throws), plus ordering rules. Read it before annotating anything, or you'll invent a second convention alongside it.

First use case — annotate one real controller end to end. Not a toy; pick something from production:

use JustSteveKing\Signal\Attributes\{Controller, DependsOn, Route, Authorize, Validates, Emits, SideEffect, Throws};

#[Controller(description: 'Handles order creation from the storefront API.', tags: ['orders', 'api'])]
#[DependsOn(class: CreateOrderAction::class)]
final class CreateOrderController
{
    #[Route(method: 'post', path: '/api/orders', description: 'Create a new order.')]
    #[Authorize(ability: 'orders.create', description: 'Requires the create-order policy.')]
    #[Validates(field: 'items.*.product_id', rules: 'required|integer|exists:products,id')]
    #[Emits(event: 'OrderPlaced', description: 'After the order is persisted.')]
    #[SideEffect(description: 'Queues a confirmation email.', tags: ['email', 'queue'])]
    #[Throws(exception: 'InsufficientStockException', description: 'When a line item cannot be fulfilled.')]
    public function __invoke(CreateOrderRequest $request): JsonResponse { /* logic unchanged */ }
}

Only use statements and attribute lines are added. That is the entire shape of day-to-day use: documentation that lives next to the declaration and can't silently drift out of the file when someone moves the class.

Then write the reader. Six lines proves the concept and tells you whether this survives contact with your codebase:

$ref = new ReflectionClass(CreateOrderController::class);
foreach ($ref->getAttributes(Route::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
    dump($attr->newInstance());
}

If that dump looks useful, you have a project. If it doesn't, uninstalling costs an afternoon.

One caveat before you sell it internally: the package's public description talks about emitting and handling application "signals" (lightweight events with a dispatch helper and listener registration). The shipped code and SKILL.md describe an attribute/annotation library. Confirm which surface you're adopting for — reading SKILL.md and the Attributes\ namespace tells you the true product.

Implementation Patterns

The one-type-plus-metadata-plus-methods rhythm. Every annotated class follows the same three-tier structure: exactly one type attribute, then zero-or-more repeatable class metadata attributes (DependsOn, ListensTo, Deprecated, Internal), then per-public-method attributes in the order Route → Authorize → Validates → Cached → Emits → SideEffect → Throws. Enforce that order in review and the attribute block reads like a spec instead of a pile.

Annotate the highest-churn layers first. Controllers, actions, jobs, and listeners yield the most information per line, because Route, Validates, Authorize, Emits, SideEffect, and ListensTo all apply there. Leave Aggregate, ValueObject, and Query for teams already modelling DDD/CQRS — bolting DDD attributes onto a CRUD app produces a vocabulary nobody believes.

Build a reflection inventory command as your primary consumer. This is the reference implementation most teams land on:

// app/Console/Commands/ArchitectureInventory.php
$classes = collect(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(app_path())))
    ->filter(fn ($f) => $f->isFile() && $f->getExtension() === 'php')
    ->map(fn ($f) => /* path → FQCN */)
    ->filter(fn ($c) => class_exists($c))
    ->map(fn ($c) => new ReflectionClass($c))
    ->map(fn ($r) => [
        'class'   => $r->getName(),
        'types'   => collect($r->getAttributes())->map(fn ($a) => $a->getName())->all(),
        'methods' => collect($r->getMethods(ReflectionMethod::IS_PUBLIC))
            ->mapWithKeys(fn ($m) => [$m->getName() => collect($m->getAttributes())
                ->map(fn ($a) => $a->newInstance())->all()]),
    ]);

Emit JSON for a graph exporter, or render docs/architecture.md on a nightly schedule. The point is that the output is generated from source, not curated by hand.

CI architecture rules over the inventory. Once the attributes exist, DependsOn becomes a lint surface. A tiny rule engine over the inventory catches the things teams actually argue about in pull requests:

No Controller may DependsOn a Repository directly. Every Job must declare a non-default queue. Every Listener must carry at least one ListensTo. Every Route method on a Controller must have at least one Authorize unless tagged public.

Fail the build on violations. This turns an architectural opinion from a review comment into a merge blocker — without touching runtime.

Agent-assisted annotation pass, with human review. SKILL.md is written for a coding agent: it says "do not refactor, rename, or add comments" and "be accurate, not exhaustive." The workflow that works is point an agent at one bounded module (10–20 files), produce annotated files, then spend human time reviewing the diff for fabricated annotations — invented events, invented validation rules, invented abilities. The agent is fast at pattern matching and unreliable at knowing what your code actually does; treat it as a first drafter, not an authority.

Use Emits/ListensTo as your event map, not as a dispatcher. If you're already using illuminate/events with lightweight payloads, #[Emits(event: 'OrderPlaced')] on the action and #[ListensTo(event: 'OrderPlaced')] on the listener give you a searchable wiring diagram that costs nothing at runtime. Keep the actual event(new OrderPlaced($order)) call where it belongs — the attribute describes it, it doesn't replace it.

Pair with a docs generator, not instead of one. Signal is a schema, not a product. If you want a browsable site, map the reflection output into whatever static site generator you already use. If you want OpenAPI, don't try to make Signal emit it — #[Route] is metadata, not a spec. Feed a generator; don't replace one with it.

Staged adoption. Smoke test in a throwaway skeleton → pilot one module with SKILL.md applied verbatim → ship the inventory command → add one CI rule → widen. Do not attempt a big-bang annotation pass over a monolith; you will burn the goodwill that makes the pilot possible.

Testing. Attribute hydration is trivially unit-testable, and that's the layer worth testing:

$attrs = (new ReflectionMethod(CreateOrderController::class, '__invoke'))
    ->getAttributes(Route::class);

$this->assertCount(1, $attrs);
$this->assertSame('post', $attrs[0]->newInstance()->method);

Snapshot the inventory JSON in CI so unintended attribute changes show up as diffs, and keep annotation assertions out of your event-fake test paths — they're a different concern entirely.

Gotchas and Tips

#[Route] does not register routes. This is documentation. It is not Illuminate\Routing\Attributes\Route. Nothing dispatches, nothing binds. Likewise #[Authorize] and #[Validates] describe checks the controller must actually perform — they do not perform them. If a new engineer reads the attributes and assumes the framework is enforcing them, you've created a security-shaped documentation hazard. Say so in the style guide, loudly.

Public description and code surface diverge. The marketing text describes a health-check/event package with a JSON endpoint; the shipped code and SKILL.md describe an attribute library. Read SKILL.md and the Attributes\ namespace before you commit to a mental model.

Pin ^1.0.1, and re-smoke-test on upgrades. The 1.0.0 defect broke autoloading specifically in the dependency case — the primary install mode. That suggests the release pipeline may not exercise composer require into a real Laravel app. Budget a five-minute smoke test per bump: install, annotate one class, ReflectionClass::getAttributes(), confirm non-empty, run once with composer dump-autoload -o and once inside a Docker build with a cached vendor layer.

Nothing enforces annotation accuracy. #[Emits] won't fail if the method never dispatches that event. #[Throws] won't fail if the exception changed last sprint. Stale annotations are worse than none, because they get trusted. Mitigations that actually work: a review checklist item ("does the diff change behaviour? if so, does it change attributes?"), a CI heuristic that flags files whose git log touched event(/dispatch( without touching attributes, and a scheduled "annotation sweep" ticket. Accept that some drift is unavoidable.

One type attribute per class, and be strict. The SKILL.md rule is "pick the most specific." A class that could be a Service or an Action should be an Action. A class with two type attributes breaks every consumer that assumes a single discriminant.

FQCN in DependsOn, short names everywhere else. DependsOn(class: OrderRepository::class) — use the ::class constant so refactors rename correctly. Emits(event: 'OrderPlaced') and ListensTo(event: 'OrderPlaced') use short names, which means a consumer matching emitters to listeners has to resolve short name → FQCN itself. If you have both App\Orders\Events\OrderPlaced and App\Billing\Events\OrderPlaced, your matcher will conflate them. Pick one namespace strategy and document it, or store FQCNs and accept the verbosity.

Don't annotate private/protected methods. The library targets the public surface deliberately. If you want to document internals, use docblocks — mixing the two creates an inconsistent mental model.

Watch for collisions in intent, not code. #[Cached(ttl: 3600)] sits next to a real cache()->remember($key, 300, ...); #[Deprecated] sits next to @deprecated; #[Route] sits next to routes/api.php. PHP won't complain, so reviewers must. Pick one source of truth per concern in the style guide — otherwise you'll have a #[Cached] saying one hour and code saying five minutes, with no way to tell which is right.

Named arguments are your BC safety net, and also a trap. Every attribute uses named args (description:, tags:, queue:, priority:, ttl:). That's good — semver applies at 1.0.x and parameter renames should be major-version events. But defaults mean a missing argument is silent: #[Job(description: '...')] with no queue: resolves to 'default', and a consumer checking "does this job specify a queue?" cannot distinguish intentional default from forgot to annotate. If that distinction matters, don't rely on the default — annotate it explicitly, or make your CI rule inspect the raw attribute arguments rather than the hydrated instance.

Attributes are cheap at reflection time, expensive across a whole codebase. getAttributes() on a single class is negligible. Iterating every class in app/ and calling newInstance() on every attribute is not — especially if an attribute constructor does work. Cache the inventory to a JSON artifact in CI, or memoise it per-request in the dev command. Never put a full-codebase scan on a hot path.

Extension points are minimal by design. There's no registry, no container binding, no middleware. Your extension points are: your own attribute classes (define and read them alongside Signal's), your own reflection consumer, and PHPStan/Deptrac rules that consume the same data. If you need runtime behaviour, this package is the wrong layer — reach for illuminate/events, the router, or queue middleware.

Bus factor. Single maintainer, ~45 stars, zero dependents, MIT. Removal is find-and-delete, which keeps the risk low, but there are no battle-tested integration patterns to crib from. If the pilot succeeds and CI rules start blocking merges, consider vendoring or forking — you don't want your build pipeline dependent on an unmaintained package's release cadence.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
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