Skip to content

Commit

Permalink
feat: implement ACF fields for external content filtering (#1335)
Browse files Browse the repository at this point in the history
  • Loading branch information
thorbrink authored Mar 6, 2025
1 parent eefcfc4 commit b2d54b6
Show file tree
Hide file tree
Showing 9 changed files with 519 additions and 168 deletions.
226 changes: 167 additions & 59 deletions library/AcfFields/json/external-content-settings.json

Large diffs are not rendered by default.

228 changes: 168 additions & 60 deletions library/AcfFields/php/external-content-settings.php

Large diffs are not rendered by default.

118 changes: 113 additions & 5 deletions library/ExternalContent/Config/SourceConfigFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
namespace Municipio\ExternalContent\Config;

use Municipio\Config\Features\SchemaData\SchemaDataConfigInterface;
use Municipio\ExternalContent\Filter\FilterDefinition\Contracts\Enums\Operator;
use Municipio\ExternalContent\Filter\FilterDefinition\FilterDefinition;
use Municipio\ExternalContent\Filter\FilterDefinition\Contracts\FilterDefinition as FilterDefinitionInterface;
use Municipio\ExternalContent\Filter\FilterDefinition\Rule;
use Municipio\ExternalContent\Filter\FilterDefinition\RuleSet;
use WpService\Contracts\GetOption;
use WpService\Contracts\GetOptions;

Expand All @@ -22,7 +26,8 @@ class SourceConfigFactory
'source_typesense_protocol',
'source_typesense_host',
'source_typesense_port',
'source_typesense_collection'
'source_typesense_collection',
'rules',
];

private array $taxonomySubFieldNames = [
Expand All @@ -32,6 +37,12 @@ class SourceConfigFactory
'hierarchical'
];

private array $filterRulesSubFieldNames = [
'property_path',
'operator',
'value',
];

/**
* SourceConfigFactory constructor.
*
Expand Down Expand Up @@ -79,7 +90,7 @@ private function createSourceConfigsFromNamedSettings(array $namedSettings): Sou
$namedSettings['source_typesense_host'] ?? '',
$namedSettings['source_typesense_port'] ?? '',
$namedSettings['source_typesense_collection'] ?? '',
new FilterDefinition([])
$this->getFilterDefinitionFromNamedSettings($namedSettings['rules']),
);
}

Expand Down Expand Up @@ -113,6 +124,22 @@ private function getArrayOfSourceTaxonomyConfigs(string $schemaType, array $taxo
return array_filter($taxonomyConfigurations);
}

/**
* Get filter definition from named settings.
*
* @param array $namedSettings The named settings array.
* @return FilterDefinitionInterface The filter definition.
*/
public function getFilterDefinitionFromNamedSettings(array $namedSettings): FilterDefinitionInterface
{
$rules = array_map(function ($rule) {
$operator = $rule['operator'] === 'NOT_EQUALS' ? Operator::NOT_EQUALS : Operator::EQUALS;
return new Rule($rule['property_path'], $rule['value'], $operator);
}, $namedSettings);

return new FilterDefinition([new RuleSet($rules)]);
}

/**
* Get named settings array.
*
Expand All @@ -127,9 +154,10 @@ private function getNamedSettingsArray(): array
return [];
}

$options = $this->fetchOptions($groupName, $nbrOfRows, $this->subFieldNames);
$taxonomyOptions = $this->fetchTaxonomyOptions($groupName, $nbrOfRows, $options);
$settings = array_merge($options, $taxonomyOptions);
$options = $this->fetchOptions($groupName, $nbrOfRows, $this->subFieldNames);
$taxonomyOptions = $this->fetchTaxonomyOptions($groupName, $nbrOfRows, $options);
$filterRulesOptions = $this->fetchFilterRulesOptions($groupName, $nbrOfRows, $options);
$settings = array_merge($options, $taxonomyOptions, $filterRulesOptions);

return $this->buildNamedSettings($groupName, $nbrOfRows, $settings);
}
Expand Down Expand Up @@ -199,6 +227,37 @@ private function fetchTaxonomyOptions(string $groupName, int $nbrOfRows, array $
return $this->wpService->getOptions($taxonomyOptionNames);
}

/**
* Fetch filter rules options from the database.
*
* @param string $groupName The group name.
* @param int $nbrOfRows The number of rows.
* @param array $options The options.
* @return array The fetched filter rules options.
*/
private function fetchFilterRulesOptions(string $groupName, int $nbrOfRows, array $options): array
{
$filterRulesOptionNames = [];

foreach (range(1, $nbrOfRows) as $row) {
$rowIndex = $row - 1;
$nbrOfFilterRules = intval($options["{$groupName}_{$rowIndex}_rules"] ?? 0);

if ($nbrOfFilterRules === 0) {
continue;
}

foreach (range(1, $nbrOfFilterRules) as $filterRuleRow) {
$filterRuleRowIndex = $filterRuleRow - 1;
foreach ($this->filterRulesSubFieldNames as $subFieldName) {
$filterRulesOptionNames[] = "{$groupName}_{$rowIndex}_rules_{$filterRuleRowIndex}_{$subFieldName}";
}
}
}

return $this->wpService->getOptions($filterRulesOptionNames);
}

