Skip to content

Commit b2d54b6

Browse files
authored
feat: implement ACF fields for external content filtering (#1335)
1 parent eefcfc4 commit b2d54b6

9 files changed

+519
-168
lines changed

library/AcfFields/json/external-content-settings.json

+167-59
Large diffs are not rendered by default.

library/AcfFields/php/external-content-settings.php

+168-60
Large diffs are not rendered by default.

library/ExternalContent/Config/SourceConfigFactory.php

+113-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
namespace Municipio\ExternalContent\Config;
44

55
use Municipio\Config\Features\SchemaData\SchemaDataConfigInterface;
6+
use Municipio\ExternalContent\Filter\FilterDefinition\Contracts\Enums\Operator;
67
use Municipio\ExternalContent\Filter\FilterDefinition\FilterDefinition;
8+
use Municipio\ExternalContent\Filter\FilterDefinition\Contracts\FilterDefinition as FilterDefinitionInterface;
9+
use Municipio\ExternalContent\Filter\FilterDefinition\Rule;
10+
use Municipio\ExternalContent\Filter\FilterDefinition\RuleSet;
711
use WpService\Contracts\GetOption;
812
use WpService\Contracts\GetOptions;
913

@@ -22,7 +26,8 @@ class SourceConfigFactory
2226
'source_typesense_protocol',
2327
'source_typesense_host',
2428
'source_typesense_port',
25-
'source_typesense_collection'
29+
'source_typesense_collection',
30+
'rules',
2631
];
2732

2833
private array $taxonomySubFieldNames = [
@@ -32,6 +37,12 @@ class SourceConfigFactory
3237
'hierarchical'
3338
];
3439

