Skip to content

Commit ab13585

Browse files
authored
feat: implement language agnostic support for manual Posts module queries with Polylang integration (#2022)
1 parent b6e84cb commit ab13585

7 files changed

Lines changed: 475 additions & 7 deletions

File tree

Modularity/source/php/Module/Posts/Helper/GetPosts/GetPosts.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Modularity\Module\Posts\Helper\GetPosts\PostsResult;
1010
use Modularity\Module\Posts\Helper\GetPosts\PostsResultInterface;
1111
use Modularity\Module\Posts\Helper\GetPosts\PostTypesFromSchemaType\PostTypesFromSchemaTypeResolverInterface;
12+
use WpService\Contracts\ApplyFilters;
1213
use WpService\Contracts\GetPermalink;
1314
use WpService\Contracts\GetPostType;
1415
use WpService\Contracts\GetTheID;
@@ -17,11 +18,13 @@
1718

1819
class GetPosts implements GetPostsInterface
1920
{
21+
private const GET_POSTS_ARGS_FILTER = 'Modularity/Module/Posts/GetPosts/Args';
22+
2023
public function __construct(
2124
private array $fields,
2225
private int $page,
2326
private ?\Municipio\StickyPost\Helper\GetStickyOption $getStickyOption,
24-
private IsUserLoggedIn&GetPermalink&GetPostType&IsArchive&GetTheID $wpService,
27+
private IsUserLoggedIn&GetPermalink&GetPostType&IsArchive&GetTheID&ApplyFilters $wpService,
2528
private WpQueryFactoryInterface $wpQueryFactory,
2629
private PostTypesFromSchemaTypeResolverInterface $postTypesFromSchemaTypeResolver,
2730
) {}
@@ -219,6 +222,19 @@ public function getPostArgs(array $fields, int $page, array $stickyPostIds = [])
219222
// Exclude current post if needed
220223
$getPostsArgs = $this->excludeCurrentPostFromArgs($getPostsArgs);
221224

225+
// Allow integrations to adjust query arguments for the Posts module.
226+
$filteredGetPostsArgs = $this->wpService->applyFilters(
227+
self::GET_POSTS_ARGS_FILTER,
228+
$getPostsArgs,
229+
$fields,
230+
$page,
231+
$stickyPostIds,
232+
);
233+
234+
if (is_array($filteredGetPostsArgs)) {
235+
return $filteredGetPostsArgs;
236+
}
237+
222238
return $getPostsArgs;
223239
}
224240

Modularity/source/php/Module/Posts/Helper/GetPosts/GetPostsTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,58 @@ public function testSortPostsDoesNotSortIfInvalidSortOrderIsProvided()
276276
$this->assertEquals(2, $sortedPosts[2]->menu_order);
277277
}
278278

