Skip to content

Commit 08a66b0

Browse files
authored
Allow attributes to be used multiple times (#611)
* Allow attributes to be used multiple times * Testing CI
1 parent 4f49030 commit 08a66b0

File tree

6 files changed

+313
-9
lines changed

6 files changed

+313
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Updated the `Mantle\Support\Helpers\defer` helper to be able to used outside
1919
of the Mantle Framework via the `shutdown` hook.
2020

21+
### Fixed
22+
23+
- Allow `Filter`/`Action` attributes to be used multiple times on the same method.
24+
2125
## v1.3.2 - 2024-12-17
2226

2327
- Allow stray requests to be ignored and pass through when stray requests are being prevented.

src/mantle/support/attributes/class-action.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*
1515
* Used to hook a method to an WordPress hook at a specific priority.
1616
*/
17-
#[Attribute]
17+
#[Attribute( Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION )]
1818
class Action {
1919
/**
2020
* Constructor.

src/mantle/support/attributes/class-filter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*
1515
* Used to hook a method to an WordPress hook at a specific priority.
1616
*/
17-
#[Attribute]
17+
#[Attribute( Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION )]
1818
class Filter {
1919
/**
2020
* Constructor.

src/mantle/support/traits/trait-hookable.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
* the respective WordPress hooks.
2525
*/
2626
trait Hookable {
27+
/**
28+
* Flag to determine if the hooks have been registered.
29+
*/
30+
protected bool $hooks_registered = false;
31+
2732
/**
2833
* Constructor (can be overridden by the trait user).
2934
*/
@@ -40,6 +45,10 @@ public function __construct() {
4045
* respective WordPress hooks.
4146
*/
4247
protected function register_hooks(): void {
48+
if ( $this->hooks_registered ) {
49+
return;
50+
}
51+
4352
$this->collect_action_methods()
4453
->merge( $this->collect_attribute_hooks() )
4554
->unique()
@@ -51,16 +60,20 @@ function ( array $item ): void {
5160
} else {
5261
\Mantle\Support\Helpers\add_filter( $item['hook'], [ $this, $item['method'] ], $item['priority'] );
5362
}
54-
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
55-
// Use the default WordPress action/filter methods.
56-
if ( 'action' === $item['type'] ) {
57-
\add_action( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
58-
} else {
59-
\add_filter( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
60-
}
63+
64+
return;
65+
}
66+
67+
// Use the default WordPress action/filter methods.
68+
if ( 'action' === $item['type'] ) {
69+
\add_action( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
70+
} else {
71+
\add_filter( $item['hook'], [ $this, $item['method'] ], $item['priority'], 999 );
6172
}
6273
},
6374
);
75+
76+
$this->hooks_registered = true;
6477
}
6578

6679
/**
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
namespace Mantle\Tests\Support;
4+
5+
use Mantle\Support\Attributes\Action;
6+
use Mantle\Support\Attributes\Filter;
7+
use Mantle\Support\Traits\Hookable;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class HookableAttributeTest extends TestCase {
11+
public function setUp(): void {
12+
parent::setUp();
13+
14+
remove_all_actions( 'example_action' );
15+
}
16+
17+
public function test_action_from_method_name(): void {
18+
$_SERVER['__hook_fired'] = false;
19+
20+
$class = new class {
21+
use Hookable;
22+
23+
#[Action( 'example_action' )]
24+
public function example_action( mixed $args ): void {
25+
$_SERVER['__hook_fired'] = $args;
26+
}
27+
};
28+
29+
new $class;
30+
31+
$this->assertFalse( $_SERVER['__hook_fired'] );
32+
33+
do_action( 'example_action', 'foo' );
34+
35+
$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
36+
}
37+
38+
public function test_action_from_method_name_with_priority(): void {
39+
40+
$_SERVER['__hook_fired'] = [];
41+
42+
$class = new class {
43+
use Hookable;
44+
45+
#[Action( 'example_action', 20 )]
46+
public function action_at_20( mixed $args ): void {
47+
$_SERVER['__hook_fired'][] = 20;
48+
}
49+
50+
#[Action( 'example_action' )]
51+
public function action_at_10( mixed $args ): void {
52+
$_SERVER['__hook_fired'][] = 10;
53+
}
54+
55+
};
56+
57+
// Remove the action that was added by creating the anonymous class.
58+
remove_all_actions( 'example_action' );
59+
60+
new $class;
61+
62+
$this->assertEmpty( $_SERVER['__hook_fired'] );
63+
64+
do_action( 'example_action', 'foo' );
65+
66+
$this->assertSame( [ 10, 20 ], $_SERVER['__hook_fired'] );
67+
}
68+
69+
public function test_filter_from_method_name(): void {
70+
$_SERVER['__hook_fired'] = false;
71+
72+
$class = new class {
73+
use Hookable;
74+
75+
#[Filter( 'example_action' )]
76+
public function filter_the_value( mixed $value ): mixed {
77+
$_SERVER['__hook_fired'] = $value;
78+
79+
return 'bar';
80+
}
81+
};
82+
83+
remove_all_filters( 'example_action' );
84+
85+
new $class;
86+
87+
$this->assertFalse( $_SERVER['__hook_fired'] );
88+
89+
$value = apply_filters( 'example_action', 'foo' );
90+
91+
$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
92+
$this->assertSame( 'bar', $value );
93+
}
94+
95+
public function test_filter_from_method_name_with_priority(): void {
96+
$_SERVER['__hook_fired'] = [];
97+
98+
$class = new class {
99+
use Hookable;
100+
101+
#[Filter( 'example_action', priority: 20 )]
102+
public function filter_at_20( int $value ): int {
103+
$_SERVER['__hook_fired'][] = $value;
104+
105+
return $value + 20;
106+
}
107+
108+
#[Filter( 'example_action' )]
109+
public function filter_at_10( int $value ): int {
110+
$_SERVER['__hook_fired'][] = $value;
111+
112+
return $value + 10;
113+
}
114+
115+
};
116+
117+
// Remove the action that was added by creating the anonymous class.
118+
remove_all_actions( 'example_action' );
119+
120+
new $class;
121+
122+
$this->assertEmpty( $_SERVER['__hook_fired'] );
123+
124+
$value = apply_filters( 'example_action', 5 );
125+
126+
$this->assertSame( [ 5, 15 ], $_SERVER['__hook_fired'] );
127+
$this->assertSame( 35, $value );
128+
}
129+
130+
public function test_multiple_filters_on_one_method(): void {
131+
$_SERVER['__hook_fired'] = [];
132+
133+
$class = new class {
134+
use Hookable;
135+
136+
#[Filter( 'another_filter' )]
137+
#[Filter( 'example_action' )]
138+
public function filter_to_call( int $value ): int {
139+
$_SERVER['__hook_fired'][] = $value;
140+
141+
return $value + 20;
142+
}
143+
};
144+
145+
// Remove the action that was added by creating the anonymous class.
146+
remove_all_actions( 'another_filter' );
147+
remove_all_actions( 'example_action' );
148+
149+
new $class;
150+
151+
$this->assertTrue( has_filter( 'another_filter' ) );
152+
$this->assertTrue( has_filter( 'example_action' ) );
153+
154+
$this->assertEmpty( $_SERVER['__hook_fired'] );
155+
156+
$value = apply_filters( 'example_action', 5 );
157+
158+
$this->assertSame( [ 5 ], $_SERVER['__hook_fired'] );
159+
$this->assertSame( 25, $value );
160+
161+
$_SERVER['__hook_fired'] = [];
162+
163+
$value = apply_filters( 'another_filter', 10 );
164+
165+
$this->assertSame( [ 10 ], $_SERVER['__hook_fired'] );
166+
$this->assertSame( 30, $value );
167+
}
168+
}

tests/Support/HookableTest.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace Mantle\Tests\Support;
4+
5+
use Mantle\Support\Traits\Hookable;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class HookableTest extends TestCase {
9+
public function setUp(): void {
10+
parent::setUp();
11+
12+
remove_all_actions( 'example_action' );
13+
}
14+
15+
public function test_action_from_method_name(): void {
16+
$_SERVER['__hook_fired'] = false;
17+
18+
$class = new class {
19+
use Hookable;
20+
21+
public function action__example_action( mixed $args ): void {
22+
$_SERVER['__hook_fired'] = $args;
23+
}
24+
};
25+
26+
new $class;
27+
28+
$this->assertFalse( $_SERVER['__hook_fired'] );
29+
30+
do_action( 'example_action', 'foo' );
31+
32+
$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
33+
}
34+
35+
public function test_action_from_method_name_with_priority(): void {
36+
37+
$_SERVER['__hook_fired'] = [];
38+
39+
$class = new class {
40+
use Hookable;
41+
42+
public function action__example_action_at_20( mixed $args ): void {
43+
$_SERVER['__hook_fired'][] = 20;
44+
}
45+
46+
public function action__example_action_at_10( mixed $args ): void {
47+
$_SERVER['__hook_fired'][] = 10;
48+
}
49+
};
50+
51+
// Remove the action that was added by creating the anonymous class.
52+
remove_all_actions( 'example_action' );
53+
54+
new $class;
55+
56+
$this->assertEmpty( $_SERVER['__hook_fired'] );
57+
58+
do_action( 'example_action', 'foo' );
59+
60+
$this->assertSame( [ 10, 20 ], $_SERVER['__hook_fired'] );
61+
}
62+
63+
public function test_filter_from_method_name(): void {
64+
$_SERVER['__hook_fired'] = false;
65+
66+
$class = new class {
67+
use Hookable;
68+
69+
public function filter__example_action( mixed $value ): mixed {
70+
$_SERVER['__hook_fired'] = $value;
71+
72+
return 'bar';
73+
}
74+
};
75+
76+
remove_all_filters( 'example_action' );
77+
78+
new $class;
79+
80+
$this->assertFalse( $_SERVER['__hook_fired'] );
81+
82+
$value = apply_filters( 'example_action', 'foo' );
83+
84+
$this->assertSame( 'foo', $_SERVER['__hook_fired'] );
85+
$this->assertSame( 'bar', $value );
86+
}
87+
88+
public function test_filter_from_method_name_with_priority(): void {
89+
$_SERVER['__hook_fired'] = [];
90+
91+
$class = new class {
92+
use Hookable;
93+
94+
public function filter__example_action_at_20( int $value ): int {
95+
$_SERVER['__hook_fired'][] = $value;
96+
97+
return $value + 20;
98+
}
99+
100+
public function filter__example_action_at_10( int $value ): int {
101+
$_SERVER['__hook_fired'][] = $value;
102+
103+
return $value + 10;
104+
}
105+
};
106+
107+
// Remove the action that was added by creating the anonymous class.
108+
remove_all_actions( 'example_action' );
109+
110+
new $class;
111+
112+
$this->assertEmpty( $_SERVER['__hook_fired'] );
113+
114+
$value = apply_filters( 'example_action', 5 );
115+
116+
$this->assertSame( [ 5, 15 ], $_SERVER['__hook_fired'] );
117+
$this->assertSame( 35, $value );
118+
}
119+
}

0 commit comments

Comments
 (0)