40+
private array $filterRulesSubFieldNames = [
41+
'property_path',
42+
'operator',
43+
'value',
44+
];
45+
3546
/**
3647
* SourceConfigFactory constructor.
3748
*
@@ -79,7 +90,7 @@ private function createSourceConfigsFromNamedSettings(array $namedSettings): Sou
7990
$namedSettings['source_typesense_host'] ?? '',
8091
$namedSettings['source_typesense_port'] ?? '',
8192
$namedSettings['source_typesense_collection'] ?? '',
82-
new FilterDefinition([])
93+
$this->getFilterDefinitionFromNamedSettings($namedSettings['rules']),
8394
);
8495
}
8596

@@ -113,6 +124,22 @@ private function getArrayOfSourceTaxonomyConfigs(string $schemaType, array $taxo
113124
return array_filter($taxonomyConfigurations);
114125
}
115126

127+
/**
128+
* Get filter definition from named settings.
129+
*
130+
* @param array $namedSettings The named settings array.
131+
* @return FilterDefinitionInterface The filter definition.
132+
*/
133+
public function getFilterDefinitionFromNamedSettings(array $namedSettings): FilterDefinitionInterface
134+
{
135+
$rules = array_map(function ($rule) {
136+
$operator = $rule['operator'] === 'NOT_EQUALS' ? Operator::NOT_EQUALS : Operator::EQUALS;
137+
return new Rule($rule['property_path'], $rule['value'], $operator);
138+
}, $namedSettings);
139+
140+
return new FilterDefinition([new RuleSet($rules)]);
141+
}
142+
116143
/**
117144
* Get named settings array.
118145
*
@@ -127,9 +154,10 @@ private function getNamedSettingsArray(): array
127154
return [];
128155
}
129156

130-
$options = $this->fetchOptions($groupName, $nbrOfRows, $this->subFieldNames);
131-
$taxonomyOptions = $this->fetchTaxonomyOptions($groupName, $nbrOfRows, $options);
132-
$settings = array_merge($options, $taxonomyOptions);
157+
$options = $this->fetchOptions($groupName, $nbrOfRows, $this->subFieldNames);
158+
$taxonomyOptions = $this->fetchTaxonomyOptions($groupName, $nbrOfRows, $options);
159+
$filterRulesOptions = $this->fetchFilterRulesOptions($groupName, $nbrOfRows, $options);
160+
$settings = array_merge($options, $taxonomyOptions, $filterRulesOptions);
133161

134162
return $this->buildNamedSettings($groupName, $nbrOfRows, $settings);
135163
}
@@ -199,6 +227,37 @@ private function fetchTaxonomyOptions(string $groupName, int $nbrOfRows, array $
199227
return $this->wpService->getOptions($taxonomyOptionNames);
200228
}
201229

230+
/**
231+
* Fetch filter rules options from the database.
232+
*
233+
* @param string $groupName The group name.
234+
* @param int $nbrOfRows The number of rows.
235+
* @param array $options The options.
236+
* @return array The fetched filter rules options.
237+
*/
238+
private function fetchFilterRulesOptions(string $groupName, int $nbrOfRows, array $options): array
239+
{
240+
$filterRulesOptionNames = [];
241+
242+
foreach (range(1, $nbrOfRows) as $row) {
243+
$rowIndex = $row - 1;
244+
$nbrOfFilterRules = intval($options["{$groupName}_{$rowIndex}_rules"] ?? 0);
245+
246+
if ($nbrOfFilterRules === 0) {
247+
continue;
248+
}
249+
250+
foreach (range(1, $nbrOfFilterRules) as $filterRuleRow) {
251+
$filterRuleRowIndex = $filterRuleRow - 1;
252+
foreach ($this->filterRulesSubFieldNames as $subFieldName) {
253+
$filterRulesOptionNames[] = "{$groupName}_{$rowIndex}_rules_{$filterRuleRowIndex}_{$subFieldName}";
254+
}
255+
}
256+
}
257+
258+
return $this->wpService->getOptions($filterRulesOptionNames);
259+
}
260+
202261
/**
203262
* Build named settings.
204263
*
@@ -238,6 +297,7 @@ private function buildRowSettings(string $groupName, int $rowIndex, array $setti
238297
}
239298

240299
$rowSettings['taxonomies'] = $this->buildTaxonomySettings($groupName, $rowIndex, $settings);
300+
$rowSettings['rules'] = $this->buildFilterRulesSettings($groupName, $rowIndex, $settings);
241301

242302
return $rowSettings;
243303
}
@@ -267,6 +327,31 @@ private function buildTaxonomySettings(string $groupName, int $rowIndex, array $
267327
return $taxonomies;
268328
}
269329

330+
/**
331+
* Build filter rules settings.
332+
*
333+
* @param string $groupName The group name.
334+
* @param int $rowIndex The row index.
335+
* @param array $settings The settings.
336+
* @return array The filter rules settings.
337+
*/
338+
private function buildFilterRulesSettings(string $groupName, int $rowIndex, array $settings): array
339+
{
340+
$nbrOfFilterRules = intval($settings["{$groupName}_{$rowIndex}_rules"] ?? 0);
341+
$filterRules = [];
342+
343+
if ($nbrOfFilterRules === 0) {
344+
return $filterRules;
345+
}
346+
347+
foreach (range(1, $nbrOfFilterRules) as $filterRuleRow) {
348+
$filterRuleRowIndex = $filterRuleRow - 1;
349+
$filterRules[] = $this->buildSingleFilterRule($groupName, $rowIndex, $filterRuleRowIndex, $settings);
350+
}
351+
352+
return $filterRules;
353+
}
354+
270355
/**
271356
* Build single taxonomy.
272357
*
@@ -289,4 +374,27 @@ private function buildSingleTaxonomy(string $groupName, int $rowIndex, int $taxo
289374

290375
return $taxonomy;
291376
}
377+
378+
/**
379+
* Build single filter rule.
380+
*
381+
* @param string $groupName The group name.
382+
* @param int $rowIndex The row index.
383+
* @param int $filterRuleRowIndex The filter rule row index.
384+
* @param array $settings The settings.
385+
* @return array The filter rule.
386+
*/
387+
private function buildSingleFilterRule(string $groupName, int $rowIndex, int $filterRuleRowIndex, array $settings): array
388+
{
389+
$filterRule = [];
390+
391+
foreach ($this->filterRulesSubFieldNames as $subFieldName) {
392+
$key = "{$groupName}_{$rowIndex}_rules_{$filterRuleRowIndex}_{$subFieldName}";
393+
if (isset($settings[$key])) {
394+
$filterRule[$subFieldName] = $settings[$key];
395+
}
396+
}
397+
398+
return $filterRule;
399+
}
292400
}

