*/ private array $records = array(); /** @var array */ private array $productProviderUrlIndex = array(); /** * Next synthetic database id. */ private int $nextId = 1; /** * @param list $seed Seed records. */ public function __construct( array $seed = array() ) { foreach ( $seed as $record ) { $id = $record->id(); if ( $id === null ) { throw new InvalidArgumentException( 'InMemoryProductUrlRepository seed records must carry an id.' ); } if ( array_key_exists( $id, $this->records ) ) { throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository seed contains duplicate id %d.', $id ) ); } $key = self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() ); if ( array_key_exists( $key, $this->productProviderUrlIndex ) ) { throw new InvalidArgumentException( 'InMemoryProductUrlRepository seed contains duplicate product/provider/url.' ); } $this->records[ $id ] = $record; $this->productProviderUrlIndex[ $key ] = $id; if ( $id >= $this->nextId ) { $this->nextId = $id + 1; } } } /** * Persist a product URL record. * * @param ProductUrlRecord $record URL record. */ public function save( ProductUrlRecord $record ): ProductUrlRecord { $id = $record->id(); return $id === null ? $this->insert( $record ) : $this->update( $id, $record ); } /** * Insert a new URL record. * * @param ProductUrlRecord $record Unsaved URL record. */ private function insert( ProductUrlRecord $record ): ProductUrlRecord { $key = self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() ); if ( array_key_exists( $key, $this->productProviderUrlIndex ) ) { throw new RuntimeException( 'InMemoryProductUrlRepository: duplicate product/provider/url on insert.' ); } $id = $this->nextId++; $persisted = $record->withId( $id ); $this->records[ $id ] = $persisted; $this->productProviderUrlIndex[ $key ] = $id; return $persisted; } /** * Update an existing URL record. * * @param int $id Database row id. * @param ProductUrlRecord $record URL record. */ private function update( int $id, ProductUrlRecord $record ): ProductUrlRecord { if ( ! array_key_exists( $id, $this->records ) ) { throw new RuntimeException( sprintf( 'InMemoryProductUrlRepository: cannot update missing id %d.', $id ) ); } $old = $this->records[ $id ]; $oldKey = self::productProviderUrlKey( $old->productId(), $old->providerType(), $old->urlHash() ); $newKey = self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() ); if ( $oldKey !== $newKey && array_key_exists( $newKey, $this->productProviderUrlIndex ) ) { throw new RuntimeException( 'InMemoryProductUrlRepository: update would collide with an existing product/provider/url.' ); } if ( $oldKey !== $newKey ) { unset( $this->productProviderUrlIndex[ $oldKey ] ); $this->productProviderUrlIndex[ $newKey ] = $id; } $this->records[ $id ] = $record; return $record; } /** * Find a URL record by database id. * * @param int $id Database row id. */ public function findById( int $id ): ?ProductUrlRecord { if ( $id <= 0 ) { throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::findById() requires a positive id, got %d.', $id ) ); } return $this->records[ $id ] ?? null; } /** * Find URL records attached to a product. * * @param int $productId Product id. * * @return list */ public function findByProductId( int $productId ): array { if ( $productId <= 0 ) { throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::findByProductId() requires a positive productId, got %d.', $productId ) ); } $result = array(); foreach ( $this->records as $record ) { if ( $record->productId() === $productId ) { $result[] = $record; } } usort( $result, static fn ( ProductUrlRecord $a, ProductUrlRecord $b ): int => ( $a->id() ?? 0 ) <=> ( $b->id() ?? 0 ) ); return $result; } /** * Find a product URL row by product, provider, and URL hash. * * @param int $productId Product id. * @param string $providerType Provider namespace. * @param string $urlHash Normalized URL hash. */ public function findByProductProviderUrl( int $productId, string $providerType, string $urlHash ): ?ProductUrlRecord { if ( $productId <= 0 ) { throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::findByProductProviderUrl() requires a positive productId, got %d.', $productId ) ); } $key = self::productProviderUrlKey( $productId, $providerType, $urlHash ); $id = $this->productProviderUrlIndex[ $key ] ?? null; return $id === null ? null : ( $this->records[ $id ] ?? null ); } /** * Find a URL row by provider-side item id. * * @param string $providerType Provider namespace. * @param string $providerItemId Provider-side item identifier. */ public function findByProviderItem( string $providerType, string $providerItemId ): ?ProductUrlRecord { if ( trim( $providerType ) === '' || trim( $providerItemId ) === '' ) { throw new InvalidArgumentException( 'InMemoryProductUrlRepository::findByProviderItem() requires non-empty providerType and providerItemId.' ); } foreach ( $this->records as $record ) { if ( $record->providerType() === $providerType && $record->providerItemId() === $providerItemId ) { return $record; } } return null; } /** * Delete a URL row by database id. * * @param int $id Database row id. */ public function delete( int $id ): bool { if ( $id <= 0 ) { throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::delete() requires a positive id, got %d.', $id ) ); } if ( ! array_key_exists( $id, $this->records ) ) { return false; } $record = $this->records[ $id ]; unset( $this->records[ $id ] ); unset( $this->productProviderUrlIndex[ self::productProviderUrlKey( $record->productId(), $record->providerType(), $record->urlHash() ) ] ); return true; } /** * Delete URL rows for a product/provider pair. * * @param int $productId Product id. * @param string $providerType Provider namespace. */ public function deleteByProductAndProvider( int $productId, string $providerType ): int { if ( $productId <= 0 ) { throw new InvalidArgumentException( sprintf( 'InMemoryProductUrlRepository::deleteByProductAndProvider() requires a positive productId, got %d.', $productId ) ); } if ( trim( $providerType ) === '' ) { throw new InvalidArgumentException( 'InMemoryProductUrlRepository::deleteByProductAndProvider() requires a non-empty providerType.' ); } $count = 0; foreach ( array_keys( $this->records ) as $id ) { $record = $this->records[ $id ]; if ( $record->productId() === $productId && $record->providerType() === $providerType ) { $this->delete( $id ); ++$count; } } return $count; } /** * Count stored URL rows. */ public function count(): int { return count( $this->records ); } /** * Build the in-memory unique-key index value. * * @param int $productId Product id. * @param string $providerType Provider namespace. * @param string $urlHash Normalized URL hash. */ private static function productProviderUrlKey( int $productId, string $providerType, string $urlHash ): string { return $productId . "\0" . $providerType . "\0" . $urlHash; } }