Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.67% covered (success)
91.67%
33 / 36
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MarcInJson
91.67% covered (success)
91.67%
33 / 36
70.00% covered (warning)
70.00%
7 / 10
20.23
0.00% covered (danger)
0.00%
0 / 1
 canParse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canParseCollection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canParseCollectionFile
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 collectionFromString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 openCollectionFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 rewind
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getNextRecord
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 jsonEncode
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3/**
4 * MARC-in-JSON format support class.
5 *
6 * PHP version 7
7 *
8 * Copyright (C) The National Library of Finland 2022.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  MARC
25 * @author   Ere Maijala <ere.maijala@helsinki.fi>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
28 */
29
30namespace VuFind\Marc\Serialization;
31
32use pcrov\JsonReader\JsonReader;
33
34use function is_array;
35use function strlen;
36
37/**
38 * MARC-in-JSON format support class.
39 *
40 * @category VuFind
41 * @package  MARC
42 * @author   Ere Maijala <ere.maijala@helsinki.fi>
43 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
44 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
45 */
46class MarcInJson extends AbstractSerializationFile implements SerializationInterface
47{
48    /**
49     * Current file
50     *
51     * @var string
52     */
53    protected $fileName = '';
54
55    /**
56     * JSON Reader for current file
57     *
58     * @var JsonReader
59     */
60    protected $reader = null;
61
62    /**
63     * Check if this class can parse the given MARC string
64     *
65     * @param string $marc MARC
66     *
67     * @return bool
68     */
69    public static function canParse(string $marc): bool
70    {
71        // A pretty naïve check, but it's enough to tell the different formats apart
72        return substr(trim($marc), 0, 1) === '{';
73    }
74
75    /**
76     * Check if the serialization class can parse the given MARC collection string
77     *
78     * @param string $marc MARC
79     *
80     * @return bool
81     */
82    public static function canParseCollection(string $marc): bool
83    {
84        // A pretty naïve check, but it's enough to tell the different formats apart
85        return substr(trim($marc), 0, 1) === '[';
86    }
87
88    /**
89     * Check if the serialization class can parse the given MARC collection file
90     *
91     * @param string $file File name
92     *
93     * @return bool
94     */
95    public static function canParseCollectionFile(string $file): bool
96    {
97        if (false === ($f = fopen($file, 'rb'))) {
98            throw new \Exception("Cannot open file '$file' for reading");
99        }
100        $s = '';
101        do {
102            $s .= fgets($f, 10);
103        } while (strlen(ltrim($s)) < 5 && !feof($f));
104        fclose($f);
105
106        return self::canParseCollection($s);
107    }
108
109    /**
110     * Parse MARC collection from a string into an array
111     *
112     * @param string $collection MARC record collection in the format supported by
113     * the serialization class
114     *
115     * @throws \Exception
116     * @return array
117     */
118    public static function collectionFromString(string $collection): array
119    {
120        return json_decode($collection, true);
121    }
122
123    /**
124     * Parse MARC-in-JSON
125     *
126     * @param string $marc JSON
127     *
128     * @throws \Exception
129     * @return array
130     */
131    public static function fromString(string $marc): array
132    {
133        return json_decode($marc, true);
134    }
135
136    /**
137     * Convert record to ISO2709 string
138     *
139     * @param array $record Record data
140     *
141     * @return string
142     */
143    public static function toString(array $record): string
144    {
145        return self::jsonEncode($record);
146    }
147
148    /**
149     * Open a collection file
150     *
151     * @param string $file File name
152     *
153     * @return void
154     *
155     * @throws \Exception
156     */
157    public function openCollectionFile(string $file): void
158    {
159        $this->fileName = $file;
160        $this->reader = new JsonReader();
161        $this->reader->open($file);
162        // Move into the record array:
163        $this->reader->read();
164    }
165
166    /**
167     * Rewind the collection file
168     *
169     * @return void
170     *
171     * @throws \Exception
172     */
173    public function rewind(): void
174    {
175        if ('' === $this->fileName) {
176            throw new \Exception('Collection file not open');
177        }
178        $this->openCollectionFile($this->fileName);
179    }
180
181    /**
182     * Get next record from the file or an empty string on EOF
183     *
184     * @return string
185     *
186     * @throws \Exception
187     */
188    public function getNextRecord(): string
189    {
190        if (null === $this->reader) {
191            throw new \Exception('Collection file not open');
192        }
193        // We have to rely on the depth since the elements are anonymous:
194        if ($this->reader->depth() === 0) {
195            // Level 0 is the array enclosing the record objects, read into it:
196            $this->reader->read();
197        } else {
198            // Level 1 is an object, get the next one:
199            $this->reader->next();
200        }
201        $value = $this->reader->value();
202        return $value ? self::jsonEncode($value) : '';
203    }
204
205    /**
206     * Convert a record array to a JSON string
207     *
208     * @param array $record Record
209     *
210     * @return string
211     */
212    protected static function jsonEncode(array $record): string
213    {
214        // We need to cast any subfield with '0' as key to an object; otherwise it
215        // would be encoded as a simple array instead of an object:
216        foreach ($record['fields'] as &$fieldData) {
217            $field = current($fieldData);
218            if (!is_array($field)) {
219                continue;
220            }
221            foreach ($field['subfields'] as &$subfield) {
222                if (key($subfield) == 0) {
223                    $subfield = (object)$subfield;
224                }
225            }
226            unset($subfield);
227            $fieldData = [key($fieldData) => $field];
228        }
229        unset($fieldData);
230        return json_encode($record, JSON_UNESCAPED_UNICODE);
231    }
232}