Skip to content

[2.x] chore(flags): dispatch model events when flags are deleted#4818

Open
DavideIadeluca wants to merge 4 commits into
flarum:2.xfrom
glowingblue:di/flags-change
Open

[2.x] chore(flags): dispatch model events when flags are deleted#4818
DavideIadeluca wants to merge 4 commits into
flarum:2.xfrom
glowingblue:di/flags-change

Conversation

@DavideIadeluca

@DavideIadeluca DavideIadeluca commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #0000
No eloquent model events being dispatched when a flag gets deleted

Changes proposed in this pull request:

  • Change how flags are deleted. With the new approach model events are fired after each delete, allowing Observers to hook into this.
  • This in turn also allows the Audit Integration to loose some complexity with the macro

Reviewers should focus on:

Screenshot

Necessity

  • Has the problem that is being solved here been clearly explained?
  • If applicable, have various options for solving this problem been considered?
  • For core PRs, does this need to be in core, or could it be in an extension?
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation.
  • Backend changes: tests are green (run composer test).
  • Core developer confirmed locally this works as intended.
  • Tests have been added, or are not appropriate here.

Required changes:

  • Related documentation PR: (Remove if irrelevant)

@DavideIadeluca
DavideIadeluca marked this pull request as ready for review July 16, 2026 17:35
@DavideIadeluca
DavideIadeluca requested a review from a team as a code owner July 16, 2026 17:35

@imorland imorland left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction here is good — replacing the global HasMany::macro('delete') with an explicit Dismissed event is a real improvement and takes some fragile complexity out of AuditIntegration. A few things need addressing before it can go in, though.

Flags are deleted in two places, and only one is covered. DeleteFlagsHandler dismisses via the UI, but Listener/DeleteFlags also deletes a post's flags in response to Post\Event\Deleted (when a flagged post is deleted). The old macro hooked HasMany::delete globally, so it caught both paths; the new Dismissed event is only dispatched from the command handler. As it stands, deleting a flagged post no longer logs post.dismissed_flags — that's a regression from the current behaviour.

The stated goal isn't fully met for the same reason. The point of the change is to fire model events on flag deletion so observers can hook in, but Listener/DeleteFlags still uses a bulk ->delete(), which fires no model events. So an observer would see UI dismissals but silently miss the post-deletion case.

Needs test coverage for the post-deletion path. AuditTest::delete() only exercises the command path (which still works), so this gap passes CI unnoticed. Please add a test that deletes a flagged post and asserts the dismissal is handled (audit log entry / events fired), so the second path is actually covered.

So: apply the same treatment to Listener/DeleteFlags so both deletion paths behave consistently, and add the post-deletion test. Happy to look again once that's in.

@DavideIadeluca

Copy link
Copy Markdown
Contributor Author

@imorland Thanks for the review — I dug into the post-deletion path and the regression turns out not to exist.

flags.post_id has had ON DELETE CASCADE since 2018. Post\Event\Deleted fires after the row is deleted, so the old listener's flags()->delete() always hit an already-empty relation — the macro's count() guard was always 0 and post.dismissed_flags was never logged on post deletion. The macro only ever fired on the command path, which still works.

That's also why the macro was semantically questionable: it hooked HasMany::delete globally and inferred "flags were dismissed" from any bulk delete on a post's flags relation. But deleting a flagged post isn't a dismissal — the flag was acted upon, not dismissed. The frontend already reflects this: the destructive control on a flagged post also calls the flags.delete endpoint, so all actual moderator dismissals go through DeleteFlagsHandler and dispatch Dismissed with a real actor. So Dismissed intentionally stays command-path-only.

For model events: hooking Deleted can never fire them (the cascade already removed the rows), so I moved Listener/DeleteFlags to Post\Event\Deleting and delete each flag via Eloquent while they still exist. Observers now see both paths — which the old code never achieved.

One trade-off with listening to Deleting: the flags are removed before the post delete is final, so if the deletion were to abort after the event, the flags would already be gone. Previously the cascade removed them atomically with the post row. The window is a single delete statement on the API path, so I think it's acceptable — but if we want to avoid it entirely, we could snapshot the flags on Deleting (loadMissing('flags')) and run their Eloquent deletes on Deleted: the rows are already cascade-deleted by then, but Model::delete() still fires the model events. Happy to go that route if you prefer.

Post-deletion path is now covered: DeleteFlagsOnPostDeletionTest asserts model events fire per flag, and AuditTest::deletingFlaggedPostDoesntLogFlagDismissal pins down that no audit entry is written (matching pre-existing behaviour).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants