Skip to content

Commit ed79880

Browse files
committed
Add AI rules for how to deal with Listeners in general within Snipe-IT
1 parent fea8146 commit ed79880

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

.ai/rules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Before planning or editing, find the row whose globs match the file's path and r
77
| app/Actions/** | .ai/rules/actions.md |
88
| app/Http/Controllers/Api/** | .ai/rules/api.md |
99
| app/** | .ai/rules/app.md |
10+
| app/Listeners/**, app/Events/** | .ai/rules/listeners.md |
1011
| app/Http/**, routes/** | .ai/rules/boost/http-routes.md |
1112
| app/Livewire/**, resources/views/** | .ai/rules/boost/livewire-views.md |
1213
| database/migrations/** | .ai/rules/boost/migrations.md |

.ai/rules/listeners.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
paths:
3+
- 'app/Listeners/**'
4+
- 'app/Events/**'
5+
---
6+
7+
# Listeners & Events
8+
9+
## No EventServiceProvider — auto-discovery is by exact string, not by class
10+
11+
This app has no `EventServiceProvider`. Laravel auto-discovers listeners by scanning
12+
`app/Listeners/*::handle()` for a type-hinted event parameter, then caches the map in
13+
`bootstrap/cache/events.php` keyed by the **literal string** it found in your source
14+
(the `use` import / type hint), not by the resolved class.
15+
16+
PHP class name resolution is case-insensitive, so a typo like
17+
`use App\Events\CheckoutableCheckedin;` (lowercase `in`) still "works" at runtime — but
18+
it produces a *different* array key than the real `App\Events\CheckoutableCheckedIn`,
19+
because PHP array keys are case-sensitive strings. The listener silently registers under
20+
a dead key and never fires for the real event, with no error anywhere.
21+
22+
- When adding or renaming a listener, match the event's `use` import and `handle()`
23+
type hint character-for-character against the event class's actual declared name.
24+
- After touching anything in `app/Listeners/` or `app/Events/`, run
25+
`php artisan event:clear` (or `optimize:clear`) so a stale cache can't paper over a
26+
typo you just introduced or just fixed.
27+
- If a listener "isn't firing" and there's no obvious reason, check
28+
`bootstrap/cache/events.php` for a duplicate/near-duplicate key before anything else.
29+
30+
## Dedup side effects across listeners on the same event with a property on the event
31+
32+
Several classes (`CheckoutableCheckedOut`, `CheckoutableCheckedIn`, ...) fan out to
33+
multiple listeners (`*EmailNotification`, `*WebhookNotification`, `*LogCheckin`, ...).
34+
None of these implement `ShouldQueue`, so Laravel dispatches the exact same event
35+
*instance* to each of them in turn within one synchronous dispatch.
36+
37+
When more than one listener needs the result of a side-effecting action (e.g. creating
38+
a `CheckoutAcceptance` row) for the same event, don't let each listener call the
39+
side-effecting action independently — that duplicates the effect once per listener.
40+
Instead, cache the result on a public property of the event itself, e.g.
41+
`$event->checkoutAcceptance`, and have each listener check-then-set:
42+
43+
```php
44+
if (! $event->checkoutAcceptance) {
45+
$event->checkoutAcceptance = CreateCheckoutAcceptanceAction::run(...);
46+
}
47+
return $event->checkoutAcceptance;
48+
```
49+
50+
This only works because the listeners themselves are synchronous and share the literal
51+
event object — `getCheckoutAcceptance()` runs entirely inside `handle()`, before any
52+
`Notification`/`Mail` object is built or sent, so it's unaffected by queueing on those
53+
downstream classes. Many `app/Notifications/*` already implement `ShouldQueue`, and
54+
`app/Mail/*` Mailables are trending that way too — that's fine and doesn't touch this
55+
caching at all, because by the time a `ShouldQueue` notification/mailable is handed off
56+
to the queue, it already has the resolved `CheckoutAcceptance` baked into its
57+
constructor; only *delivery* is deferred, not the acceptance lookup.
58+
59+
What WOULD break this: adding `ShouldQueue` to one of the *Listener* classes
60+
themselves (e.g. `CheckoutableCheckedOutEmailNotification implements ShouldQueue`).
61+
That's a different, much bigger change than queueing a Notification/Mailable — it would
62+
give each listener its own independently-serialized copy of `$event`, dispatched to
63+
possibly-concurrent workers, and this caching would silently stop deduping (or worse,
64+
race). If that's ever proposed, move the idempotency into
65+
`CreateCheckoutAcceptanceAction` itself (check-for-existing-pending-then-create inside
66+
a transaction/lock) instead of relying on the event instance.

0 commit comments

Comments
 (0)