279+
#[TestDox('getPosts() applies common args filter before creating manual selection query')]
280+
public function testGetPostsAppliesCommonArgsFilterBeforeCreatingManualSelectionQuery(): void
281+
{
282+
$fields = [
283+
'posts_data_source' => 'manual',
284+
'posts_data_posts' => [403, 407],
285+
'posts_count' => 10,
286+
'posts_sort_by' => 'false',
287+
'posts_sort_order' => 'asc',
288+
];
289+
290+
$wpService = new FakeWpService([
291+
'getTheID' => 1,
292+
'isArchive' => false,
293+
'isUserLoggedIn' => false,
294+
'applyFilters' => static function (string $hookName, mixed $args, array $filterFields, int $filterPage): array {
295+
if ($hookName !== 'Modularity/Module/Posts/GetPosts/Args') {
296+
return $args;
297+
}
298+
299+
$args['lang'] = '';
300+
301+
return $args;
302+
},
303+
]);
304+
305+
$wpQuery = $this->createMock(\WP_Query::class);
306+
$wpQuery->method('get_posts')->willReturn([]);
307+
$wpQuery->max_num_pages = 0;
308+
309+
$wpQueryFactory = $this->createMock(WpQueryFactoryInterface::class);
310+
$wpQueryFactory
311+
->expects($this->once())
312+
->method('create')
313+
->with($this->callback(function (array $args): bool {
314+
$this->assertSame([403, 407], $args['post__in'] ?? null);
315+
$this->assertSame('', $args['lang'] ?? null);
316+
$this->assertSame('post__in', $args['orderby'] ?? null);
317+
318+
return true;
319+
}))
320+
->willReturn($wpQuery);
321+
322+
$postTypeFromSchemaTypeResolver = new NullPostTypesFromSchemaTypeResolver();
323+
$getPosts = new GetPosts($fields, 1, null, $wpService, $wpQueryFactory, $postTypeFromSchemaTypeResolver);
324+
325+
$result = $getPosts->getPosts();
326+
327+
$this->assertSame('Modularity/Module/Posts/GetPosts/Args', $wpService->methodCalls['applyFilters'][0][0]);
328+
$this->assertInstanceOf(PostsResultInterface::class, $result);
329+
}
330+
279331
private function getWpPostMock(array $data = []): WP_Post|MockObject
280332
{
281333
$wpPost = $this->createStub(stdClass::class);

library/App.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ private function setUpPolylangIntegration(): void
522522
new \Municipio\Integrations\Polylang\ResolvePageTreeTranslatedChildren($this->wpService),
523523
new \Municipio\Integrations\Polylang\ResolveNavigationItemsLanguage($this->wpService),
524524
new \Municipio\Integrations\Polylang\ResolveAcfPostsManualSelectionLanguage($this->wpService),
525+
new \Municipio\Integrations\Polylang\ResolvePostsModuleManualSelectionLanguage($this->wpService),
525526
new \Municipio\Integrations\Polylang\ResolveTranslatedPageLink($this->wpService),
526527
new \Municipio\Integrations\Polylang\ResolveTranslatedPostTypeLink($this->wpService),
527528
new \Municipio\Integrations\Polylang\ResolveTranslatedPostTypeArchiveLink($this->wpService),

library/Integrations/Polylang/ResolveAcfPostsManualSelectionLanguage.php

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
*/
1414
class ResolveAcfPostsManualSelectionLanguage implements Hookable
1515
{
16+
private const MANUAL_POSTS_FIELD_NAME = 'posts_data_posts';
17+
private const MANUAL_POSTS_FIELD_KEY = 'field_571dfc6ff8115';
18+
1619
/**
1720
* Constructor.
1821
*
@@ -22,6 +25,7 @@ class ResolveAcfPostsManualSelectionLanguage implements Hookable
2225
public function __construct(
2326
private AddFilter $wpService,
2427
private ?Closure $polylangIsActiveResolver = null,
28+
private ?Closure $callStackResolver = null,
2529
) {}
2630

2731
/**
@@ -33,12 +37,28 @@ public function addHooks(): void
3337
return;
3438
}
3539

40+
// Register both field name and key hooks to support classic and block editor field contexts.
41+
$this->wpService->addFilter(
42+
sprintf('acf/fields/post_object/query/name=%s', self::MANUAL_POSTS_FIELD_NAME),
43+
[$this, 'makeManualPostsFieldLanguageAgnostic'],
44+
20,
45+
3,
46+
);
47+
3648
$this->wpService->addFilter(
37-
'acf/fields/post_object/query/name=posts_data_posts',
49+
sprintf('acf/fields/post_object/query/key=%s', self::MANUAL_POSTS_FIELD_KEY),
3850
[$this, 'makeManualPostsFieldLanguageAgnostic'],
3951
20,
4052
3,
4153
);
54+
55+
// Keep selected post_object values visible on load in block editor for this field.
56+
$this->wpService->addFilter(
57+
'acf/acf_get_posts/args',
58+
[$this, 'makeManualPostsFieldLoadLanguageAgnostic'],
59+
20,
60+
1,
61+
);
4262
}
4363

4464
/**
@@ -62,6 +82,28 @@ public function makeManualPostsFieldLanguageAgnostic(array $args, array $field,
6282
return $args;
6383
}
6484

85+
/**
86+
* Makes loading selected posts for the manual posts field language agnostic.
87+
*
88+
* @param array<int|string, mixed> $args ACF get_posts arguments.
89+
*
90+
* @return array<int|string, mixed>
91+
*/
92+
public function makeManualPostsFieldLoadLanguageAgnostic(array $args): array
93+
{
94+
if (!$this->isPolylangActive()) {
95+
return $args;
96+
}
97+
98+
if (empty($args['post__in']) || !$this->isManualPostsFieldInCurrentCallStack()) {
99+
return $args;
100+
}
101+
102+
$args['lang'] = '';
103+
104+
return $args;
105+
}
106+
65107
/**
66108
* Determines if the field is the Posts module manual post selector.
67109
*
@@ -71,7 +113,7 @@ public function makeManualPostsFieldLanguageAgnostic(array $args, array $field,
71113
*/
72114
private function isManualPostsField(array $field): bool
73115
{
74-
return ($field['name'] ?? null) === 'posts_data_posts';
116+
return ($field['name'] ?? null) === self::MANUAL_POSTS_FIELD_NAME || ($field['key'] ?? null) === self::MANUAL_POSTS_FIELD_KEY;
75117
}
76118

77119
/**
@@ -84,6 +126,33 @@ private function isPolylangActive(): bool
84126
return $this->getPolylangIsActiveResolver()?->__invoke() ?? false;
85127
}
86128

129+
/**
130+
* Determines whether current call stack contains the manual posts post_object field context.
131+
*
132+
* @return bool
133+
*/
134+
private function isManualPostsFieldInCurrentCallStack(): bool
135+
{
136+
foreach ($this->getCallStackResolver()?->__invoke() ?? [] as $frame) {
137+
if (!is_array($frame)) {
138+
continue;
139+
}
140+
141+
$function = $frame['function'] ?? null;
142+
$args = $frame['args'] ?? [];
143+
144+
if ($function !== 'get_posts' || !is_array($args) || !isset($args[1]) || !is_array($args[1])) {
145+
continue;
146+
}
147+
148+
if ($this->isManualPostsField($args[1])) {
149+
return true;
150+
}
151+
}
152+
153+
return false;
154+
}
155+
87156
/**
88157
* Gets the Polylang availability resolver.
89158
*
@@ -101,4 +170,18 @@ private function getPolylangIsActiveResolver(): ?Closure
101170

102171
return static fn(): bool => true;
103172
}
173+
174+
/**
175+
* Gets the call stack resolver.
176+
*
177+
* @return Closure
178+
*/
179+
private function getCallStackResolver(): Closure
180+
{
181+
if ($this->callStackResolver instanceof Closure) {
182+
return $this->callStackResolver;
183+
}
184+
185+
return static fn(): array => debug_backtrace();
186+
}
104187
}

library/Integrations/Polylang/ResolveAcfPostsManualSelectionLanguageTest.php

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function testCanBeInstantiated(): void
1717
static::assertInstanceOf(ResolveAcfPostsManualSelectionLanguage::class, $this->getSut());
1818
}
1919