/**
* Build named settings.
*
Expand Down Expand Up @@ -238,6 +297,7 @@ private function buildRowSettings(string $groupName, int $rowIndex, array $setti
}

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

return $rowSettings;
}
Expand Down Expand Up @@ -267,6 +327,31 @@ private function buildTaxonomySettings(string $groupName, int $rowIndex, array $
return $taxonomies;
}

/**
* Build filter rules settings.
*
* @param string $groupName The group name.
* @param int $rowIndex The row index.
* @param array $settings The settings.
* @return array The filter rules settings.
*/
private function buildFilterRulesSettings(string $groupName, int $rowIndex, array $settings): array
{
$nbrOfFilterRules = intval($settings["{$groupName}_{$rowIndex}_rules"] ?? 0);
$filterRules = [];

if ($nbrOfFilterRules === 0) {
return $filterRules;
}

foreach (range(1, $nbrOfFilterRules) as $filterRuleRow) {
$filterRuleRowIndex = $filterRuleRow - 1;
$filterRules[] = $this->buildSingleFilterRule($groupName, $rowIndex, $filterRuleRowIndex, $settings);
}

return $filterRules;
}

/**
* Build single taxonomy.
*
Expand All @@ -289,4 +374,27 @@ private function buildSingleTaxonomy(string $groupName, int $rowIndex, int $taxo

return $taxonomy;
}

/**
* Build single filter rule.
*
* @param string $groupName The group name.
* @param int $rowIndex The row index.
* @param int $filterRuleRowIndex The filter rule row index.
* @param array $settings The settings.
* @return array The filter rule.
*/
private function buildSingleFilterRule(string $groupName, int $rowIndex, int $filterRuleRowIndex, array $settings): array
{
$filterRule = [];

foreach ($this->filterRulesSubFieldNames as $subFieldName) {
$key = "{$groupName}_{$rowIndex}_rules_{$filterRuleRowIndex}_{$subFieldName}";
if (isset($settings[$key])) {
$filterRule[$subFieldName] = $settings[$key];
}
}

return $filterRule;
}
}
91 changes: 65 additions & 26 deletions library/ExternalContent/Config/SourceConfigFactory.test.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Municipio\ExternalContent\Config;

use Municipio\Config\Features\SchemaData\SchemaDataConfigInterface;
use Municipio\ExternalContent\Filter\FilterDefinition\Contracts\Enums\Operator;
use PHPUnit\Framework\TestCase;
use WpService\Implementations\FakeWpService;

Expand Down Expand Up @@ -40,8 +41,7 @@ public function testAcfConfig()
*/
public function testAcfConfigContainsRepeaterField()
{
$jsonFileContents = file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json');
$json = json_decode($jsonFileContents, true);
$json = $this->getAcfFields();

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

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

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

Expand All @@ -71,15 +70,15 @@ public function testAcfConfigContainsExpectedSubFields()
$this->assertContains('source_typesense_collection', $subFieldNames);
$this->assertContains('automatic_import_schedule', $subFieldNames);
$this->assertContains('taxonomies', $subFieldNames);
$this->assertContains('rules', $subFieldNames);
}

/**
* @testdox ACF json taxonomies field contains expected sub fields
*/
public function testAcfConfigContainsExpectedTaxonomiesSubFields()
{
$jsonFileContents = file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json');
$json = json_decode($jsonFileContents, true);
$json = $this->getAcfFields();
$fields = $json[0]['fields'];
$subFields = $fields[0]['sub_fields'];
$taxonomiesField = array_values(array_filter($subFields, fn($subField) => $subField['name'] === 'taxonomies'));
Expand All @@ -91,6 +90,22 @@ public function testAcfConfigContainsExpectedTaxonomiesSubFields()
$this->assertContains('hierarchical', $taxonomiesSubFieldNames);
}

/**
* @testdox ACF json taxonomies field contains expected filter sub fields
*/
public function testAcfConfigContainsExpectedTaxonomiesFilterSubFields()
{
$json = $this->getAcfFields();
$fields = $json[0]['fields'];
$subFields = $fields[0]['sub_fields'];
$rulesField = array_values(array_filter($subFields, fn($subField) => $subField['name'] === 'rules'));
$rulesSubFieldNames = array_map(fn($subField) => $subField['name'], $rulesField[0]['sub_fields']);

$this->assertContains('property_path', $rulesSubFieldNames);
$this->assertContains('operator', $rulesSubFieldNames);
$this->assertContains('value', $rulesSubFieldNames);
}

