From eba6b96460b72816124640210c61e67987b97c58 Mon Sep 17 00:00:00 2001 From: redcuillin Date: Tue, 14 Apr 2026 23:06:14 -0400 Subject: [PATCH 1/7] Add section for php8.4 support fork --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e5aee53..226c069 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![Latest Stable Version](https://img.shields.io/packagist/v/scriptotek/marc)](https://packagist.org/packages/scriptotek/marc) [![Total Downloads](https://img.shields.io/packagist/dt/scriptotek/marc)](https://packagist.org/packages/scriptotek/marc) +# Fork by redcuillin for php8.4 support + # scriptotek/marc This package provides a simple interface to work with MARC21 records using the excellent From 3b8d0078e6f54e8f1206c1efb7336a84a3a9e53c Mon Sep 17 00:00:00 2001 From: Roland Tanner Date: Tue, 14 Apr 2026 23:19:04 -0400 Subject: [PATCH 2/7] Enhance type hinting and return types across multiple classes for improved PHP 8.4 compatibility. Update .gitignore to include .phpunit.result.cache. --- .gitignore | 1 + src/BibliographicRecord.php | 26 ++++++++++++++++++-------- src/Collection.php | 4 ++-- src/Fields/Field.php | 8 ++++---- src/Fields/SerializableField.php | 2 +- src/Fields/Subfield.php | 6 +++--- src/Importers/Importer.php | 2 +- src/Importers/XmlImporter.php | 18 +++++++++--------- src/MagicAccess.php | 4 +++- src/QueryResult.php | 10 +++++----- src/Record.php | 6 +++--- 11 files changed, 50 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 987e2a2..e96516b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ composer.lock vendor +.phpunit.result.cache diff --git a/src/BibliographicRecord.php b/src/BibliographicRecord.php index a31bf9e..ebe28b6 100644 --- a/src/BibliographicRecord.php +++ b/src/BibliographicRecord.php @@ -18,8 +18,18 @@ class BibliographicRecord extends Record * @var string[] List of properties to be included when serializing the record using the `toArray()` method. */ public array $properties = [ - 'id', 'isbns', 'title', 'publisher', 'pub_year', 'edition', 'creators', - 'subjects', 'classifications', 'toc', 'summary', 'part_of' + 'id', + 'isbns', + 'title', + 'publisher', + 'pub_year', + 'edition', + 'creators', + 'subjects', + 'classifications', + 'toc', + 'summary', + 'part_of' ]; /** @@ -114,7 +124,7 @@ public function getToc(): ?array } else { // Basic return $field->mapSubFields([ - 'a' => 'text', + 'a' => 'text', ]); } } @@ -131,8 +141,8 @@ public function getSummary(): array|null $field = $this->getField('520'); if ($field) { return $field->mapSubFields([ - 'a' => 'text', - 'c' => 'assigning_source', + 'a' => 'text', + 'c' => 'assigning_source', ]); } return null; @@ -146,7 +156,7 @@ public function getSummary(): array|null * @param string|string[]|null $tag * @return SubjectInterface[] */ - public function getSubjects(string $vocabulary = null, array|string $tag = null): array + public function getSubjects(?string $vocabulary = null, array|string|null $tag = null): array { $tag = is_null($tag) ? [] : (is_array($tag) ? $tag : [$tag]); @@ -165,7 +175,7 @@ public function getSubjects(string $vocabulary = null, array|string $tag = null) * @param string|null $scheme * @return Classification[] */ - public function getClassifications(string $scheme = null): array + public function getClassifications(?string $scheme = null): array { return array_values(array_filter(Classification::get($this), function ($classifications) use ($scheme) { $a = is_null($scheme) || $scheme == $classifications->getScheme(); @@ -181,7 +191,7 @@ public function getClassifications(string $scheme = null): array * @param string|string[]|null $tag * @return Person[] */ - public function getCreators(array|string $tag = null): array + public function getCreators(array|string|null $tag = null): array { $tag = is_null($tag) ? [] : (is_array($tag) ? $tag : [$tag]); diff --git a/src/Collection.php b/src/Collection.php index b1b4e58..122b1ed 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -24,7 +24,7 @@ class Collection implements \Iterator * * @param File_MARCXML|File_MARC|null $parser */ - public function __construct(File_MARCXML|File_MARC $parser = null) + public function __construct(File_MARCXML|File_MARC|null $parser = null) { $this->parser = $parser; } @@ -206,7 +206,7 @@ public function rewind(): void * Magic *********************************************************/ - public function __call($name, $arguments) + public function __call(string $name, array $arguments): mixed { return call_user_func_array([$this->parser, $name], $arguments); } diff --git a/src/Fields/Field.php b/src/Fields/Field.php index d92d54d..d466066 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -115,7 +115,7 @@ public function getField(): File_MARC_Field * * @return mixed */ - public function __call(string $name, array $args) + public function __call(string $name, array $args): mixed { return call_user_func_array([$this->field, $name], $args); } @@ -140,7 +140,7 @@ public function __toString(): string * * @return string */ - protected function clean(string $value = null, array $options = []): string + protected function clean(?string $value = null, array $options = []): string { if (is_null($value)) { return ""; @@ -223,7 +223,7 @@ public function asLineMarc(string $sep = '$', string $blank = ' '): ?string $ind2 = $blank; } - return "${tag} ${ind1}${ind2} " . implode(' ', $subfields); + return "{$tag} {$ind1}{$ind2} " . implode(' ', $subfields); } /** @@ -235,7 +235,7 @@ public function asLineMarc(string $sep = '$', string $blank = ' '): ?string * The fallback value to return if the subfield does not exist. * @return string|null */ - public function sf(string $code, string $default = null): ?string + public function sf(string $code, ?string $default = null): ?string { // In PHP, ("a" == 0) will evaluate to TRUE, so it's actually very important that we ensure type here! $code = (string) $code; diff --git a/src/Fields/SerializableField.php b/src/Fields/SerializableField.php index 529985e..0324a03 100644 --- a/src/Fields/SerializableField.php +++ b/src/Fields/SerializableField.php @@ -4,7 +4,7 @@ trait SerializableField { - public function jsonSerialize(): array|string + public function jsonSerialize(): mixed { if (count($this->properties)) { $o = []; diff --git a/src/Fields/Subfield.php b/src/Fields/Subfield.php index 94fffb6..1854822 100644 --- a/src/Fields/Subfield.php +++ b/src/Fields/Subfield.php @@ -28,7 +28,7 @@ public function delete() $this->__destruct(); } - public function jsonSerialize(): string|array + public function jsonSerialize(): mixed { return (string) $this; } @@ -38,12 +38,12 @@ public function __toString(): string return $this->subfield->getData(); } - public function __call($name, $args) + public function __call(string $name, array $args): mixed { return call_user_func_array([$this->subfield, $name], $args); } - public function __get($key) + public function __get(string $key): mixed { $method = 'get' . ucfirst($key); if (method_exists($this, $method)) { diff --git a/src/Importers/Importer.php b/src/Importers/Importer.php index 67abbff..fa85c7d 100644 --- a/src/Importers/Importer.php +++ b/src/Importers/Importer.php @@ -11,7 +11,7 @@ class Importer { private Factory $factory; - public function __construct(Factory $factory = null) + public function __construct(?Factory $factory = null) { $this->factory = $factory ?? new Factory(); } diff --git a/src/Importers/XmlImporter.php b/src/Importers/XmlImporter.php index ded3c40..e641591 100644 --- a/src/Importers/XmlImporter.php +++ b/src/Importers/XmlImporter.php @@ -11,10 +11,9 @@ class XmlImporter { - protected $factory; + protected Factory $factory; - /* var SimpleXMLElement */ - protected $source; + protected SimpleXMLElement $source; /** * XmlImporter constructor. @@ -22,11 +21,11 @@ class XmlImporter * @param string|SimpleXMLElement $data Filename, XML string or SimpleXMLElement object * @param string $ns URI or prefix of the namespace * @param bool $isPrefix TRUE if $ns is a prefix, FALSE if it's a URI; defaults to FALSE - * @param string $factory (optional) Object factory, probably no need to set this outside testing. + * @param Factory|null $factory (optional) Object factory, probably no need to set this outside testing. */ - public function __construct($data, $ns = '', $isPrefix = false, $factory = null) + public function __construct($data, string $ns = '', bool $isPrefix = false, ?Factory $factory = null) { - $this->factory = isset($factory) ? $factory : new Factory(); + $this->factory = $factory ?? new Factory(); if (is_a($data, SimpleXMLElement::class)) { $this->source = $data; @@ -40,10 +39,11 @@ public function __construct($data, $ns = '', $isPrefix = false, $factory = null) // Store errors internally so that we can fetch them with libxml_get_errors() later libxml_use_internal_errors(true); - $this->source = simplexml_load_string($data, 'SimpleXMLElement', 0, $ns, $isPrefix); - if (false === $this->source) { + $source = simplexml_load_string($data, 'SimpleXMLElement', 0, $ns, $isPrefix); + if (false === $source) { throw new XmlException(libxml_get_errors()); } + $this->source = $source; } public function getMarcNamespace($namespaces) @@ -98,7 +98,7 @@ public function getFirstRecord() $parser = $this->factory->make('File_MARCXML', $record, File_MARCXML::SOURCE_SIMPLEXMLELEMENT, $ns); - return (new Collection($parser))->$this->getFirstRecord(); + return (new Collection($parser))->first(); } public function getCollection(): Collection diff --git a/src/MagicAccess.php b/src/MagicAccess.php index 16a08e1..4047177 100644 --- a/src/MagicAccess.php +++ b/src/MagicAccess.php @@ -9,7 +9,7 @@ */ trait MagicAccess { - public function __get($key) + public function __get(string $key): mixed { // Convert key from underscore_case to camelCase. $key_uc = preg_replace_callback( @@ -24,5 +24,7 @@ function ($matches) { if (method_exists($this, $method)) { return call_user_func([$this, $method]); } + + return null; } } diff --git a/src/QueryResult.php b/src/QueryResult.php index b6ae713..b73cffd 100644 --- a/src/QueryResult.php +++ b/src/QueryResult.php @@ -29,7 +29,7 @@ public function __construct(File_MARC_Reference $ref) $this->data = $ref->data; $this->content = $ref->content; - for ($i=0; $i < count($this->data); $i++) { + for ($i = 0; $i < count($this->data); $i++) { if (is_a($this->data[$i], File_MARC_Field::class)) { $this->data[$i] = new Field($this->data[$i]); } @@ -77,7 +77,7 @@ public function getIterator(): Traversable|ArrayIterator * @param mixed $offset An offset to check for. * @return boolean true on success or false on failure. */ - public function offsetExists($offset): bool + public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); } @@ -88,7 +88,7 @@ public function offsetExists($offset): bool * @param mixed $offset The offset to retrieve. * @return Field|File_MARC_Subfield|null */ - public function offsetGet($offset): Field|File_MARC_Subfield|null + public function offsetGet(mixed $offset): Field|File_MARC_Subfield|null { return $this->data[$offset]; } @@ -99,7 +99,7 @@ public function offsetGet($offset): Field|File_MARC_Subfield|null * @param mixed $offset The offset to assign the value to. * @param mixed $value The value to set. */ - public function offsetSet($offset, $value): void + public function offsetSet(mixed $offset, mixed $value): void { $this->data[$offset] = $value; } @@ -109,7 +109,7 @@ public function offsetSet($offset, $value): void * @link http://php.net/manual/en/arrayaccess.offsetunset.php * @param mixed $offset The offset to unset. */ - public function offsetUnset($offset): void + public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); } diff --git a/src/Record.php b/src/Record.php index f324c28..4840512 100644 --- a/src/Record.php +++ b/src/Record.php @@ -219,7 +219,7 @@ public function getId() * * @return array */ - public function jsonSerialize(): array|string + public function jsonSerialize(): mixed { $o = []; foreach ($this->properties as $prop) { @@ -255,7 +255,7 @@ public function jsonSerialize(): array|string * * @return mixed */ - public function __call($name, $args) + public function __call(string $name, array $args): mixed { return call_user_func_array([$this->record, $name], $args); } @@ -265,7 +265,7 @@ public function __call($name, $args) * * @return string */ - public function __toString() + public function __toString(): string { return strval($this->record); } From a79f4fa6121b2a3cf0cc211a65521f4402cb6b7e Mon Sep 17 00:00:00 2001 From: Roland Tanner Date: Tue, 14 Apr 2026 23:19:31 -0400 Subject: [PATCH 3/7] Update composer.json to require PHP 8.4 and format keywords for consistency. --- composer.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 7b19b3d..3464151 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,9 @@ "name": "scriptotek/marc", "type": "library", "description": "Simple interface to parsing MARC records using File_MARC", - "keywords": ["marc"], + "keywords": [ + "marc" + ], "license": "MIT", "authors": [ { @@ -11,7 +13,7 @@ } ], "require": { - "php": ">=8.0", + "php": ">=8.4", "ext-xml": "*", "ext-json": "*", "ext-simplexml": "*", @@ -31,4 +33,4 @@ "scripts": { "test": "phpunit" } -} +} \ No newline at end of file From 32a6ce90aa8bf860381dcabaab4078deea41b715 Mon Sep 17 00:00:00 2001 From: Roland Tanner Date: Fri, 17 Apr 2026 15:51:19 -0400 Subject: [PATCH 4/7] Add sorting functionality to Collection class based on control field values. Implemented `sorter` method to order records by a specified control field tag, defaulting to '001'. Added tests to verify sorting behavior and handle cases with missing control fields. Included sample XML data for testing. --- src/Collection.php | 59 ++++++++++++++++++++++++++++++- tests/CollectionTest.php | 36 +++++++++++++++++++ tests/data/sort-three-records.xml | 18 ++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/data/sort-three-records.xml diff --git a/src/Collection.php b/src/Collection.php index 122b1ed..744126e 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -3,6 +3,7 @@ namespace Scriptotek\Marc; use File_MARC; +use File_MARC_Control_Field; use File_MARC_Record; use File_MARCXML; use Scriptotek\Marc\Exceptions\RecordNotFound; @@ -110,13 +111,69 @@ public static function getRecordType(File_MARC_Record $record): string /** * Returns an array representation of the collection. * - * @return Collection[] + * @return Record[] */ public function toArray(): array { return iterator_to_array($this); } + /** + * Sort records by the numeric value of a control field (MARC tag), default 001. + * + * Loads the entire collection into memory if it is not already cached, then + * reorders records by comparing integer keys parsed from the control field + * value (leading digits when the value is not purely numeric). Records with + * no such field, or no parseable digits, sort after those with a key. + * + * @param string $controlFieldTag Three-digit control field tag, e.g. "001". + * @return $this + */ + public function sorter(string $controlFieldTag = '001'): self + { + $records = iterator_to_array($this, false); + usort( + $records, + function (Record $a, Record $b) use ($controlFieldTag): int { + return self::numericKeyFromControlField($a, $controlFieldTag) + <=> self::numericKeyFromControlField($b, $controlFieldTag); + } + ); + $this->_records = $records; + $this->useCache = true; + $this->parser = null; + $this->rewind(); + + return $this; + } + + /** + * Integer sort key from a control field for use with {@see self::sorter()}. + */ + private static function numericKeyFromControlField(Record $record, string $tag): int + { + $wrapper = $record->getField($tag); + if ($wrapper === null) { + return PHP_INT_MAX; + } + $marc = $wrapper->getField(); + if (!$marc instanceof File_MARC_Control_Field) { + return PHP_INT_MAX; + } + $raw = trim($marc->getData()); + if ($raw === '') { + return PHP_INT_MAX; + } + if (ctype_digit($raw)) { + return (int) $raw; + } + if (preg_match('/-?\d+/', $raw, $m)) { + return (int) $m[0]; + } + + return PHP_INT_MAX; + } + /** * Return the first record in the collection. * diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index 7fbd44b..fc3d615 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -92,4 +92,40 @@ public function testInitializeFromSimpleXmlElement($filename, $expected) $this->assertCount($expected, $collection->toArray()); } + + public function testSorterOrdersRecordsByControlField001() + { + $collection = Collection::fromFile(self::pathTo('sort-three-records.xml')); + $collection->sorter(); + + $ids = array_map(function (BibliographicRecord $r) { + return (string) $r->getId(); + }, $collection->toArray()); + + $this->assertSame(['100', '200', '300'], $ids); + } + + public function testSorterPutsRecordsWithoutFieldLast() + { + $xml = '' + . '' + . '00778cam a22002531u 4500' + . '110607s1964 xx#|||||| |000|u|eng|d' + . '00778cam a22002531u 4500' + . '50' + . '110607s1964 xx#|||||| |000|u|eng|d' + . ''; + $collection = Collection::fromString($xml)->sorter(); + + $ids = array_map(function (BibliographicRecord $r) { + $f = $r->getRecord()->getField('001'); + if ($f === false || !$f->isControlField()) { + return ''; + } + + return trim($f->getData()); + }, $collection->toArray()); + + $this->assertSame(['50', ''], $ids); + } } diff --git a/tests/data/sort-three-records.xml b/tests/data/sort-three-records.xml new file mode 100644 index 0000000..9b83a6b --- /dev/null +++ b/tests/data/sort-three-records.xml @@ -0,0 +1,18 @@ + + + + 00778cam a22002531u 4500 + 300 + 110607s1964 xx#|||||| |000|u|eng|d + + + 00778cam a22002531u 4500 + 100 + 110607s1964 xx#|||||| |000|u|eng|d + + + 00778cam a22002531u 4500 + 200 + 110607s1964 xx#|||||| |000|u|eng|d + + From 226c750adcc649099fea49f4dddaf8ae9cc3173a Mon Sep 17 00:00:00 2001 From: Roland Tanner Date: Fri, 17 Apr 2026 16:06:38 -0400 Subject: [PATCH 5/7] Refactor Collection and Record classes by removing the sorter method and adding sortFieldsByTag method. The new method in Record class sorts fields by tag, ensuring LDR fields are prioritized. Updated tests to validate new sorting functionality and removed obsolete test cases related to the sorter method. --- src/Collection.php | 59 +-------------------- src/Record.php | 48 +++++++++++++++++ tests/CollectionTest.php | 36 ------------- tests/RecordTest.php | 88 +++++++++++++++++++++++++++++++ tests/data/sort-three-records.xml | 18 ------- 5 files changed, 137 insertions(+), 112 deletions(-) delete mode 100644 tests/data/sort-three-records.xml diff --git a/src/Collection.php b/src/Collection.php index 744126e..122b1ed 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -3,7 +3,6 @@ namespace Scriptotek\Marc; use File_MARC; -use File_MARC_Control_Field; use File_MARC_Record; use File_MARCXML; use Scriptotek\Marc\Exceptions\RecordNotFound; @@ -111,69 +110,13 @@ public static function getRecordType(File_MARC_Record $record): string /** * Returns an array representation of the collection. * - * @return Record[] + * @return Collection[] */ public function toArray(): array { return iterator_to_array($this); } - /** - * Sort records by the numeric value of a control field (MARC tag), default 001. - * - * Loads the entire collection into memory if it is not already cached, then - * reorders records by comparing integer keys parsed from the control field - * value (leading digits when the value is not purely numeric). Records with - * no such field, or no parseable digits, sort after those with a key. - * - * @param string $controlFieldTag Three-digit control field tag, e.g. "001". - * @return $this - */ - public function sorter(string $controlFieldTag = '001'): self - { - $records = iterator_to_array($this, false); - usort( - $records, - function (Record $a, Record $b) use ($controlFieldTag): int { - return self::numericKeyFromControlField($a, $controlFieldTag) - <=> self::numericKeyFromControlField($b, $controlFieldTag); - } - ); - $this->_records = $records; - $this->useCache = true; - $this->parser = null; - $this->rewind(); - - return $this; - } - - /** - * Integer sort key from a control field for use with {@see self::sorter()}. - */ - private static function numericKeyFromControlField(Record $record, string $tag): int - { - $wrapper = $record->getField($tag); - if ($wrapper === null) { - return PHP_INT_MAX; - } - $marc = $wrapper->getField(); - if (!$marc instanceof File_MARC_Control_Field) { - return PHP_INT_MAX; - } - $raw = trim($marc->getData()); - if ($raw === '') { - return PHP_INT_MAX; - } - if (ctype_digit($raw)) { - return (int) $raw; - } - if (preg_match('/-?\d+/', $raw, $m)) { - return (int) $m[0]; - } - - return PHP_INT_MAX; - } - /** * Return the first record in the collection. * diff --git a/src/Record.php b/src/Record.php index 4840512..73ae6f7 100644 --- a/src/Record.php +++ b/src/Record.php @@ -115,6 +115,54 @@ public function getFields($spec = null, $pcre = null) }, $this->record->getFields($spec, $pcre))); } + /** + * Reorder all fields in this record by tag ascending. + * + * Field tags are either `LDR` (leader, when stored as a field) or three-digit + * strings `001` through `999`. Any `LDR` fields are + * moved to the front; remaining fields are sorted by tag string (numeric order + * for standard digit tags). Duplicate tags keep their relative order. The + * record leader from {@see getLeader()} is separate from the field list and is + * not modified. + * + * @return $this + */ + public function sortFieldsByTag(): self + { + $list = $this->record->getFields(); + $ldr = []; + $rest = []; + $i = 0; + foreach ($list as $field) { + $item = [$i++, $field]; + if (strtoupper($field->getTag()) === 'LDR') { + $ldr[] = $item; + } else { + $rest[] = $item; + } + } + usort( + $rest, + static function (array $a, array $b): int { + $cmp = strcmp($a[1]->getTag(), $b[1]->getTag()); + if ($cmp !== 0) { + return $cmp; + } + + return $a[0] <=> $b[0]; + } + ); + $ordered = array_merge($ldr, $rest); + while ($list->count() > 0) { + $list->shift(); + } + foreach ($ordered as [, $field]) { + $this->record->appendField($field); + } + + return $this; + } + /************************************************************************* * Data loading *************************************************************************/ diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php index fc3d615..7fbd44b 100644 --- a/tests/CollectionTest.php +++ b/tests/CollectionTest.php @@ -92,40 +92,4 @@ public function testInitializeFromSimpleXmlElement($filename, $expected) $this->assertCount($expected, $collection->toArray()); } - - public function testSorterOrdersRecordsByControlField001() - { - $collection = Collection::fromFile(self::pathTo('sort-three-records.xml')); - $collection->sorter(); - - $ids = array_map(function (BibliographicRecord $r) { - return (string) $r->getId(); - }, $collection->toArray()); - - $this->assertSame(['100', '200', '300'], $ids); - } - - public function testSorterPutsRecordsWithoutFieldLast() - { - $xml = '' - . '' - . '00778cam a22002531u 4500' - . '110607s1964 xx#|||||| |000|u|eng|d' - . '00778cam a22002531u 4500' - . '50' - . '110607s1964 xx#|||||| |000|u|eng|d' - . ''; - $collection = Collection::fromString($xml)->sorter(); - - $ids = array_map(function (BibliographicRecord $r) { - $f = $r->getRecord()->getField('001'); - if ($f === false || !$f->isControlField()) { - return ''; - } - - return trim($f->getData()); - }, $collection->toArray()); - - $this->assertSame(['50', ''], $ids); - } } diff --git a/tests/RecordTest.php b/tests/RecordTest.php index 5649756..b409f79 100644 --- a/tests/RecordTest.php +++ b/tests/RecordTest.php @@ -3,6 +3,7 @@ namespace Tests; use File_MARC; +use File_MARC_Control_Field; use File_MARC_Record; use Scriptotek\Marc\AuthorityRecord; use Scriptotek\Marc\BibliographicRecord; @@ -159,4 +160,91 @@ public function testInitializeFromSimpleXmlElement() $this->assertInstanceOf(Record::class, $record); $this->assertInstanceOf(BibliographicRecord::class, $record); } + + public function testSortFieldsByTagOrdersDataFieldsNumericallyByTag() + { + $source = ' + + 00000nam a2200277 a 4500 + trs_sho_id_308 + 260417s2026 can ob 001 0 eng d + + Edinburgh : + + + 9780748616275 + + '; + + $record = Record::fromString($source); + $record->sortFieldsByTag(); + + $tags = []; + foreach ($record->getRecord()->getFields() as $field) { + $tags[] = $field->getTag(); + } + + $this->assertSame(['001', '008', '020', '264'], $tags); + } + + public function testSortFieldsByTagPreservesOrderAmongDuplicateTags() + { + $source = ' + + 00000nam a2200277 a 4500 + id1 + + Topic A + + + Topic B + + '; + + $record = Record::fromString($source); + $record->sortFieldsByTag(); + + $sixFifties = $record->getFields('650'); + $this->assertCount(2, $sixFifties); + $this->assertStringContainsString('Topic A', (string) $sixFifties[0]); + $this->assertStringContainsString('Topic B', (string) $sixFifties[1]); + } + + public function testSortFieldsByTagPutsLdrFieldBeforeNumericTags() + { + $source = ' + + 00000nam a2200277 a 4500 + 9780123456789 + 00000nam a2200277 a 4500 + '; + + $record = Record::fromString($source); + $record->sortFieldsByTag(); + + $tags = []; + foreach ($record->getRecord()->getFields() as $field) { + $tags[] = $field->getTag(); + } + + $this->assertSame(['LDR', '020'], $tags); + } + + public function testSortFieldsByTagRecognizesLdrTagCaseInsensitively() + { + $record = Record::fromString(' + + 00000nam a2200277 a 4500 + id + '); + $record->getRecord()->appendField(new File_MARC_Control_Field('ldr', '00000nam a2200277 a 4500')); + $record->sortFieldsByTag(); + + $tags = []; + foreach ($record->getRecord()->getFields() as $field) { + $tags[] = $field->getTag(); + } + + $this->assertSame(['ldr', '001'], $tags); + } } diff --git a/tests/data/sort-three-records.xml b/tests/data/sort-three-records.xml deleted file mode 100644 index 9b83a6b..0000000 --- a/tests/data/sort-three-records.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - 00778cam a22002531u 4500 - 300 - 110607s1964 xx#|||||| |000|u|eng|d - - - 00778cam a22002531u 4500 - 100 - 110607s1964 xx#|||||| |000|u|eng|d - - - 00778cam a22002531u 4500 - 200 - 110607s1964 xx#|||||| |000|u|eng|d - - From e21beb6ce5e483fa470c69b689d818610a18f32d Mon Sep 17 00:00:00 2001 From: Roland Tanner Date: Tue, 21 Apr 2026 12:41:45 -0400 Subject: [PATCH 6/7] Readme update --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 226c069..33349e7 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,19 @@ Records can be edited using the editing capabilities of File_MARC See [an example](https://github.com/scriptotek/php-marc/issues/13#issuecomment-522036879) to get started. +### Sorting field order (`Record::sortFieldsByTag`) + +To reorder the fields inside a single record by MARC tag (so `020` comes before `264`, +for example), call `sortFieldsByTag()` on the record. Any field whose tag is `LDR` +(case-insensitive) is moved to the front; all other fields are sorted by their +three-digit tag (`001`–`999`). When several fields share the same tag, their +relative order is preserved. The leader string from `getLeader()` is not part of +the field list and is left unchanged. + +```php +$record->sortFieldsByTag(); // returns $record for chaining +``` + ## Querying with MARCspec Use the `Record::query()` method to query a record using the From e56cc07f33eaad88337435efea8842e374cf52f7 Mon Sep 17 00:00:00 2001 From: Roland Tanner Date: Tue, 21 Apr 2026 13:31:08 -0400 Subject: [PATCH 7/7] Readme for fork --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33349e7..a6dfd25 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,33 @@ +## Fork (redcuillin) + +Maintained for PHP 8.4 support. Upstream: [scriptotek/php-marc](https://github.com/scriptotek/php-marc). + +### Using this fork with Composer + +Add the following to your `composer.json` `repositories` and `require` sections (merge with existing entries as needed): + +```json +"repositories": [ + { + "type": "vcs", + "url": "https://github.com/redcuillin/php-marc" + } +], +"require": { + "scriptotek/marc": "dev-main as 4.999.0" +} +``` + +----- + +## Upstream readme: + [![Coverage](https://img.shields.io/codecov/c/github/scriptotek/php-marc)](https://codecov.io/gh/scriptotek/php-marc) [![StyleCI](https://github.styleci.io/repos/41363199/shield?branch=main)](https://styleci.io/repos/41363199) [![Code Climate](https://img.shields.io/codeclimate/maintainability/scriptotek/php-marc)](https://codeclimate.com/github/scriptotek/php-marc) [![Latest Stable Version](https://img.shields.io/packagist/v/scriptotek/marc)](https://packagist.org/packages/scriptotek/marc) [![Total Downloads](https://img.shields.io/packagist/dt/scriptotek/marc)](https://packagist.org/packages/scriptotek/marc) -# Fork by redcuillin for php8.4 support - # scriptotek/marc This package provides a simple interface to work with MARC21 records using the excellent