20-
#[TestDox('addHooks() registers the ACF post object query filter when Polylang is active')]
20+
#[TestDox('addHooks() registers the ACF post object query filters when Polylang is active')]
2121
public function testAddHooksRegistersFilterWhenPolylangIsActive(): void
2222
{
2323
$wpService = new FakeWpService([
@@ -32,7 +32,11 @@ public function testAddHooksRegistersFilterWhenPolylangIsActive(): void
3232
$sut->addHooks();
3333

3434
static::assertSame(
35-
[['acf/fields/post_object/query/name=posts_data_posts', [$sut, 'makeManualPostsFieldLanguageAgnostic'], 20, 3]],
35+
[
36+
['acf/fields/post_object/query/name=posts_data_posts', [$sut, 'makeManualPostsFieldLanguageAgnostic'], 20, 3],
37+
['acf/fields/post_object/query/key=field_571dfc6ff8115', [$sut, 'makeManualPostsFieldLanguageAgnostic'], 20, 3],
38+
['acf/acf_get_posts/args', [$sut, 'makeManualPostsFieldLoadLanguageAgnostic'], 20, 1],
39+
],
3640
$wpService->methodCalls['addFilter'],
3741
);
3842
}
@@ -75,6 +79,105 @@ public function testMakeManualPostsFieldLanguageAgnosticSetsEmptyLangForManualFi
7579
static::assertSame('', $result['lang']);
7680
}
7781

82+
#[TestDox('makeManualPostsFieldLanguageAgnostic() sets empty lang when field key matches manual field')]
83+
public function testMakeManualPostsFieldLanguageAgnosticSetsEmptyLangForManualFieldKey(): void
84+
{
85+
$sut = $this->getSut(
86+
static fn(): bool => true,
87+
);
88+
89+
$args = [
90+
'post_type' => ['post', 'evenemang'],
91+
'lang' => 'sv',
92+
];
93+
94+
$field = [
95+
'key' => 'field_571dfc6ff8115',
96+
];
97+
98+
$result = $sut->makeManualPostsFieldLanguageAgnostic($args, $field, 13018);
99+
100+
static::assertSame('', $result['lang']);
101+
}
102+
103+
#[TestDox('makeManualPostsFieldLoadLanguageAgnostic() sets empty lang when loading selected values for manual posts field')]
104+
public function testMakeManualPostsFieldLoadLanguageAgnosticSetsEmptyLangForManualField(): void
105+
{
106+
$sut = $this->getSut(
107+
static fn(): bool => true,
108+
static fn(): array => [
109+
[
110+
'function' => 'get_posts',
111+
'args' => [
112+
[407],
113+
['key' => 'field_571dfc6ff8115'],
114+
],
115+
],
116+
],
117+
);
118+
119+
$args = [
120+
'post__in' => [407],
121+
'lang' => 'sv',
122+
];
123+
124+
$result = $sut->makeManualPostsFieldLoadLanguageAgnostic($args);
125+
126+
static::assertSame('', $result['lang']);
127+
}
128+
129+
#[TestDox('makeManualPostsFieldLoadLanguageAgnostic() leaves args unchanged for unrelated field context')]
130+
public function testMakeManualPostsFieldLoadLanguageAgnosticLeavesArgsUnchangedForUnrelatedField(): void
131+
{
132+
$sut = $this->getSut(
133+
static fn(): bool => true,
134+
static fn(): array => [
135+
[
136+
'function' => 'get_posts',
137+
'args' => [
138+
[407],
139+
['key' => 'field_571dfcd6b5cf9'],
140+
],
141+
],
142+
],
143+
);
144+
145+
$args = [
146+
'post__in' => [407],
147+
'lang' => 'sv',
148+
];
149+
150+
$result = $sut->makeManualPostsFieldLoadLanguageAgnostic($args);
151+
152+
static::assertSame($args, $result);
153+
}
154+
155+
#[TestDox('makeManualPostsFieldLoadLanguageAgnostic() leaves args unchanged when Polylang is unavailable')]
156+
public function testMakeManualPostsFieldLoadLanguageAgnosticLeavesArgsUnchangedWhenPolylangUnavailable(): void
157+
{
158+
$sut = $this->getSut(
159+
static fn(): bool => false,
160+
static fn(): array => [
161+
[
162+
'function' => 'get_posts',
163+
'args' => [
164+
[407],
165+
['key' => 'field_571dfc6ff8115'],
166+
],
167+
],
168+
],
169+
);
170+
171+
$args = [
172+
'post__in' => [407],
173+
'lang' => 'sv',
174+
];
175+
176+
$result = $sut->makeManualPostsFieldLoadLanguageAgnostic($args);
177+
178+
static::assertSame($args, $result);
179+
}
180+
78181
#[TestDox('makeManualPostsFieldLanguageAgnostic() leaves args unchanged for other fields')]
79182
public function testMakeManualPostsFieldLanguageAgnosticLeavesOtherFieldsUnchanged(): void
80183
{
@@ -124,13 +227,16 @@ public function testMakeManualPostsFieldLanguageAgnosticLeavesArgsUnchangedWhenP
124227
*
125228
* @return ResolveAcfPostsManualSelectionLanguage
126229
*/
127-
private function getSut(?Closure $polylangIsActiveResolver = null): ResolveAcfPostsManualSelectionLanguage
128-
{
230+
private function getSut(
231+
?Closure $polylangIsActiveResolver = null,
232+
?Closure $callStackResolver = null,
233+
): ResolveAcfPostsManualSelectionLanguage {
129234
return new ResolveAcfPostsManualSelectionLanguage(
130235
new FakeWpService([
131236
'addFilter' => true,
132237
]),
133238
$polylangIsActiveResolver,
239+
$callStackResolver,
134240
);
135241
}
136242
}

0 commit comments

Comments
 (0)