From e56adb521fa2b4cdf0fa7ec68d703df882495655 Mon Sep 17 00:00:00 2001 From: Thor Brink Date: Mon, 3 Mar 2025 09:11:04 +0000 Subject: [PATCH] feat: run with new sync --- library/App.php | 28 ++- .../ExternalContent/Config/SourceConfig.php | 8 + .../Config/SourceConfigFactory.php | 197 ++++++------------ .../Config/SourceConfigInterface.php | 5 + .../Config/SourceConfigWithUniqueId.php | 132 ++++++++++++ .../Config/SourceConfigWithUniqueId.test.php | 52 +++++ .../WpCronJobFromPostTypeSettings.test.php | 4 + .../Factories/SourceReaderFromConfig.php | 2 +- .../HttpApi/TypesenseApi/TypesenseApi.php | 3 +- .../TypesenseApi/TypesenseApi.test.php | 4 +- .../SourceReaders/TypesenseSourceReader.php | 54 ++++- .../TypesenseSourceReader.test.php | 47 ++++- library/ExternalContent/Sync/SyncBuilder.php | 4 +- .../SyncHandler/SyncHandler.php | 40 ++++ .../SyncHandler/SyncHandler.test.php | 64 ++++++ .../SyncHandler/SyncHandlerInterface.php | 13 ++ ...rtPostOnlyIfSyncIsNotAlreadyInProgress.php | 44 ++++ ...tOnlyIfSyncIsNotAlreadyInProgress.test.php | 59 ++++++ .../Factory/Factory.php | 58 +++++- .../Factory/Factory.test.php | 42 ++++ .../TermsDecorator.php | 43 ++-- .../TermsDecorator.test.php | 35 ++-- library/Helper/User/User.php | 4 + 23 files changed, 726 insertions(+), 216 deletions(-) create mode 100644 library/ExternalContent/Config/SourceConfigWithUniqueId.php create mode 100644 library/ExternalContent/Config/SourceConfigWithUniqueId.test.php create mode 100644 library/ExternalContent/SyncHandler/SyncHandler.php create mode 100644 library/ExternalContent/SyncHandler/SyncHandler.test.php create mode 100644 library/ExternalContent/SyncHandler/SyncHandlerInterface.php create mode 100644 library/ExternalContent/SyncHandler/WpInsertPost/InsertPostOnlyIfSyncIsNotAlreadyInProgress.php create mode 100644 library/ExternalContent/SyncHandler/WpInsertPost/InsertPostOnlyIfSyncIsNotAlreadyInProgress.test.php create mode 100644 library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.test.php diff --git a/library/App.php b/library/App.php index af337f092..e6da6f7dd 100644 --- a/library/App.php +++ b/library/App.php @@ -951,16 +951,28 @@ private function setupExternalContent(): void $this->wpService ))->createSources(); + /** + * Sync external content. + */ + $this->wpService->addAction('Municipio/ExternalContent/Sync', function (string $postType, ?int $postId = null) use ($sourceConfigs) { + $sourceConfig = reset(array_filter($sourceConfigs, fn($config) => $config->getPostType() === $postType)); + $sourceReader = (new \Municipio\ExternalContent\SourceReaders\Factories\SourceReaderFromConfig())->create($sourceConfig); + $wpPostArgsFromSchemaObject = (new \Municipio\ExternalContent\WpPostArgsFromSchemaObject\Factory\Factory($sourceConfig))->create(); + $syncHandler = new \Municipio\ExternalContent\SyncHandler\SyncHandler($sourceReader, $wpPostArgsFromSchemaObject, $this->wpService); + + $syncHandler->sync(); + }); + /** * Start sync if event is triggered. */ - $syncEventListener = new \Municipio\ExternalContent\Sync\SyncEventListener( - $sources, - $taxonomyItems, - $this->wpService, - $this->wpdb - ); - $this->hooksRegistrar->register($syncEventListener); + // $syncEventListener = new \Municipio\ExternalContent\Sync\SyncEventListener( + // $sources, + // $taxonomyItems, + // $this->wpService, + // $this->wpdb + // ); + // $this->hooksRegistrar->register($syncEventListener); /** * Only run the following if user is admin. @@ -1031,7 +1043,7 @@ private function setupExternalContent(): void * Trigger sync of external content. */ $triggerSync = new TriggerSync($this->wpService); - $triggerSync = new TriggerSyncIfNotInProgress(new PostTypeSyncInProgress($this->wpService), $triggerSync); + // $triggerSync = new TriggerSyncIfNotInProgress(new PostTypeSyncInProgress($this->wpService), $triggerSync); $triggerSync = new \Municipio\ExternalContent\Sync\Triggers\TriggerSyncFromGetParams( $this->wpService, $triggerSync diff --git a/library/ExternalContent/Config/SourceConfig.php b/library/ExternalContent/Config/SourceConfig.php index 46650ab2c..73355515c 100644 --- a/library/ExternalContent/Config/SourceConfig.php +++ b/library/ExternalContent/Config/SourceConfig.php @@ -126,4 +126,12 @@ public function getSourceTypesenseCollection(): string { return $this->sourceTypesenseCollection; } + + /** + * @inheritDoc + */ + public function getId(): string + { + return $this->postType; + } } diff --git a/library/ExternalContent/Config/SourceConfigFactory.php b/library/ExternalContent/Config/SourceConfigFactory.php index f7bee79fd..dafef3c9f 100644 --- a/library/ExternalContent/Config/SourceConfigFactory.php +++ b/library/ExternalContent/Config/SourceConfigFactory.php @@ -65,149 +65,76 @@ private function createSourceConfigsFromNamedSettings(array $namedSettings): Sou isset($namedSettings['post_type']) ? ($this->schemaDataConfig->tryGetSchemaTypeFromPostType($namedSettings['post_type']) ?? "") : ''; - return new class ($namedSettings, $schemaType) implements SourceConfigInterface { - /** - * Constructor. - */ - public function __construct( - private array $namedSettings, - private string $schemaType - ) { - } - - /** - * @inheritDoc - */ - public function getPostType(): string - { - return $this->namedSettings['post_type']; - } - - /** - * @inheritDoc - */ - public function getSchemaType(): string - { - return $this->schemaType; - } - - /** - * @inheritDoc - */ - public function getAutomaticImportSchedule(): string - { - return $this->namedSettings['automatic_import_schedule']; - } - /** - * @inheritDoc - */ - public function getSourceType(): string - { - return $this->namedSettings['source_type']; - } - - /** - * @inheritDoc - */ - public function getSourceJsonFilePath(): string - { - return $this->namedSettings['source_json_file_path']; - } - - /** - * @inheritDoc - */ - public function getSourceTypesenseApiKey(): string - { - return $this->namedSettings['source_typesense_api_key']; - } + return new SourceConfig( + $namedSettings['post_type'] ?? '', + $namedSettings['automatic_import_schedule'] ?? '', + $schemaType, + $namedSettings['source_type'] ?? '', + $this->getArrayOfSourceTaxonomyConfigs($namedSettings['taxonomies']), + $namedSettings['source_json_file_path'] ?? '', + $namedSettings['source_typesense_api_key'] ?? '', + $namedSettings['source_typesense_protocol'] ?? '', + $namedSettings['source_typesense_host'] ?? '', + $namedSettings['source_typesense_port'] ?? '', + $namedSettings['source_typesense_collection'] ?? '' + ); + } - /** - * @inheritDoc - */ - public function getSourceTypesenseProtocol(): string - { - return $this->namedSettings['source_typesense_protocol']; - } + /** + * Retrieves an array of source taxonomy configurations. + * + * @param array $taxonomies An array of taxonomies to get configurations for. + * @return array An array of source taxonomy configurations. + */ + private function getArrayOfSourceTaxonomyConfigs(array $taxonomies): array + { + if (empty($taxonomies)) { + return []; + } - /** - * @inheritDoc - */ - public function getSourceTypesenseHost(): string - { - return $this->namedSettings['source_typesense_host']; - } + return array_map(function ($taxonomy) { + return new class ($taxonomy) implements SourceTaxonomyConfigInterface { + /** + * Constructor. + */ + public function __construct(private array $taxonomy) + { + } - /** - * @inheritDoc - */ - public function getSourceTypesensePort(): string - { - return $this->namedSettings['source_typesense_port']; - } + /** + * @inheritDoc + */ + public function getFromSchemaProperty(): string + { + return $this->taxonomy['from_schema_property']; + } - /** - * @inheritDoc - */ - public function getSourceTypesenseCollection(): string - { - return $this->namedSettings['source_typesense_collection']; - } + /** + * @inheritDoc + */ + public function getSingularName(): string + { + return $this->taxonomy['singular_name']; + } - /** - * @inheritDoc - */ - public function getTaxonomies(): array - { - if (empty($this->namedSettings['taxonomies'])) { - return []; + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->taxonomy['name']; } - return array_map(function ($taxonomy) { - return new class ($taxonomy) implements SourceTaxonomyConfigInterface { - /** - * Constructor. - */ - public function __construct(private array $taxonomy) - { - } - - /** - * @inheritDoc - */ - public function getFromSchemaProperty(): string - { - return $this->taxonomy['from_schema_property']; - } - - /** - * @inheritDoc - */ - public function getSingularName(): string - { - return $this->taxonomy['singular_name']; - } - - /** - * @inheritDoc - */ - public function getName(): string - { - return $this->taxonomy['name']; - } - - /** - * @inheritDoc - */ - public function isHierarchical(): bool - { - return in_array($this->taxonomy['hierarchical'], [1, true, '1', 'true']); - } - }; - }, $this->namedSettings['taxonomies']); - } - }; + /** + * @inheritDoc + */ + public function isHierarchical(): bool + { + return in_array($this->taxonomy['hierarchical'], [1, true, '1', 'true']); + } + }; + }, $taxonomies); } /** diff --git a/library/ExternalContent/Config/SourceConfigInterface.php b/library/ExternalContent/Config/SourceConfigInterface.php index 76e6dc037..8c235b735 100644 --- a/library/ExternalContent/Config/SourceConfigInterface.php +++ b/library/ExternalContent/Config/SourceConfigInterface.php @@ -80,4 +80,9 @@ public function getSourceTypesensePort(): string; * @return string */ public function getSourceTypesenseCollection(): string; + + /** + * Get the source unique ID + */ + public function getId(): string; } diff --git a/library/ExternalContent/Config/SourceConfigWithUniqueId.php b/library/ExternalContent/Config/SourceConfigWithUniqueId.php new file mode 100644 index 000000000..013425f57 --- /dev/null +++ b/library/ExternalContent/Config/SourceConfigWithUniqueId.php @@ -0,0 +1,132 @@ +inner->getPostType(); + } + + /** + * @inheritDoc + */ + public function getAutomaticImportSchedule(): string + { + return $this->inner->getAutomaticImportSchedule(); + } + + /** + * @inheritDoc + */ + public function getSchemaType(): string + { + return $this->inner->getSchemaType(); + } + + /** + * @inheritDoc + */ + public function getSourceType(): string + { + return $this->inner->getSourceType(); + } + + /** + * @inheritDoc + */ + public function getTaxonomies(): array + { + return $this->inner->getTaxonomies(); + } + + /** + * @inheritDoc + */ + public function getSourceJsonFilePath(): string + { + return $this->inner->getSourceJsonFilePath(); + } + + /** + * @inheritDoc + */ + public function getSourceTypesenseApiKey(): string + { + return $this->inner->getSourceTypesenseApiKey(); + } + + /** + * @inheritDoc + */ + public function getSourceTypesenseProtocol(): string + { + return $this->inner->getSourceTypesenseProtocol(); + } + + /** + * @inheritDoc + */ + public function getSourceTypesenseHost(): string + { + return $this->inner->getSourceTypesenseHost(); + } + + /** + * @inheritDoc + */ + public function getSourceTypesensePort(): string + { + return $this->inner->getSourceTypesensePort(); + } + + /** + * @inheritDoc + */ + public function getSourceTypesenseCollection(): string + { + return $this->inner->getSourceTypesenseCollection(); + } + + /** + * @inheritDoc + */ + public function getId(): string + { + if ($this->uniqueId === null) { + $this->uniqueId = $this->generateUniqueId($this->inner->getId()); + } + + return $this->uniqueId; + } + + private function generateUniqueId(string $id): string + { + static $idRegistry = []; + + $id = md5($id); + + if (in_array($id, $idRegistry)) { + return $this->generateUniqueId($id); + } + + $idRegistry[] = $id; + + return $id; + } +} diff --git a/library/ExternalContent/Config/SourceConfigWithUniqueId.test.php b/library/ExternalContent/Config/SourceConfigWithUniqueId.test.php new file mode 100644 index 000000000..fdd718eef --- /dev/null +++ b/library/ExternalContent/Config/SourceConfigWithUniqueId.test.php @@ -0,0 +1,52 @@ +getInnerSourceConfigMock()); + $this->assertInstanceOf(SourceConfigWithUniqueId::class, $sourceConfig); + } + + /** + * @testdox getId() always returns unique id + */ + public function testGetId() + { + $innerSourceConfig = $this->getInnerSourceConfigMock(); + $innerSourceConfig->method('getId')->willReturn('test-id'); + + $sourceConfigOne = new SourceConfigWithUniqueId($innerSourceConfig); + $sourceConfigTwo = new SourceConfigWithUniqueId($innerSourceConfig); + $sourceConfigThree = new SourceConfigWithUniqueId($innerSourceConfig); + + $this->assertNotSame($sourceConfigOne->getId(), $sourceConfigTwo->getId()); + $this->assertNotSame($sourceConfigOne->getId(), $sourceConfigThree->getId()); + $this->assertNotSame($sourceConfigTwo->getId(), $sourceConfigThree->getId()); + } + + /** + * @testdox getId() returns same id for same instance + */ + public function testGetIdReturnsSameIdForSameInstance() + { + $innerSourceConfig = $this->getInnerSourceConfigMock(); + $innerSourceConfig->method('getId')->willReturn('test-id'); + + $sourceConfig = new SourceConfigWithUniqueId($innerSourceConfig); + $this->assertSame($sourceConfig->getId(), $sourceConfig->getId()); + } + + private function getInnerSourceConfigMock(): SourceConfigInterface|MockObject + { + return $this->createMock(SourceConfigInterface::class); + } +} diff --git a/library/ExternalContent/Cron/WpCronJobFromPostTypeSettings/WpCronJobFromPostTypeSettings.test.php b/library/ExternalContent/Cron/WpCronJobFromPostTypeSettings/WpCronJobFromPostTypeSettings.test.php index 38575f6a9..8cf74a0e5 100644 --- a/library/ExternalContent/Cron/WpCronJobFromPostTypeSettings/WpCronJobFromPostTypeSettings.test.php +++ b/library/ExternalContent/Cron/WpCronJobFromPostTypeSettings/WpCronJobFromPostTypeSettings.test.php @@ -97,6 +97,10 @@ public function getSourceTypesenseProtocol(): string { return ''; } + public function getId(): string + { + return 'test-id'; + } }; } } diff --git a/library/ExternalContent/SourceReaders/Factories/SourceReaderFromConfig.php b/library/ExternalContent/SourceReaders/Factories/SourceReaderFromConfig.php index e7ff25917..cc2be1fb7 100644 --- a/library/ExternalContent/SourceReaders/Factories/SourceReaderFromConfig.php +++ b/library/ExternalContent/SourceReaders/Factories/SourceReaderFromConfig.php @@ -34,7 +34,7 @@ public function create(SourceConfigInterface $config): SourceReaderInterface { return match ($config->getSourceType()) { 'json' => new JsonFileSourceReader($config->getSourceJsonFilePath(), new FileSystem(), new SimpleJsonConverter()), - 'typesense' => new TypesenseSourceReader($this->getTypesenApi($config), ''), + 'typesense' => new TypesenseSourceReader($this->getTypesenApi($config), '', new SimpleJsonConverter()), default => new SourceReader() }; } diff --git a/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.php b/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.php index 5b7e9db99..599d4f7c9 100644 --- a/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.php +++ b/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.php @@ -106,7 +106,8 @@ public function getStatusCode(): int */ public function getBody(): array { - return json_decode($this->response['body'], true); + $decodedBody = json_decode($this->response['body'], true); + return $decodedBody['hits'] ?? []; } /** diff --git a/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.test.php b/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.test.php index 2ed571381..cfddf8bb2 100644 --- a/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.test.php +++ b/library/ExternalContent/SourceReaders/HttpApi/TypesenseApi/TypesenseApi.test.php @@ -57,7 +57,7 @@ public function testGetThrowsExceptionIfWpErrorIsReturned() public function testGetReturnsApiResponseObject() { $wpService = new FakeWpService([ - 'wpRemoteGet' => ['body' => json_encode(['prop' => 'value']), 'headers' => ['test-header'], 'response' => ['code' => 200]], + 'wpRemoteGet' => ['body' => json_encode(['hits' => [['prop' => 'value']]]), 'headers' => ['test-header'], 'response' => ['code' => 200]], ]); $typesenseApi = new TypesenseApi($this->getConfigMock(), $wpService); @@ -65,7 +65,7 @@ public function testGetReturnsApiResponseObject() $apiResponse = $typesenseApi->get('test-endpoint'); $this->assertEquals(200, $apiResponse->getStatusCode()); - $this->assertEquals(['prop' => 'value'], $apiResponse->getBody()); + $this->assertEquals([['prop' => 'value']], $apiResponse->getBody()); $this->assertEquals(['test-header'], $apiResponse->getHeaders()); } diff --git a/library/ExternalContent/SourceReaders/TypesenseSourceReader.php b/library/ExternalContent/SourceReaders/TypesenseSourceReader.php index 19de3493c..630c8b45b 100644 --- a/library/ExternalContent/SourceReaders/TypesenseSourceReader.php +++ b/library/ExternalContent/SourceReaders/TypesenseSourceReader.php @@ -2,8 +2,8 @@ namespace Municipio\ExternalContent\SourceReaders; +use Municipio\ExternalContent\JsonToSchemaObjects\JsonToSchemaObjects; use Municipio\ExternalContent\SourceReaders\HttpApi\ApiGET; -use Municipio\ExternalContent\SourceReaders\HttpApi\ApiResponse; /** * Class TypesenseSourceReader @@ -13,8 +13,11 @@ class TypesenseSourceReader implements SourceReaderInterface /** * Constructor. */ - public function __construct(private ApiGET $api, private string $endpoint) - { + public function __construct( + private ApiGET $api, + private string $endpoint, + private JsonToSchemaObjects $jsonConverter + ) { } /** @@ -25,27 +28,56 @@ public function getSourceData(): array $data = []; $page = 1; - while ($response = $this->fetchPageData($page)) { - $data[] = $response; + while ($dataFromApi = $this->fetchPageData($page)) { + $data[] = array_push($data, ...$dataFromApi); $page++; } - return $data; + $documents = array_map(fn($item) => $item['document'] ?? null, $data); + $documents = array_filter($documents); + $schemaObjects = $this->jsonConverter->transform(json_encode($documents)); + + return $schemaObjects; } /** * Fetches data for a specific page from the API. * * @param int $page The page number to fetch data for. - * @return ApiResponse|null The API response object or null if the request fails. + * @return array|null The data for the given page, or null if the request failed. */ - private function fetchPageData(int $page): ?ApiResponse + private function fetchPageData(int $page): ?array { - $endpointContainsGetParams = strpos($this->endpoint, '?') !== false; - $endpoint = $this->endpoint . ($endpointContainsGetParams ? '&' : '?') . 'page=' . $page; + $endpoint = $this->appendPageToEndpoint($page, $this->endpoint); + $endpoint = $this->appendQueryToEndpoint($endpoint); + $endpoint = $this->appendPerPageToEndpoint($endpoint); $apiResponse = $this->api->get($endpoint); - return $apiResponse->getStatusCode() === 200 ? $apiResponse : null; + if ($apiResponse->getStatusCode() !== 200 || empty($apiResponse->getBody())) { + return null; + } + + return $apiResponse->getBody(); + } + + private function appendPageToEndpoint(int $page, string $endpoint): string + { + return $endpoint . ($this->endpointContainsGetParams($endpoint) ? '&' : '?') . 'page=' . $page; + } + + private function appendQueryToEndpoint(string $endpoint): string + { + return $endpoint . ($this->endpointContainsGetParams($endpoint) ? '&' : '?') . 'q=*'; + } + + private function appendPerPageToEndpoint(string $endpoint): string + { + return $endpoint . ($this->endpointContainsGetParams($endpoint) ? '&' : '?') . 'per_page=250'; + } + + private function endpointContainsGetParams(string $endpoint): bool + { + return strpos($endpoint, '?') !== false; } } diff --git a/library/ExternalContent/SourceReaders/TypesenseSourceReader.test.php b/library/ExternalContent/SourceReaders/TypesenseSourceReader.test.php index 99f8a2dcb..17efa39b1 100644 --- a/library/ExternalContent/SourceReaders/TypesenseSourceReader.test.php +++ b/library/ExternalContent/SourceReaders/TypesenseSourceReader.test.php @@ -2,6 +2,7 @@ namespace Municipio\ExternalContent\SourceReaders; +use Municipio\ExternalContent\JsonToSchemaObjects\SimpleJsonConverter; use Municipio\ExternalContent\SourceReaders\HttpApi\ApiGET; use Municipio\ExternalContent\SourceReaders\HttpApi\ApiResponse; use PHPUnit\Framework\MockObject\MockObject; @@ -14,22 +15,22 @@ class TypesenseSourceReaderTest extends TestCase */ public function testCanBeInstantiated() { - $typesenseSourceReader = new TypesenseSourceReader($this->getApiMock(), 'end/point'); + $typesenseSourceReader = new TypesenseSourceReader($this->getApiMock(), 'end/point', new SimpleJsonConverter()); $this->assertInstanceOf(TypesenseSourceReader::class, $typesenseSourceReader); } /** - * @testdox getSourceData calls api for data untill it gets a null response + * @testdox getSourceData calls api for data untill it gets a empty response */ public function testGetSourceDataCallsApiForDataUntillItGetsANullResponse() { - $api = $this->getApiMock([ $this->getApiResponseMock(), $this->getApiResponseMock([], 404) ]); - $typesenseSourceReader = new TypesenseSourceReader($api, 'end/point'); + $api = $this->getApiMock([ $this->getApiResponseMock(['foo']), $this->getApiResponseMock(['foo']), $this->getApiResponseMock([], 404) ]); + $typesenseSourceReader = new TypesenseSourceReader($api, 'end/point', new SimpleJsonConverter()); $typesenseSourceReader->getSourceData(); - $this->assertEquals('end/point?page=1', $api->calls[0]); - $this->assertEquals('end/point?page=2', $api->calls[1]); + $this->assertStringContainsString('?page=1', $api->calls[0]); + $this->assertStringContainsString('?page=2', $api->calls[1]); } /** @@ -37,13 +38,39 @@ public function testGetSourceDataCallsApiForDataUntillItGetsANullResponse() */ public function testPageNumberIsAppendedCorrectlyToEndpointWithAlreadyDefinedGetParameters() { - $api = $this->getApiMock([$this->getApiResponseMock(), $this->getApiResponseMock([], 404)]); - $typesenseSourceReader = new TypesenseSourceReader($api, 'end/point?param=value'); + $api = $this->getApiMock([$this->getApiResponseMock(['foo']), $this->getApiResponseMock(['foo']), $this->getApiResponseMock([], 404)]); + $typesenseSourceReader = new TypesenseSourceReader($api, 'end/point?param=value', new SimpleJsonConverter()); $typesenseSourceReader->getSourceData(); - $this->assertEquals('end/point?param=value&page=1', $api->calls[0]); - $this->assertEquals('end/point?param=value&page=2', $api->calls[1]); + $this->assertStringContainsString('&page=1', $api->calls[0]); + $this->assertStringContainsString('&page=2', $api->calls[1]); + } + + /** + * @testdox per_page param is appended to endpoint + */ + public function testPerPageParamIsAppendedToEndpoint() + { + $api = $this->getApiMock([$this->getApiResponseMock(['foo']), $this->getApiResponseMock()]); + $typesenseSourceReader = new TypesenseSourceReader($api, 'end/point', new SimpleJsonConverter()); + + $typesenseSourceReader->getSourceData(); + + $this->assertStringContainsString('&per_page=250', $api->calls[0]); + } + + /** + * @testdox query param is appended to endpoint + */ + public function testQueryParamIsAppendedToEndpoint() + { + $api = $this->getApiMock([$this->getApiResponseMock(['foo']), $this->getApiResponseMock()]); + $typesenseSourceReader = new TypesenseSourceReader($api, 'end/point', new SimpleJsonConverter()); + + $typesenseSourceReader->getSourceData(); + + $this->assertStringContainsString('&q=*', $api->calls[0]); } private function getApiMock(array $consecutiveReturns = []): ApiGET diff --git a/library/ExternalContent/Sync/SyncBuilder.php b/library/ExternalContent/Sync/SyncBuilder.php index 1489084e3..1ea9f6892 100644 --- a/library/ExternalContent/Sync/SyncBuilder.php +++ b/library/ExternalContent/Sync/SyncBuilder.php @@ -15,8 +15,8 @@ use Municipio\ExternalContent\WpPostArgsFromSchemaObject\SourceIdDecorator; use Municipio\ExternalContent\WpPostArgsFromSchemaObject\TermsDecorator; use Municipio\ExternalContent\WpPostArgsFromSchemaObject\ThumbnailDecorator; -use Municipio\ExternalContent\WpPostArgsFromSchemaObject\WpPostFactory; use Municipio\ExternalContent\WpPostArgsFromSchemaObject\VerifyChecksum; +use Municipio\ExternalContent\WpPostArgsFromSchemaObject\WpPostArgsFromSchemaObject; use wpdb; use WpService\WpService; @@ -58,7 +58,7 @@ public function build(): SyncSourceToLocalInterface $wpTermFactory = new \Municipio\ExternalContent\WpTermFactory\WpTermFactory(); $wpTermFactory = new \Municipio\ExternalContent\WpTermFactory\WpTermUsingSchemaObjectName($wpTermFactory); - $postArgsFromSchemaObject = new WpPostFactory(); + $postArgsFromSchemaObject = new WpPostArgsFromSchemaObject(); $postArgsFromSchemaObject = new PostTypeDecorator($this->postType, $postArgsFromSchemaObject); $postArgsFromSchemaObject = new DateDecorator($postArgsFromSchemaObject); $postArgsFromSchemaObject = new IdDecorator($this->postType, $source->getId(), $postArgsFromSchemaObject, $this->wpService); diff --git a/library/ExternalContent/SyncHandler/SyncHandler.php b/library/ExternalContent/SyncHandler/SyncHandler.php new file mode 100644 index 000000000..73441a452 --- /dev/null +++ b/library/ExternalContent/SyncHandler/SyncHandler.php @@ -0,0 +1,40 @@ +sourceReader->getSourceData(); + $wpPostArgsArray = array_map(fn($schemaObject) => $this->wpPostArgsFromSchemaObject->transform($schemaObject), $schemaObjects); + + foreach ($wpPostArgsArray as $postArgs) { + $this->wpService->wpInsertPost($postArgs); + } + } +} diff --git a/library/ExternalContent/SyncHandler/SyncHandler.test.php b/library/ExternalContent/SyncHandler/SyncHandler.test.php new file mode 100644 index 000000000..37bf0ef46 --- /dev/null +++ b/library/ExternalContent/SyncHandler/SyncHandler.test.php @@ -0,0 +1,64 @@ +getSourceReaderMock(); + $wpPostFactory = $this->getWpPostFactoryMock(); + $wpService = $this->getWpServiceMock(); + + $syncHandler = new SyncHandler($sourceReader, $wpPostFactory, $wpService); + + $this->assertInstanceOf(SyncHandler::class, $syncHandler); + } + + /** + * @testdox inserts posts + */ + public function testInsertsPosts() + { + $sourceReader = $this->getSourceReaderMock(); + $wpPostFactory = $this->getWpPostFactoryMock(); + $wpService = $this->getWpServiceMock(); + $syncHandler = new SyncHandler($sourceReader, $wpPostFactory, $wpService); + $sourceData = [ Schema::thing() ]; + $wpPostArgs = ['title' => 'Title 1']; + + $sourceReader->method('getSourceData')->willReturn($sourceData); + $wpPostFactory->method('transform')->willReturn($wpPostArgs); + + $syncHandler->sync(); + $firstParamOfFirstCallToWpInsertPost = $wpService->methodCalls['wpInsertPost'][0][0]; + + $this->assertSame($wpPostArgs, $firstParamOfFirstCallToWpInsertPost); + } + + private function getSourceReaderMock(): SourceReaderInterface|MockObject + { + return $this->createMock(SourceReaderInterface::class); + } + + private function getWpPostFactoryMock(): WpPostArgsFromSchemaObjectInterface|MockObject + { + return $this->createMock(WpPostArgsFromSchemaObjectInterface::class); + } + + private function getWpServiceMock(): WpInsertPost + { + return new FakeWpService(['wpInsertPost' => 1]); + } +} diff --git a/library/ExternalContent/SyncHandler/SyncHandlerInterface.php b/library/ExternalContent/SyncHandler/SyncHandlerInterface.php new file mode 100644 index 000000000..3e0aaa59e --- /dev/null +++ b/library/ExternalContent/SyncHandler/SyncHandlerInterface.php @@ -0,0 +1,13 @@ +shouldAllowInsert($postarr) + ? $this->wpService->wpInsertPost($postarr, $wpError, $fireAfterHooks) + : 0; + } + + /** + * Determines if a post insert should be allowed based on whether a sync is already in progress. + * + * @param array $postarr The array of post data. + * @return bool True if the insert should be allowed, false otherwise. + */ + private function shouldAllowInsert($postarr): bool + { + return !($postarr['meta_input']['schemaData']['@preventSync'] ?? false); + } +} diff --git a/library/ExternalContent/SyncHandler/WpInsertPost/InsertPostOnlyIfSyncIsNotAlreadyInProgress.test.php b/library/ExternalContent/SyncHandler/WpInsertPost/InsertPostOnlyIfSyncIsNotAlreadyInProgress.test.php new file mode 100644 index 000000000..1de5521d5 --- /dev/null +++ b/library/ExternalContent/SyncHandler/WpInsertPost/InsertPostOnlyIfSyncIsNotAlreadyInProgress.test.php @@ -0,0 +1,59 @@ +assertInstanceOf(InsertPostOnlyIfSyncIsNotAlreadyInProgress::class, $insertPostOnlyIfSyncIsNotAlreadyInProgress); + } + + /** + * @testdox does not insert post if preventSync is true + */ + public function testDoesNotInsertPostIfPreventSyncIsTrue() + { + $wpService = new FakeWpService([]); + $insertPostOnlyIfSyncIsNotAlreadyInProgress = new InsertPostOnlyIfSyncIsNotAlreadyInProgress($wpService); + + $postarr = [ 'meta_input' => [ 'schemaData' => [ '@preventSync' => true ] ] ]; + + $this->assertSame(0, $insertPostOnlyIfSyncIsNotAlreadyInProgress->wpInsertPost($postarr)); + } + + /** + * @testdox inserts post if preventSync is false + */ + public function testInsertsPostIfPreventSyncIsFalse() + { + $wpService = new FakeWpService(['wpInsertPost' => 1]); + $insertPostOnlyIfSyncIsNotAlreadyInProgress = new InsertPostOnlyIfSyncIsNotAlreadyInProgress($wpService); + + $postarr = [ 'meta_input' => [ 'schemaData' => [ '@preventSync' => false ] ] ]; + + $this->assertSame(1, $insertPostOnlyIfSyncIsNotAlreadyInProgress->wpInsertPost($postarr)); + } + + /** + * @testdox inserts post if preventSync is not set + */ + public function testInsertsPostIfPreventSyncIsNotSet() + { + $wpService = new FakeWpService(['wpInsertPost' => 1]); + $insertPostOnlyIfSyncIsNotAlreadyInProgress = new InsertPostOnlyIfSyncIsNotAlreadyInProgress($wpService); + + $postarr = [ 'meta_input' => [ 'schemaData' => [] ] ]; + + $this->assertSame(1, $insertPostOnlyIfSyncIsNotAlreadyInProgress->wpInsertPost($postarr)); + } +} diff --git a/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.php b/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.php index 22b13ba76..15e5295fb 100644 --- a/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.php +++ b/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.php @@ -2,14 +2,70 @@ namespace Municipio\ExternalContent\WpPostArgsFromSchemaObject\Factory; -use Municipio\ExternalContent\WpPostArgsFromSchemaObject\WpPostArgsFromSchemaObjectInterface; +use Municipio\ExternalContent\Config\SourceConfigInterface; +use Municipio\ExternalContent\WpPostArgsFromSchemaObject\{ + AddChecksum, + DateDecorator, + IdDecorator, + JobPostingDecorator, + MetaPropertyValueDecorator, + OriginIdDecorator, + PostTypeDecorator, + SchemaDataDecorator, + SourceIdDecorator, + TermsDecorator, + ThumbnailDecorator, + VerifyChecksum, + WpPostArgsFromSchemaObject, + WpPostArgsFromSchemaObjectInterface, +}; +use Municipio\ExternalContent\WpTermFactory\WpTermFactoryInterface; +use Municipio\Helper\WpService; +/** + * Factory for creating WpPostArgsFromSchemaObject instances. + */ class Factory implements FactoryInterface { + /** + * Class constructor + * + * @param SourceConfigInterface $sourceConfig + */ + public function __construct(private SourceConfigInterface $sourceConfig) + { + } + /** * @inheritDoc */ public function create(): WpPostArgsFromSchemaObjectInterface { + $postArgsFromSchemaObject = new WpPostArgsFromSchemaObject(); + $postArgsFromSchemaObject = new PostTypeDecorator($this->sourceConfig->getPostType(), $postArgsFromSchemaObject); + $postArgsFromSchemaObject = new DateDecorator($postArgsFromSchemaObject); + $postArgsFromSchemaObject = new IdDecorator($this->sourceConfig->getPostType(), $this->sourceConfig->getId(), $postArgsFromSchemaObject, WpService::get()); + $postArgsFromSchemaObject = new JobPostingDecorator($postArgsFromSchemaObject); + $postArgsFromSchemaObject = new SchemaDataDecorator($postArgsFromSchemaObject); + $postArgsFromSchemaObject = new OriginIdDecorator($postArgsFromSchemaObject); + $postArgsFromSchemaObject = new ThumbnailDecorator($postArgsFromSchemaObject, WpService::get()); + $postArgsFromSchemaObject = new SourceIdDecorator($this->sourceConfig->getId(), $postArgsFromSchemaObject); + $postArgsFromSchemaObject = new MetaPropertyValueDecorator($postArgsFromSchemaObject); + $postArgsFromSchemaObject = new TermsDecorator($this->sourceConfig->getTaxonomies(), $this->getWpTermFactory(), WpService::get(), $postArgsFromSchemaObject); // phpcs:ignore Generic.Files.LineLength.TooLong + $postArgsFromSchemaObject = new AddChecksum($postArgsFromSchemaObject); + $postArgsFromSchemaObject = new VerifyChecksum($postArgsFromSchemaObject, WpService::get()); + + return $postArgsFromSchemaObject; + } + + /** + * Retrieves an instance of WpTermFactoryInterface. + * + * @return WpTermFactoryInterface An instance of WpTermFactoryInterface. + */ + private function getWpTermFactory(): WpTermFactoryInterface + { + $wpTermFactory = new \Municipio\ExternalContent\WpTermFactory\WpTermFactory(); + return new \Municipio\ExternalContent\WpTermFactory\WpTermUsingSchemaObjectName($wpTermFactory); } } diff --git a/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.test.php b/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.test.php new file mode 100644 index 000000000..363aa7346 --- /dev/null +++ b/library/ExternalContent/WpPostArgsFromSchemaObject/Factory/Factory.test.php @@ -0,0 +1,42 @@ +getSourceConfig(), new FakeWpService()); + $this->assertInstanceOf(Factory::class, $factory); + } + + /** + * @testdox create() returns a WpPostArgsFromSchemaObjectInterface + */ + public function testCreateReturnsAWpPostArgsFromSchemaObjectInterface() + { + $factory = new Factory($this->getSourceConfig(), new FakeWpService()); + $this->assertInstanceOf(WpPostArgsFromSchemaObjectInterface::class, $factory->create()); + } + + private function getSourceConfig(): SourceConfigInterface|MockObject + { + return $this->createMock(SourceConfigInterface::class); + } +} diff --git a/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.php b/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.php index 6eb93c86e..ed51df8b3 100644 --- a/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.php +++ b/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.php @@ -2,7 +2,7 @@ namespace Municipio\ExternalContent\WpPostArgsFromSchemaObject; -use Municipio\ExternalContent\Taxonomy\TaxonomyItemInterface; +use Municipio\ExternalContent\Config\SourceTaxonomyConfigInterface; use Municipio\ExternalContent\WpTermFactory\WpTermFactoryInterface; use Spatie\SchemaOrg\BaseType; use Spatie\SchemaOrg\PropertyValue; @@ -18,11 +18,13 @@ class TermsDecorator implements WpPostArgsFromSchemaObjectInterface /** * WpPostMetaFactoryVersionDecorator constructor. * - * @param TaxonomyItemInterface[] $taxonomyItems + * @param SourceTaxonomyConfigInterface[] $taxonomyItems * @param WpPostMetaFactoryInterface $inner + * @param WpTermFactoryInterface $wpTermFactory + * @param WpPostArgsFromSchemaObjectInterface $inner */ public function __construct( - private array $taxonomyItems, + private array $taxonomyConfigs, private WpTermFactoryInterface $wpTermFactory, private WpInsertTerm&TermExists $wpService, private WpPostArgsFromSchemaObjectInterface $inner @@ -34,22 +36,21 @@ public function __construct( */ public function transform(BaseType $schemaObject): array { - $post = $this->inner->transform($schemaObject); - $matchingTaxonomyItems = $this->tryGetMatchingTaxonomyItems($schemaObject); + $post = $this->inner->transform($schemaObject); if (!isset($post['tax_input'])) { $post['tax_input'] = []; } - foreach ($matchingTaxonomyItems as $taxonomyItem) { - $termNames = $this->getTermNamesFromSchemaProperty($schemaObject, $taxonomyItem); + foreach ($this->taxonomyConfigs as $taxonomyConfig) { + $termNames = $this->getTermNamesFromSchemaProperty($schemaObject, $taxonomyConfig); $wpTerms = array_map(fn ($term) => $this->wpTermFactory->create( $term, - $taxonomyItem->getName() + $taxonomyConfig->getName() ), $termNames); - $termIds = $this->getTermIdsFromTerms($wpTerms, $taxonomyItem->getName()); + $termIds = $this->getTermIdsFromTerms($wpTerms, $taxonomyConfig->getName()); - $post['tax_input'][$taxonomyItem->getName()] = $termIds; + $post['tax_input'][$taxonomyConfig->getName()] = $termIds; } return $post; @@ -59,17 +60,17 @@ public function transform(BaseType $schemaObject): array * Get term names from schema property. * * @param BaseType $schemaObject - * @param TaxonomyItemInterface $taxonomyItem + * @param SourceTaxonomyConfigInterface $taxonomyItem * @return string[] */ private function getTermNamesFromSchemaProperty( BaseType $schemaObject, - TaxonomyItemInterface $taxonomyItem + SourceTaxonomyConfigInterface $taxonomyItem ): array { $results = []; $value = $this->getSchemaObjectPropertyValueByPropertyPath( $schemaObject, - $taxonomyItem->getSchemaObjectProperty() + $taxonomyItem->getFromSchemaProperty() ); array_walk_recursive($value, function ($item) use (&$results) { @@ -132,22 +133,6 @@ private function convertPropertyValueToTermNames(mixed $value): ?string }; } - /** - * Get matching taxonomy items. - * - * @param BaseType $schemaObject - * @return TaxonomyItemInterface[] - */ - private function tryGetMatchingTaxonomyItems(BaseType $schemaObject): array - { - return array_filter( - $this->taxonomyItems, - fn($taxonomyItem) => - $taxonomyItem->getSchemaObjectType() === $schemaObject->getType() && - $schemaObject->hasProperty($taxonomyItem->getSchemaObjectProperty()) - ); - } - /** * Get term ids from terms. * If term does not exist, it will be created. diff --git a/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.test.php b/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.test.php index 94ad1a350..52239d9ef 100644 --- a/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.test.php +++ b/library/ExternalContent/WpPostArgsFromSchemaObject/TermsDecorator.test.php @@ -2,10 +2,9 @@ namespace Municipio\ExternalContent\WpPostArgsFromSchemaObject; +use Municipio\ExternalContent\Config\SourceTaxonomyConfigInterface; use Municipio\ExternalContent\Sources\Source; use Municipio\ExternalContent\Sources\SourceInterface; -use Municipio\ExternalContent\Taxonomy\TaxonomyItemInterface; -use Municipio\ExternalContent\Taxonomy\NullTaxonomyItem; use Municipio\ExternalContent\WpTermFactory\WpTermFactoryInterface; use Municipio\TestUtils\WpMockFactory; use PHPUnit\Framework\TestCase; @@ -66,7 +65,7 @@ public function testCanCreateTermsFromSchemaPropertyThatContainsOtherSchemaTypes $schemaObject->actor(Schema::person()->name('testPerson')); $wpService = new FakeWpService(['termExists' => ['term_id' => 3]]); $termFactory = $this->getWpTermFactory(); - $taxonomyItem = $this->getTaxonomyItem('Event', 'actor', 'test_taxonomy'); + $taxonomyItem = $this->getTaxonomyItem('actor', 'test_taxonomy'); $termsDecorator = new TermsDecorator([$taxonomyItem], $termFactory, $wpService, new WpPostArgsFromSchemaObject()); $termsDecorator->transform($schemaObject, $this->getSource()); @@ -83,7 +82,7 @@ public function testCanCreateTermsFromNestedSchemaProperty(): void $schemaObject->actor(Schema::person()->name('Heath Ledger')->callSign('The Joker')); $wpService = new FakeWpService(['termExists' => ['term_id' => 3]]); $termFactory = $this->getWpTermFactory(); - $taxonomyItem = $this->getTaxonomyItem('Event', 'actor.callSign', 'test_taxonomy'); + $taxonomyItem = $this->getTaxonomyItem('actor.callSign', 'test_taxonomy'); $termsDecorator = new TermsDecorator([$taxonomyItem], $termFactory, $wpService, new WpPostArgsFromSchemaObject()); $termsDecorator->transform($schemaObject, $this->getSource()); @@ -100,7 +99,7 @@ public function testCanCreateTermsFromMetaPropertyValueArray(): void $schemaObject->setProperty('@meta', [Schema::propertyValue()->name('illness')->value('Mental')]); $wpService = new FakeWpService(['termExists' => ['term_id' => 3]]); $termFactory = $this->getWpTermFactory(); - $taxonomyItem = $this->getTaxonomyItem('Event', '@meta.illness', 'test_taxonomy'); + $taxonomyItem = $this->getTaxonomyItem('@meta.illness', 'test_taxonomy'); $termsDecorator = new TermsDecorator([$taxonomyItem], $termFactory, $wpService, new WpPostArgsFromSchemaObject()); $termsDecorator->transform($schemaObject, $this->getSource()); @@ -117,7 +116,7 @@ public function testCanCreateTermsFromMetaPropertyValue(): void $schemaObject->setProperty('@meta', Schema::propertyValue()->name('illness')->value('Mental')); $wpService = new FakeWpService(['termExists' => ['term_id' => 3]]); $termFactory = $this->getWpTermFactory(); - $taxonomyItem = $this->getTaxonomyItem('Event', '@meta.illness', 'test_taxonomy'); + $taxonomyItem = $this->getTaxonomyItem('@meta.illness', 'test_taxonomy'); $termsDecorator = new TermsDecorator([$taxonomyItem], $termFactory, $wpService, new WpPostArgsFromSchemaObject()); $termsDecorator->transform($schemaObject, $this->getSource()); @@ -138,7 +137,7 @@ public function testDoesNotCreateFromPropertyValueIfNameIsNotThePropertyName(): ]); $wpService = new FakeWpService(['termExists' => ['term_id' => 3]]); $termFactory = $this->getWpTermFactory(); - $taxonomyItem = $this->getTaxonomyItem('Event', '@meta.foo', 'test_taxonomy'); + $taxonomyItem = $this->getTaxonomyItem('@meta.foo', 'test_taxonomy'); $termsDecorator = new TermsDecorator([$taxonomyItem], $termFactory, $wpService, new WpPostArgsFromSchemaObject()); $termsDecorator->transform($schemaObject, $this->getSource()); @@ -160,31 +159,35 @@ public function create(BaseType|string $schemaObject, string $taxonomy): WP_Term } private function getTaxonomyItem( - string $type = 'Event', string $property = 'keywords', string $name = 'test_taxonomy' - ): TaxonomyItemInterface { - return new class ($type, $property, $name) extends NullTaxonomyItem { + ): SourceTaxonomyConfigInterface { + return new class ($property, $name) implements SourceTaxonomyConfigInterface { public function __construct( - private string $type, private string $property, private string $name ) { } - public function getSchemaObjectType(): string - { - return $this->type; - } public function getName(): string { return $this->name; } - public function getSchemaObjectProperty(): string + public function getFromSchemaProperty(): string { return $this->property; } + + public function getSingularName(): string + { + return ''; + } + + public function isHierarchical(): bool + { + return false; + } }; } diff --git a/library/Helper/User/User.php b/library/Helper/User/User.php index 21feb3c74..b73491d93 100644 --- a/library/Helper/User/User.php +++ b/library/Helper/User/User.php @@ -209,6 +209,10 @@ public function getUserPrefersGroupUrl(null|WP_User|int $user = null): ?bool return null; } + if (!$this->wpService->isMultisite()) { + return null; + } + $perfersGroupUrl = $this->siteSwitcher->runInSite( $this->wpService->getMainSiteId(), function () use ($user) {