Skip to content

(feat) Application: Go-style defer() for guaranteed cleanup - #365

Merged
techmahedy merged 3 commits into
doppar:4.xfrom
techmahedy:techmahedy-4.x
Oct 14, 2026
Merged

techmahedy merged 3 commits into
doppar:4.xfrom
techmahedy:techmahedy-4.x

Conversation

@techmahedy

@techmahedy techmahedy commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Pull Request Checklist

Q A
Branch? 4.x
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

Problem (solution of this issue )

Code that acquires a temporary resource (a cache lock, a file handle, a temp file, a tracing span, a transaction) had to manage its cleanup by hand, or know which lifecycle hook runs late enough to be safe. terminating() is application-wide and registered once at boot, so it does not express local resource ownership, and its ordering is registration order.

Solution

defer() registers a callback that runs when the current execution ends, after success or after an exception, in last-in-first-out order.

$lock = cache()->locked('report:' . $reportId, seconds: 30);

if (! $lock->get()) {
    throw new \RuntimeException('The report is already being generated.');
}

app()->defer(static fn () => $lock->release());

Available as app()->defer(), App::defer() and the defer() helper.

Semantics

  • Callbacks run in reverse registration order.
  • Every callback is attempted even if one throws.
  • Cleanup exceptions are reported, never thrown. They do not replace the original exception and do not fail a successful execution. Reports go to the log service; reportDeferredExceptionsUsing() overrides this.
  • A callback belongs to one execution scope and never reaches the next request, job, or command in a long-lived worker.
  • Nested scopes have independent stacks. Ending a scope first ends any inner scope that was never closed.
  • defer() outside an active scope throws a LogicException. There is no implicit application-lifetime fallback.
  • Callbacks registered while cleanup is running are run too.

Lifecycle adapters

All adapters share one implementation (DeferScope plus the scope methods on
Application).

Execution Scope Callbacks run
HTTP opened in dispatch() DispatchResult::terminate(), after terminating() callbacks and before request-scoped services are dropped; immediately if handling or error rendering throws
Console each command, in Console::doRunCommand() when the command finishes, before Symfony renders an uncaught exception
Anything else app()->runInDeferScope($callback) when the callback returns or throws

Checklist

  • Tests have been added or updated
  • Documentation has been updated (versions/4.x/defer)
  • Code follows the project coding standards
  • All tests pass locally

@techmahedy techmahedy added the feat new feature label Oct 13, 2026

@techmahedy techmahedy left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

as per my check, this PR is ok @NazmusShakib @abdulmajidcse @rrr63

@abdulmajidcse

abdulmajidcse commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

@techmahedy,

Please preserve exceptions thrown by terminating() callbacks when reporting deferred cleanup failures.

Currently, endDeferScope() receives only the exception captured during dispatch. If the request succeeds but a terminating() callback throws, deferred failures are reported with $original === null, losing the actual terminating exception.

Please capture the terminating exception, pass it to endDeferScope(), and add a test where both a terminating callback and a deferred callback throw.

Also, the PR states that versions/4.x/defer documentation was added, but no documentation file is included. Please add the referenced documentation or update the PR description/checklist.

@techmahedy

Copy link
Copy Markdown
Member Author

@techmahedy,

Please preserve exceptions thrown by terminating() callbacks when reporting deferred cleanup failures.

Currently, endDeferScope() receives only the exception captured during dispatch. If the request succeeds but a terminating() callback throws, deferred failures are reported with $original === null, losing the actual terminating exception.

Please capture the terminating exception, pass it to endDeferScope(), and add a test where both a terminating callback and a deferred callback throw.

Also, the PR states that versions/4.x/defer documentation was added, but no documentation file is included. Please add the referenced documentation or update the PR description/checklist.

@abdulmajidcse docs page: https://github.com/doppar/docs/blob/41923567f2b98d8d8b2b8a624484eb9e72af75c1/versions/4.x/defer.md

@techmahedy

Copy link
Copy Markdown
Member Author

@abdulmajidcse please check now "Please preserve exceptions thrown by terminating() callbacks when reporting deferred cleanup failures." about this issue

Comment thread src/Phaseolies/Application.php Resolved
@techmahedy
techmahedy merged commit d24bafe into doppar:4.x Oct 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants