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/README.md b/README.md
index e5aee53..a6dfd25 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,27 @@
+## 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:
+
[](https://codecov.io/gh/scriptotek/php-marc)
[](https://styleci.io/repos/41363199)
[](https://codeclimate.com/github/scriptotek/php-marc)
@@ -84,6 +108,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
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
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..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
*************************************************************************/
@@ -219,7 +267,7 @@ public function getId()
*
* @return array
*/
- public function jsonSerialize(): array|string
+ public function jsonSerialize(): mixed
{
$o = [];
foreach ($this->properties as $prop) {
@@ -255,7 +303,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 +313,7 @@ public function __call($name, $args)
*
* @return string
*/
- public function __toString()
+ public function __toString(): string
{
return strval($this->record);
}
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);
+ }
}