library/ExternalContent/Config/SourceConfigFactory.test.php

+65-26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Municipio\ExternalContent\Config;
44

55
use Municipio\Config\Features\SchemaData\SchemaDataConfigInterface;
6+
use Municipio\ExternalContent\Filter\FilterDefinition\Contracts\Enums\Operator;
67
use PHPUnit\Framework\TestCase;
78
use WpService\Implementations\FakeWpService;
89

@@ -40,8 +41,7 @@ public function testAcfConfig()
4041
*/
4142
public function testAcfConfigContainsRepeaterField()
4243
{
43-
$jsonFileContents = file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json');
44-
$json = json_decode($jsonFileContents, true);
44+
$json = $this->getAcfFields();
4545

4646
$fields = $json[0]['fields'];
4747

@@ -54,8 +54,7 @@ public function testAcfConfigContainsRepeaterField()
5454
*/
5555
public function testAcfConfigContainsExpectedSubFields()
5656
{
57-
$jsonFileContents = file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json');
58-
$json = json_decode($jsonFileContents, true);
57+
$json = $this->getAcfFields();
5958

6059
$fields = $json[0]['fields'];
6160

@@ -71,15 +70,15 @@ public function testAcfConfigContainsExpectedSubFields()
7170
$this->assertContains('source_typesense_collection', $subFieldNames);
7271
$this->assertContains('automatic_import_schedule', $subFieldNames);
7372
$this->assertContains('taxonomies', $subFieldNames);
73+
$this->assertContains('rules', $subFieldNames);
7474
}
7575

7676
/**
7777
* @testdox ACF json taxonomies field contains expected sub fields
7878
*/
7979
public function testAcfConfigContainsExpectedTaxonomiesSubFields()
8080
{
81-
$jsonFileContents = file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json');
82-
$json = json_decode($jsonFileContents, true);
81+
$json = $this->getAcfFields();
8382
$fields = $json[0]['fields'];
8483
$subFields = $fields[0]['sub_fields'];
8584
$taxonomiesField = array_values(array_filter($subFields, fn($subField) => $subField['name'] === 'taxonomies'));
@@ -91,6 +90,22 @@ public function testAcfConfigContainsExpectedTaxonomiesSubFields()
9190
$this->assertContains('hierarchical', $taxonomiesSubFieldNames);
9291
}
9392

93+
/**
94+
* @testdox ACF json taxonomies field contains expected filter sub fields
95+
*/
96+
public function testAcfConfigContainsExpectedTaxonomiesFilterSubFields()
97+
{
98+
$json = $this->getAcfFields();
99+
$fields = $json[0]['fields'];
100+
$subFields = $fields[0]['sub_fields'];
101+
$rulesField = array_values(array_filter($subFields, fn($subField) => $subField['name'] === 'rules'));
102+
$rulesSubFieldNames = array_map(fn($subField) => $subField['name'], $rulesField[0]['sub_fields']);
103+
104+
$this->assertContains('property_path', $rulesSubFieldNames);
105+
$this->assertContains('operator', $rulesSubFieldNames);
106+
$this->assertContains('value', $rulesSubFieldNames);
107+
}
108+
94109
/**
95110
* @testdox create returns an empty array if no rows are found
96111
*/
@@ -109,7 +124,7 @@ public function testCreateReturnsAnEmptyArrayIfNoRowsAreFound()
109124
public function testExpectedOptionsAreFetched()
110125
{
111126
$getOption = fn($option, $default) => $option === 'options_external_content_sources' ? '1' : $default;
112-
$getOptions = fn($options) => ['options_external_content_sources_0_taxonomies' => '1'];
127+
$getOptions = fn($options) => $this->getTestAcfData();
113128
$wpService = new FakeWpService(['getOption' => $getOption, 'getOptions' => $getOptions]);
114129

115130
@(new SourceConfigFactory($this->getSchemaDataConfig(), $wpService))->create();
@@ -125,6 +140,7 @@ public function testExpectedOptionsAreFetched()
125140
'options_external_content_sources_0_source_typesense_host',
126141
'options_external_content_sources_0_source_typesense_port',
127142
'options_external_content_sources_0_source_typesense_collection',
143+
'options_external_content_sources_0_rules',
128144

129145
], $wpService->methodCalls['getOptions'][0][0]);
130146

