Repository navigation
PHP 8: test first class callable; fix static enum method - #273
BrianHenryIE wants to merge 5 commits into
Conversation
Claude: "[SomeEnum::class, 'staticMethod'] as a hook callback throws. HookedCallback::react() builds an AnyInstance from the class name, and AnyInstance::__construct calls newInstanceWithoutConstructor(), which cannot instantiate an enum. Error at AnyInstance.php:26."
faisalahammad
left a comment
There was a problem hiding this comment.
Thanks Brian. The tests are a nice addition, and I confirmed the enum bug. On trunk, add_action('x', [SampleEnum::class, 'staticMethod']) throws Error: Cannot instantiate enum WP_Mock\Tests\Mocks\SampleEnum from AnyInstance::__construct().
I'm requesting changes for one regression in the HookedCallback::react() change. Details and a suggestion are inline.
Regression: the new guard skips the AnyInstance lookup for every static method. It also skips it for any method method_exists() can't see, such as __call. That breaks the documented pattern in docs/usage/mocking-wp-action-and-filter-hooks.md:
WP_Mock::expectActionAdded('probe', [new AnyInstance(SampleClass::class), 'staticMethod']);
add_action('probe', [SampleClass::class, 'staticMethod']);
WP_Mock::assertHooksAdded();This passes on trunk but fails on this branch with InvalidCountException: Method intercepted(<Any Arguments>) ... should be called at least 1 times but called 0 times. The same happens for [new SampleClass(), 'magic'] with an AnyInstance expectation.
The only case that needs to skip AnyInstance is an enum class name. Enum instances already work through the is_object() branch. I've suggested a narrower guard inline. With it on PHP 8.3, all 189 tests pass (including your new enum tests), phpstan and phpcs are clean, and both probes above pass. Writing 'UnitEnum' as a string keeps phpstan happy without the ignore, since phpstan analyses as PHP 7.4. On 7.4 and 8.0, is_a() simply returns false.
Could you also add the static-method AnyInstance case above as a regression test?
Other notes
- The
php-tests (8.1)failure comes from the Coveralls upload step, not the tests. That job reportsTests: 189, Assertions: 300, Skipped: 1, so a re-run should clear it. - Using
eval()for the 8.1 syntax is fine as a stopgap until #269 lands.
| if (is_array($callback) && count($callback) === 2 && method_exists($callback[0], $callback[1])) { | ||
| $isEnum = false; | ||
| if(method_exists(\ReflectionClass::class, 'isEnum')) { | ||
| try { | ||
| $rc = new \ReflectionClass($callback[0]); | ||
| /** @phpstan-ignore-next-line (requires PHP 8.1+) */ | ||
| $isEnum = $rc->isEnum(); | ||
| } catch (\ReflectionException $e) { | ||
| // Unreachable: method_exists() above guarantees the class exists. | ||
| } | ||
| } | ||
| try { | ||
| $isStatic = (new \ReflectionMethod($callback[0], $callback[1]))->isStatic(); | ||
| } catch (\ReflectionException $e) { | ||
| // Unreachable: method_exists() above guarantees the method exists. | ||
| $isStatic = false; | ||
| } | ||
| if(!$isEnum && !$isStatic) { | ||
| $any_instance_callback = array( new AnyInstance( $callback[0] ), $callback[1] ); | ||
| $safe_any_instance_callback = $this->safe_offset( $any_instance_callback ); | ||
| if ( ! empty( $this->processors[ $safe_any_instance_callback ] ) ) { | ||
| $safe_callback = $safe_any_instance_callback; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
I suggest narrowing this to just the enum-class-name case, so static methods and __call methods keep matching AnyInstance expectations:
| if (is_array($callback) && count($callback) === 2 && method_exists($callback[0], $callback[1])) { | |
| $isEnum = false; | |
| if(method_exists(\ReflectionClass::class, 'isEnum')) { | |
| try { | |
| $rc = new \ReflectionClass($callback[0]); | |
| /** @phpstan-ignore-next-line (requires PHP 8.1+) */ | |
| $isEnum = $rc->isEnum(); | |
| } catch (\ReflectionException $e) { | |
| // Unreachable: method_exists() above guarantees the class exists. | |
| } | |
| } | |
| try { | |
| $isStatic = (new \ReflectionMethod($callback[0], $callback[1]))->isStatic(); | |
| } catch (\ReflectionException $e) { | |
| // Unreachable: method_exists() above guarantees the method exists. | |
| $isStatic = false; | |
| } | |
| if(!$isEnum && !$isStatic) { | |
| $any_instance_callback = array( new AnyInstance( $callback[0] ), $callback[1] ); | |
| $safe_any_instance_callback = $this->safe_offset( $any_instance_callback ); | |
| if ( ! empty( $this->processors[ $safe_any_instance_callback ] ) ) { | |
| $safe_callback = $safe_any_instance_callback; | |
| } | |
| } | |
| } | |
| // AnyInstance cannot instantiate an enum from its class name. | |
| if (is_array($callback) && ! (is_string($callback[0]) && is_a($callback[0], 'UnitEnum', true))) { | |
| $any_instance_callback = array( new AnyInstance($callback[0]), $callback[1] ); | |
| $safe_any_instance_callback = $this->safe_offset($any_instance_callback); | |
| if (! empty($this->processors[ $safe_any_instance_callback ])) { | |
| $safe_callback = $safe_any_instance_callback; | |
| } | |
| } |
Summary
Tests verifying PHP 8 syntax and one fix for hooking static enum methods.
Closes: N/A
Details
WP_Mock::expectActionAdded()with callable array for static and instance methods before potential nearby changes. 237818eAnyInstancecode. I narrowed the use of that code.AnyInstancecan probably be deprecated after fix(hook): use stable Type string key in safe_offset #270 is merged. 4a03475Functions::type()with enums - worked without changes. cb9803dContributor checklist
Testing
Note
The PHP 8 syntax fails to parse under 7.4 so is contained in the tests as strings that are
eval()ed. #269 will raise the required PHP level to 8.1 and this workaround can be removed.Reviewer checklist