Skip to content

Make $this available as object in non-static closures and arrow functions outside class context#5543

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-8yswnas
Closed

Make $this available as object in non-static closures and arrow functions outside class context#5543
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-8yswnas

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a closure using $this was created outside a class context and stored in a variable, PHPStan reported "Undefined variable: $this" at the definition site. However, closures passed inline to Closure::bind() worked correctly because PHPStan already handled that case specially via StaticCallHandler. This fix makes $this available with type object inside all non-static closures and arrow functions, even outside class context, since Closure::bind() can bind $this to any closure at runtime.

Changes

  • src/Analyser/MutatingScope.php:

    • enterAnonymousFunctionWithoutReflection(): Added an elseif branch that sets $this to ObjectWithoutClassType when the closure is non-static, the enclosing scope lacks $this, and we are not inside a Closure::bind() call (checked via isInClosureBind())
    • enterArrowFunctionWithoutReflection(): Same fix for arrow functions — add $this as object when the arrow function is non-static, the parent scope lacks $this, and we are not in a closure bind scope
    • restoreThis(): Added an elseif branch to preserve $this from the restore scope when not in a class but $this is defined. Previously the else branch unconditionally removed $this when isInClass() was false, which caused $this to disappear after processing closure arguments with @param-closure-this annotations inside non-class closures
    • Added use PHPStan\Type\ObjectWithoutClassType import
  • Updated test expectations in:

    • tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php: Removed expected error for $this in non-static closure outside class (line 26 of this.php)
    • tests/PHPStan/Analyser/nsrt/param-closure-this.php: Changed *ERROR* to object for $this in non-static closures outside class context
    • tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php: Added $this (Yes): object to expected scope output for closures outside class

Root cause

MutatingScope::enterAnonymousFunctionWithoutReflection() only copied $this into the closure scope when the enclosing scope had $this defined (i.e. inside a class). Non-static closures outside a class never received $this, even though PHP allows $this in such closures when they are later bound via Closure::bind(). The inline Closure::bind() case worked because StaticCallHandler created a special closureBindScope with $this already set before entering the closure.

A secondary issue was that restoreThis() unconditionally removed $this when the restore scope was not in a class, which caused $this (now set to object by the fix) to be lost after processing method calls with @param-closure-this annotations.

Analogous cases probed

  • Arrow functions: Same bug existed — fixed in enterArrowFunctionWithoutReflection() with the same approach
  • Static closures/arrow functions: Correctly remain without $this — no fix needed, verified with tests
  • Closure::bind() with null target: Correctly does NOT add $this — the isInClosureBind() guard prevents the new elseif from firing in bind scopes
  • @param-closure-this interaction: Fixed via restoreThis() change — $this is now properly preserved after processing closure arguments with this annotation

Test

  • Rule test (tests/PHPStan/Rules/Variables/data/bug-1348.php): Tests that non-static closures and arrow functions outside class context do NOT report "Undefined variable: $this", while static closures/arrow functions still do
  • Type inference test (tests/PHPStan/Analyser/nsrt/bug-1348.php): Verifies $this has type object in closures outside class, $this(ClassName) inside class closures, and specific bound type inside inline Closure::bind()

Fixes phpstan/phpstan#1348

…unctions outside class context

- In `MutatingScope::enterAnonymousFunctionWithoutReflection()`, add `$this`
  with type `object` when the closure is non-static and the enclosing scope
  does not have `$this` and is not inside a `Closure::bind()` call. This
  removes the false positive "Undefined variable: $this" for closures that
  will later be bound via `Closure::bind()`.
- Apply the same fix in `MutatingScope::enterArrowFunctionWithoutReflection()`
  for non-static arrow functions outside class context.
- Fix `MutatingScope::restoreThis()` to preserve `$this` from the restore
  scope when not in a class but `$this` is defined (e.g. as `object` in a
  closure outside a class). Previously it unconditionally removed `$this`
  when `isInClass()` was false, which caused `$this` to disappear after
  method calls with `@param-closure-this` inside non-class closures.
- Update existing tests that asserted the old buggy behavior.
@staabm staabm deleted the create-pull-request/patch-8yswnas branch April 26, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants