Skip to content

PHP 8: test first class callable; fix static enum method - #273

Open
BrianHenryIE wants to merge 5 commits into
10up:trunkfrom
BrianHenryIE:php8
Open

BrianHenryIE wants to merge 5 commits into
10up:trunkfrom
BrianHenryIE:php8

Conversation

@BrianHenryIE

Copy link
Copy Markdown
Contributor

Summary

Tests verifying PHP 8 syntax and one fix for hooking static enum methods.

Closes: N/A

Details

Contributor checklist

  • I agree to follow this project's Code of Conduct.
  • I have updated the documentation accordingly
  • I have added tests to cover changes introduced by this pull request
  • All new and existing tests pass

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

  • Code changes review
  • Documentation changes review
  • Unit tests pass
  • Static analysis passes

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."
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 65.495% (-0.6%) from 66.142% — BrianHenryIE:php8 into 10up:trunk

@faisalahammad faisalahammad left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 reports Tests: 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.

Comment on lines +26 to 50
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;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I suggest narrowing this to just the enum-class-name case, so static methods and __call methods keep matching AnyInstance expectations:

Suggested change
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;
}
}

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.

3 participants