@@ -135,31 +151,22 @@ public function testExpectedOptionsAreFetched()
135151
'options_external_content_sources_0_taxonomies_0_hierarchical',
136152

137153
], $wpService->methodCalls['getOptions'][1][0]);
154+
155+
$this->assertEquals([
156+
'options_external_content_sources_0_rules_0_property_path',
157+
'options_external_content_sources_0_rules_0_operator',
158+
'options_external_content_sources_0_rules_0_value',
159+
160+
], $wpService->methodCalls['getOptions'][2][0]);
138161
}
139162

140163
/**
141164
* @testdox array of SourceConfigInterface objects are returned
142165
*/
143-
public function test()
166+
public function testReturnsExpectedSourceConfigObjects()
144167
{
145-
$getOption = fn($option, $default) => $option === 'options_external_content_sources' ? '1' : $default;
146-
$getOptions = fn($options) => [
147-
'options_external_content_sources_0_post_type' => 'test_post_type',
148-
'options_external_content_sources_0_automatic_import_schedule' => 'test_schedule',
149-
'options_external_content_sources_0_taxonomies' => '1',
150-
'options_external_content_sources_0_source_type' => 'test_source_type',
151-
'options_external_content_sources_0_source_json_file_path' => 'test_json_file_path',
152-
'options_external_content_sources_0_source_typesense_api_key' => 'test_api_key',
153-
'options_external_content_sources_0_source_typesense_protocol' => 'test_protocol',
154-
'options_external_content_sources_0_source_typesense_host' => 'test_host',
155-
'options_external_content_sources_0_source_typesense_port' => 'test_port',
156-
'options_external_content_sources_0_source_typesense_collection' => 'test_collection',
157-
'options_external_content_sources_0_taxonomies_0_from_schema_property' => 'test_from_schema_property',
158-
'options_external_content_sources_0_taxonomies_0_singular_name' => 'test_singular_name',
159-
'options_external_content_sources_0_taxonomies_0_name' => 'test_name',
160-
'options_external_content_sources_0_taxonomies_0_hierarchical' => true,
161-
];
162-
$wpService = new FakeWpService(['getOption' => $getOption, 'getOptions' => $getOptions]);
168+
$getOption = fn($option, $default) => $option === 'options_external_content_sources' ? '1' : $default;
169+
$wpService = new FakeWpService(['getOption' => $getOption, 'getOptions' => fn($options) => $this->getTestAcfData()]);
163170

164171
$sourceConfigs = (new SourceConfigFactory($this->getSchemaDataConfig(), $wpService))->create();
165172

@@ -175,6 +182,9 @@ public function test()
175182
$this->assertEquals('test_from_schema_property', $sourceConfigs[0]->getTaxonomies()[0]->getFromSchemaProperty());
176183
$this->assertEquals('test_singular_name', $sourceConfigs[0]->getTaxonomies()[0]->getSingularName());
177184
$this->assertEquals('test_schema_type_test_from_schem', $sourceConfigs[0]->getTaxonomies()[0]->getName());
185+
$this->assertEquals('test_property_path', $sourceConfigs[0]->getFilterDefinition()->getRuleSets()[0]->getRules()[0]->getPropertyPath());
186+
$this->assertEquals(Operator::EQUALS, $sourceConfigs[0]->getFilterDefinition()->getRuleSets()[0]->getRules()[0]->getOperator());
187+
$this->assertEquals('test_value', $sourceConfigs[0]->getFilterDefinition()->getRuleSets()[0]->getRules()[0]->getValue());
178188
$this->assertEquals(true, $sourceConfigs[0]->getTaxonomies()[0]->isHierarchical());
179189
}
180190

