├── .gitignore ├── tests ├── data │ ├── examples │ │ ├── authority.json │ │ ├── holdings.json │ │ ├── bibliographic.json │ │ ├── bibliographic3.json │ │ ├── holdings.xml │ │ ├── bibliographic2.json │ │ ├── authority.xml │ │ ├── bibliographic4.json │ │ ├── bibliographic.xml │ │ ├── bibliographic2.xml │ │ ├── bibliographic3.xml │ │ └── bibliographic4.xml │ ├── sandburg.mrc │ ├── binary-marc.mrc │ ├── alma-bibs-api-invalid.xml │ ├── sru-loc2.xml │ └── sru-alma.xml ├── PersonFieldTest.php ├── TestCase.php ├── ExamplesTest.php ├── EditionTest.php ├── LocationTest.php ├── ClassificationFieldTest.php ├── QueryResultTest.php ├── CollectionTest.php ├── FieldsTest.php ├── SubjectFieldTest.php ├── RecordTest.php └── TitleFieldTest.php ├── .styleci.yml ├── src ├── AuthorityRecord.php ├── Exceptions │ ├── RecordNotFound.php │ ├── UnknownRecordType.php │ └── XmlException.php ├── Fields │ ├── SubjectInterface.php │ ├── FieldInterface.php │ ├── AuthorityInterface.php │ ├── ControlField.php │ ├── Isbn.php │ ├── Edition.php │ ├── SerializableField.php │ ├── Publisher.php │ ├── SeeAlso.php │ ├── UncontrolledSubject.php │ ├── Subfield.php │ ├── Title.php │ ├── Person.php │ ├── Corporation.php │ ├── Location.php │ ├── Classification.php │ ├── Subject.php │ └── Field.php ├── Marc21.php ├── MagicAccess.php ├── Factory.php ├── Importers │ ├── Importer.php │ └── XmlImporter.php ├── HoldingsRecord.php ├── QueryResult.php ├── BibliographicRecord.php ├── Collection.php └── Record.php ├── .editorconfig ├── ruleset.xml ├── phpunit.xml ├── .github └── workflows │ └── main.yml ├── composer.json ├── LICENSE ├── .overcommit.yml ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /tests/data/examples/authority.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "90081146" 3 | } -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | 3 | enabled: 4 | - concat_with_spaces 5 | -------------------------------------------------------------------------------- /src/AuthorityRecord.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | PSR2 without namespace requirement. 4 | 5 | 6 | */tests/* 7 | 8 | 9 | */tests/* 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Fields/AuthorityInterface.php: -------------------------------------------------------------------------------- 1 | field->getData(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Exceptions/XmlException.php: -------------------------------------------------------------------------------- 1 | message; 13 | }, $errors); 14 | parent::__construct('Failed loading XML: \n' . implode('\n', $details)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Fields/Isbn.php: -------------------------------------------------------------------------------- 1 | sf('a', ''); 12 | } 13 | 14 | /** 15 | * @param Record $record 16 | * @return static[] 17 | */ 18 | public static function get(Record $record): array 19 | { 20 | return static::makeFieldObjects($record, '020'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Fields/Edition.php: -------------------------------------------------------------------------------- 1 | query('250{$a}') as $field) { 12 | return new static($field->getField()); 13 | } 14 | return null; 15 | } 16 | 17 | public function __toString(): string 18 | { 19 | return $this->sf('a'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | 9 | src 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Marc21.php: -------------------------------------------------------------------------------- 1 | properties)) { 10 | $o = []; 11 | foreach ($this->properties as $prop) { 12 | $value = $this->$prop; 13 | if (is_object($value)) { 14 | $o[$prop] = $value->jsonSerialize(); 15 | } elseif ($value) { 16 | $o[$prop] = $value; 17 | } 18 | } 19 | return $o; 20 | } 21 | return (string) $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | php: 11 | - "8.0" 12 | - "8.1" 13 | - "8.2" 14 | - "8.3" 15 | runs-on: ubuntu-latest 16 | name: PHP ${{ matrix.php }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: php-actions/composer@v6 20 | with: 21 | php_version: ${{ matrix.php }} 22 | - uses: php-actions/phpstan@v3 23 | with: 24 | php_version: ${{ matrix.php }} 25 | path: src/ 26 | - run: composer test 27 | - run: bash <(curl -s https://codecov.io/bash) 28 | -------------------------------------------------------------------------------- /tests/PersonFieldTest.php: -------------------------------------------------------------------------------- 1 | getNthrecord('sru-alma.xml', 1); 10 | 11 | # Vocabulary from indicator2 12 | $person = $record->creators[0]; 13 | $this->assertEquals('Gell-Mann, Murray', strval($person)); 14 | } 15 | 16 | public function testWithDatesAndIsbd() 17 | { 18 | $record = $this->getNthrecord('sru-loc2.xml', 1); 19 | 20 | # Vocabulary from indicator2 21 | $person = $record->creators[0]; 22 | $this->assertEquals('Einstein, Albert (1879-1955)', strval($person)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/MagicAccess.php: -------------------------------------------------------------------------------- 1 | query('264{$b}') as $field) { 16 | return new static($field->getField()); 17 | } 18 | foreach ($record->query('260{$b}') as $field) { 19 | return new static($field->getField()); 20 | } 21 | return null; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return $this->sf('b'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs($args); 11 | 12 | return $instance; 13 | } 14 | 15 | public function make(): mixed 16 | { 17 | $args = func_get_args(); 18 | $className = array_shift($args); 19 | 20 | return $this->genMake($className, $args); 21 | } 22 | 23 | public function makeField() 24 | { 25 | $args = func_get_args(); 26 | $className = 'Scriptotek\\Marc\\Fields\\' . array_shift($args); 27 | 28 | return $this->genMake($className, $args); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Fields/SeeAlso.php: -------------------------------------------------------------------------------- 1 | Person::class, 19 | '510' => Corporation::class, 20 | // TODO: Add more classes 21 | '550' => Subject::class, 22 | ]; 23 | 24 | foreach ($record->getFields('5..', true) as $field) { 25 | $tag = $field->getTag(); 26 | if (isset($classMap[$tag])) { 27 | $seeAlsos[] = new $classMap[$tag]($field->getField()); 28 | } 29 | } 30 | 31 | return $seeAlsos; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptotek/marc", 3 | "type": "library", 4 | "description": "Simple interface to parsing MARC records using File_MARC", 5 | "keywords": ["marc"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Dan Michael O. Heggø", 10 | "email": "d.m.heggo@ub.uio.no" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=8.0", 15 | "ext-xml": "*", 16 | "ext-json": "*", 17 | "ext-simplexml": "*", 18 | "ck/file_marc_reference": "^1.2", 19 | "pear/file_marc": "@dev" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^8.0 | ^9.0", 23 | "squizlabs/php_codesniffer": "^3.3" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Scriptotek\\Marc\\": "src/", 28 | "Tests\\": "tests/" 29 | } 30 | }, 31 | "scripts": { 32 | "test": "phpunit" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | getTestCollection($filename)->toArray(); 23 | 24 | return $records[$n - 1]; 25 | } 26 | 27 | protected function makeMinimalRecord($value) 28 | { 29 | return Record::fromString(' 30 | 31 | 99999cam a2299999 u 4500 32 | 98218834x 33 | ' . $value . ' 34 | '); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Importers/Importer.php: -------------------------------------------------------------------------------- 1 | factory = $factory ?? new Factory(); 17 | } 18 | 19 | public function fromFile(string $filename): Collection 20 | { 21 | $data = file_get_contents($filename); 22 | 23 | return $this->fromString($data); 24 | } 25 | 26 | public function fromString(string $data): Collection 27 | { 28 | $isXml = str_starts_with($data, '<'); 29 | if ($isXml) { 30 | $importer = new XmlImporter($data); 31 | 32 | return $importer->getCollection(); 33 | } else { 34 | $parser = $this->factory->make('File_MARC', $data, File_MARC::SOURCE_STRING); 35 | return new Collection($parser); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Fields/UncontrolledSubject.php: -------------------------------------------------------------------------------- 1 | subfield->getData(); 28 | } 29 | 30 | public function getParts(): array 31 | { 32 | return [$this->getTerm()]; 33 | } 34 | 35 | public function __toString(): string 36 | { 37 | return $this->getTerm(); 38 | } 39 | 40 | public function jsonSerialize(): string|array 41 | { 42 | return [ 43 | 'type' => $this->getType(), 44 | 'term' => $this->getTerm(), 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/data/sandburg.mrc: -------------------------------------------------------------------------------- 1 | 01142cam 2200301 a 4500001001300000003000400013005001700017008004100034010001700075020002500092040001800117042000900135050002600144082001600170100003200186245008600218250001200304260005200316300004900368500004000417520022800457650003300685650003300718650002400751650002100775650002300796700002100819 92005291 DLC19930521155141.9920219s1993 caua j 000 0 eng  a 92005291  a0152038655 :c$15.95 aDLCcDLCdDLC alcac00aPS3537.A618bA88 199300a811/.522201 aSandburg, Carl,d1878-1967.10aArithmetic /cCarl Sandburg ; illustrated as an anamorphic adventure by Ted Rand. a1st ed. aSan Diego :bHarcourt Brace Jovanovich,cc1993. a1 v. (unpaged) :bill. (some col.) ;c26 cm. aOne Mylar sheet included in pocket. aA poem about numbers and their characteristics. Features anamorphic, or distorted, drawings which can be restored to normal by viewing from a particular angle or by viewing the image's reflection in the provided Mylar cone. 0aArithmeticxJuvenile poetry. 0aChildren's poetry, American. 1aArithmeticxPoetry. 1aAmerican poetry. 1aVisual perception.1 aRand, Ted,eill. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 University of Oslo Science Library 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "999401461934702201", 3 | "isbns": [], 4 | "title": "The eightfold way", 5 | "publisher": "W.A. Benjamin", 6 | "pub_year": "1964", 7 | "edition": "Third edition", 8 | "creators": [ 9 | { 10 | "type": "100", 11 | "name": "Gell-Mann, Murray", 12 | "id": "(NO-TrBIB)x90569757" 13 | }, 14 | { 15 | "type": "700", 16 | "name": "Ne'eman, Yuval", 17 | "id": "(NO-TrBIB)x90061707" 18 | } 19 | ], 20 | "subjects": [ 21 | { 22 | "type": "650", 23 | "vocabulary": "lcsh", 24 | "term": "Eightfold way (Nuclear physics) : Addresses, essays, lectures" 25 | }, 26 | { 27 | "type": "650", 28 | "vocabulary": "lcsh", 29 | "term": "Nuclear reactions : Addresses, essays, lectures" 30 | } 31 | ], 32 | "classifications": [ 33 | { 34 | "scheme": "msc", 35 | "number": "81" 36 | } 37 | ], 38 | "toc": null, 39 | "summary": null, 40 | "part_of": null 41 | } 42 | -------------------------------------------------------------------------------- /tests/ExamplesTest.php: -------------------------------------------------------------------------------- 1 | assertJsonStringEqualsJsonString($jsonData, json_encode($record)); 26 | } 27 | } 28 | 29 | public static function exampleDataProvider() 30 | { 31 | foreach (glob(self::pathTo('examples/*.xml')) as $filename) { 32 | yield [$filename]; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Use this file to configure the Overcommit hooks you wish to use. This will 2 | # extend the default configuration defined in: 3 | # https://github.com/brigade/overcommit/blob/master/config/default.yml 4 | # 5 | # At the topmost level of this YAML file is a key representing type of hook 6 | # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can 7 | # customize each hook, such as whether to only run it on certain files (via 8 | # `include`), whether to only display output if it fails (via `quiet`), etc. 9 | # 10 | # For a complete list of hooks, see: 11 | # https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook 12 | # 13 | # For a complete list of options that you can use to customize hooks, see: 14 | # https://github.com/brigade/overcommit#configuration 15 | # 16 | # Uncomment the following lines to make the configuration take effect. 17 | 18 | PreCommit: 19 | TrailingWhitespace: 20 | enabled: true 21 | on_warn: fail 22 | PhpCs: 23 | enabled: true 24 | problem_on_unmodified_line: warn 25 | command: 'vendor/bin/phpcs' 26 | flags: ['--standard=ruleset.xml', '--report=csv', '-s'] 27 | PhpLint: 28 | enabled: true 29 | on_warn: fail 30 | exclude: 31 | - '**/*.blade.php' 32 | 33 | -------------------------------------------------------------------------------- /src/HoldingsRecord.php: -------------------------------------------------------------------------------- 1 | getLocations(); 43 | 44 | return count($locations) ? $locations[0] : null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/EditionTest.php: -------------------------------------------------------------------------------- 1 | 16 | 17 | 99999cam a2299999 u 4500 18 | 98218834x 19 | 20 | 2nd ed. 21 | 22 | '); 23 | 24 | $this->assertEquals('2nd ed.', $record->edition); 25 | } 26 | 27 | public function testMissingA() 28 | { 29 | $record = Record::fromString(' 30 | 31 | 99999cam a2299999 u 4500 32 | 98218834x 33 | 34 | 2nd ed. 35 | 36 | '); 37 | 38 | $this->assertNull($record->edition); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Fields/Subfield.php: -------------------------------------------------------------------------------- 1 | field = $field; 15 | $this->subfield = $subfield; 16 | } 17 | 18 | public function __destruct() 19 | { 20 | $this->field = null; 21 | $this->subfield = null; 22 | } 23 | 24 | public function delete() 25 | { 26 | $this->subfield->delete(); 27 | $this->field->deleteSubfield($this->subfield); 28 | $this->__destruct(); 29 | } 30 | 31 | public function jsonSerialize(): string|array 32 | { 33 | return (string) $this; 34 | } 35 | 36 | public function __toString(): string 37 | { 38 | return $this->subfield->getData(); 39 | } 40 | 41 | public function __call($name, $args) 42 | { 43 | return call_user_func_array([$this->subfield, $name], $args); 44 | } 45 | 46 | public function __get($key) 47 | { 48 | $method = 'get' . ucfirst($key); 49 | if (method_exists($this, $method)) { 50 | return call_user_func([$this, $method]); 51 | } 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for considering contributing to this project! Pull requests are welcome, 2 | and if you want to discuss something before starting (or just check if the maintainer is still alive), 3 | feel free to open an issue first. 4 | 5 | ## Code style 6 | 7 | This project comes with a [ruleset.xml](https://github.com/scriptotek/php-marc/blob/master/ruleset.xml) file that 8 | defines the code style (which is PSR-2 at the time of this writing), so it can be checked with 9 | [phpcs](https://github.com/squizlabs/PHP_CodeSniffer): 10 | 11 | phpcs --standard=ruleset.xml src 12 | 13 | The project also comes with an [.overcommit.yml](https://github.com/scriptotek/php-marc/blob/master/.overcommit.yml) 14 | file so you can use [Overcommit](https://github.com/sds/overcommit)'s Git hooks to have your changes checked before 15 | each commit. 16 | 17 | ## Tests 18 | 19 | Tests are run using [PhpUnit](https://phpunit.de/): 20 | 21 | ./vendor/bin/phpunit 22 | 23 | If you add new functionality, please consider also adding a test case for it. 24 | 25 | ## Changelog 26 | 27 | Consider adding an entry to the [CHANGELOG](https://github.com/scriptotek/php-marc/blob/master/CHANGELOG.md) as part 28 | of your commit. 29 | 30 | ## Code of conduct 31 | 32 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By 33 | participating, you are expected to honor this code. 34 | 35 | [code-of-conduct]: https://github.com/civiccc/code-of-conduct 36 | -------------------------------------------------------------------------------- /tests/LocationTest.php: -------------------------------------------------------------------------------- 1 | loc = new Location($field); 23 | } 24 | 25 | public function testCallcode() 26 | { 27 | $this->assertEquals('793.24 Cra', strval($this->loc->callcode)); 28 | } 29 | 30 | public function testLocation() 31 | { 32 | $this->assertNull($this->loc->location); 33 | } 34 | 35 | public function testSublocation() 36 | { 37 | $this->assertEquals('1030310', strval($this->loc->sublocation)); 38 | } 39 | 40 | public function testShelvinglocation() 41 | { 42 | $this->assertEquals('k00481', strval($this->loc->shelvinglocation)); 43 | } 44 | 45 | public function testPublicNote() 46 | { 47 | $this->assertEquals('A public note', strval($this->loc->publicNote)); 48 | } 49 | 50 | public function testNonPublicNote() 51 | { 52 | $this->assertEquals('A non-public note', strval($this->loc->nonPublicNote)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "990929710914702204", 3 | "isbns": [ 4 | "9788202308414" 5 | ], 6 | "title": "Å nærme seg en katt", 7 | "publisher": "Cappelen Damm", 8 | "pub_year": "2009", 9 | "edition": null, 10 | "creators": [ 11 | { 12 | "type": "100", 13 | "name": "Eliot, T.S.", 14 | "dates": "1888-1965", 15 | "id": "(NO-TrBIB)90052479" 16 | }, 17 | { 18 | "type": "700", 19 | "name": "Brekke, Paal,", 20 | "dates": "1923-1993,", 21 | "id": "(NO-TrBIB)90079454", 22 | "relator_term": "overs.", 23 | "relationship": "trl" 24 | }, 25 | { 26 | "type": "700", 27 | "name": "Scheffler, Axel,", 28 | "dates": "1957-", 29 | "id": "(NO-TrBIB)90983587", 30 | "relator_term": "illustr." 31 | } 32 | ], 33 | "subjects": [ 34 | { 35 | "type": "653", 36 | "term": "regler" 37 | }, 38 | { 39 | "type": "653", 40 | "term": "dikt" 41 | }, 42 | { 43 | "type": "653", 44 | "term": "katter" 45 | } 46 | ], 47 | "classifications": [ 48 | { 49 | "scheme": "udc", 50 | "number": "820" 51 | }, 52 | { 53 | "scheme": "ddc", 54 | "number": "821", 55 | "edition": "5/nor" 56 | }, 57 | { 58 | "scheme": "oosk", 59 | "number": "S 13c/US" 60 | } 61 | ], 62 | "toc": null, 63 | "summary": null, 64 | "part_of": null 65 | } -------------------------------------------------------------------------------- /tests/data/examples/holdings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 00384nx a2200133ui 4500 4 | h2051843-47bibsys_ubo 5 | 011715073-47bibsys_ubo 6 | ta 7 | 1511020|||||||||4 uu 8 | mini 9 | 10 | 011715073 11 | (NO-TrBIB) 12 | 13 | 14 | 75ns18199-47bibsys_ubo 15 | 16 | 17 | 1030310 18 | k00473 19 | Plv 157 20 | 21 | 22 | 1030310 23 | k00445 24 | Plv 157 25 | 0 26 | 0 27 | 2015-11-02 28 | 29 | 30 | 75ns18199 31 | mini 32 | 75ns18199 33 | 75ns18199 34 | BOOK 35 | (Gammel sign.: Plv 189) (Begrenset utlån) 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/data/binary-marc.mrc: -------------------------------------------------------------------------------- 1 | 01853 a2200517 450000100110000000300070001100800390001802000260005703500150008304000070009804200120010508400180011708400180013508400210015308400220017410000300019624500630022625000130028926000580030230-0033003604400037003935000023004305990010004537400024004637750034004878410048005218410049005698410047006188410048006658410047007138410047007608520038008078520021008458520013008668520016008798520028008958520021009239000056009449000061010009000057010619000056011189000057011749000060012319760027012910050017013180000000044EMILDA980120s1998 fi j 000 0 swe a9515008808cFIM 72:00 99515008808 aNB 9NB9SEE aHcd,u2kssb/6 5NBauHc2kssb 5SEEaHcf2kssb/6 5QaHcd,uf2kssb/61 aJansson, Tove,d1914-200104aDet osynliga barnet och andra bert̃telser /cTove Jansson a7. uppl. aHelsingfors :bSchildt,c1998 ;e(Falun :fScandbook) a166, [4] s. :bill. ;c21 cm 0aMumin-biblioteket,x99-0698931-9 aOriginaluppl. 1962 aLi: S4 aDet osynliga barnet1 z951-50-0385-7w9515003857907 5Liaxab0201080u 0 4000uu |000000e1 5SEEaxab0201080u 0 4000uu |000000e1 5Laxab0201080u 0 4000uu |000000e1 5NBaxab0201080u 0 4000uu |000000e1 5Qaxab0201080u 0 4000uu |000000e1 5Saxab0201080u 0 4000uu |000000e1 5NBbNBcNB98:12hpliktjR, 980520 5LibLicCNBhh,u 5SEEbSEE 5QbQj98947 5LbLc0100h98/j3043 H 5SbShSv97j72351saYanson, Tobe,d1914-2001uJansson, Tove,d1914-20011saJanssonov,̀ Tove,d1914-2001uJansson, Tove,d1914-20011saJansone, Tuve,d1914-2001uJansson, Tove,d1914-20011saJanson, Tuve,d1914-2001uJansson, Tove,d1914-20011saJansson, Tuve,d1914-2001uJansson, Tove,d1914-20011saJanssonova, Tove,d1914-2001uJansson, Tove,d1914-2001 2aHcd,ubSkn̲litteratur20050204111518.0 -------------------------------------------------------------------------------- /tests/ClassificationFieldTest.php: -------------------------------------------------------------------------------- 1 | getNthrecord('sru-alma.xml', 1); 12 | 13 | # Vocabulary from indicator2 14 | $cls = $record->classifications[0]; 15 | $this->assertInstanceOf('Scriptotek\Marc\Fields\Classification', $cls); 16 | $this->assertEquals('msc', $cls->scheme); 17 | $this->assertEquals('81', strval($cls)); 18 | $this->assertEquals(Classification::OTHER_SCHEME, $cls->type); 19 | } 20 | 21 | public function testJsonSerialization() 22 | { 23 | $record = $this->getNthrecord('sru-alma.xml', 3); 24 | $cls = $record->classifications[1]; 25 | 26 | $this->assertJsonStringEqualsJsonString( 27 | json_encode([ 28 | 'scheme' => 'inspec', 29 | 'number' => 'a1130', 30 | ]), 31 | json_encode($cls) 32 | ); 33 | } 34 | 35 | public function testRepeatedA() 36 | { 37 | $record = $this->makeMinimalRecord(' 38 | 39 | 330 40 | 380 41 | 650 42 | DE-101 43 | sdnb 44 | 45 | '); 46 | 47 | $this->assertCount(3, $record->classifications); 48 | 49 | $this->assertEquals('DE-101', $record->classifications[2]->assigningVocabulary); 50 | $this->assertEquals('sdnb', $record->classifications[2]->scheme); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Fields/Title.php: -------------------------------------------------------------------------------- 1 | field->getSubfield('a'); 24 | $title = $a ? trim($a->getData()) : ''; 25 | 26 | // $b is not repeated 27 | $b = $this->field->getSubfield('b'); 28 | if ($b) { 29 | if (!in_array(substr($title, strlen($title) - 1), [':', ';', '=', '.'])) { 30 | // Add colon if no ISBD marker present ("British style") 31 | $title .= ' :'; 32 | } 33 | $title .= ' ' . trim($b->getData()); 34 | } 35 | 36 | // Part number and title can be repeated 37 | foreach ($this->field->getSubfields() as $sf) { 38 | if (in_array($sf->getCode(), ['n', 'p'])) { 39 | $title .= ' ' . $sf->getData(); 40 | } 41 | } 42 | 43 | // Strip off 'Statement of responsibility' marker 44 | // I would like to strip of the final dot as well, but we can't really distinguish 45 | // between dot as an ISBD marker and dot as part of the actual title 46 | // (for instance when the title is an abbreviation) 47 | $title = rtrim($title, ' /'); 48 | 49 | return $title; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Fields/Person.php: -------------------------------------------------------------------------------- 1 | getTag(); 42 | } 43 | 44 | /** 45 | * Return the Authority record control number 46 | */ 47 | public function getId(): ?string 48 | { 49 | // preg_match('/^\((.+)\)(.+)$/', $sf0->getData(), $matches); 50 | return $this->sf('0'); 51 | } 52 | 53 | public function getName(): ?string 54 | { 55 | return $this->sf('a'); 56 | } 57 | 58 | public function getTitulation(): ?string 59 | { 60 | return $this->sf('c'); 61 | } 62 | 63 | public function getDates(): ?string 64 | { 65 | return $this->sf('d'); 66 | } 67 | 68 | public function getRelatorTerm(): ?string 69 | { 70 | return $this->sf('e'); 71 | } 72 | 73 | public function getRelationship(): ?string 74 | { 75 | return $this->sf('4'); 76 | } 77 | 78 | public function __toString(): string 79 | { 80 | $tpl = $this->getDates() ? self::$formatWithDate : '{name}'; 81 | 82 | return str_replace( 83 | ['{name}', '{dates}'], 84 | [$this->clean($this->getName()), $this->clean($this->getDates())], 85 | $tpl 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Fields/Corporation.php: -------------------------------------------------------------------------------- 1 | getTag(); 41 | } 42 | 43 | /** 44 | * Return the Authority record control number 45 | */ 46 | public function getId(): ?string 47 | { 48 | // preg_match('/^\((.+)\)(.+)$/', $sf0->getData(), $matches); 49 | return $this->sf('0'); 50 | } 51 | 52 | public function getName(): ?string 53 | { 54 | return $this->sf('a'); 55 | } 56 | 57 | public function getSubordinateUnit(): ?string 58 | { 59 | return $this->sf('b'); 60 | } 61 | 62 | public function getLocation(): ?string 63 | { 64 | return $this->sf('c'); 65 | } 66 | 67 | public function getDate(): ?string 68 | { 69 | return $this->sf('d'); 70 | } 71 | 72 | public function getNumber(): ?string 73 | { 74 | return $this->sf('n'); 75 | } 76 | 77 | public function getRelationship(): ?string 78 | { 79 | return $this->sf('4'); 80 | } 81 | 82 | public function __toString(): string 83 | { 84 | $out = []; 85 | foreach ($this->getSubfields() as $sf) { 86 | if (in_array($sf->getCode(), static::$headingComponentCodes)) { 87 | $out[] = $sf->getData(); 88 | } 89 | } 90 | return str_replace('/ /', ' ', implode(' ', $out)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Fields/Location.php: -------------------------------------------------------------------------------- 1 | sf('a'); 39 | } 40 | 41 | public function getSublocation(): ?string 42 | { 43 | return $this->sf('b'); 44 | } 45 | 46 | public function getShelvinglocation(): ?string 47 | { 48 | return $this->sf('c'); 49 | } 50 | 51 | public function getCallcode(): ?string 52 | { 53 | return $this->toString([ 54 | 'h', // Classification part (NR) 55 | 'i', // Item part (R) 56 | 'j', // Shelving control number (NR) 57 | 'k', // Call number prefix 58 | 'l', // Shelving form of title 59 | 'm', // Call number suffix 60 | ]); 61 | } 62 | 63 | public function getNonpublicNote(): ?string 64 | { 65 | return $this->sf('x'); 66 | } 67 | 68 | public function getPublicNote(): ?string 69 | { 70 | return $this->sf('z'); 71 | } 72 | 73 | public function __toString(): string 74 | { 75 | return $this->toString(['a', 'b', 'c', 'h', 'i', 'j', 'k', 'l', 'm']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Fields/Classification.php: -------------------------------------------------------------------------------- 1 | getFields('08[0234]', true) as $field) { 26 | foreach ($field->getSubfields('a') as $sfa) { 27 | $out[] = new Classification($field, $sfa); 28 | } 29 | } 30 | return $out; 31 | } 32 | 33 | public function getType(): string 34 | { 35 | return $this->getTag(); 36 | } 37 | 38 | public function getTag(): string 39 | { 40 | return $this->field->getTag(); 41 | } 42 | 43 | public function getScheme(): string 44 | { 45 | $typeMap = [ 46 | '080' => 'udc', 47 | '082' => 'ddc', 48 | '083' => 'ddc', 49 | ]; 50 | 51 | $tag = $this->field->getTag(); 52 | 53 | if ($tag == '084') { 54 | return $this->field->sf('2'); 55 | } 56 | 57 | return $typeMap[$tag]; 58 | } 59 | 60 | public function getEdition(): ?string 61 | { 62 | if (in_array($this->field->getTag(), ['080', '082', '083'])) { 63 | return $this->field->sf('2'); 64 | } 65 | return null; 66 | } 67 | 68 | public function getNumber(): string 69 | { 70 | return $this->subfield->getData(); 71 | } 72 | 73 | public function getAssigningVocabulary(): ?string 74 | { 75 | return $this->field->sf('q'); 76 | } 77 | 78 | public function getId(): ?string 79 | { 80 | // NOTE: Both $a and $0 are repeatable, but there's no examples of how that would look like. 81 | // I'm guessing that they would alternate: $a ... $0 ... $a ... $0 ... , but not sure. 82 | return $this->field->sf('0'); 83 | } 84 | 85 | public function __toString(): string 86 | { 87 | return $this->getNumber(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "999217148824702204", 3 | "isbns": [ 4 | "0471616389" 5 | ], 6 | "title": "Acousto-optic devices : principles, design, and applications", 7 | "publisher": "Wiley", 8 | "pub_year": "1992", 9 | "edition": null, 10 | "creators": [ 11 | { 12 | "type": "100", 13 | "name": "Xu, Jieping", 14 | "id": "(NO-TrBIB)90625942" 15 | }, 16 | { 17 | "type": "700", 18 | "name": "Stroud, Robert", 19 | "id": "(NO-TrBIB)90625943" 20 | } 21 | ], 22 | "subjects": [ 23 | { 24 | "type": "650", 25 | "vocabulary": "lcsh", 26 | "term": "Acoustooptical devices" 27 | }, 28 | { 29 | "type": "650", 30 | "vocabulary": "noubomn", 31 | "term": "Komponenter" 32 | }, 33 | { 34 | "type": "650", 35 | "vocabulary": "noubomn", 36 | "term": "Akustikk", 37 | "id": "(NO-TrBIB)REAL013572" 38 | }, 39 | { 40 | "type": "650", 41 | "vocabulary": "tekord", 42 | "term": "Optikk" 43 | }, 44 | { 45 | "type": "650", 46 | "vocabulary": "tekord", 47 | "term": "Akustikk" 48 | }, 49 | { 50 | "type": "650", 51 | "vocabulary": "tekord", 52 | "term": "Optiske instrumenter" 53 | }, 54 | { 55 | "type": "653", 56 | "term": "akustooptiske" 57 | }, 58 | { 59 | "type": "653", 60 | "term": "effekter" 61 | }, 62 | { 63 | "type": "653", 64 | "term": "komponenter" 65 | } 66 | ], 67 | "classifications": [ 68 | { 69 | "scheme": "udc", 70 | "number": "535" 71 | }, 72 | { 73 | "scheme": "udc", 74 | "number": "681.7" 75 | }, 76 | { 77 | "scheme": "udc", 78 | "number": "534" 79 | }, 80 | { 81 | "scheme": "inspec", 82 | "number": "b4170" 83 | }, 84 | { 85 | "scheme": "inspec", 86 | "number": "a7820h" 87 | }, 88 | { 89 | "scheme": "inspec", 90 | "number": "a4280" 91 | } 92 | ], 93 | "toc": null, 94 | "summary": null, 95 | "part_of": null 96 | } -------------------------------------------------------------------------------- /tests/QueryResultTest.php: -------------------------------------------------------------------------------- 1 | record = Record::fromString(' 16 | 17 | 99999cam a2299999 u 4500 18 | 98218834x 19 | 20 | 8200424421 21 | h. 22 | Nkr 98.00 23 | 24 | 25 | 9788200424420 26 | ib. 27 | 28 | '); 29 | } 30 | 31 | public function testInitialization() 32 | { 33 | $result = $this->record->query('020'); 34 | $this->assertInstanceOf(QueryResult::class, $result); 35 | } 36 | 37 | public function testFirstField() 38 | { 39 | $result = $this->record->query('020{$a}')->first(); 40 | $this->assertInstanceOf(Field::class, $result); 41 | } 42 | 43 | public function testControlField() 44 | { 45 | $result = $this->record->query('001')->first(); 46 | $this->assertInstanceOf(Field::class, $result); 47 | } 48 | 49 | public function testFirstSubfield() 50 | { 51 | $result = $this->record->query('020$a')->first(); 52 | $this->assertInstanceOf('File_MARC_Subfield', $result); 53 | } 54 | 55 | public function testText() 56 | { 57 | $result = $this->record->query('020$a')->text(); 58 | $this->assertEquals('8200424421', $result); 59 | } 60 | 61 | public function testIndicator() 62 | { 63 | $result = $this->record->query('020')->first()->getIndicator(2); 64 | $this->assertEquals('3', $result); 65 | } 66 | 67 | public function testTextPattern() 68 | { 69 | $result = $this->record->query('020$a{$q=\ib.}')->text(); 70 | $this->assertEquals('9788200424420', $result); 71 | } 72 | 73 | public function testCount() 74 | { 75 | $result = $this->record->query('02.'); 76 | $this->assertCount(2, $result); 77 | } 78 | 79 | public function testEmptyCount() 80 | { 81 | $result = $this->record->query('03.'); 82 | $this->assertCount(0, $result); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/data/examples/authority.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 99999nz a2299999n 4500 4 | 90081146 5 | NO-TrBIB 6 | 20170420112705.0 7 | 080923n| adz|naabn| |a|ana| 8 | 9 | x90081146 10 | NO-TrBIB 11 | 12 | 13 | http://hdl.handle.net/11250/1360467 14 | hdl 15 | 16 | 17 | 0000000120169813 18 | isni 19 | 20 | 21 | http://viaf.org/viaf/803679 22 | viaf 23 | 24 | 25 | (NO-TrBIB)90081146 26 | 27 | 28 | NO-TrBIB 29 | nob 30 | NO-TrBIB 31 | noraf 32 | 33 | 34 | no 35 | 36 | 37 | Hamsun, Marie 38 | 1881-1969 39 | 40 | 41 | f 42 | 43 | 44 | Andersen, Marie 45 | 1881-1969 46 | 47 | 48 | Gamsun, Marija 49 | 50 | 51 | Hamuzun, Marî 52 | 53 | 54 | kat3 55 | 56 | -------------------------------------------------------------------------------- /tests/CollectionTest.php: -------------------------------------------------------------------------------- 1 | '; 17 | 18 | $collection = Collection::fromString($source); 19 | $this->assertCount(0, $collection->toArray()); 20 | } 21 | 22 | /** 23 | * Test that it XmlException is thrown when the specified encoding (UTF-16) 24 | * differs from the actual encoding (UTF-8). 25 | */ 26 | public function testExceptionOnInvalidEncoding() 27 | { 28 | $this->expectException(XmlException::class); 29 | $this->getTestCollection('alma-bibs-api-invalid.xml'); 30 | } 31 | 32 | /** 33 | * Define a list of sample binary MARC files that we can test with, 34 | * and the expected number of records in each. 35 | * 36 | * @return array 37 | */ 38 | public static function mrcFiles() 39 | { 40 | return [ 41 | ['sandburg.mrc', 1], // Single binary MARC file 42 | ]; 43 | } 44 | 45 | /** 46 | * Define a list of sample XML files from different sources that we can test with, 47 | * and the expected number of records in each. 48 | * 49 | * @return array 50 | */ 51 | public static function xmlFiles() 52 | { 53 | return [ 54 | ['oaipmh-bibsys.xml', 89], // Records encapsulated in OAI-PMH response 55 | ['sru-loc.xml', 10], // Records encapsulated in SRU response 56 | ['sru-bibsys.xml', 117], // (Another one) 57 | ['sru-zdb.xml', 8], // (Another one) 58 | ['sru-kth.xml', 10], // (Another one) 59 | ['sru-alma.xml', 3], // (Another one) 60 | ]; 61 | } 62 | 63 | /** 64 | * Test that the sample files can be loaded using Collection::fromFile 65 | * 66 | * @dataProvider mrcFiles 67 | * @dataProvider xmlFiles 68 | * @param string $filename 69 | * @param int $expected 70 | */ 71 | public function testCollectionFromFile($filename, $expected) 72 | { 73 | $records = $this->getTestCollection($filename)->toArray(); 74 | 75 | $this->assertCount($expected, $records); 76 | $this->assertInstanceOf(BibliographicRecord::class, $records[0]); 77 | } 78 | 79 | 80 | /** 81 | * Test that the sample files can be loaded using Collection::fromSimpleXMLElement. 82 | * 83 | * @dataProvider xmlFiles 84 | * @param string $filename 85 | * @param int $expected 86 | */ 87 | public function testInitializeFromSimpleXmlElement($filename, $expected) 88 | { 89 | $el = simplexml_load_file(self::pathTo($filename)); 90 | 91 | $collection = Collection::fromSimpleXMLElement($el); 92 | 93 | $this->assertCount($expected, $collection->toArray()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "999919884907102204", 3 | "isbns": [ 4 | "9781107084940", 5 | "1107084946" 6 | ], 7 | "title": "Physics of electronic materials : fundamentals to device applications", 8 | "publisher": "Cambridge University Press,", 9 | "pub_year": "2017", 10 | "edition": null, 11 | "creators": [ 12 | { 13 | "type": "100", 14 | "name": "Rammer, Jørgen", 15 | "id": "90614467", 16 | "relationship": "aut" 17 | } 18 | ], 19 | "subjects": [ 20 | { 21 | "type": "650", 22 | "vocabulary": "lcsh", 23 | "term": "Semiconductors" 24 | }, 25 | { 26 | "type": "650", 27 | "vocabulary": "noubomn", 28 | "term": "Halvledere", 29 | "id": "(NoOU)REAL005740" 30 | }, 31 | { 32 | "type": "650", 33 | "vocabulary": "noubomn", 34 | "term": "Elektriske kretser", 35 | "id": "(NoOU)REAL003680" 36 | }, 37 | { 38 | "type": "653", 39 | "term": "halvledere" 40 | }, 41 | { 42 | "type": "653", 43 | "term": "kvantemekanikk" 44 | }, 45 | { 46 | "type": "653", 47 | "term": "elektronikk" 48 | } 49 | ], 50 | "classifications": [ 51 | { 52 | "scheme": "ddc", 53 | "number": "537.622", 54 | "edition": "23" 55 | }, 56 | { 57 | "scheme": "ddc", 58 | "number": "537.62", 59 | "edition": "23/nor" 60 | } 61 | ], 62 | "toc": { 63 | "text": "Quantum mechanics -- Quantum tunneling -- Standard metal model -- Standard conductor model -- Electric circuit theory -- Quantum wells -- Particle in a periodic potential -- Bloch currents -- Crystalline solids -- Semiconductor doping -- Transistors -- Heterostructures -- Mesoscopic physics -- Arithmetic, logic and machines." 64 | }, 65 | "summary": { 66 | "text": "\"Electronic devices play a crucial role in todays societies and in the physical sciences where they originated. Contemplating that in just a few decades, technology guiding electrons and photons has emerged that makes possible oral and visual communication between peoples on opposite sides of the planet is truly a triumph of science and technology. Not to mention that equipped with a computer with access to the Internet, one can instantly access a wealth of human knowledge. The physical principles providing the understanding of the functioning of present day electronic devices should therefore be of interest not only to physicists, electrical engineers and material scientists, but to anyone with a general interest in how the wired world around us is functioning. Present day information technology is based on the physical properties of semiconductors, in particular the functioning of the transistor. The intension of this book is to take the reader from the principles of quantum mechanics through the quantum theory of metals and semiconductors all the way to how devices are used to perform their duties in electric circuits: for example functioning as amplifiers, switches, and in the hard ware of computers. The mechanics of arithmetic and logical operations are discussed and it is shown how electronic devices in the present day CMOS-technology can be carriers of arithmetic calculations and logic operations in computers\"--", 67 | "assigning_source": "Provided by publisher." 68 | }, 69 | "part_of": null 70 | } 71 | -------------------------------------------------------------------------------- /src/QueryResult.php: -------------------------------------------------------------------------------- 1 | ref = $ref->ref; 29 | $this->data = $ref->data; 30 | $this->content = $ref->content; 31 | 32 | for ($i=0; $i < count($this->data); $i++) { 33 | if (is_a($this->data[$i], File_MARC_Field::class)) { 34 | $this->data[$i] = new Field($this->data[$i]); 35 | } 36 | } 37 | } 38 | 39 | public function getReference() 40 | { 41 | return $this->ref; 42 | } 43 | 44 | /** 45 | * Get the first result (field or subfield), or null if no results. 46 | * 47 | * @return Field|File_MARC_Subfield|null 48 | */ 49 | public function first(): Field|File_MARC_Subfield|null 50 | { 51 | return $this->data[0] ?? null; 52 | } 53 | 54 | /** 55 | * Get the text content of the first result, or null if no results. 56 | * 57 | * @return string|null 58 | */ 59 | public function text(): ?string 60 | { 61 | return $this->content[0] ?? null; 62 | } 63 | 64 | /** 65 | * Retrieve an external iterator 66 | * @link http://php.net/manual/en/iteratoraggregate.getiterator.php 67 | * @return Traversable 68 | */ 69 | public function getIterator(): Traversable|ArrayIterator 70 | { 71 | return new ArrayIterator($this->data); 72 | } 73 | 74 | /** 75 | * Whether a offset exists 76 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php 77 | * @param mixed $offset An offset to check for. 78 | * @return boolean true on success or false on failure. 79 | */ 80 | public function offsetExists($offset): bool 81 | { 82 | return isset($this->data[$offset]); 83 | } 84 | 85 | /** 86 | * Offset to retrieve 87 | * @link http://php.net/manual/en/arrayaccess.offsetget.php 88 | * @param mixed $offset The offset to retrieve. 89 | * @return Field|File_MARC_Subfield|null 90 | */ 91 | public function offsetGet($offset): Field|File_MARC_Subfield|null 92 | { 93 | return $this->data[$offset]; 94 | } 95 | 96 | /** 97 | * Offset to set 98 | * @link http://php.net/manual/en/arrayaccess.offsetset.php 99 | * @param mixed $offset The offset to assign the value to. 100 | * @param mixed $value The value to set. 101 | */ 102 | public function offsetSet($offset, $value): void 103 | { 104 | $this->data[$offset] = $value; 105 | } 106 | 107 | /** 108 | * Offset to unset 109 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php 110 | * @param mixed $offset The offset to unset. 111 | */ 112 | public function offsetUnset($offset): void 113 | { 114 | unset($this->data[$offset]); 115 | } 116 | 117 | /** 118 | * Count elements of an object 119 | * @link http://php.net/manual/en/countable.count.php 120 | * @return int The number of results 121 | */ 122 | public function count(): int 123 | { 124 | return count($this->data); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Fields/Subject.php: -------------------------------------------------------------------------------- 1 | 'lcsh', // 0: Library of Congress Subject Headings 39 | '1' => 'lccsh', // 1: LC subject headings for children's literature 40 | '2' => 'mesh', // 2: Medical Subject Headings 41 | '3' => 'atg', // 3: National Agricultural Library subject authority file (?) 42 | // 4: Source not specified 43 | '5' => 'cash', // 5: Canadian Subject Headings 44 | '6' => 'rvm', // 6: Répertoire de vedettes-matière 45 | // 7: Source specified in subfield $2 46 | ]; 47 | 48 | /** 49 | * @param Record $record 50 | * @return (UncontrolledSubject|Subject)[] 51 | */ 52 | public static function get(Record $record): array 53 | { 54 | $subjects = []; 55 | 56 | foreach (static::makeFieldObjects($record, '6..', true) as $subject) { 57 | if ($subject->getTag() == '653') { 58 | foreach ($subject->getSubfields('a') as $sfa) { 59 | $subjects[] = new UncontrolledSubject($subject, $sfa); 60 | } 61 | } else { 62 | $subjects[] = $subject; 63 | } 64 | } 65 | 66 | return $subjects; 67 | } 68 | 69 | public function getType(): string 70 | { 71 | return $this->getTag(); 72 | } 73 | 74 | public function getVocabulary(): ?string 75 | { 76 | $ind2 = $this->field->getIndicator(2); 77 | $sf2 = $this->field->getSubfield('2'); 78 | if (isset($this->vocabularies[$ind2])) { 79 | return $this->vocabularies[$ind2]; 80 | } 81 | if ($sf2) { 82 | return $sf2->getData(); 83 | } 84 | 85 | return null; 86 | } 87 | 88 | /** 89 | * Return the Authority record control number 90 | */ 91 | public function getId(): ?string 92 | { 93 | return $this->sf('0'); 94 | } 95 | 96 | public function getParts(): array 97 | { 98 | return $this->getSubfields('[' . implode('', self::$termComponentCodes) . ']', true); 99 | } 100 | 101 | public function getTerm(): ?string 102 | { 103 | return $this->toString(self::$termComponentCodes); 104 | } 105 | 106 | public function __toString(): string 107 | { 108 | return $this->getTerm(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/FieldsTest.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 99999cam a2299999 u 4500 16 | 98218834x 17 | 18 | 8200424421 19 | h. 20 | Nkr 98.00 21 | 22 | '; 23 | 24 | $record = Record::fromString($source); 25 | $this->assertEquals(['8200424421'], $record->isbns); 26 | $this->assertEquals('Nkr 98.00', $record->isbns[0]->sf('c')); 27 | $this->assertEquals( 28 | json_encode(['isbns' => ['8200424421']]), 29 | json_encode(['isbns' => $record->isbns]) 30 | ); 31 | } 32 | 33 | public function testMapSubfields() 34 | { 35 | $record = Record::fromString(' 36 | 37 | 99999cam a2299999 u 4500 38 | 39 | Levy, Silvio 40 | (NO-TrBIB)x90579165 41 | 42 | '); 43 | 44 | $this->assertEquals([ 45 | 'name' => 'Levy, Silvio', 46 | 'identifier' => '(NO-TrBIB)x90579165', 47 | ], $record->getField('700')->mapSubfields([ 48 | 'a' => 'name', 49 | 'b' => 'numeration', 50 | '0' => 'identifier', 51 | ])); 52 | } 53 | 54 | public function test020withoutA() 55 | { 56 | $source = ' 57 | 58 | 99999cam a2299999 u 4500 59 | 98218834x 60 | 61 | h. 62 | Nkr 98.00 63 | 64 | '; 65 | 66 | $record = Record::fromString($source); 67 | $this->assertEquals([''], $record->isbns); 68 | $this->assertEquals( 69 | json_encode(['isbns' => ['']]), 70 | json_encode(['isbns' => $record->isbns]) 71 | ); 72 | } 73 | 74 | public function testId() 75 | { 76 | $source = ' 77 | 78 | 99999cam a2299999 u 4500 79 | 98218834x 80 | '; 81 | 82 | $record = Record::fromString($source); 83 | $this->assertEquals('98218834x', $record->id); 84 | $this->assertEquals( 85 | json_encode(['id' => '98218834x']), 86 | json_encode(['id' => $record->id]) 87 | ); 88 | } 89 | 90 | public function testAsLineMarc() 91 | { 92 | $source = ' 93 | 94 | 99999cam a2299999 u 4500 95 | 98218834x 96 | 97 | h. 98 | Nkr 98.00 99 | 100 | '; 101 | 102 | $record = Record::fromString($source); 103 | $field = $record->isbns[0]; 104 | 105 | $this->assertEquals('020 $q h. $c Nkr 98.00', $field->asLineMarc()); 106 | $this->assertEquals('020 $$q h. $$c Nkr 98.00', $field->asLineMarc('$$')); 107 | $this->assertEquals('020 ## $$q h. $$c Nkr 98.00', $field->asLineMarc('$$', '#')); 108 | 109 | $field->delete(); 110 | $this->assertNull($field->asLineMarc()); 111 | } 112 | 113 | /** 114 | * Test the getField method. 115 | */ 116 | public function testGetField() 117 | { 118 | $wrapped_field = new File_MARC_Field('020', '$q h. $c Nkr 98.00'); 119 | $wrapper = new Field($wrapped_field); 120 | 121 | // Make sure that the exact same wrapped field object is returned 122 | // by the getter. 123 | $this->assertSame($wrapped_field, $wrapper->getField()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Importers/XmlImporter.php: -------------------------------------------------------------------------------- 1 | factory = isset($factory) ? $factory : new Factory(); 30 | 31 | if (is_a($data, SimpleXMLElement::class)) { 32 | $this->source = $data; 33 | return; 34 | } 35 | 36 | if (strlen($data) < 256 && file_exists($data)) { 37 | $data = file_get_contents($data); 38 | } 39 | 40 | // Store errors internally so that we can fetch them with libxml_get_errors() later 41 | libxml_use_internal_errors(true); 42 | 43 | $this->source = simplexml_load_string($data, 'SimpleXMLElement', 0, $ns, $isPrefix); 44 | if (false === $this->source) { 45 | throw new XmlException(libxml_get_errors()); 46 | } 47 | } 48 | 49 | public function getMarcNamespace($namespaces) 50 | { 51 | foreach ($namespaces as $prefix => $ns) { 52 | if ($ns == 'info:lc/xmlns/marcxchange-v1') { 53 | return [$prefix, $ns]; 54 | } elseif ($ns == 'http://www.loc.gov/MARC21/slim') { 55 | return [$prefix, $ns]; 56 | } 57 | } 58 | 59 | return ['', '']; 60 | } 61 | 62 | public function getRecords() 63 | { 64 | $this->source->registerXPathNamespace('m', 'http://www.loc.gov/MARC21/slim'); 65 | $this->source->registerXPathNamespace('x', 'info:lc/xmlns/marcxchange-v1'); 66 | 67 | // If root node is record: 68 | if ($this->source->getName() == 'record') { 69 | return [$this->source]; 70 | } 71 | 72 | $marcRecords = $this->source->xpath('.//x:record'); 73 | if (count($marcRecords)) { 74 | return $marcRecords; 75 | } 76 | $marcRecords = $this->source->xpath('.//m:record'); 77 | if (count($marcRecords)) { 78 | return $marcRecords; 79 | } 80 | $marcRecords = $this->source->xpath('.//record'); 81 | if (count($marcRecords)) { 82 | return $marcRecords; 83 | } 84 | 85 | return []; 86 | } 87 | 88 | public function getFirstRecord() 89 | { 90 | $records = $this->getRecords(); 91 | if (!count($records)) { 92 | throw new RecordNotFound(); 93 | } 94 | 95 | $record = $records[0]; 96 | 97 | list($prefix, $ns) = $this->getMarcNamespace($record->getNamespaces(true)); 98 | 99 | $parser = $this->factory->make('File_MARCXML', $record, File_MARCXML::SOURCE_SIMPLEXMLELEMENT, $ns); 100 | 101 | return (new Collection($parser))->$this->getFirstRecord(); 102 | } 103 | 104 | public function getCollection(): Collection 105 | { 106 | $records = $this->getRecords(); 107 | if (!count($records)) { 108 | return new Collection(); 109 | } 110 | 111 | list($prefix, $ns) = $this->getMarcNamespace($records[0]->getNamespaces(true)); 112 | 113 | $pprefix = empty($prefix) ? '' : "$prefix:"; 114 | 115 | $records = array_map(function (SimpleXMLElement $record) { 116 | $x = $record->asXML(); 117 | 118 | // Strip away XML declaration. 119 | // Tried LIBXML_NOXMLDECL first, but didn't work, 120 | // https://bugs.php.net/bug.php?id=50989 121 | $x = trim(preg_replace('/^\<\?xml.*?\?\>/', '', $x)); 122 | 123 | return $x; 124 | }, $records); 125 | 126 | $nsDef = ''; 127 | if (!empty($ns)) { 128 | if (empty($prefix)) { 129 | $nsDef = " xmlns=\"$ns\""; 130 | } else { 131 | $nsDef = " xmlns:$prefix=\"$ns\""; 132 | } 133 | } 134 | $marcCollection = '' . 135 | '<' . $pprefix . 'collection' . $nsDef . '>' . 136 | implode('', $records) . 137 | ''; 138 | 139 | $parser = $this->factory->make('File_MARCXML', $marcCollection, File_MARCXML::SOURCE_STRING, $prefix, true); 140 | 141 | return new Collection($parser); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/SubjectFieldTest.php: -------------------------------------------------------------------------------- 1 | getNthrecord('sru-alma.xml', 1); 13 | 14 | # Vocabulary from indicator2 15 | $sub = $record->subjects[0]; 16 | $this->assertEquals('lcsh', $sub->vocabulary); 17 | $this->assertEquals(Subject::TOPICAL_TERM, $sub->type); 18 | $this->assertEquals('Eightfold way (Nuclear physics) : Addresses, essays, lectures', strval($sub)); 19 | $this->assertEquals('Eightfold way (Nuclear physics) : Addresses, essays, lectures', $sub->getTerm()); 20 | 21 | $sf0 = new \File_MARC_Subfield('a', 'Eightfold way (Nuclear physics)'); 22 | $sf0->setPosition(0); 23 | $sf1 = new \File_MARC_Subfield('x', 'Addresses, essays, lectures'); 24 | $sf1->setPosition(1); 25 | $this->assertEquals([$sf0, $sf1], $sub->getParts()); 26 | } 27 | 28 | public function testChopPunctuation() 29 | { 30 | $record = $this->getNthrecord('sru-loc.xml', 2); 31 | 32 | # Vocabulary from indicator2 33 | $sub = $record->subjects[0]; 34 | $this->assertEquals('lcsh', $sub->vocabulary); 35 | $this->assertEquals(Subject::TOPICAL_TERM, $sub->type); 36 | $this->assertEquals('Popular music : 1961-1970', strval($sub)); 37 | } 38 | 39 | public function testSubjects() 40 | { 41 | $record = $this->getNthrecord('sru-alma.xml', 3); 42 | 43 | $subject = $record->subjects[1]; 44 | $this->assertInstanceOf('Scriptotek\Marc\Fields\Subject', $subject); 45 | $this->assertEquals('noubomn', $subject->vocabulary); 46 | $this->assertEquals('Elementærpartikler', strval($subject)); 47 | $this->assertEquals(Subject::TOPICAL_TERM, $subject->getType()); 48 | $this->assertEquals('Elementærpartikler', $subject->getTerm()); 49 | $this->assertEquals([new \File_MARC_Subfield('a', 'Elementærpartikler')], $subject->getParts()); 50 | 51 | $this->assertNull($subject->getId()); 52 | } 53 | 54 | public function testRepeated653a() 55 | { 56 | $record = $this->getNthrecord('sru-alma.xml', 3); 57 | 58 | $subjects = $record->getSubjects(null, Subject::UNCONTROLLED_INDEX_TERM); 59 | $this->assertCount(2, $subjects); 60 | 61 | $this->assertInstanceOf(UncontrolledSubject::class, $subjects[0]); 62 | $this->assertEquals('elementærpartikler', (string) $subjects[0]); 63 | $this->assertEquals(Subject::UNCONTROLLED_INDEX_TERM, $subjects[0]->getType()); 64 | $this->assertEquals('symmetri', (string) $subjects[1]); 65 | } 66 | 67 | public function testGetSubjectsFiltering() 68 | { 69 | $record = $this->getNthrecord('sru-alma.xml', 3); 70 | 71 | $lcsh = $record->getSubjects('lcsh'); 72 | $noubomn = $record->getSubjects('noubomn'); 73 | $noubomn_topic = $record->getSubjects('noubomn', Subject::TOPICAL_TERM); 74 | $noubomn_place = $record->getSubjects('noubomn', Subject::GEOGRAPHIC_NAME); 75 | $type_combo = $record->getSubjects(null, [Subject::TOPICAL_TERM, Subject::UNCONTROLLED_INDEX_TERM]); 76 | 77 | $this->assertCount(1, $lcsh); 78 | $this->assertCount(2, $noubomn); 79 | $this->assertCount(2, $noubomn_topic); 80 | $this->assertCount(0, $noubomn_place); 81 | $this->assertCount(5, $type_combo); 82 | } 83 | 84 | public function testEdit() 85 | { 86 | $record = $this->getNthrecord('sru-alma.xml', 3); 87 | $this->assertCount(5, $record->subjects); 88 | 89 | $this->assertInstanceOf(Subject::class, $record->subjects[0]); 90 | $record->subjects[0]->delete(); 91 | 92 | $this->assertInstanceOf(Subject::class, $record->subjects[0]); 93 | $record->subjects[0]->delete(); 94 | 95 | $this->assertInstanceOf(Subject::class, $record->subjects[0]); 96 | $record->subjects[0]->delete(); 97 | $this->assertCount(2, $record->subjects); 98 | 99 | $this->assertInstanceOf(UncontrolledSubject::class, $record->subjects[0]); 100 | $record->subjects[0]->delete(); 101 | $this->assertCount(1, $record->subjects); 102 | 103 | $this->assertInstanceOf(UncontrolledSubject::class, $record->subjects[0]); 104 | $record->subjects[0]->delete(); 105 | $this->assertCount(0, $record->subjects); 106 | } 107 | 108 | public function testJsonSerialization() 109 | { 110 | $record = $this->getNthrecord('sru-alma.xml', 3); 111 | $subject = $record->subjects[1]; 112 | 113 | $this->assertJsonStringEqualsJsonString( 114 | json_encode([ 115 | 'vocabulary' => 'noubomn', 116 | 'type' => Subject::TOPICAL_TERM, 117 | 'term' => 'Elementærpartikler' 118 | ]), 119 | json_encode($subject) 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/data/alma-bibs-api-invalid.xml: -------------------------------------------------------------------------------- 1 | 01733cam a2200553 u 4500 990114012304702201 20140827090540.0 ta 140827s2001 xx#|||||||||||000|u|eng|d 0471391069 ib. 011401230-47bibsys_ubo (NO-TrBIB)011401230 NO-TrBIB nob katreg 535.6 541.14 535.6 23 a8732q NoOU inspec a8732n NoOU inspec a6170d NoOU inspec a0760d NoOU inspec 0.6nas LOCAL a0760d LOCAL a6170d LOCAL a8732n LOCAL a8732q LOCAL Nassau, Kurt The physics and chemistry of color : the fifteen causes of color Kurt Nassau 2nd ed. New York Wiley c2001 XX, 481 s., pl. ill. Wiley series in pure and applied optics Fysikalsk kjemi noubomn Kjemi noubomn Fotokjemi noubomn Farger noubomn Fargelære noubomn Fysikk noubomn Farger tekord farger fargelære fysikk fysikalsk kjemi fotokjemi Wiley series in pure and applied optics Beskrivelse fra forlaget (kort) http://content.bibsys.no/content/?type=descr_publ_brief&isbn=0471391069 Beskrivelse fra forlaget (lang) http://content.bibsys.no/content/?type=descr_publ_full&isbn=0471391069 Omslagsbilde http://innhold.bibsys.no/bilde/forside/?size=mini&id=0471391069.JPG image/jpeg 80 -------------------------------------------------------------------------------- /tests/data/examples/bibliographic.xml: -------------------------------------------------------------------------------- 1 | 2 | 00778cam a22002531u 4500 3 | 999401461934702201 4 | 20110607103830.0 5 | ta 6 | 110607s1964 xx#|||||| |000|u|eng|d 7 | 8 | 940146193-47bibsys_ubo 9 | 10 | 11 | (NO-TrBIB)940146193 12 | 13 | 14 | (Alma)999401461934702204 15 | 16 | 17 | NO-TrBIB 18 | nob 19 | katreg 20 | 21 | 22 | 81 23 | msc 24 | 25 | 26 | 81 27 | LOCAL 28 | 29 | 30 | Gell-Mann, Murray 31 | (NO-TrBIB)x90569757 32 | 33 | 34 | The eightfold way 35 | Murray Gell-Mann, Yuval Ne'eman 36 | 37 | 38 | Third edition 39 | 40 | 41 | New York 42 | W.A. Benjamin 43 | 1964 44 | 45 | 46 | XI, 317 s. 47 | ill. 48 | 49 | 50 | Frontiers in physics 51 | 52 | 53 | Eightfold way (Nuclear physics) 54 | Addresses, essays, lectures 55 | 56 | 57 | Nuclear reactions 58 | Addresses, essays, lectures 59 | 60 | 61 | Ne'eman, Yuval 62 | (NO-TrBIB)x90061707 63 | 64 | 65 | Frontiers in physics 66 | 67 | 68 | konv 69 | 70 | 71 | 47BIBSYS_UBO 72 | 1030310 73 | UREAL Fyssaml 74 | XIa:276 75 | available 76 | 2 77 | 0 78 | 1466 79 | 8 80 | 1 81 | 82 | 83 | 47BIBSYS_UBO 84 | 1030310 85 | UREAL Fsaml 86 | F 8155 (Etter 1850) 87 | available 88 | 1 89 | 0 90 | 1463 91 | 8 92 | 2 93 | 94 | 95 | 47BIBSYS_UBO 96 | 1030310 97 | UREAL Fyssaml 98 | XIa:276a 99 | available 100 | 1 101 | 0 102 | 1466 103 | 8 104 | 3 105 | 106 | 107 | 47BIBSYS_UBO 108 | 1030310 109 | UREAL Mat 110 | 81 GEL 111 | available 112 | 1 113 | 0 114 | 1489 115 | 8 116 | 4 117 | 118 | 119 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 01167cam a2200373 c 4500 5 | 999217148824702204 6 | 20171003030038.0 7 | ta 8 | 150604s1992 xx#|||||||||||000|u|eng|d 9 | 10 | 0471616389 11 | 12 | 13 | 921714882-47bibsys_network 14 | 15 | 16 | (NO-TrBIB)921714882 17 | 18 | 19 | (EXLNZ-47BIBSYS_NETWORK)999217148824702201 20 | 21 | 22 | NO-TrBIB 23 | nob 24 | katreg 25 | 26 | 27 | 535 28 | 29 | 30 | 681.7 31 | 32 | 33 | 534 34 | 35 | 36 | b4170 37 | NoOU 38 | inspec 39 | 40 | 41 | a7820h 42 | NoOU 43 | inspec 44 | 45 | 46 | a4280 47 | NoOU 48 | inspec 49 | 50 | 51 | 2.6xuj 52 | LOCAL 53 | 54 | 55 | a4280 56 | LOCAL 57 | 58 | 59 | a7820h 60 | LOCAL 61 | 62 | 63 | b4170 64 | LOCAL 65 | 66 | 67 | Xu, Jieping 68 | (NO-TrBIB)90625942 69 | 70 | 71 | Acousto-optic devices : 72 | principles, design, and applications 73 | Jieping Xu, Robert Stroud 74 | 75 | 76 | New York 77 | Wiley 78 | c1992 79 | 80 | 81 | xvii, 652 s. 82 | ill. 83 | 84 | 85 | Wiley series in pure and applied optics 86 | 87 | 88 | "A Wiley-Interscience publication" 89 | 90 | 91 | Acoustooptical devices 92 | 93 | 94 | Komponenter 95 | noubomn 96 | 97 | 98 | Akustikk 99 | noubomn 100 | (NO-TrBIB)REAL013572 101 | 102 | 103 | Optikk 104 | tekord 105 | 106 | 107 | Akustikk 108 | tekord 109 | 110 | 111 | Optiske instrumenter 112 | tekord 113 | 114 | 115 | akustooptiske 116 | effekter 117 | komponenter 118 | 119 | 120 | Stroud, Robert 121 | (NO-TrBIB)90625943 122 | 123 | 124 | 80 125 | 126 | 127 | 999217148824702204 128 | 22113844840002204 129 | 47BIBSYS_UBO 130 | 1030310 131 | UREAL Fys. 132 | 2.6 XUJ 133 | available 134 | 1 135 | 0 136 | k00440 137 | 8 138 | 1 139 | UiO Realfagsbiblioteket 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 01420cam a2200397 c 4500 4 | 990929710914702204 5 | 20160622155135.0 6 | ta 7 | cr|||||||||||| 8 | 130618s2009 no#||||j |||||000|p|nob| 9 | 10 | 9788202308414 11 | ib. 12 | Nkr 279.00 13 | 14 | 15 | 092971091-47bibsys_network 16 | 17 | 18 | (NO-TrBIB)092971091 19 | 20 | 21 | (NO-OsBA)0249171 22 | 23 | 24 | (NO-OsBA)0249171 25 | 26 | 27 | (NO-TrBIB)122619072 28 | 29 | 30 | (EXLNZ-47BIBSYS_NETWORK)990929710914702201 31 | 32 | 33 | NO-OsNB 34 | nob 35 | katreg 36 | 37 | 38 | eng 39 | 40 | 41 | norbibl 42 | 43 | 44 | no 45 | 46 | 47 | 820 48 | 49 | 50 | 821 51 | NO-OsNB 52 | 5/nor 53 | 54 | 55 | S 13c/US 56 | oosk 57 | 58 | 59 | 820 60 | NoOU 61 | LOCAL 62 | 63 | 64 | Eliot, T.S. 65 | 1888-1965 66 | (NO-TrBIB)90052479 67 | 68 | 69 | Å nærme seg en katt 70 | T.S. Eliot ; gjendiktet av Paal Brekke ; illustrert av Axel Scheffler 71 | 72 | 73 | Old Possum's book of practical cats 74 | Originaltittel 75 | 76 | 77 | [Oslo] 78 | Cappelen Damm 79 | 2009 80 | 81 | 82 | 64 s. 83 | ill. 84 | 85 | 86 | 1. norske utg. Oslo : Grøndahl, 1985 87 | 88 | 89 | regler 90 | dikt 91 | katter 92 | 93 | 94 | Brekke, Paal, 95 | 1923-1993, 96 | overs. 97 | trl 98 | (NO-TrBIB)90079454 99 | 100 | 101 | Scheffler, Axel, 102 | 1957- 103 | illustr. 104 | (NO-TrBIB)90983587 105 | 106 | 107 | Beskrivelse fra forlaget (kort) 108 | http://content.bibsys.no/content/?type=descr_publ_brief&isbn=8202308410 109 | 110 | 111 | Beskrivelse fra Forlagssentralen 112 | http://content.bibsys.no/content/?type=descr_forlagssentr&isbn=8202308410 113 | 114 | 115 | 90 116 | 117 | 118 | Norbok 119 | NB 120 | 121 | 122 | 990929710914702204 123 | 22110775660002204 124 | 47BIBSYS_UBO 125 | 1030300 126 | UHS Mag312 127 | 820 Eli:Old 128 | available 129 | 1 130 | 0 131 | k00025 132 | 8 133 | 1 134 | UiO HumSam-biblioteket 135 | 136 | -------------------------------------------------------------------------------- /tests/RecordTest.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 99999cam a2299999 u 4500 23 | 98218834x 24 | 25 | 8200424421 26 | h. 27 | Nkr 98.00 28 | 29 | '; 30 | 31 | $record = Record::fromString($source); 32 | $this->assertInstanceOf(Record::class, $record); 33 | $this->assertInstanceOf(BibliographicRecord::class, $record); 34 | } 35 | 36 | public function testExampleWithoutNs() 37 | { 38 | $source = ' 39 | 40 | 99999cam a2299999 u 4500 41 | 98218834x 42 | 43 | 8200424421 44 | h. 45 | Nkr 98.00 46 | 47 | '; 48 | 49 | $record = Record::fromString($source); 50 | $this->assertInstanceOf(Record::class, $record); 51 | $this->assertInstanceOf(BibliographicRecord::class, $record); 52 | } 53 | 54 | public function testExampleWithCustomPrefix() 55 | { 56 | $source = ' 57 | 58 | 99999cam a2299999 u 4500 59 | 98218834x 60 | 61 | 8200424421 62 | h. 63 | Nkr 98.00 64 | 65 | '; 66 | 67 | $record = Record::fromString($source); 68 | $this->assertInstanceOf(Record::class, $record); 69 | $this->assertInstanceOf(BibliographicRecord::class, $record); 70 | } 71 | 72 | public function testBinaryMarc() 73 | { 74 | $record = Record::fromFile(self::pathTo('binary-marc.mrc')); 75 | $this->assertInstanceOf(Record::class, $record); 76 | } 77 | 78 | public function testThatFieldObjectsAreReturned() 79 | { 80 | $record = Record::fromFile(self::pathTo('binary-marc.mrc')); 81 | $this->assertInstanceOf(Field::class, $record->getField('020')); 82 | $this->assertInstanceOf(Field::class, $record->getFields('020')[0]); 83 | } 84 | 85 | public function testRecordTypeBiblio() 86 | { 87 | $source = ' 88 | 89 | 99999cam a2299999 u 4500 90 | '; 91 | 92 | $record = Record::fromString($source); 93 | $this->assertInstanceOf(Record::class, $record); 94 | $this->assertInstanceOf(BibliographicRecord::class, $record); 95 | } 96 | 97 | public function testRecordTypeDescriptiveCatalogingForm() 98 | { 99 | $source = ' 100 | 101 | 99999cam a2299999 c 4500 102 | '; 103 | 104 | $record = Record::fromString($source); 105 | $this->assertEquals(Marc21::ISBD_PUNCTUATION_OMITTED, $record->catalogingForm); 106 | } 107 | 108 | /** 109 | * Test the getRecord method. 110 | * 111 | * @throws \File_MARC_Exception 112 | */ 113 | public function testGetRecord() 114 | { 115 | $source = ' 116 | 117 | 99999cam a2299999 c 4500 118 | '; 119 | $wrapped_record = new File_MARC_Record(new File_MARC($source, File_MARC::SOURCE_STRING)); 120 | $wrapper = new Record($wrapped_record); 121 | 122 | // Make sure that the exact same wrapped record object is returned 123 | // by the getter. 124 | $this->assertSame($wrapped_record, $wrapper->getRecord()); 125 | } 126 | 127 | /** 128 | * Test that a Record wrapper object will not be initialized from 129 | * an SimpleXMLElement object that doesn't contain a MARC record. 130 | */ 131 | public function testInitializeFromInvalidSimpleXMLElement() 132 | { 133 | $source = simplexml_load_string( 134 | '' 135 | ); 136 | 137 | $this->expectException(RecordNotFound::class); 138 | $record = Record::fromSimpleXMLElement($source); 139 | } 140 | 141 | /** 142 | * Test that a Record wrapper object can be initialized from 143 | * a SimpleXMLElement object. 144 | */ 145 | public function testInitializeFromSimpleXmlElement() 146 | { 147 | $source = simplexml_load_string(' 148 | 149 | 99999cam a2299999 u 4500 150 | 98218834x 151 | 152 | 8200424421 153 | h. 154 | Nkr 98.00 155 | 156 | '); 157 | 158 | $record = Record::fromSimpleXMLElement($source); 159 | $this->assertInstanceOf(Record::class, $record); 160 | $this->assertInstanceOf(BibliographicRecord::class, $record); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/BibliographicRecord.php: -------------------------------------------------------------------------------- 1 | record->getLeader(); 36 | return substr($leader, 18, 1); 37 | } 38 | 39 | /************************************************************************* 40 | * Helper methods for specific fields. Each of these are supported by 41 | * a class in src/Fields/ 42 | *************************************************************************/ 43 | 44 | /** 45 | * Get an array of the 020 fields as `Isbn` objects. 46 | * 47 | * @return Isbn[] 48 | */ 49 | public function getIsbns(): array 50 | { 51 | return Isbn::get($this); 52 | } 53 | 54 | /** 55 | * Get the 245 field as a `Title` object. Returns null if no such field was found. 56 | * 57 | * @return Title|null 58 | */ 59 | public function getTitle(): ?Title 60 | { 61 | return Title::get($this); 62 | } 63 | 64 | /** 65 | * Get 250 as an `Edition` object. Returns null if no such field was found. 66 | * 67 | * @return Edition|null 68 | */ 69 | public function getEdition(): ?Edition 70 | { 71 | return Edition::get($this); 72 | } 73 | 74 | /** 75 | * Get 26[04]$b as a `Publisher` object. Returns null if no such field was found. 76 | * 77 | * @return Publisher|null 78 | */ 79 | public function getPublisher(): ?Publisher 80 | { 81 | return Publisher::get($this); 82 | } 83 | 84 | /** 85 | * Get the publication year from 008 86 | * 87 | * @return string 88 | */ 89 | public function getPubYear(): string 90 | { 91 | return substr($this->query('008')->text(), 7, 4); 92 | } 93 | 94 | /** 95 | * Get TOC 96 | * 97 | * @return array|null 98 | */ 99 | public function getToc(): ?array 100 | { 101 | $field = $this->getField('505'); 102 | if ($field) { 103 | if ($field->getIndicator(2) === '0') { 104 | // Enhanced 105 | $out = [ 106 | 'text' => [], 107 | ]; 108 | foreach ($field->getSubfields('t') as $sf) { 109 | $out['text'][] = $sf->getData(); 110 | } 111 | $out['text'] = implode("\n", $out['text']); 112 | 113 | return $out; 114 | } else { 115 | // Basic 116 | return $field->mapSubFields([ 117 | 'a' => 'text', 118 | ]); 119 | } 120 | } 121 | return null; 122 | } 123 | 124 | /** 125 | * Get Summary 126 | * 127 | * @return array|null 128 | */ 129 | public function getSummary(): array|null 130 | { 131 | $field = $this->getField('520'); 132 | if ($field) { 133 | return $field->mapSubFields([ 134 | 'a' => 'text', 135 | 'c' => 'assigning_source', 136 | ]); 137 | } 138 | return null; 139 | } 140 | 141 | /** 142 | * Get an array of the 6XX fields as `SubjectInterface` objects, optionally 143 | * filtered by vocabulary and/or tag. 144 | * 145 | * @param string|null $vocabulary 146 | * @param string|string[]|null $tag 147 | * @return SubjectInterface[] 148 | */ 149 | public function getSubjects(string $vocabulary = null, array|string $tag = null): array 150 | { 151 | $tag = is_null($tag) ? [] : (is_array($tag) ? $tag : [$tag]); 152 | 153 | return array_values(array_filter(Subject::get($this), function (SubjectInterface $subject) use ($vocabulary, $tag) { 154 | $a = is_null($vocabulary) || $vocabulary == $subject->getVocabulary(); 155 | $b = empty($tag) || in_array($subject->getType(), $tag); 156 | 157 | return $a && $b; 158 | })); 159 | } 160 | 161 | /** 162 | * Get an array of the 080, 082, 083, 084 fields as `Classification` objects, optionally 163 | * filtered by scheme and/or tag. 164 | * 165 | * @param string|null $scheme 166 | * @return Classification[] 167 | */ 168 | public function getClassifications(string $scheme = null): array 169 | { 170 | return array_values(array_filter(Classification::get($this), function ($classifications) use ($scheme) { 171 | $a = is_null($scheme) || $scheme == $classifications->getScheme(); 172 | 173 | return $a; 174 | })); 175 | } 176 | 177 | /** 178 | * Get an array of the 100 and 700 fields as `Person` objects, optionally 179 | * filtered by tag. 180 | * 181 | * @param string|string[]|null $tag 182 | * @return Person[] 183 | */ 184 | public function getCreators(array|string $tag = null): array 185 | { 186 | $tag = is_null($tag) ? [] : (is_array($tag) ? $tag : [$tag]); 187 | 188 | return array_values(array_filter(Person::get($this), function (Person $person) use ($tag) { 189 | return empty($tag) || in_array($person->getType(), $tag); 190 | })); 191 | } 192 | 193 | /** 194 | * Get part of from 773. 195 | * 196 | * @return array|null 197 | */ 198 | public function getPartOf(): ?array 199 | { 200 | $field = $this->getField('773'); 201 | if ($field) { 202 | return $field->mapSubFields([ 203 | 'i' => 'relationship', 204 | 't' => 'title', 205 | 'x' => 'issn', 206 | 'w' => 'id', 207 | 'v' => 'volume', 208 | ]); 209 | } 210 | return null; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 30 | } 31 | 32 | /** 33 | * Load records from a file (Binary MARC or XML). 34 | * 35 | * @param string $filename 36 | * @return Collection 37 | */ 38 | public static function fromFile(string $filename): Collection 39 | { 40 | $importer = new Importer(); 41 | 42 | return $importer->fromFile($filename); 43 | } 44 | 45 | /** 46 | * Load records from a string (Binary MARC or XML). 47 | * 48 | * @param string $data 49 | * @return Collection 50 | */ 51 | public static function fromString(string $data): Collection 52 | { 53 | $importer = new Importer(); 54 | 55 | return $importer->fromString($data); 56 | } 57 | 58 | /** 59 | * Load records from a SimpleXMLElement object. 60 | * 61 | * @param SimpleXMLElement $element 62 | * @return Collection 63 | */ 64 | public static function fromSimpleXMLElement(SimpleXMLElement $element): Collection 65 | { 66 | $importer = new XmlImporter($element); 67 | 68 | return $importer->getCollection(); 69 | } 70 | 71 | /** 72 | * Determines if a record is a bibliographic, holdings or authority record. 73 | * 74 | * @param File_MARC_Record $record 75 | * @return string 76 | */ 77 | public static function getRecordType(File_MARC_Record $record): string 78 | { 79 | $leader = $record->getLeader(); 80 | $recordType = substr($leader, 6, 1); 81 | 82 | switch ($recordType) { 83 | case 'a': // Language material 84 | case 'c': // Notated music 85 | case 'd': // Manuscript notated music 86 | case 'e': // Cartographic material 87 | case 'f': // Manuscript cartographic material 88 | case 'g': // Projected medium 89 | case 'i': // Nonmusical sound recording 90 | case 'j': // Musical sound recording 91 | case 'k': // Two-dimensional nonprojectable graphic 92 | case 'm': // Computer file 93 | case 'o': // Kit 94 | case 'p': // Mixed materials 95 | case 'r': // Three-dimensional artifact or naturally occurring object 96 | case 't': // Manuscript language material 97 | return Marc21::BIBLIOGRAPHIC; 98 | case 'z': 99 | return Marc21::AUTHORITY; 100 | case 'u': // Unknown 101 | case 'v': // Multipart item holdings 102 | case 'x': // Single-part item holdings 103 | case 'y': // Serial item holdings 104 | return Marc21::HOLDINGS; 105 | default: 106 | throw new UnknownRecordType(); 107 | } 108 | } 109 | 110 | /** 111 | * Returns an array representation of the collection. 112 | * 113 | * @return Collection[] 114 | */ 115 | public function toArray(): array 116 | { 117 | return iterator_to_array($this); 118 | } 119 | 120 | /** 121 | * Return the first record in the collection. 122 | * 123 | * @return Record|HoldingsRecord|BibliographicRecord|AuthorityRecord|null 124 | * @throws RecordNotFound if the collection is empty 125 | */ 126 | public function first(): Record|HoldingsRecord|BibliographicRecord|AuthorityRecord|null 127 | { 128 | $this->rewind(); 129 | if (is_null($this->current())) { 130 | throw new RecordNotFound(); 131 | } 132 | return $this->current(); 133 | } 134 | 135 | /** 136 | * Creates a Record object from a File_MARC_Record object. 137 | * 138 | * @param File_MARC_Record $record 139 | * @return Record|HoldingsRecord|BibliographicRecord|AuthorityRecord|null 140 | */ 141 | public function recordFactory(File_MARC_Record $record): Record|HoldingsRecord|BibliographicRecord|AuthorityRecord|null 142 | { 143 | try { 144 | $recordType = self::getRecordType($record); 145 | } catch (UnknownRecordType $e) { 146 | return new Record($record); 147 | } 148 | return match ($recordType) { 149 | Marc21::BIBLIOGRAPHIC => new BibliographicRecord($record), 150 | Marc21::HOLDINGS => new HoldingsRecord($record), 151 | Marc21::AUTHORITY => new AuthorityRecord($record), 152 | default => null, 153 | }; 154 | } 155 | 156 | /********************************************************* 157 | * Iterator 158 | *********************************************************/ 159 | 160 | public function valid(): bool 161 | { 162 | return !is_null($this->_current); 163 | } 164 | 165 | public function current(): Record|HoldingsRecord|BibliographicRecord|AuthorityRecord|null 166 | { 167 | return $this->_current; 168 | } 169 | 170 | public function key(): int 171 | { 172 | return $this->position; 173 | } 174 | 175 | public function next(): void 176 | { 177 | ++$this->position; 178 | if ($this->useCache) { 179 | $rec = $this->_records[$this->position] ?? false; 180 | } else { 181 | $rec = isset($this->parser) ? $this->parser->next() : null; 182 | if ($rec) { 183 | $rec = $this->recordFactory($rec); 184 | $this->_records[] = $rec; 185 | } 186 | } 187 | $this->_current = $rec ?: null; 188 | } 189 | 190 | public function rewind(): void 191 | { 192 | $this->position = -1; 193 | if (is_null($this->_records)) { 194 | $this->_records = []; 195 | } else { 196 | $this->useCache = true; 197 | } 198 | $this->next(); 199 | } 200 | 201 | // public function count() 202 | // { 203 | // } 204 | 205 | /********************************************************* 206 | * Magic 207 | *********************************************************/ 208 | 209 | public function __call($name, $arguments) 210 | { 211 | return call_user_func_array([$this->parser, $name], $arguments); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /tests/TitleFieldTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Eternal darkness : sanity\'s requiem : Prima\'s official strategy guide', strval($title)); 42 | } 43 | 44 | public function testIsbdUkStyle() 45 | { 46 | // ISBD style: UK 47 | // Title components: [Main title, Other title information, Other title information] 48 | $field = new File_MARC_Data_Field('245', array( 49 | new File_MARC_Subfield('a', 'Eternal darkness'), 50 | new File_MARC_Subfield('b', 'sanity\'s requiem : Prima\'s official strategy guide'), 51 | new File_MARC_Subfield('c', 'the Stratton Bros.'), 52 | )); 53 | $title = new Title($field); 54 | $this->assertEquals('Eternal darkness : sanity\'s requiem : Prima\'s official strategy guide', strval($title)); 55 | } 56 | 57 | public function testParallelTitleUs() 58 | { 59 | // ISBD style: US 60 | // Title components: Main title, Parallel title 61 | $field = new File_MARC_Data_Field('245', array( 62 | new File_MARC_Subfield('a', 'Lageru ='), 63 | new File_MARC_Subfield('b', 'The land of eternal darkness /'), 64 | new File_MARC_Subfield('c', 'Kwak Pyŏng-gyu chŏ.'), 65 | )); 66 | $title = new Title($field); 67 | $this->assertEquals('Lageru = The land of eternal darkness', strval($title)); 68 | } 69 | 70 | public function testParallelTitleUk() 71 | { 72 | // ISBD style: UK (note: = mark still included) 73 | // Components: Main title, Parallel title 74 | $field = new File_MARC_Data_Field('245', array( 75 | new File_MARC_Subfield('a', 'Byggekunst ='), 76 | new File_MARC_Subfield('b', 'The Norwegian review of architecture'), 77 | new File_MARC_Subfield('c', 'Norske arkitekters landsforbund'), 78 | )); 79 | $title = new Title($field); 80 | $this->assertEquals('Byggekunst = The Norwegian review of architecture', strval($title)); 81 | } 82 | 83 | public function testIsbdSymbolsInTitle() 84 | { 85 | # http://lccn.loc.gov/2006589502 86 | # Here, the = does not indicate the start of a parallel title, but is part of the title. 87 | # How can we know?? Because it don't have a space in front? 88 | $field = new File_MARC_Data_Field('245', array( 89 | new File_MARC_Subfield('a', '2 + 2 = 5 :'), 90 | new File_MARC_Subfield('b', 'innovative ways of organising people in the Australian Public Service.'), 91 | )); 92 | $title = new Title($field); 93 | $this->assertEquals('2 + 2 = 5 : innovative ways of organising people in the Australian Public Service.', strval($title)); 94 | } 95 | 96 | public function testMultipleTitles() 97 | { 98 | # http://lccn.loc.gov/2006589502 99 | # An example of where we really shouldn't strip of the final dot(s) 100 | $field = new File_MARC_Data_Field('245', array( 101 | new File_MARC_Subfield('a', 'Hamlet ;'), 102 | new File_MARC_Subfield('b', 'Romeo and Juliette ; Othello ...'), 103 | )); 104 | $title = new Title($field); 105 | $this->assertEquals('Hamlet ; Romeo and Juliette ; Othello ...', strval($title)); 106 | } 107 | 108 | public function testTitleWithPart() 109 | { 110 | $field = new File_MARC_Data_Field('245', array( 111 | new File_MARC_Subfield('a', 'Love from Joy :'), 112 | new File_MARC_Subfield('b', 'letters from a farmer’s wife.'), 113 | new File_MARC_Subfield('n', 'Part III,'), 114 | new File_MARC_Subfield('p', '1987-1995, At the bungalow.'), 115 | )); 116 | $title = new Title($field); 117 | $this->assertEquals('Love from Joy : letters from a farmer’s wife. Part III, 1987-1995, At the bungalow.', strval($title)); 118 | } 119 | 120 | public function testTitleWithMultiplePartSubfields() 121 | { 122 | $field = new File_MARC_Data_Field('245', array( 123 | new File_MARC_Subfield('a', 'Zentralblatt für Bakteriologie.'), 124 | new File_MARC_Subfield('n', '1. Abt. Originale.'), 125 | new File_MARC_Subfield('n', 'Reihe B,'), 126 | new File_MARC_Subfield('p', 'Hygiene, Krankenhaushygiene, Betriebshygiene, präventive Medizin.'), 127 | )); 128 | $title = new Title($field); 129 | $this->assertEquals('Zentralblatt für Bakteriologie. 1. Abt. Originale. Reihe B, Hygiene, Krankenhaushygiene, Betriebshygiene, präventive Medizin.', strval($title)); 130 | } 131 | 132 | public function testJsonSerialization() 133 | { 134 | $field = new File_MARC_Data_Field('245', array( 135 | new File_MARC_Subfield('a', 'Zentralblatt für Bakteriologie.'), 136 | new File_MARC_Subfield('n', '1. Abt. Originale.'), 137 | new File_MARC_Subfield('n', 'Reihe B,'), 138 | new File_MARC_Subfield('p', 'Hygiene, Krankenhaushygiene, Betriebshygiene, präventive Medizin.'), 139 | )); 140 | $title = new Title($field); 141 | 142 | $this->assertJsonStringEqualsJsonString( 143 | json_encode([ 144 | 'title' => 'Zentralblatt für Bakteriologie. 1. Abt. Originale. Reihe B, Hygiene, Krankenhaushygiene, Betriebshygiene, präventive Medizin.', 145 | ]), 146 | json_encode(['title' => $title]) 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | Nothing yet 10 | 11 | ## [3.0.0] - 2024-02-27 12 | 13 | ### Changed 14 | 15 | - Updated to `pear/file_marc@dev` since the last stable release is not compatible with PHP >= 8.1 16 | ([#26](https://github.com/scriptotek/php-marc/pull/26), by [@danmichaelo](https://github.com/danmichaelo)) 17 | - Add PHP 8.3 to test matrix 18 | 19 | ## [2.2.3] - 2022-12-17 20 | 21 | ### Changed 22 | 23 | - Fixed return type for PHP 8.1 compatibility 24 | ([#23](https://github.com/scriptotek/php-marc/pull/23), by [@gerricom](https://github.com/gerricom)) 25 | - Add PHP 8.2 to test matrix 26 | 27 | ## [2.2.1] - 2021-05-06 28 | 29 | ### Changed 30 | 31 | - Fixed crash when 250 is missing 32 | ([6850aed](https://github.com/scriptotek/php-marc/commit/6850aedf9a3a41f49fd026336283c487e844919c)) 33 | 34 | ### Changed 35 | 36 | - Made `getSubfieldValues()` public. 37 | ([#21](https://github.com/scriptotek/php-marc/pull/21), by [@rudolfbyker](https://github.com/rudolfbyker)) 38 | 39 | ## [2.2.0] - 2020-09-16 40 | 41 | ### Added 42 | 43 | - Added edition property to BibliographicRecord. 44 | ([da949e6](https://github.com/scriptotek/php-marc/commit/da949e640e86be7498f26d0e74fbb6c26bfcbce3)) 45 | 46 | ### Changed 47 | 48 | - Made `getSubfieldValues()` public. 49 | ([#21](https://github.com/scriptotek/php-marc/pull/21), by [@rudolfbyker](https://github.com/rudolfbyker)) 50 | 51 | ### Fixed 52 | 53 | - Fixed return types in @method annotations. 54 | ([#20](https://github.com/scriptotek/php-marc/pull/20), by [@rudolfbyker](https://github.com/rudolfbyker)) 55 | 56 | ## [2.1.0] - 2019-11-20 57 | 58 | ### Added 59 | 60 | - Added info to contributors (CONTRIBUTING.md). 61 | ([62949a1](https://github.com/scriptotek/php-marc/commit/62949a1b2e1c309e3bf8bb58f9f8f138c0398d46)) 62 | - Added initialization from SimpleXMLElement object through the new methods 63 | `Collection:fromSimpleXMLElement($obj)` and `Record:fromSimpleXMLElement($obj)`. 64 | 65 | ### Fixed 66 | 67 | - Improved documentation and support for IDE code analysis. 68 | ([#15](https://github.com/scriptotek/php-marc/issues/15) 69 | by [@rudolfbyker](https://github.com/rudolfbyker)) 70 | 71 | ## [2.0.2] - 2019-09-13 72 | 73 | ### Added 74 | 75 | - Added new method `Field::asLineMarc()` to return a line mode Marc string 76 | representation of the field. 77 | ([ba20a6d](https://github.com/scriptotek/php-marc/commit/ba20a6deadc9402bb65807cd63e33797d2893dea)) 78 | 79 | ### Fixed 80 | 81 | - Fixed the `Subject::getParts()` method. 82 | ([1fe8408](https://github.com/scriptotek/php-marc/commit/1fe8408e49c6c3afba9ec379b441c82f64ce0336)) 83 | - Added additional subject subfield codes that were missing. 84 | ([7908616](https://github.com/scriptotek/php-marc/commit/79086165dfce9b9d2f490d38e9f50f70fef5641f)) 85 | - Added 852 $i and $j to `Location.callCode`. 86 | ([cba1508](https://github.com/scriptotek/php-marc/commit/cba15083422bb2ac812b6b355341feab2cff308a)) 87 | - Fixed the string representation of the `Location` class. 88 | ([74652a3](https://github.com/scriptotek/php-marc/commit/74652a3bf4cc3e9fe3c916057a0a9bd47419f601)) 89 | 90 | ## [2.0.1] - 2019-01-09 91 | 92 | ### Fixed 93 | 94 | - Fixed strict comparison in `Field::mapSubFields()` to avoid matching `0` 95 | to other subfields. 96 | 97 | ## [2.0.0] - 2018-10-23 98 | 99 | ### Added 100 | 101 | - Added new helper methods to `HoldingsRecord`: `getLocation()` and `getLocations()` for 852 fields. 102 | - Added new helper methods to `BibliographicRecord`: 103 | - `getCreators()` for 100 and 700 fields. 104 | - `getClassifications()` for 080, 082, 083, 084 fields. 105 | - `getPublisher()` for 26[04]$b 106 | - `getPubYear()` for pub year in 008 107 | - `getToc()` for 505 fields 108 | - `getSummary()` for 520 fields 109 | - `getPartOf()` for 773 fields 110 | - Added a `mapSubFields()` method to the `Field` class. 111 | - Made the `Record` class JSON serializable. 112 | - Added a `getType()` and `getTag()` method to `Classification`. 113 | 114 | ### Changed 115 | 116 | - Changed the `Field::sf()` method to return `NULL`, not an empty string, 117 | when no matching subfield was found. 118 | - Changed `Record::query()`, `Record::getField()` etc. to return `Field` 119 | objects rather than raw File_MARC objects. 120 | - Split the `Record` class into classes that reflect the type of 121 | record (`HoldingsRecord`, `AuthorityRecord` and `BibliographicRecord`) 122 | and inherit from the `Record` class. 123 | - Renamed `Subject::getControlNumber()` to `Subject::getId()`. 124 | - Added chopping of ending punctuation from the string representations of 125 | `Subject` and `Person` in the same way as done by Library of Congress 126 | when they convert MARC21 to MODS and BibFrame 127 | (see discussion on ISBD punctuation in [MARC DISCUSSION PAPER NO. 2010-DP01](https://www.loc.gov/marc/marbi/2010/2010-dp01.html)). 128 | 129 | ## [1.0.1] - 2017-12-04 130 | ### Fixed 131 | 132 | - Fixed a bug in `QueryResult::count()`. 133 | 134 | ## [1.0.0] - 2017-07-02 135 | ### Changed 136 | 137 | - Removed support for PHP 5.5, now requires PHP 5.6 or 7.x 138 | 139 | ## [0.3.2] - 2017-01-15 140 | 141 | ### Changed 142 | 143 | - Added `JsonSerializable` implementations to the `Field` classes to make them behave better when passed through `json_encode()`. 144 | - Officially removed PHP 5.4 support 145 | - Re-licensed as MIT (But since the dependency File_MARC is licensed under LGPL-2.1, the library cannot be used without complying with LGPL-2.1). 146 | 147 | ## [0.3.1] - 2017-01-15 148 | ### Fixed 149 | 150 | - Fixed a bug where `makeFieldObjects()` would not create the correct class. 151 | 152 | ## [0.3.0] - 2016-11-19 153 | 154 | ### Changed 155 | - `Record::get()` was replaced by `Record::query()`, which returns a `QueryResult` object rather than an array of strings. 156 | This allows access to the marc fields / subfields matched by the query. 157 | - `Collection::records` has been removed in favor of making the records available directly on the `Collection` class. 158 | Replace `foreach ($collection->records as $record)` with `foreach ($collection as $record)`. 159 | - `Subject::getType()` now returns the tag number (like "650)" instead of a string representing the tag (like "topic"). 160 | Constants have been defined on `Subject` for comparison, so to check if a subject is a topical term, 161 | you can do `$subject->type == Subject::TOPICAL_TERM`. 162 | - `Record::fromString` now throws a `RecordNotFound` exception rather than an `ErrorException` exception if no record was found. 163 | - `Record::getType` now throws a `UnknownRecordType` exception rather than an `ErrorException`. 164 | 165 | [Unreleased]: https://github.com/scriptotek/php-marc/compare/v2.1.0...HEAD 166 | [2.1.0]: https://github.com/scriptotek/php-marc/compare/v2.0.2...v2.1.0 167 | [2.0.2]: https://github.com/scriptotek/php-marc/compare/v2.0.1...v2.0.2 168 | [2.0.1]: https://github.com/scriptotek/php-marc/compare/v2.0.0...v2.0.1 169 | [2.0.0]: https://github.com/scriptotek/php-marc/compare/v1.0.1...v2.0.0 170 | [1.0.1]: https://github.com/scriptotek/php-marc/compare/v1.0.0...v1.0.1 171 | [1.0.0]: https://github.com/scriptotek/php-marc/compare/v0.3.2...v1.0.0 172 | [0.3.2]: https://github.com/scriptotek/php-marc/compare/v0.3.1...v0.3.2 173 | [0.3.1]: https://github.com/scriptotek/php-marc/compare/v0.3.0...v0.3.1 174 | [0.3.0]: https://github.com/scriptotek/php-marc/compare/v0.2.1...v0.3.0 175 | -------------------------------------------------------------------------------- /tests/data/examples/bibliographic4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 02915cam a2200337 i 4500 5 | 999919884907102204 6 | 20170721125733.0 7 | ta 8 | 160915s2017 enka 00| 0 eng|c 9 | 10 | 2016041198 11 | 12 | 13 | 9781107084940 14 | innbundet. 15 | 16 | 17 | 1107084946 18 | (hardback) 19 | 20 | 21 | (EXLNZ-47BIBSYS_NETWORK)999920236483202201 22 | 23 | 24 | OU/DLC 25 | OU 26 | rda 27 | DLC 28 | nob 29 | NO-TrBIB 30 | 31 | 32 | pcc 33 | 34 | 35 | QC611 36 | .R26 2017 37 | 38 | 39 | 537.622 40 | 23 41 | 42 | 43 | 537.62 44 | 23/nor 45 | NoOU 46 | 47 | 48 | Rammer, Jørgen 49 | 90614467 50 | aut 51 | 52 | 53 | Physics of electronic materials : 54 | fundamentals to device applications / 55 | Jørgen Rammer, Lund University. 56 | 57 | 58 | 1712 59 | 60 | 61 | Cambridge, United Kingdom ; 62 | New York, NY : 63 | Cambridge University Press, 64 | 2017 65 | 66 | 67 | XII, 438 sider. 68 | illustrasjoner 69 | 70 | 71 | text 72 | txt 73 | rdacontent 74 | 75 | 76 | unmediated 77 | n 78 | rdamedia 79 | 80 | 81 | volume 82 | nc 83 | rdacarrier 84 | 85 | 86 | Quantum mechanics -- Quantum tunneling -- Standard metal model -- Standard conductor model -- Electric circuit theory -- Quantum wells -- Particle in a periodic potential -- Bloch currents -- Crystalline solids -- Semiconductor doping -- Transistors -- Heterostructures -- Mesoscopic physics -- Arithmetic, logic and machines. 87 | 88 | 89 | "Electronic devices play a crucial role in todays societies and in the physical sciences where they originated. Contemplating that in just a few decades, technology guiding electrons and photons has emerged that makes possible oral and visual communication between peoples on opposite sides of the planet is truly a triumph of science and technology. Not to mention that equipped with a computer with access to the Internet, one can instantly access a wealth of human knowledge. The physical principles providing the understanding of the functioning of present day electronic devices should therefore be of interest not only to physicists, electrical engineers and material scientists, but to anyone with a general interest in how the wired world around us is functioning. Present day information technology is based on the physical properties of semiconductors, in particular the functioning of the transistor. The intension of this book is to take the reader from the principles of quantum mechanics through the quantum theory of metals and semiconductors all the way to how devices are used to perform their duties in electric circuits: for example functioning as amplifiers, switches, and in the hard ware of computers. The mechanics of arithmetic and logical operations are discussed and it is shown how electronic devices in the present day CMOS-technology can be carriers of arithmetic calculations and logic operations in computers"-- 90 | Provided by publisher. 91 | 92 | 93 | Semiconductors. 94 | 95 | 96 | Halvledere 97 | (NoOU)REAL005740 98 | noubomn 99 | 100 | 101 | Elektriske kretser 102 | (NoOU)REAL003680 103 | noubomn 104 | 105 | 106 | halvledere 107 | kvantemekanikk 108 | elektronikk 109 | 110 | 111 | 999919884907102204 112 | 22199228190002204 113 | 47BIBSYS_UBO 114 | 1030310 115 | UREAL Boksamling 116 | 537.62 Ram 117 | available 118 | 1 119 | 0 120 | k00423 121 | 1 122 | 1 123 | UiO Realfagsbiblioteket 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/Record.php: -------------------------------------------------------------------------------- 1 | record = $record; 68 | } 69 | 70 | /** 71 | * Get the wrapped record. 72 | * 73 | * @return \File_MARC_Record 74 | */ 75 | public function getRecord() 76 | { 77 | return $this->record; 78 | } 79 | 80 | /** 81 | * Find and wrap the specified MARC field. 82 | * 83 | * @param string $spec 84 | * The tag name. 85 | * @param bool $pcre 86 | * If true, match as a regular expression. 87 | * 88 | * @return \Scriptotek\Marc\Fields\Field|null 89 | * A wrapped field, or NULL if not found. 90 | */ 91 | public function getField($spec = null, $pcre = null) 92 | { 93 | $q = $this->record->getField($spec, $pcre); 94 | if ($q) { 95 | return new Field($q); 96 | } 97 | return null; 98 | } 99 | 100 | /** 101 | * Find and wrap the specified MARC fields. 102 | * 103 | * @param string $spec 104 | * The tag name. 105 | * @param bool $pcre 106 | * If true, match as a regular expression. 107 | * 108 | * @return \Scriptotek\Marc\Fields\Field[] 109 | * An array of wrapped fields. 110 | */ 111 | public function getFields($spec = null, $pcre = null) 112 | { 113 | return array_values(array_map(function (File_MARC_Field $field) { 114 | return new Field($field); 115 | }, $this->record->getFields($spec, $pcre))); 116 | } 117 | 118 | /************************************************************************* 119 | * Data loading 120 | *************************************************************************/ 121 | 122 | /** 123 | * Returns the first record found in the file $filename. 124 | * 125 | * @param string $filename 126 | * The name of the file containing the MARC records. 127 | * @return BibliographicRecord|HoldingsRecord|AuthorityRecord 128 | * A wrapped MARC record. 129 | * @throws RecordNotFound 130 | * When the file does not contain a MARC record. 131 | */ 132 | public static function fromFile($filename) 133 | { 134 | return Collection::fromFile($filename)->first(); 135 | } 136 | 137 | /** 138 | * Returns the first record found in the string $data. 139 | * 140 | * @param string $data 141 | * The string in which to look for MARC records. 142 | * @return BibliographicRecord|HoldingsRecord|AuthorityRecord 143 | * A wrapped MARC record. 144 | * @throws RecordNotFound 145 | * When the string does not contain a MARC record. 146 | */ 147 | public static function fromString($data) 148 | { 149 | return Collection::fromString($data)->first(); 150 | } 151 | 152 | /** 153 | * Returns the first record found in the SimpleXMLElement object 154 | * 155 | * @param SimpleXMLElement $element 156 | * The SimpleXMLElement object in which to look for MARC records. 157 | * @return BibliographicRecord|HoldingsRecord|AuthorityRecord 158 | * A wrapped MARC record. 159 | * @throws RecordNotFound 160 | * When the object does not contain a MARC record. 161 | */ 162 | public static function fromSimpleXMLElement(SimpleXMLElement $element) 163 | { 164 | return Collection::fromSimpleXMLElement($element)->first(); 165 | } 166 | 167 | /************************************************************************* 168 | * Query 169 | *************************************************************************/ 170 | 171 | /** 172 | * @param string $spec 173 | * The MARCspec string 174 | * @return QueryResult 175 | */ 176 | public function query($spec) 177 | { 178 | return new QueryResult(new File_MARC_Reference($spec, $this->record)); 179 | } 180 | 181 | /************************************************************************* 182 | * Helper methods for LDR 183 | *************************************************************************/ 184 | 185 | /** 186 | * Get the record type based on the value of LDR/6. 187 | * 188 | * @return string 189 | * Any of the Marc21::BIBLIOGRAPHIC, Marc21::AUTHORITY or Marc21::HOLDINGS 190 | * constants. 191 | * @throws UnknownRecordType 192 | */ 193 | public function getType() 194 | { 195 | return Collection::getRecordType($this->record); 196 | } 197 | 198 | /************************************************************************* 199 | * Helper methods for specific fields. Each of these are supported by 200 | * a class in src/Fields/ 201 | *************************************************************************/ 202 | 203 | /** 204 | * Get the value of the 001 field as a `ControlField` object. 205 | * 206 | * @return ControlField 207 | */ 208 | public function getId() 209 | { 210 | return ControlField::get($this, '001'); 211 | } 212 | 213 | /************************************************************************* 214 | * Support methods 215 | *************************************************************************/ 216 | 217 | /** 218 | * Convert the MARC record into an array structure fit for `json_encode`. 219 | * 220 | * @return array 221 | */ 222 | public function jsonSerialize(): array|string 223 | { 224 | $o = []; 225 | foreach ($this->properties as $prop) { 226 | $value = $this->$prop; 227 | if (is_null($value)) { 228 | $o[$prop] = $value; 229 | } elseif (is_array($value)) { 230 | $t = []; 231 | foreach ($value as $k => $v) { 232 | if (is_object($v)) { 233 | $t[$k] = $v->jsonSerialize(); 234 | } else { 235 | $t[$k] = (string) $v; 236 | } 237 | } 238 | $o[$prop] = $t; 239 | } elseif (is_object($value)) { 240 | $o[$prop] = $value->jsonSerialize(); 241 | } else { 242 | $o[$prop] = $value; 243 | } 244 | } 245 | return $o; 246 | } 247 | 248 | /** 249 | * Delegate all unknown method calls to the wrapped record. 250 | * 251 | * @param string $name 252 | * The name of the method being called. 253 | * @param array $args 254 | * The arguments being passed to the method. 255 | * 256 | * @return mixed 257 | */ 258 | public function __call($name, $args) 259 | { 260 | return call_user_func_array([$this->record, $name], $args); 261 | } 262 | 263 | /** 264 | * Get a string representation of this record. 265 | * 266 | * @return string 267 | */ 268 | public function __toString() 269 | { 270 | return strval($this->record); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /tests/data/sru-loc2.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.11marcxmlxml 3 | 03313cim a2200649 a 4500 4 | 536510 5 | 20160209192238.0 6 | m e h f 7 | cr|nna||||||da 8 | sz||mnnnnn|ned 9 | 080422s2005 dcunnnnes z| n eng 10 | 11 | 2002997016 12 | 13 | 14 | us-nls-db52877 15 | dtb 16 | 17 | 18 | DLC-B 19 | eng 20 | DLC-B 21 | 22 | 23 | eng 24 | ger 25 | 26 | 27 | SCIEAN 28 | NBDL 29 | 30 | 31 | 530.1 32 | ANF 33 | 34 | 35 | DB 52877 (May be available only for download) 36 | z 37 | 38 | 39 | Einstein, Albert, 40 | 1879-1955. 41 | 42 | 43 | Über die spezielle und die allgemeine Relativitätstheorie. 44 | English 45 | 46 | 47 | Relativity : 48 | the special and the general theory / 49 | by Albert Einstein ; authorized translation by Robert W. Lawson. 50 | [sound recording] 51 | 52 | 53 | Washington, D.C. : 54 | National Library Service for the Blind and Physically Handicapped, Library of Congress, 55 | 2005. 56 | (APH, recording studio). 57 | 58 | 59 | 1 online resource (audio (4 hours, 56 minutes)) 60 | 61 | 62 | 045600 63 | 64 | 65 | spoken word 66 | spw 67 | rdacontent 68 | 69 | 70 | audio 71 | s 72 | rdamedia 73 | 74 | 75 | computer 76 | c 77 | rdamedia 78 | 79 | 80 | online resource 81 | cr 82 | rdacarrier 83 | 84 | 85 | digital 86 | mono 87 | rda 88 | 89 | 90 | audio file 91 | Daisy 92 | rda 93 | 94 | 95 | age 96 | Adults 97 | lcdgt 98 | 99 | 100 | Originally issued by NLS on cassette in 2002. 101 | 102 | 103 | Includes bibliographical references. 104 | 105 | 106 | Availability restricted to persons meeting the eligibility requirements of the National Library Service for the Blind and Physically Handicapped, Library of Congress. 107 | 108 | 109 | Narrated by: Fred Major. 110 | 111 | 112 | Digital talking book. 1 level and 49 navigation points. Digitally mastered. 113 | 114 | 115 | Scientist Albert Einstein presents his theory of relativity--the measurement and study of space and time--for the layman who "is not conversant with the mathematical apparatus of theoretical physics." Originally published in 1916. This fifteenth edition includes five appendixes. 1952. 116 | 117 | 118 | Male narrator. 119 | NLS/BPH 120 | 121 | 122 | May also be available for loan on cartridge. Contact your cooperating library for more information. 123 | 124 | 125 | Recorded from: 126 | 15th ed. 127 | New York : Three Rivers Press, c1961. 128 | 0517884410 129 | 130 | 131 | Full audio and structure. 132 | 133 | 134 | System requirements: NLS authorized ANSI/NISO Z39.86-2002 digital talking book (dtb) player compatible with NLS flash cartridges. Web version requires computer with Internet access, BARD password and NLS authorized digital talking book player. Contact your cooperating library or the National Library Service for the Blind and Physically Handicapped, Library of Congress, for more information. 135 | 136 | 137 | 1961 138 | Estate of Albert Einstein 139 | 140 | 141 | Description based on cassette record. 142 | 143 | 144 | 1 145 | 49 146 | DM 147 | 148 | 149 | Relativity (Physics) 150 | 151 | 152 | Downloadable books. 153 | 154 | 155 | Nonfiction. 156 | 157 | 158 | Talking books. 159 | lcgft 160 | 161 | 162 | Major, Fred, 163 | narrator. 164 | 165 | 166 | DLC-B 167 | DB 52877 168 | NLS/BPH 169 | 170 | 171 | http://hdl.loc.gov/loc.nls/db.52877 172 | Downloadable talking book. 173 | xbard audio 174 | 175 | 176 | 7 177 | cbc 178 | blndbks 179 | 180 | 181 | 2002 182 | 183 | 184 | retrordapartial 185 | 186 | 187 | Retrospective copy allotment 2008 First report 188 | 189 | 190 | NLS DB GRP 5 191 | 192 | 11.1dc.creator=Einstein AND dc.date=200510xmlmarcxml 193 | -------------------------------------------------------------------------------- /src/Fields/Field.php: -------------------------------------------------------------------------------- 1 | field = $field; 96 | } 97 | 98 | /** 99 | * Get the wrapped field. 100 | * 101 | * @return File_MARC_Field 102 | */ 103 | public function getField(): File_MARC_Field 104 | { 105 | return $this->field; 106 | } 107 | 108 | /** 109 | * Delegate all unknown method calls to the wrapped field. 110 | * 111 | * @param string $name 112 | * The name of the method being called. 113 | * @param array $args 114 | * The arguments being passed to the method. 115 | * 116 | * @return mixed 117 | */ 118 | public function __call(string $name, array $args) 119 | { 120 | return call_user_func_array([$this->field, $name], $args); 121 | } 122 | 123 | /** 124 | * Get a string representation of this field. 125 | * 126 | * @return string 127 | */ 128 | public function __toString(): string 129 | { 130 | return $this->field->__toString(); 131 | } 132 | 133 | /** 134 | * Remove extra whitespace and punctuation from field values. 135 | * 136 | * @param string|null $value 137 | * The value to clean. 138 | * @param array $options 139 | * A list of options. Currently only the chopPunctuation key is used. 140 | * 141 | * @return string 142 | */ 143 | protected function clean(string $value = null, array $options = []): string 144 | { 145 | if (is_null($value)) { 146 | return ""; 147 | } 148 | $chopPunctuation = $options['chopPunctuation'] ?? static::$chopPunctuation; 149 | $value = trim($value); 150 | if ($chopPunctuation) { 151 | $value = rtrim($value, '[.:,;]$'); 152 | } 153 | return $value; 154 | } 155 | 156 | /** 157 | * Extract values from subfields of this field. 158 | * 159 | * @param string|string[] $codes 160 | * The subfield code or an array of such codes. 161 | * @return string[] 162 | * The values that were contained in the requested subfields. 163 | */ 164 | public function getSubfieldValues(array|string $codes): array 165 | { 166 | if (!is_array($codes)) { 167 | $codes = [$codes]; 168 | } 169 | $parts = []; 170 | /** @var File_MARC_Subfield $sf */ 171 | foreach ($this->field->getSubfields() as $sf) { 172 | if (in_array($sf->getCode(), $codes)) { 173 | $parts[] = trim($sf->getData()); 174 | } 175 | } 176 | 177 | return $parts; 178 | } 179 | 180 | /** 181 | * Return concatenated string of the given subfields. 182 | * 183 | * @param string[] $codes 184 | * The subfield codes to retrieve. 185 | * @param array $options 186 | * Options to pass to the `clean` method. 187 | * @return string 188 | * The concatenated subfield values. 189 | */ 190 | protected function toString(array $codes, array $options = []): string 191 | { 192 | $glue = $options['glue'] ?? static::$glue; 193 | return $this->clean(implode($glue, $this->getSubfieldValues($codes)), $options); 194 | } 195 | 196 | /** 197 | * Get a line MARC representation of the field. 198 | * 199 | * @param string $sep 200 | * Subfield separator character, defaults to '$' 201 | * @param string $blank 202 | * Blank indicator character, defaults to ' ' 203 | * @return string|null 204 | * A line MARC representation of the field or NULL if the field is empty. 205 | */ 206 | public function asLineMarc(string $sep = '$', string $blank = ' '): ?string 207 | { 208 | if ($this->field->isEmpty()) { 209 | return null; 210 | } 211 | $subfields = []; 212 | /** @var File_MARC_Subfield $sf */ 213 | foreach ($this->field->getSubfields() as $sf) { 214 | $subfields[] = $sep . $sf->getCode() . ' ' . $sf->getData(); 215 | } 216 | $tag = $this->field->getTag(); 217 | $ind1 = $this->field->getIndicator(1); 218 | $ind2 = $this->field->getIndicator(2); 219 | if ($ind1 == ' ') { 220 | $ind1 = $blank; 221 | } 222 | if ($ind2 == ' ') { 223 | $ind2 = $blank; 224 | } 225 | 226 | return "${tag} ${ind1}${ind2} " . implode(' ', $subfields); 227 | } 228 | 229 | /** 230 | * Return the data value of the *first* subfield with a given code. 231 | * 232 | * @param string $code 233 | * The subfield identifier. 234 | * @param string|null $default 235 | * The fallback value to return if the subfield does not exist. 236 | * @return string|null 237 | */ 238 | public function sf(string $code, string $default = null): ?string 239 | { 240 | // In PHP, ("a" == 0) will evaluate to TRUE, so it's actually very important that we ensure type here! 241 | $code = (string) $code; 242 | 243 | /** @var \File_MARC_Subfield $subfield */ 244 | $subfield = $this->field->getSubfield($code); 245 | if (!$subfield) { 246 | return $default; 247 | } 248 | 249 | return trim($subfield->getData()); 250 | } 251 | 252 | /** 253 | * TODO: document this function. 254 | * 255 | * @param $map 256 | * TODO: ? 257 | * @param bool $includeNullValues 258 | * TODO: ? 259 | * 260 | * @return array 261 | * TODO: ? 262 | */ 263 | public function mapSubFields(array $map, bool $includeNullValues = false): array 264 | { 265 | $o = []; 266 | foreach ($map as $code => $prop) { 267 | $value = $this->sf($code); 268 | 269 | /** @var File_MARC_Subfield $q */ 270 | foreach ($this->field->getSubfields() as $q) { 271 | if ($q->getCode() === $code) { 272 | $value = $q->getData(); 273 | } 274 | } 275 | 276 | if (!is_null($value) || $includeNullValues) { 277 | $o[$prop] = $value; 278 | } 279 | } 280 | return $o; 281 | } 282 | 283 | /** 284 | * TODO: document this function. 285 | * 286 | * @param Record $record 287 | * TODO: ? 288 | * @param string $tag 289 | * The tag name. 290 | * @param bool $pcre 291 | * If true, match as a regular expression. 292 | * 293 | * @return static|null 294 | * TODO: ? 295 | */ 296 | public static function makeFieldObject(Record $record, string $tag, bool $pcre = false): ?static 297 | { 298 | $field = $record->getField($tag, $pcre); 299 | 300 | // Note: `new static()` is a way of creating a new instance of the 301 | // called class using late static binding. 302 | return $field ? new static($field->getField()) : null; 303 | } 304 | 305 | /** 306 | * TODO: document this function. 307 | * 308 | * @param Record $record 309 | * TODO: ? 310 | * @param string $tag 311 | * The tag name. 312 | * @param bool $pcre 313 | * If true, match as a regular expression. 314 | * 315 | * @return static[] 316 | * TODO: ? 317 | */ 318 | public static function makeFieldObjects(Record $record, string $tag, bool $pcre = false): array 319 | { 320 | return array_map(function (Field $field) { 321 | // Note: `new static()` is a way of creating a new instance of the 322 | // called class using late static binding. 323 | return new static($field->getField()); 324 | }, $record->getFields($tag, $pcre)); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage](https://img.shields.io/codecov/c/github/scriptotek/php-marc)](https://codecov.io/gh/scriptotek/php-marc) 2 | [![StyleCI](https://github.styleci.io/repos/41363199/shield?branch=main)](https://styleci.io/repos/41363199) 3 | [![Code Climate](https://img.shields.io/codeclimate/maintainability/scriptotek/php-marc)](https://codeclimate.com/github/scriptotek/php-marc) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/scriptotek/marc)](https://packagist.org/packages/scriptotek/marc) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/scriptotek/marc)](https://packagist.org/packages/scriptotek/marc) 6 | 7 | # scriptotek/marc 8 | 9 | This package provides a simple interface to work with MARC21 records using the excellent 10 | [File_MARC](https://github.com/pear/File_MARC) and [MARCspec](http://marcspec.github.io/) 11 | packages. 12 | It doesn't do any of the heavy lifting itself, but instead 13 | 14 | - makes it a little bit easier to load data by automatically determining what you throw 15 | at it (Binary MARC or MARCXML, namespaced XML or not, a collection of records in some 16 | container or a single record). 17 | - adds a few extra convenience methods and a fluent interface to MARCspec. 18 | 19 | If you don't need any of this, you might want to use File_MARC directly instead. 20 | 21 | Want to contribute to this project? 22 | Please see [CONTRIBUTING.md](CONTRIBUTING.md). 23 | 24 | ## Installation using Composer: 25 | 26 | If you have [Composer](https://getcomposer.org/) installed, the package can 27 | be installed by running 28 | 29 | ``` 30 | composer require scriptotek/marc 31 | ``` 32 | 33 | ## Reading records 34 | 35 | Use `Collection::fromFile`, `Collection::fromString` or `Collection::fromSimpleXMLElement` 36 | to read one or more MARC records from a file or string. The methods autodetect the data 37 | format (Binary XML or MARCXML) and whether the XML is namespaced or not. 38 | 39 | ```php 40 | use Scriptotek\Marc\Collection; 41 | 42 | $collection = Collection::fromFile($someFileName); 43 | foreach ($collection as $record) { 44 | echo $record->getField('250')->getSubfield('a')->getData() . "\n"; 45 | } 46 | ``` 47 | 48 | The `$collection` object is an iterator. If you rather want a normal array, 49 | for instance in order to count the number of records, you can get that from 50 | `$collection->toArray()`. 51 | 52 | The loader can extract MARC records from any container XML, so you can pass 53 | in an SRU or OAI-PMH response directly: 54 | 55 | ```php 56 | $response = file_get_contents('http://lx2.loc.gov:210/lcdb?' . http_build_query([ 57 | 'operation' => 'searchRetrieve', 58 | 'recordSchema' => 'marcxml', 59 | 'version' => '1.1', 60 | 'maximumRecords' => '10', 61 | 'query' => 'bath.isbn=0761532692', 62 | ])); 63 | 64 | $records = Collection::fromString($response); 65 | foreach ($records as $record) { 66 | ... 67 | } 68 | ``` 69 | 70 | If you only have a single record, you can also use `Record::fromFile`, 71 | `Record::fromString` or `Record::fromSimpleXMLElement`. These use the 72 | `Collection` methods under the hood, but returns a single `Record` object. 73 | 74 | ```php 75 | use Scriptotek\Marc\Record; 76 | 77 | $record = Record::fromFile($someFileName); 78 | ``` 79 | 80 | ## Editing records 81 | 82 | Records can be edited using the editing capabilities of File_MARC 83 | ([API docs](https://pear.php.net/package/File_MARC/docs/latest/)). 84 | See [an example](https://github.com/scriptotek/php-marc/issues/13#issuecomment-522036879) 85 | to get started. 86 | 87 | ## Querying with MARCspec 88 | 89 | Use the `Record::query()` method to query a record using the 90 | [MARCspec](http://marcspec.github.io/) language as implemented in the 91 | [php-marc-spec package](https://github.com/MARCspec/php-marc-spec) package. 92 | The method returns a `QueryResult` object, which is a small wrapper around 93 | `File_MARC_Reference`. 94 | 95 | Example: To loop over all `650` fields having `$2 noubomn`: 96 | 97 | ```php 98 | foreach ($record->query('650{$2=\noubomn}') as $field) { 99 | echo $field->getSubfield('a')->getData(); 100 | } 101 | ``` 102 | 103 | or we could reference the subfield directly, like so: 104 | 105 | ```php 106 | foreach ($record->query('650$a{$2=\noubomn}') as $subfield) { 107 | echo $subfield->getData(); 108 | } 109 | ``` 110 | 111 | You can retrieve single results using `first()`, which returns the first match, 112 | or `null` if no matches were found: 113 | 114 | ```php 115 | $record->query('250$a')->first(); 116 | ``` 117 | 118 | In the same way, `text()` returns the data content of the first match, or `null` 119 | if no matches were found: 120 | 121 | ```php 122 | $record->query('250$a')->text(); 123 | ``` 124 | 125 | ## Convenience methods on the Record class 126 | 127 | The `Record` class extends `File_MARC_Record` with a few convenience methods to 128 | get data from commonly used fields. Each of these methods, except `getType()`, 129 | returns an object or an array of objects of one of the field classes (located in 130 | `src/Fields`). For instance `getIsbns()` returns an array of 131 | `Scriptotek\Marc\Isbn` objects. All the field classes implements at minimum a 132 | `__toString()` method so you easily can get a string representation of the field 133 | for presentation purpose. 134 | 135 | Note that all the get methods can also be accessed as attributes thanks to a 136 | little PHP magic (`__get`). So instead of calling `$record->getId()`, you can 137 | use the shorthand variant `$record->id`. 138 | 139 | ### type 140 | 141 | `$record->getType()` or `$record->type` returns either 'Bibliographic', 'Authority' 142 | or 'Holdings' based on the value of the sixth character in the leader. 143 | See `Marc21.php` for supporting constants. 144 | 145 | ```php 146 | if ($record->type == Marc21::BIBLIOGRAPHIC) { 147 | // ... 148 | } 149 | ``` 150 | 151 | ### catalogingForm 152 | 153 | `$record->getCatalogingForm()` or `$record->catalogingForm` returns the value 154 | of LDR/18. See `Marc21.php` for supporting constants. 155 | 156 | ### id 157 | 158 | `$record->getId()` or `$record->id` returns the record id from 001 control field. 159 | 160 | ### isbns 161 | 162 | `$record->getIsbns()` or `$record->isbns` returns an array of `Isbn` objects from 163 | 020 fields. 164 | 165 | ```php 166 | use Scriptotek\Marc\Record; 167 | 168 | $record = Record::fromString(' 169 | 170 | 99999cam a2299999 u 4500 171 | 98218834x 172 | 173 | 8200424421 174 | h. 175 | Nkr 98.00 176 | 177 | '); 178 | $isbn = $record->isbns[0]; 179 | 180 | // Get the string representation of the field: 181 | echo $isbn . "\n"; // '8200424421' 182 | 183 | // Get the value of $q using the standard FILE_MARC interface: 184 | echo $isbn->getSubfield('q')->getData() . "\n"; // 'h.' 185 | 186 | // or using the shorthand `sf()` method from the Field class: 187 | echo $isbn->sf('q') . "\n"; // 'h.' 188 | ``` 189 | 190 | ### title 191 | 192 | `$record->getTitle()` or `$record->title` returns a `Title` objects from 245 193 | field, or null if no such field is present. 194 | 195 | Beware that the default string representation may or may not fit your needs. 196 | It's currently a concatenation of `$a` (title), `$b` (remainder of title), 197 | `$n`(part number) and `$p` (part title). For the remaining subfields like `$f`, 198 | `$g` and `$k`, I haven't decided whether to handle them or not. 199 | 200 | Parallel titles are unfortunately encoded in such a way that there's no way I'm 201 | aware of to identify them in a secure manner, meaning there's also no secure way 202 | to remove them if you don't want to include them.[1](#f1) 203 | 204 | I'm trimming off any final '`/`' ISBD marker. I would have loved to be able to 205 | also trim off final dots, but that's not trivial for the same reason identifying 206 | parallel titles is not[1](#f1) – there's just no safe way to 207 | tell if the final dot is an ISBD marker or part of the title.[2](#f2) Since explicit ISBD markers are included in records 209 | catalogued in the American tradition, but not in records catalogued in the 210 | British tradition, a mix of records from both traditions will look silly. 211 | 212 | ### subjects 213 | 214 | `$record->getSubjects($vocabulary, $tag)` or `$record->subjects` returns an array of 215 | `Subject` and `UncontrolledSubject` objects from all 216 | [the 6XX fields](http://www.loc.gov/marc/bibliographic/bd6xx.html). 217 | The `getSubjects()` method have two optional arguments you can use to limit by 218 | vocabulary and/or tag. 219 | 220 | ```php 221 | foreach ($record->getSubjects('mesh', Subject::TOPICAL_TERM) as $subject) { 222 | echo "{$subject->vocabulary} {$subject->type} {$subject}"; 223 | } 224 | ``` 225 | 226 | Static options: 227 | 228 | * `Subject::glue` (default: ` : `) defines what string is used to glue the subfields 229 | together in the string representation. For instance, `650 $aPhysics $xHistory $yHistory` 230 | becomes `Physics : History : 20th century` when using ` : ` as glue, or 231 | `Physics--History--20th century` with `'--'`. 232 | * `Subject::chopPunctuation` (default: `true`) defines if ending punctuation (.:,;/) 233 | is to be chopped off at the end of subjects. Usually, any ending punctuation is an 234 | ISBD character that can be safely chopped off, but it might also indicate an abbreviation, 235 | and unfortunately there is no way to know. 236 | 237 | ## Notes 238 | 239 | It's unfortunately easy to err when trying to present data from MARC records in 240 | end user applications. A developer learning by example might for instance assume 241 | that `300 $a` is a subfield for "number of pages".[3](#f3) A 242 | quick glance at e.g. [LC's MARC 243 | documentation](https://www.loc.gov/marc/bibliographic/bd300.html) would be 244 | enough to prove that wrong, but in other cases it's harder to avoid making false 245 | assumptions without deep familiarity with cataloguing rules and practices. 246 | 247 | 1 That might change in the future. But even if I decide to remove parallel titles, 248 | I'm not really sure how to do it in a safe way. Parallel titles are identified by a leading `=` 249 | ISBD marker. If the marker is at the end of subfield `$a`, we can be certain it's an ISBD marker, 250 | but since the `$a` and `$c` subfields are not repeatable, multiple titles are just added to the 251 | `$c` subfield. So if we encounter an `=` sign in the middle middle of `$c` somewhere, how can we 252 | tell if it's an ISBD marker or just an equal sign part of the title (like in the fictive book 253 | `"$aEating the right way : The 2 + 2 = 5 diet"`)? Some kind of escaping would have made that clear, 254 | but the ISBD principles doesn't seem to call for that, leaving us completely in the dark. 255 | *That* is seriously annoying :weary: [↩](#a1) 256 | 257 | 2 [According to](http://www.loc.gov/marc/bibliographic/bd245.html) 258 | ISBD principles "field 245 ends with a period, even when another mark of punctuation is present, 259 | unless the last word in the field is an abbreviation, initial/letter, or data that ends with final 260 | punctuation." Determining if something is "an abbreviation, initial/letter, or data that ends with 261 | final punctuation" is certainly not an easy task for anything but humans and AI. [↩](#a2) 262 | 263 | 3 Our old OPAC used to output something like 264 | "Number of pages: One video disc (DVD)…" for DVDs – the developers had apparently just assumed that the 265 | content of `300 $a` could be represented as "number of pages" in all cases. While that sounds silly, getting 266 | the *number* of pages (for documents that actually have pages) from MARC records can be ridiculously hard; 267 | you can safely extract the number from strings like `149 p.` (English), `149 s.` (Norwegian), etc., but you 268 | must ignore the numbers in strings like `10 boxes`, `11 v.` (volumes) etc. So for a start you need a 269 | list of valid abbreviations for "pages" in all relevant languages. Then there's the more complicated cases 270 | like `1 score (16 p.)` – at first sight it looks like we can tokenize that into (number, unit) pairs, like 271 | `("1 score", "16 p.")` and only accept the item(s) having an allowed unit (like `p.`). But then suddenly 272 | comes a case like `"74 p. of ill., 15 p."`, which we would turn into `("74 p. of ill.", "15 p.")`, accepting 273 | `15 p.`, not the correct `74 p.`. So we bite into the grass and start writing rules; if a valid match is found 274 | as the start of the string, then accept it, else if …, else try tokenization, etc... it quickly becomes messy 275 | and it will certainly fail in some cases. Sad to say, after a few years in the library, I still haven't 276 | figured out a general way to extract the number of pages a document have using library data. [↩](#a3) 277 | -------------------------------------------------------------------------------- /tests/data/sru-alma.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.2 3 | 3 4 | 5 | 6 | marcxml 7 | xml 8 | 9 | 10 | 00778cam a22002531u 4500 11 | 999401461934702201 12 | 20110607103830.0 13 | ta 14 | 110607s1964 xx#|||||| |000|u|eng|d 15 | 16 | 940146193-47bibsys_ubo 17 | 18 | 19 | (NO-TrBIB)940146193 20 | 21 | 22 | (Alma)999401461934702204 23 | 24 | 25 | NO-TrBIB 26 | nob 27 | katreg 28 | 29 | 30 | 81 31 | msc 32 | 33 | 34 | 81 35 | LOCAL 36 | 37 | 38 | Gell-Mann, Murray 39 | (NO-TrBIB)x90569757 40 | 41 | 42 | The eightfold way 43 | Murray Gell-Mann, Yuval Ne'eman 44 | 45 | 46 | New York 47 | W.A. Benjamin 48 | 1964 49 | 50 | 51 | XI, 317 s. 52 | ill. 53 | 54 | 55 | Frontiers in physics 56 | 57 | 58 | Eightfold way (Nuclear physics) 59 | Addresses, essays, lectures 60 | 61 | 62 | Nuclear reactions 63 | Addresses, essays, lectures 64 | 65 | 66 | Ne'eman, Yuval 67 | (NO-TrBIB)x90061707 68 | 69 | 70 | Frontiers in physics 71 | 72 | 73 | konv 74 | 75 | 76 | 47BIBSYS_UBO 77 | 1030310 78 | UREAL Fyssaml 79 | XIa:276 80 | available 81 | 2 82 | 0 83 | 1466 84 | 8 85 | 1 86 | 87 | 88 | 47BIBSYS_UBO 89 | 1030310 90 | UREAL Fsaml 91 | F 8155 (Etter 1850) 92 | available 93 | 1 94 | 0 95 | 1463 96 | 8 97 | 2 98 | 99 | 100 | 47BIBSYS_UBO 101 | 1030310 102 | UREAL Fyssaml 103 | XIa:276a 104 | available 105 | 1 106 | 0 107 | 1466 108 | 8 109 | 3 110 | 111 | 112 | 47BIBSYS_UBO 113 | 1030310 114 | UREAL Mat 115 | 81 GEL 116 | available 117 | 1 118 | 0 119 | 1489 120 | 8 121 | 4 122 | 123 | 124 | 125 | 1 126 | 127 | 128 | marcxml 129 | xml 130 | 131 | 132 | 01113cam a2200289 u 4500 133 | 999914250144702201 134 | 20110607125822.0 135 | ta 136 | 110607s1999 xx#|||||| |100|u|eng|d 137 | 138 | 0521660661 139 | ib. 140 | 141 | 142 | 0521004195 143 | h. 144 | 145 | 146 | 991425014-47bibsys_ubo 147 | 148 | 149 | (NO-TrBIB)991425014 150 | 151 | 152 | (Alma)999914250144702204 153 | 154 | 155 | NO-TrBIB 156 | nob 157 | katreg 158 | 159 | 160 | 14 161 | msc 162 | 163 | 164 | 14 165 | LOCAL 166 | 167 | 168 | The Eightfold way : 169 | the beauty of Klein's quartic curve 170 | edited by Silvio Levy 171 | 172 | 173 | Cambridge 174 | Cambridge University Press 175 | c1999 176 | 177 | 178 | X, 331 s. 179 | 180 | 181 | Mathematical Sciences Research Institute publications 182 | 35 183 | 184 | 185 | Eightfold Way (Nuclear physics) 186 | 187 | 188 | Mathematical physics 189 | 190 | 191 | Levy, Silvio 192 | (NO-TrBIB)x90579165 193 | 194 | 195 | Mathematical Sciences Research Institute publications 196 | 0940-4740 197 | 35 198 | 999914250144702201 199 | 200 | 201 | Beskrivelse fra forlaget (kort) 202 | http://content.bibsys.no/content/?type=descr_publ_brief&isbn=0521660661 203 | 204 | 205 | Beskrivelse fra forlaget (lang) 206 | http://content.bibsys.no/content/?type=descr_publ_full&isbn=0521660661 207 | 208 | 209 | kat2 210 | 211 | 212 | 47BIBSYS_UBO 213 | 1030310 214 | UREAL Mat 215 | 14 EIG 216 | available 217 | 1 218 | 0 219 | 1489 220 | 8 221 | 1 222 | 223 | 224 | 225 | 2 226 | 227 | 228 | marcxml 229 | xml 230 | 231 | 232 | 00930cam a2200337 u 4500 233 | 997830066244702201 234 | 20131209174435.0 235 | ta 236 | cr|||||||||||| 237 | 131209s1978 xx#|||||o |000|u|eng|d 238 | 239 | 0124484603 240 | 241 | 242 | 783006624-47bibsys_ubo 243 | 244 | 245 | (NO-TrBIB)783006624 246 | 247 | 248 | (NO-TrBIB)141367571 249 | 250 | 251 | (Alma)997830066244702204 252 | 253 | 254 | NO-TrBIB 255 | nob 256 | katreg 257 | 258 | 259 | 539.12:530.131 260 | 261 | 262 | a1130 263 | inspec 264 | 265 | 266 | 3.1lic 267 | LOCAL 268 | 269 | 270 | a1130 271 | LOCAL 272 | 273 | 274 | Lichtenberg, D.B. 275 | (NO-TrBIB)x90061553 276 | 277 | 278 | Unitary symmetry and elementary particles 279 | D.B. Lichtenberg 280 | 281 | 282 | 2nd ed. 283 | 284 | 285 | New York 286 | Academic Press 287 | 1978 288 | 289 | 290 | XV, 275 s. 291 | ill. 292 | 293 | 294 | Eightfold way (Nuclear physics) 295 | 296 | 297 | Elementærpartikler 298 | noubomn 299 | 300 | 301 | Symmetri 302 | noubomn 303 | 304 | 305 | elementærpartikler 306 | symmetri 307 | 308 | 309 | 997830066244702201 310 | 311 | 312 | kat2 313 | 314 | 315 | 47BIBSYS_UBO 316 | 1030310 317 | UREAL Fys 318 | 3.1 LIC 319 | available 320 | 1 321 | 0 322 | 1464 323 | 8 324 | 1 325 | 326 | 327 | 328 | 3 329 | 330 | 331 | 332 | 333 | true 334 | 2015-07-29T13:59:02+0200 335 | 336 | 337 | --------------------------------------------------------------------------------