Skip to content

Commit

Permalink
Allow attributes to be used multiple times (#611)
Browse files Browse the repository at this point in the history
* Allow attributes to be used multiple times

* Testing CI
  • Loading branch information
srtfisher authored Jan 10, 2025
1 parent 4f49030 commit 08a66b0
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated the `Mantle\Support\Helpers\defer` helper to be able to used outside
of the Mantle Framework via the `shutdown` hook.

### Fixed

- Allow `Filter`/`Action` attributes to be used multiple times on the same method.

## v1.3.2 - 2024-12-17

- Allow stray requests to be ignored and pass through when stray requests are being prevented.
Expand Down
2 changes: 1 addition & 1 deletion src/mantle/support/attributes/class-action.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*
* Used to hook a method to an WordPress hook at a specific priority.
*/
#[Attribute]
#[Attribute( Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION )]
class Action {
/**
* Constructor.
Expand Down
2 changes: 1 addition & 1 deletion src/mantle/support/attributes/class-filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*
* Used to hook a method to an WordPress hook at a specific priority.
*/
#[Attribute]
#[Attribute( Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION )]
class Filter {
/**
* Constructor.
Expand Down
27 changes: 20 additions & 7 deletions src/mantle/support/traits/trait-hookable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
* the respective WordPress hooks.
*/
trait Hookable {
/**
* Flag to determine if the hooks have been registered.
*/
protected bool $hooks_registered = false;

/**
* Constructor (can be overridden by the trait user).
*/
Expand All @@ -40,6 +45,10 @@ public function __construct() {
* respective WordPress hooks.
*/
protected function register_hooks(): void {
if ( $this->hooks_registered ) {
return;
}

$this->collect_action_methods()
->merge( $this->collect_attribute_hooks() )
->unique()
Expand All @@ -51,16 +60,20 @@ function ( array $item ): void {
} else {
\Mantle\Support\Helpers\add_filter( $item['hook'], [ $this, $item['method'] ], $item['priority'] );
}
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
// Use the default WordPress action/filter methods.
if ( 'action' === $item['type'] ) {
\add_action( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
} else {
\add_filter( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
}

return;
}

// Use the default WordPress action/filter methods.
if ( 'action' === $item['type'] ) {
\add_action( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
} else {
\add_filter( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
}
},
);

$this->hooks_registered = true;
}

/**
Expand Down
168 changes: 168 additions & 0 deletions tests/Support/HookableAttributeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

namespace Mantle\Tests\Support;

use Mantle\Support\Attributes\Action;
use Mantle\Support\Attributes\Filter;
use Mantle\Support\Traits\Hookable;
use PHPUnit\Framework\TestCase;

class HookableAttributeTest extends TestCase {
public function setUp(): void {
parent::setUp();

remove_all_actions( 'example_action' );
}

public function test_action_from_method_name(): void {
$_SERVER['__hook_fired'] = false;

$class = new class {
use Hookable;

#[Action( 'example_action' )]
public function example_action( mixed $args ): void {
$_SERVER['__hook_fired'] = $args;
}
};

new $class;

$this->assertFalse( $_SERVER['__hook_fired'] );

do_action( 'example_action', 'foo' );

$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
}

public function test_action_from_method_name_with_priority(): void {

$_SERVER['__hook_fired'] = [];

$class = new class {
use Hookable;

#[Action( 'example_action', 20 )]
public function action_at_20( mixed $args ): void {
$_SERVER['__hook_fired'][] = 20;
}

#[Action( 'example_action' )]
public function action_at_10( mixed $args ): void {
$_SERVER['__hook_fired'][] = 10;
}

};

// Remove the action that was added by creating the anonymous class.
remove_all_actions( 'example_action' );

new $class;

$this->assertEmpty( $_SERVER['__hook_fired'] );

do_action( 'example_action', 'foo' );

$this->assertSame( [ 10, 20 ], $_SERVER['__hook_fired'] );
}

public function test_filter_from_method_name(): void {
$_SERVER['__hook_fired'] = false;

$class = new class {
use Hookable;

#[Filter( 'example_action' )]
public function filter_the_value( mixed $value ): mixed {
$_SERVER['__hook_fired'] = $value;

return 'bar';
}
};

remove_all_filters( 'example_action' );

new $class;

$this->assertFalse( $_SERVER['__hook_fired'] );

$value = apply_filters( 'example_action', 'foo' );

$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
$this->assertSame( 'bar', $value );
}

public function test_filter_from_method_name_with_priority(): void {
$_SERVER['__hook_fired'] = [];

$class = new class {
use Hookable;

#[Filter( 'example_action', priority: 20 )]
public function filter_at_20( int $value ): int {
$_SERVER['__hook_fired'][] = $value;

return $value + 20;
}

#[Filter( 'example_action' )]
public function filter_at_10( int $value ): int {
$_SERVER['__hook_fired'][] = $value;

return $value + 10;
}

};

// Remove the action that was added by creating the anonymous class.
remove_all_actions( 'example_action' );

new $class;

$this->assertEmpty( $_SERVER['__hook_fired'] );

$value = apply_filters( 'example_action', 5 );

$this->assertSame( [ 5, 15 ], $_SERVER['__hook_fired'] );
$this->assertSame( 35, $value );
}

public function test_multiple_filters_on_one_method(): void {
$_SERVER['__hook_fired'] = [];

$class = new class {
use Hookable;

#[Filter( 'another_filter' )]
#[Filter( 'example_action' )]
public function filter_to_call( int $value ): int {
$_SERVER['__hook_fired'][] = $value;

return $value + 20;
}
};

// Remove the action that was added by creating the anonymous class.
remove_all_actions( 'another_filter' );
remove_all_actions( 'example_action' );

new $class;

$this->assertTrue( has_filter( 'another_filter' ) );
$this->assertTrue( has_filter( 'example_action' ) );

$this->assertEmpty( $_SERVER['__hook_fired'] );

$value = apply_filters( 'example_action', 5 );

$this->assertSame( [ 5 ], $_SERVER['__hook_fired'] );
$this->assertSame( 25, $value );

$_SERVER['__hook_fired'] = [];

$value = apply_filters( 'another_filter', 10 );

$this->assertSame( [ 10 ], $_SERVER['__hook_fired'] );
$this->assertSame( 30, $value );
}
}
119 changes: 119 additions & 0 deletions tests/Support/HookableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Mantle\Tests\Support;

use Mantle\Support\Traits\Hookable;
use PHPUnit\Framework\TestCase;

class HookableTest extends TestCase {
public function setUp(): void {
parent::setUp();

remove_all_actions( 'example_action' );
}

public function test_action_from_method_name(): void {
$_SERVER['__hook_fired'] = false;

$class = new class {
use Hookable;

public function action__example_action( mixed $args ): void {
$_SERVER['__hook_fired'] = $args;
}
};

new $class;

$this->assertFalse( $_SERVER['__hook_fired'] );

do_action( 'example_action', 'foo' );

$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
}

public function test_action_from_method_name_with_priority(): void {

$_SERVER['__hook_fired'] = [];

$class = new class {
use Hookable;

public function action__example_action_at_20( mixed $args ): void {
$_SERVER['__hook_fired'][] = 20;
}

public function action__example_action_at_10( mixed $args ): void {
$_SERVER['__hook_fired'][] = 10;
}
};

// Remove the action that was added by creating the anonymous class.
remove_all_actions( 'example_action' );

new $class;

$this->assertEmpty( $_SERVER['__hook_fired'] );

do_action( 'example_action', 'foo' );

$this->assertSame( [ 10, 20 ], $_SERVER['__hook_fired'] );
}

public function test_filter_from_method_name(): void {
$_SERVER['__hook_fired'] = false;

$class = new class {
use Hookable;

public function filter__example_action( mixed $value ): mixed {
$_SERVER['__hook_fired'] = $value;

return 'bar';
}
};

remove_all_filters( 'example_action' );

new $class;

$this->assertFalse( $_SERVER['__hook_fired'] );

$value = apply_filters( 'example_action', 'foo' );

$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
$this->assertSame( 'bar', $value );
}

public function test_filter_from_method_name_with_priority(): void {
$_SERVER['__hook_fired'] = [];

$class = new class {
use Hookable;

public function filter__example_action_at_20( int $value ): int {
$_SERVER['__hook_fired'][] = $value;

return $value + 20;
}

public function filter__example_action_at_10( int $value ): int {
$_SERVER['__hook_fired'][] = $value;

return $value + 10;
}
};

// Remove the action that was added by creating the anonymous class.
remove_all_actions( 'example_action' );

new $class;

$this->assertEmpty( $_SERVER['__hook_fired'] );

$value = apply_filters( 'example_action', 5 );

$this->assertSame( [ 5, 15 ], $_SERVER['__hook_fired'] );
$this->assertSame( 35, $value );
}
}

0 comments on commit 08a66b0

Please sign in to comment.