@@ -206,4 +216,33 @@ public function tryGetSchemaTypeFromPostType(string $postType): ?string
206216
}
207217
};
208218
}
219+
220+
private function getAcfFields(): array
221+
{
222+
return json_decode(file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json'), true);
223+
}
224+
225+
private function getTestAcfData(): array
226+
{
227+
return [
228+
'options_external_content_sources_0_post_type' => 'test_post_type',
229+
'options_external_content_sources_0_automatic_import_schedule' => 'test_schedule',
230+
'options_external_content_sources_0_taxonomies' => '1',
231+
'options_external_content_sources_0_source_type' => 'test_source_type',
232+
'options_external_content_sources_0_source_json_file_path' => 'test_json_file_path',
233+
'options_external_content_sources_0_source_typesense_api_key' => 'test_api_key',
234+
'options_external_content_sources_0_source_typesense_protocol' => 'test_protocol',
235+
'options_external_content_sources_0_source_typesense_host' => 'test_host',
236+
'options_external_content_sources_0_source_typesense_port' => 'test_port',
237+
'options_external_content_sources_0_source_typesense_collection' => 'test_collection',
238+
'options_external_content_sources_0_taxonomies_0_from_schema_property' => 'test_from_schema_property',
239+
'options_external_content_sources_0_taxonomies_0_singular_name' => 'test_singular_name',
240+
'options_external_content_sources_0_taxonomies_0_name' => 'test_name',
241+
'options_external_content_sources_0_taxonomies_0_hierarchical' => true,
242+
'options_external_content_sources_0_rules' => '1',
243+
'options_external_content_sources_0_rules_0_property_path' => 'test_property_path',
244+
'options_external_content_sources_0_rules_0_operator' => 'test_operator',
245+
'options_external_content_sources_0_rules_0_value' => 'test_value',
246+
];
247+
}
209248
}

library/ExternalContent/Filter/FilterDefinition/RuleSet.php

-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ class RuleSet implements RuleSetInterface
2121
public function __construct(
2222
private array $rules,
2323
) {
24-
if (empty($rules)) {
25-
throw new \InvalidArgumentException('Rules must not be empty.');
26-
}
2724
}
2825

2926
/**

library/ExternalContent/Filter/FilterDefinition/RuleSet.test.php

-9
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@ public function testCanBeInstantiated()
1717
$this->assertInstanceOf(RuleSet::class, $ruleSet);
1818
}
1919

20-
/**
21-
* @testdox class can not be instantiated with empty rules
22-
*/
23-
public function testCanBeInstantiatedWithEmptyRules()
24-
{
25-
$this->expectException(\InvalidArgumentException::class);
26-
new RuleSet([]);
27-
}
28-
2920
/**
3021
* @testdox getRules() returns provided rules
3122
*/

library/ExternalContent/Filter/Transforms/FilterDefinitionToTypesenseParams.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function transform(FilterDefinition $filterDefinition): string
3030
}
3131

3232
// Join multiple rule set strings with the logical OR operator.
33-
return implode('||', $filterStrings);
33+
return 'filter_by=' . implode('||', $filterStrings);
3434
}
3535

3636
/**

0 commit comments

Comments
 (0)