/**
* @testdox create returns an empty array if no rows are found
*/
Expand All @@ -109,7 +124,7 @@ public function testCreateReturnsAnEmptyArrayIfNoRowsAreFound()
public function testExpectedOptionsAreFetched()
{
$getOption = fn($option, $default) => $option === 'options_external_content_sources' ? '1' : $default;
$getOptions = fn($options) => ['options_external_content_sources_0_taxonomies' => '1'];
$getOptions = fn($options) => $this->getTestAcfData();
$wpService = new FakeWpService(['getOption' => $getOption, 'getOptions' => $getOptions]);

@(new SourceConfigFactory($this->getSchemaDataConfig(), $wpService))->create();
Expand All @@ -125,6 +140,7 @@ public function testExpectedOptionsAreFetched()
'options_external_content_sources_0_source_typesense_host',
'options_external_content_sources_0_source_typesense_port',
'options_external_content_sources_0_source_typesense_collection',
'options_external_content_sources_0_rules',

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

Expand All @@ -135,31 +151,22 @@ public function testExpectedOptionsAreFetched()
'options_external_content_sources_0_taxonomies_0_hierarchical',

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

$this->assertEquals([
'options_external_content_sources_0_rules_0_property_path',
'options_external_content_sources_0_rules_0_operator',
'options_external_content_sources_0_rules_0_value',

], $wpService->methodCalls['getOptions'][2][0]);
}

/**
* @testdox array of SourceConfigInterface objects are returned
*/
public function test()
public function testReturnsExpectedSourceConfigObjects()
{
$getOption = fn($option, $default) => $option === 'options_external_content_sources' ? '1' : $default;
$getOptions = fn($options) => [
'options_external_content_sources_0_post_type' => 'test_post_type',
'options_external_content_sources_0_automatic_import_schedule' => 'test_schedule',
'options_external_content_sources_0_taxonomies' => '1',
'options_external_content_sources_0_source_type' => 'test_source_type',
'options_external_content_sources_0_source_json_file_path' => 'test_json_file_path',
'options_external_content_sources_0_source_typesense_api_key' => 'test_api_key',
'options_external_content_sources_0_source_typesense_protocol' => 'test_protocol',
'options_external_content_sources_0_source_typesense_host' => 'test_host',
'options_external_content_sources_0_source_typesense_port' => 'test_port',
'options_external_content_sources_0_source_typesense_collection' => 'test_collection',
'options_external_content_sources_0_taxonomies_0_from_schema_property' => 'test_from_schema_property',
'options_external_content_sources_0_taxonomies_0_singular_name' => 'test_singular_name',
'options_external_content_sources_0_taxonomies_0_name' => 'test_name',
'options_external_content_sources_0_taxonomies_0_hierarchical' => true,
];
$wpService = new FakeWpService(['getOption' => $getOption, 'getOptions' => $getOptions]);
$getOption = fn($option, $default) => $option === 'options_external_content_sources' ? '1' : $default;
$wpService = new FakeWpService(['getOption' => $getOption, 'getOptions' => fn($options) => $this->getTestAcfData()]);

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

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

Expand Down Expand Up @@ -206,4 +216,33 @@ public function tryGetSchemaTypeFromPostType(string $postType): ?string
}
};
}

private function getAcfFields(): array
{
return json_decode(file_get_contents(__DIR__ . '/../../AcfFields/json/external-content-settings.json'), true);
}

private function getTestAcfData(): array
{
return [
'options_external_content_sources_0_post_type' => 'test_post_type',
'options_external_content_sources_0_automatic_import_schedule' => 'test_schedule',
'options_external_content_sources_0_taxonomies' => '1',
'options_external_content_sources_0_source_type' => 'test_source_type',
'options_external_content_sources_0_source_json_file_path' => 'test_json_file_path',
'options_external_content_sources_0_source_typesense_api_key' => 'test_api_key',
'options_external_content_sources_0_source_typesense_protocol' => 'test_protocol',
'options_external_content_sources_0_source_typesense_host' => 'test_host',
'options_external_content_sources_0_source_typesense_port' => 'test_port',
'options_external_content_sources_0_source_typesense_collection' => 'test_collection',
'options_external_content_sources_0_taxonomies_0_from_schema_property' => 'test_from_schema_property',
'options_external_content_sources_0_taxonomies_0_singular_name' => 'test_singular_name',
'options_external_content_sources_0_taxonomies_0_name' => 'test_name',
'options_external_content_sources_0_taxonomies_0_hierarchical' => true,
'options_external_content_sources_0_rules' => '1',
'options_external_content_sources_0_rules_0_property_path' => 'test_property_path',
'options_external_content_sources_0_rules_0_operator' => 'test_operator',
'options_external_content_sources_0_rules_0_value' => 'test_value',
];
}
}
3 changes: 0 additions & 3 deletions library/ExternalContent/Filter/FilterDefinition/RuleSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ class RuleSet implements RuleSetInterface
public function __construct(
private array $rules,
) {
if (empty($rules)) {
throw new \InvalidArgumentException('Rules must not be empty.');
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,6 @@ public function testCanBeInstantiated()
$this->assertInstanceOf(RuleSet::class, $ruleSet);
}

/**
* @testdox class can not be instantiated with empty rules
*/
public function testCanBeInstantiatedWithEmptyRules()
{
$this->expectException(\InvalidArgumentException::class);
new RuleSet([]);
}

/**
* @testdox getRules() returns provided rules
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function transform(FilterDefinition $filterDefinition): string
}

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

/**
Expand Down
Loading

0 comments on commit b2d54b6

Please sign in to comment.