├── .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 | '' . $pprefix . 'collection>';
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 | [](https://codecov.io/gh/scriptotek/php-marc)
2 | [](https://styleci.io/repos/41363199)
3 | [](https://codeclimate.com/github/scriptotek/php-marc)
4 | [](https://packagist.org/packages/scriptotek/marc)
5 | [](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 |
--------------------------------------------------------------------------------