diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Record.php b/module/VuFind/src/VuFind/View/Helper/Root/Record.php index cfe7e95add2..95cb7b411c2 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/Record.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/Record.php @@ -560,10 +560,11 @@ public function getSearchResult($view) */ public function getCheckbox($idPrefix = '', $formAttr = false, $number = null) { - $id = $this->driver->getSourceIdentifier() . '|' - . $this->driver->getUniqueId(); - $context - = ['id' => $id, 'number' => $number, 'prefix' => $idPrefix]; + $context = compact('number') + [ + 'id' => $this->getUniqueIdWithSourcePrefix(), + 'checkboxElementId' => $this->getUniqueHtmlElementId($idPrefix), + 'prefix' => $idPrefix, + ]; if ($formAttr) { $context['formAttr'] = $formAttr; } @@ -857,4 +858,38 @@ protected function deduplicateLinks($links) { return array_values(array_unique($links, SORT_REGULAR)); } + + /** + * Get the source identifier + unique id of the record without spaces + * + * @param string $idPrefix Prefix for HTML ids + * + * @return string + */ + public function getUniqueHtmlElementId($idPrefix = '') + { + $resultSetId = $this->driver->getResultSetIdentifier() ?? ''; + + return preg_replace( + "/\s+/", + '_', + ($idPrefix ? $idPrefix . '-' : '') + . ($resultSetId ? $resultSetId . '-' : '') + . $this->driver->getUniqueId() + ); + } + + /** + * Get the source identifier + unique id of the record + * + * @return string + */ + public function getUniqueIdWithSourcePrefix() + { + if ($this->driver) { + return "{$this->driver->getSourceIdentifier()}" + . "|{$this->driver->getUniqueId()}"; + } + throw new \Exception('No record driver found.'); + } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php index c7f7b0c57c7..5ea2b8d5bf9 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php @@ -389,30 +389,135 @@ public function testGetLink( */ public function testGetCheckbox(): void { + $driver = $this->loadRecordFixture('testbug1.json'); + $tpl = 'record/checkbox.phtml'; $context = $this->getMockContext(); + $randomIdentifier = 'baz'; + $driver->setResultSetIdentifier($randomIdentifier); + + $expectedCalls = [ + [ + $tpl, + [ + 'number' => 1, + 'id' => 'Solr|000105196', + 'checkboxElementId' => "bar-{$randomIdentifier}-000105196", + 'prefix' => 'bar', + 'formAttr' => 'foo', + ], + ], + [ + $tpl, + [ + 'number' => 2, + 'id' => 'Solr|000105196', + 'checkboxElementId' => "bar-{$randomIdentifier}-000105196", + 'prefix' => 'bar', + 'formAttr' => 'foo', + ], + ], + ]; + $this->expectConsecutiveCalls( $context, 'renderInContext', + $expectedCalls, + ['success', 'success'] + ); + + $record = $this->getRecord($driver, [], $context); + + // We run the test twice to ensure that checkbox incrementing works properly: + $this->assertEquals('success', $record->getCheckbox('bar', 'foo', 1)); + $this->assertEquals('success', $record->getCheckbox('bar', 'foo', 2)); + } + + /** + * Test getCheckboxWithoutIdAndWithoutPrefix. + * + * @return void + */ + public function testGetCheckboxWithoutIdAndWithEmptyPrefix(): void + { + $driver = $this->loadRecordFixture('testbug1.json'); + $tpl = 'record/checkbox.phtml'; + $context = $this->getMockContext(); + + $expectedCalls = [ [ + $tpl, [ - 'record/checkbox.phtml', - ['id' => 'Solr|000105196', 'number' => 1, 'prefix' => 'bar', 'formAttr' => 'foo'], + 'number' => 1, + 'id' => 'Solr|000105196', + 'checkboxElementId' => '000105196', + 'prefix' => '', + 'formAttr' => 'foo', ], + ], + [ + $tpl, [ - 'record/checkbox.phtml', - ['id' => 'Solr|000105196', 'number' => 2, 'prefix' => 'bar', 'formAttr' => 'foo'], + 'number' => 2, + 'id' => 'Solr|000105196', + 'checkboxElementId' => '000105196', + 'prefix' => '', + 'formAttr' => 'foo', ], ], - 'success' - ); - $record = $this->getRecord( - $this->loadRecordFixture('testbug1.json'), - [], - $context + ]; + + $record = $this->getRecord($driver, [], $context); + + $this->expectConsecutiveCalls( + $context, + 'renderInContext', + $expectedCalls, + ['success', 'success'] ); + + $record = $this->getRecord($driver, [], $context); + // We run the test twice to ensure that checkbox incrementing works properly: - $this->assertEquals('success', $record->getCheckbox('bar', 'foo', 1)); - $this->assertEquals('success', $record->getCheckbox('bar', 'foo', 2)); + $this->assertEquals('success', $record->getCheckbox(formAttr: 'foo', number: 1)); + $this->assertEquals('success', $record->getCheckbox('', 'foo', 2)); + } + + /** + * Test getUniqueHtmlElementId. + * + * @return void + */ + public function testGetUniqueHtmlElementId() + { + $driver = $this->loadRecordFixture('testbug1.json'); + $record = $this->getRecord($driver); + $contextPrefix = 'foo'; + $randomIdentifier = 'bar'; + + // no result set identifier and no prefix + $this->assertEquals( + '000105196', + $record->getUniqueHtmlElementId() + ); + + // no result set identifier but with prefix + $this->assertEquals( + "{$contextPrefix}-000105196", + $record->getUniqueHtmlElementId($contextPrefix) + ); + + // with result set identifier but no prefix + $driver->setResultSetIdentifier($randomIdentifier); + $this->assertEquals( + "{$randomIdentifier}-000105196", + $record->getUniqueHtmlElementId() + ); + + // with result set identifier and with prefix + $this->assertEquals( + "{$contextPrefix}-{$randomIdentifier}-000105196", + $record->getUniqueHtmlElementId($contextPrefix) + ); } /** diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php b/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php index c039edb99a6..bee16ac7f84 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php @@ -30,9 +30,13 @@ namespace VuFindSearch\Backend; use Laminas\Log\LoggerAwareInterface; +use Laminas\Math\Rand; use VuFindSearch\Response\RecordCollectionFactoryInterface; use VuFindSearch\Response\RecordCollectionInterface; +use function count; +use function sprintf; + /** * Abstract backend. * @@ -116,6 +120,32 @@ abstract public function getRecordCollectionFactory(); protected function injectSourceIdentifier(RecordCollectionInterface $response) { $response->setSourceIdentifiers($this->identifier); + + if (count($response->getRecords()) > 0) { + // TODO: Replace custom UUID generation with Doctrine + // UUID generator once available (after the merge of #2233) + $response->setResultSetIdentifier($this->generateUuid()); + } + return $response; } + + /** + * Generates a shorter UUID-like identifier. + * + * This method uses Laminas\Math\Rand to generate cryptographically secure random bytes + * and formats them into a shorter identifier. + * + * @return string A randomly generated shorter UUID-like identifier. + */ + public function generateUuid(): string + { + $data = bin2hex(Rand::getBytes(8)); + return sprintf( + '%08s-%04s-%04s', + substr($data, 0, 8), + substr($data, 8, 4), + substr($data, 12, 4) + ); + } } diff --git a/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php index 0c7541bcc98..fa73c29cb0d 100644 --- a/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php +++ b/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php @@ -189,6 +189,23 @@ public function getSourceIdentifier() return $this->source; } + /** + * Sets the result set identifier for all records in the collection. + * + * This method assigns a given UUID to each record in the collection by calling + * the `setResultSetIdentifier` method on each record. + * + * @param string $uuid A valid UUID to be assigned to each record in the collection. + * + * @return void + */ + public function setResultSetIdentifier(string $uuid) + { + foreach ($this->records as $record) { + $record->setResultSetIdentifier($uuid); + } + } + /** * Add a record to the collection. * diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php index 9866db7becd..6c883262e20 100644 --- a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php +++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php @@ -117,6 +117,17 @@ public function setSourceIdentifiers($identifier, $searchBackendId = ''); */ public function getSourceIdentifier(); + /** + * Sets the result set identifier for the record. + * + * This method assigns a UUID or a unique string identifier to the result set. + * + * @param string $uuid A valid UUID or unique identifier to be assigned to the result set. + * + * @return void + */ + public function setResultSetIdentifier(string $uuid); + /** * Add a record to the collection. * diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php index e909f57f936..e33c4db596f 100644 --- a/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php +++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php @@ -78,6 +78,25 @@ public function getSourceIdentifier(); */ public function getSearchBackendIdentifier(); + /** + * Sets the result set identifier for the record collection. + * + * @param string $uuid A valid UUID associated with the data set. + * + * @return void + */ + public function setResultSetIdentifier(string $uuid); + + /** + * Retrieves the unique result set identifier. + * + * This method returns the UUID or similar identifier associated with the result set. + * If no identifier has been set, it will return null. + * + * @return string|null The UUID of the result set, or null if not set. + */ + public function getResultSetIdentifier(); + /** * Add a label for the record * diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordTrait.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordTrait.php index 5b6b52c25ac..c4ad4110cb6 100644 --- a/module/VuFindSearch/src/VuFindSearch/Response/RecordTrait.php +++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordTrait.php @@ -47,6 +47,16 @@ trait RecordTrait */ protected $sourceIdentifier = ''; + /** + * The unique identifier for the result set. + * + * This property stores a UUID or similar identifier that uniquely identifies + * the result set. It is typically set by calling the `setResultSetIdentifier` method. + * + * @var string|null + */ + protected $resultSetIdentifier = null; + /** * Used for identifying the search backend used to find the record * @@ -110,6 +120,33 @@ public function getSearchBackendIdentifier() return $this->searchBackendIdentifier; } + /** + * Sets the unique result set identifier. + * + * This method assigns a UUID or similar identifier to the result set. + * + * @param string $uuid A valid UUID or identifier to assign to the result set. + * + * @return void + */ + public function setResultSetIdentifier(string $uuid) + { + $this->resultSetIdentifier = $uuid; + } + + /** + * Retrieves the unique result set identifier. + * + * This method returns the UUID or similar identifier associated with the result set. + * If no identifier has been set, it will return null. + * + * @return string|null The UUID of the result set, or null if not set. + */ + public function getResultSetIdentifier(): ?string + { + return $this->resultSetIdentifier; + } + /** * Add a label for the record *