├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── config
└── config.php
├── index.html
├── phpunit.xml
├── provides.json
├── src
├── Client.php
├── Exceptions
│ ├── InvalidResponseException.php
│ └── SruErrorException.php
├── ExplainResponse.php
├── Facades
│ └── SruClient.php
├── Providers
│ └── SruServiceProvider.php
├── Record.php
├── Records.php
├── Response.php
├── ResponseInterface.php
└── SearchRetrieveResponse.php
└── tests
├── ClientTest.php
├── ExplainResponseTest.php
├── RecordTest.php
├── RecordsTest.php
├── SearchRetrieveResponseTest.php
└── TestCase.php
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | php_version:
12 | - "8.0"
13 | - "8.1"
14 | - "8.2"
15 | - "8.3"
16 | steps:
17 | - uses: actions/checkout@v4
18 | - name: Setup PHP
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: ${{ matrix.php_version }}
22 | coverage: pcov
23 | tools: composer:v2
24 |
25 | - run: composer install
26 |
27 | - run: composer test
28 |
29 | - run: bash <(curl -s https://codecov.io/bash)
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /build/
3 | composer.lock
4 | *.cache
5 |
--------------------------------------------------------------------------------
/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](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 |
9 | (nothing yet)
10 |
11 | ## [0.7.2](https://github.com/scriptotek/php-sru-client/compare/v0.7.1...v0.7.2) - 2021-12-21
12 |
13 | - PHP version: minimum PHP 8.0
14 |
15 | ## [0.7.1](https://github.com/scriptotek/php-sru-client/compare/v0.7.0...v0.7.1) - 2020-07-13
16 |
17 | ### Changed
18 |
19 | - Added request URL to SruErrorException.
20 |
21 | ## [0.7.0] - 2019-04-03
22 |
23 | ### Changed
24 | - Update from httplug to PSR-17/PSR-18. With PSR-17 follows the requirement of a HTTP factory implementation.
25 | Run `composer require http-interop/http-factory-guzzle` to continue using sru-client as before.
26 |
27 | - Remove support for PHP 5.6 and 7.0 because we updated the tests to work with PHPUnit 7+
28 | and can no longer run tests on PHP < 7.1. Also, PHP 5.6 and PHP 7.0 have now reached end-of-life.
29 |
30 | - Github: switched to [tag releases](https://github.com/scriptotek/php-sru-client/tags)
31 |
32 | ## [0.6.4] - 2019-03-06
33 | ### Fixed
34 | - Fix a few type hints.
35 | - Minor code style cleanups.
36 |
37 | ## [0.6.3] - 2017-08-01
38 | ### Added
39 | - Add support for setting custom headers.
40 |
41 | ### Changed
42 | - Don't throw `ClientErrorException` on 4XX responses, since some servers return 4XX responses whenever there is a diagnostic message, including zero result queries. Diagnostic messages are better handled by our `Response` class.
43 |
44 | ## [0.6.2] - 2017-08-01
45 | ### Added
46 | - Add `__toString()` method to `Record` to allow simple string serialization.
47 |
48 | ## [0.6.1] - 2017-08-01
49 | ### Changed
50 | - Update from quitesimplexmlelement 0.x to 1.x.
51 |
52 | ## [0.6.0] - 2017-07-01
53 | ### Changed
54 | - Replace hard dependency on Guzzle with HTTPlug client discovery.
55 | To continue use Guzzle, run `composer require php-http/guzzle6-adapter`
56 |
57 | [Unreleased]: https://github.com/scriptotek/php-sru-client/compare/v0.7.0...HEAD
58 | [0.7.0]: https://github.com/scriptotek/php-sru-client/compare/v0.6.4...v0.7.0
59 | [0.6.4]: https://github.com/scriptotek/php-sru-client/compare/v0.6.3...v0.6.4
60 | [0.6.3]: https://github.com/scriptotek/php-sru-client/compare/v0.6.2...v0.6.3
61 | [0.6.2]: https://github.com/scriptotek/php-sru-client/compare/v0.6.1...v0.6.2
62 | [0.6.1]: https://github.com/scriptotek/php-sru-client/compare/v0.6.0...v0.6.1
63 | [0.6.0]: https://github.com/scriptotek/php-sru-client/releases/tag/v0.6.0
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Dan Michael O. Heggø
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://codecov.io/gh/scriptotek/php-sru-client)
3 | [](https://scrutinizer-ci.com/g/scriptotek/php-sru-client/?branch=master)
4 | [](https://packagist.org/packages/scriptotek/sru-client)
5 | [](https://packagist.org/packages/scriptotek/sru-client)
6 |
7 | # php-sru-client
8 |
9 | Simple PHP package for making [Search/Retrieve via URL](http://www.loc.gov/standards/sru/) (SRU) requests and returning
10 | [QuiteSimpleXMLElement](//github.com/danmichaelo/quitesimplexmlelement) objects.
11 | The response object is an iterator, to support easy iteration over the results,
12 | abstracting away the process of making multiple requests.
13 |
14 | If you prefer a simple text response, you might have a look at
15 | the [php-sru-search](https://github.com/Zeitschriftendatenbank/php-sru-search) package.
16 |
17 | ## Install using Composer
18 |
19 | Use [Composer](https://getcomposer.org) to install sru-client with a HTTP library such as Guzzle:
20 |
21 | ```bash
22 | composer require scriptotek/sru-client php-http/guzzle6-adapter http-interop/http-factory-guzzle
23 | ```
24 |
25 | We use [HTTP discovery](https://github.com/http-interop/http-factory-discovery) to discover
26 | [HTTP client](https://packagist.org/providers/psr/http-client-implementation) and
27 | [HTTP factory](https://packagist.org/providers/psr/http-factory-implementation) implementations,
28 | so Guzzle can be swapped with any other
29 | [PSR-17](https://www.php-fig.org/psr/psr-17/)/[PSR-18](https://www.php-fig.org/psr/psr-18/)-compatible library.
30 |
31 | ## Configuring the client
32 |
33 | ```php
34 | require_once('vendor/autoload.php');
35 | use Scriptotek\Sru\Client as SruClient;
36 |
37 | $sru = new SruClient('http://bibsys-network.alma.exlibrisgroup.com/view/sru/47BIBSYS_NETWORK', [
38 | 'schema' => 'marcxml',
39 | 'version' => '1.2',
40 | 'user-agent' => 'MyTool/0.1',
41 | ]);
42 | ```
43 |
44 | ## Search and retrieve
45 |
46 | To get all the records matching a given CQL query:
47 |
48 | ```php
49 | $records = $sru->all('alma.title="Hello world"');
50 | foreach ($records as $record) {
51 | echo "Got record " . $record->position . " of " . $records->numberOfRecords() . "\n";
52 | // processRecord($record->data);
53 | }
54 | ```
55 |
56 | where `$record` is an instance of [Record](//scriptotek.github.io/php-sru-client/api_docs/Scriptotek/Sru/Record.html) and `$record->data` is an instance of [QuiteSimpleXMLElement](https://github.com/danmichaelo/quitesimplexmlelement).
57 |
58 | The `all()` method takes care of continuation for you under the hood for you;
59 | the [Records](//scriptotek.github.io/php-sru-client/api_docs/Scriptotek/Sru/Records.html) generator
60 | continues to fetch records until the result set is depleted. A default batch size of 10 is used,
61 | but you can give any number supported by the server as a second argument to the `all()` method.
62 |
63 | If you query for some identifier, you can use the convenience method `first()`:
64 |
65 | ```php
66 | $record = $sru->first('alma.isbn="0415919118"');
67 | ```
68 |
69 | The result is a [Record](//scriptotek.github.io/php-sru-client/api_docs/Scriptotek/Sru/Record.html)
70 | object, or `null` if not found.
71 |
72 |
73 | ## Use explain to get information about servers
74 |
75 | ```php
76 | $urls = array(
77 | 'http://sru.bibsys.no/search/biblio',
78 | 'http://lx2.loc.gov:210/LCDB',
79 | 'http://services.d-nb.de/sru/zdb',
80 | 'http://api.libris.kb.se/sru/libris',
81 | );
82 |
83 | foreach ($urls as $url) {
84 |
85 | $sru = new SruClient($url, [
86 | 'version' => '1.1',
87 | 'user-agent' => 'MyTool/0.1'
88 | ]);
89 |
90 | try {
91 | $response = $sru->explain();
92 | } catch (\Scriptotek\Sru\Exceptions\SruErrorException $e) {
93 | print 'ERROR: ' . $e->getMessage() . "\n";
94 | continue;
95 | }
96 |
97 | printf("Host: %s:%d\n", $response->host, $response->port);
98 | printf(" Database: %s\n", $response->database->identifier);
99 | printf(" %s\n", $response->database->title);
100 | printf(" %s\n", $response->database->description);
101 | print " Indexes:\n";
102 | foreach ($response->indexes as $idx) {
103 | printf(" - %s: %s\n", $idx->title, implode(' / ', $idx->maps));
104 | }
105 |
106 | }
107 | ```
108 |
109 | ## Laravel 5 integration
110 |
111 | Add the service provider to the `'providers'` array in `config/app.php`:
112 |
113 | Scriptotek\Sru\Providers\SruServiceProvider::class,
114 |
115 | Optionally, add the facade to the `'aliases'` array in the same file:
116 |
117 | 'SruClient' => Scriptotek\Sru\Facades\SruClient::class,
118 |
119 | To create the configuration file `config/sru.php`:
120 |
121 | $ php artisan vendor:publish --provider="Scriptotek\Sru\Providers\SruServiceProvider"
122 |
123 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scriptotek/sru-client",
3 | "type": "library",
4 | "description": "Package for making Search/Retrieve via URL requests and parse the responses",
5 | "homepage": "http://github.com/scriptotek/sru-client",
6 | "keywords": ["sru"],
7 | "require": {
8 | "php" : "^8.0",
9 | "danmichaelo/quitesimplexmlelement": "^1.0",
10 | "psr/http-client-implementation": "^1.0",
11 | "psr/http-factory-implementation": "^1.0",
12 | "http-interop/http-factory-discovery": "^1.4",
13 | "php-http/client-common": "^2.0"
14 | },
15 | "require-dev": {
16 | "phpunit/phpunit": "^8.0 || ^9.0",
17 | "php-http/mock-client": "^1.0",
18 | "http-interop/http-factory-guzzle": "^1.0",
19 | "php-http/guzzle6-adapter": "^2.0"
20 | },
21 | "license": "MIT",
22 | "authors": [
23 | {
24 | "name": "Dan Michael O. Heggø",
25 | "email": "danmichaelo@gmail.com"
26 | }
27 | ],
28 | "autoload": {
29 | "psr-4": {
30 | "Scriptotek\\Sru\\": "src/"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "Scriptotek\\Sru\\": "tests/"
36 | }
37 | },
38 | "scripts": {
39 | "test": "phpunit"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/config/config.php:
--------------------------------------------------------------------------------
1 | 'URL-TO-ENDPOINT',
12 |
13 | /*
14 | |--------------------------------------------------------------------------
15 | | Schema
16 | |--------------------------------------------------------------------------
17 | |
18 | | Default schema to use.
19 | |
20 | */
21 | 'schema' => 'marcxml',
22 |
23 | /*
24 | |--------------------------------------------------------------------------
25 | | Version
26 | |--------------------------------------------------------------------------
27 | |
28 | | Default version to use.
29 | |
30 | */
31 | 'version' => '1.1',
32 |
33 | /*
34 | |--------------------------------------------------------------------------
35 | | User-Agent
36 | |--------------------------------------------------------------------------
37 | |
38 | | Default user-agent to identify as.
39 | |
40 | */
41 | 'user-agent' => 'MyLaravelSite/0.1',
42 |
43 |
44 | );
45 |
46 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests/
6 |
7 |
8 |
9 |
10 | src
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/provides.json:
--------------------------------------------------------------------------------
1 | {
2 | "providers": [
3 | "Scriptotek\Sru\Providers\SruServiceProvider"
4 | ],
5 | "aliases": [
6 | {
7 | "alias": "SruClient",
8 | "facade": "Scriptotek\Sru\Facades\SruClient"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 | url = $url;
71 | $options = $options ?: [];
72 |
73 | $plugins = [];
74 |
75 | $this->schema = $options['schema'] ?? 'marcxml';
76 |
77 | $this->version = $options['version'] ?? '1.1';
78 |
79 | $this->headers = $options['headers'] ?? ['Accept' => 'application/xml'];
80 |
81 | if (isset($options['user-agent'])) {
82 | // legacy option
83 | $this->headers['User-Agent'] = $options['user-agent'];
84 | }
85 |
86 | if (isset($options['credentials'])) {
87 | $authentication = new BasicAuth($options['credentials'][0], $options['credentials'][1]);
88 | $plugins[] = new AuthenticationPlugin($authentication);
89 | }
90 |
91 | if (isset($options['proxy'])) {
92 | throw new \ErrorException('Not supported');
93 | }
94 |
95 | $this->httpClient = new PluginClient($httpClient ?: HttpClient::client(), $plugins);
96 | $this->requestFactory = $requestFactory ?: HttpFactory::requestFactory();
97 | }
98 |
99 | /**
100 | * Construct the URL for a CQL query
101 | *
102 | * @param string $cql The CQL query
103 | * @param int $start Start value in result set (optional)
104 | * @param int $count Number of records to request (optional)
105 | * @param array $extraParams Extra GET parameters
106 | * @return string
107 | */
108 | public function urlTo(string $cql, int $start = 1, int $count = 10, array $extraParams = []): string
109 | {
110 | $qs = array(
111 | 'operation' => 'searchRetrieve',
112 | 'version' => $this->version,
113 | 'recordSchema' => $this->schema,
114 | 'maximumRecords' => $count,
115 | 'query' => $cql
116 | );
117 |
118 | if ($start != 1) {
119 | // At least the BIBSYS SRU service, specifying startRecord results in
120 | // a less clear error message when there's no results
121 | $qs['startRecord'] = $start;
122 | }
123 |
124 | foreach ($extraParams as $key => $value) {
125 | $qs[$key] = $value;
126 | }
127 |
128 | return $this->url . '?' . http_build_query($qs);
129 | }
130 |
131 | /**
132 | * Perform a searchRetrieve request
133 | *
134 | * @param string $cql
135 | * @param int $start Start value in result set (optional)
136 | * @param int $count Number of records to request (optional)
137 | * @param array $extraParams Extra GET parameters
138 | * @return SearchRetrieveResponse
139 | *@deprecated
140 | */
141 | public function search(string $cql, int $start = 1, int $count = 10, array $extraParams = []): SearchRetrieveResponse
142 | {
143 | $url = $this->urlTo($cql, $start, $count, $extraParams);
144 | $body = $this->request('GET', $url);
145 |
146 | return new SearchRetrieveResponse($body, $this, $url);
147 | }
148 |
149 | /**
150 | * Perform a searchRetrieve request and return an iterator over the records
151 | *
152 | * @param string $cql
153 | * @param int $batchSize Number of records to request per request
154 | * @param array $extraParams Extra GET parameters
155 | * @return Records
156 | */
157 | public function all(string $cql, int $batchSize = 10, array $extraParams = []): Records
158 | {
159 | return new Records($cql, $this, $batchSize, $extraParams);
160 | }
161 |
162 | /**
163 | * Alias for `all()`
164 | * @param string $cql
165 | * @param int $batchSize
166 | * @param array $extraParams
167 | * @return Records
168 | *@deprecated
169 | */
170 | public function records(string $cql, int $batchSize = 10, array $extraParams = []): Records
171 | {
172 | return $this->all($cql, $batchSize, $extraParams);
173 | }
174 |
175 | /**
176 | * Perform a searchRetrieve request and return first record
177 | *
178 | * @param string $cql
179 | * @param array $extraParams Extra GET parameters
180 | * @return ?Record
181 | */
182 | public function first(string $cql, array $extraParams = []): ?Record
183 | {
184 | $recs = new Records($cql, $this, 1, $extraParams);
185 | return $recs->numberOfRecords() ? $recs->current() : null;
186 | }
187 |
188 | /**
189 | * Perform an explain request
190 | *
191 | * @return ExplainResponse
192 | */
193 | public function explain(): ExplainResponse
194 | {
195 | $url = $this->url . '?' . http_build_query(array(
196 | 'operation' => 'explain',
197 | 'version' => $this->version,
198 | ));
199 |
200 | $body = $this->request('GET', $url);
201 |
202 | return new ExplainResponse($body, $this, $url);
203 | }
204 |
205 | /**
206 | * @param string $method
207 | * @param string $url
208 | * @return string
209 | */
210 | public function request(string $method, string $url): string
211 | {
212 | $request = $this->requestFactory->createRequest($method, $url, $this->headers);
213 | $response = $this->httpClient->sendRequest($request);
214 |
215 | if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) {
216 | throw new ServerErrorException($response->getReasonPhrase(), $request, $response);
217 | }
218 |
219 | return (string) $response->getBody();
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/Exceptions/InvalidResponseException.php:
--------------------------------------------------------------------------------
1 | uri = $uri;
19 | $this->requestUrl = $requestUrl;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ExplainResponse.php:
--------------------------------------------------------------------------------
1 | indexes = [];
36 |
37 | if (is_null($this->response)) {
38 | return;
39 | }
40 | $explain = $this->response->first('/srw:explainResponse/srw:record/srw:recordData/exp:explain');
41 | if (!$explain) {
42 | return;
43 | }
44 |
45 | $this->parseExplainResponse($explain);
46 | }
47 |
48 | protected function parseExplainResponse(QuiteSimpleXMLElement $node)
49 | {
50 | $serverInfo = $node->first('exp:serverInfo');
51 | $dbInfo = $node->first('exp:databaseInfo');
52 | $indexInfo = $node->first('exp:indexInfo');
53 |
54 | $this->host = $serverInfo->text('exp:host');
55 | $this->port = (int) $serverInfo->text('exp:port');
56 | $this->database = new \StdClass();
57 | $this->database->identifier = $serverInfo->text('exp:database');
58 | $this->database->title = $dbInfo->text('exp:title');
59 | $this->database->description = $dbInfo->text('exp:description');
60 |
61 | foreach ($indexInfo->xpath('exp:index') as $index) {
62 | $ind = new \StdClass();
63 | $ind->scan = ($index->attr('scan') == 'true');
64 | $ind->search = ($index->attr('search') == 'true');
65 | $ind->sort = ($index->attr('sort') == 'true');
66 | $ind->title = $index->text('exp:title');
67 | $ind->maps = [];
68 | foreach ($index->xpath('exp:map') as $map) {
69 | $set = $map->first('exp:name')->attr('set');
70 | $name = $map->text('exp:name');
71 | $ind->maps[] = $set . '.' . $name;
72 | }
73 | $this->indexes[] = $ind;
74 | }
75 |
76 | // TODO
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Facades/SruClient.php:
--------------------------------------------------------------------------------
1 | publishes(array(
25 | __DIR__.'/../../config/config.php' => config_path('sru.php')
26 | ));
27 | }
28 |
29 | /**
30 | * Register the service provider.
31 | *
32 | * @return void
33 | */
34 | public function register()
35 | {
36 | $this->mergeConfigFrom(
37 | __DIR__.'/../../config/config.php',
38 | 'sru'
39 | );
40 | $this->app->singleton(SruClient::class, function ($app) {
41 | return new SruClient($app['config']->get('sru.endpoint'), $app['config']->get('sru'));
42 | });
43 | }
44 |
45 | /**
46 | * Get the services provided by the provider.
47 | *
48 | * @return array
49 | */
50 | public function provides()
51 | {
52 | return [SruClient::class];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Record.php:
--------------------------------------------------------------------------------
1 |
25 | {{recordSchema}}
26 | {{recordPacking}}
27 | {{position}}
28 | {{data}}
29 | ';
30 |
31 | /**
32 | * Create a new record
33 | * @param QuiteSimpleXMLElement $doc
34 | */
35 | public function __construct(QuiteSimpleXMLElement $doc)
36 | {
37 | $this->position = intval($doc->text('./srw:recordPosition'));
38 | $this->packing = $doc->text('./srw:recordPacking');
39 | $this->schema = $doc->text('./srw:recordSchema');
40 | $this->data = $doc->first('./srw:recordData');
41 | }
42 |
43 | /**
44 | * @param int $position
45 | * @param string|QuiteSimpleXMLElement $data
46 | * @param string $recordSchema
47 | * @param string $recordPacking
48 | * @return Record
49 | */
50 | public static function make(
51 | int $position,
52 | string|QuiteSimpleXMLElement $data,
53 | string $recordSchema='marcxchange',
54 | string $recordPacking='xml'
55 | ): Record {
56 | $record = str_replace(
57 | array('{{position}}', '{{data}}', '{{recordSchema}}', '{{recordPacking}}'),
58 | array($position, $data, $recordSchema, $recordPacking),
59 | self::$recordTpl
60 | );
61 |
62 | return new Record(QuiteSimpleXMLElement::make($record, Response::$nsPrefixes));
63 | }
64 |
65 | /**
66 | * Get the record data as a string.
67 | *
68 | * @return string
69 | */
70 | public function __toString()
71 | {
72 | $nodes = $this->data->xpath('./child::*');
73 | if (count($nodes) == 1) {
74 | return $nodes[0]->asXML();
75 | } elseif (count($nodes) > 1) {
76 | throw new \RuntimeException('recordData contains more than one node!');
77 | }
78 |
79 | return $this->data->text();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Records.php:
--------------------------------------------------------------------------------
1 | position = 1;
42 | $this->data = [];
43 | $this->count = $count; // number of records per request
44 | $this->extraParams = $extraParams;
45 | $this->cql = $cql;
46 | $this->client = $client;
47 | $this->fetchMore();
48 | }
49 |
50 | /**
51 | * Return the number of records
52 | */
53 | public function numberOfRecords(): int
54 | {
55 | return $this->lastResponse->numberOfRecords;
56 | }
57 |
58 | /**
59 | * Fetch more records from the service
60 | */
61 | private function fetchMore(): void
62 | {
63 | $url = $this->client->urlTo($this->cql, $this->position, $this->count, $this->extraParams);
64 | $body = $this->client->request('GET', $url);
65 | $this->lastResponse = new SearchRetrieveResponse($body);
66 | $this->data = $this->lastResponse->records;
67 |
68 | if (count($this->data) != 0 && $this->data[0]->position != $this->position) {
69 | throw new Exceptions\InvalidResponseException(
70 | 'Wrong index of first record in result set. '
71 | . 'Expected: ' .$this->position . ', got: ' . $this->data[0]->position
72 | );
73 | }
74 | }
75 |
76 | /**
77 | * Return the current element
78 | */
79 | public function current(): Record
80 | {
81 | return $this->data[0];
82 | }
83 |
84 | /**
85 | * Return the key of the current element
86 | */
87 | public function key(): int
88 | {
89 | return $this->position;
90 | }
91 |
92 | /**
93 | * Rewind the Iterator to the first element
94 | */
95 | public function rewind(): void
96 | {
97 | if ($this->position != 1) {
98 | $this->position = 1;
99 | $this->data = [];
100 | $this->fetchMore();
101 | }
102 | }
103 |
104 | /**
105 | * Move forward to next element
106 | */
107 | public function next(): void
108 | {
109 | if (count($this->data) > 0) {
110 | array_shift($this->data);
111 | }
112 | ++$this->position;
113 |
114 | if ($this->position > $this->numberOfRecords()) {
115 | return;
116 | }
117 |
118 | if (count($this->data) == 0) {
119 | $this->fetchMore();
120 | }
121 | }
122 |
123 | /**
124 | * Check if current position is valid
125 | */
126 | public function valid(): bool
127 | {
128 | return count($this->data) != 0;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/Response.php:
--------------------------------------------------------------------------------
1 | 'http://www.loc.gov/zing/srw/',
14 | 'exp' => 'http://explain.z3950.org/dtd/2.0/',
15 | 'd' => 'http://www.loc.gov/zing/srw/diagnostic/',
16 | 'marc' => 'http://www.loc.gov/MARC21/slim',
17 | );
18 |
19 | public static array $errorMessages = array(
20 | 'info:srw/diagnostic/1/1' => 'General system error',
21 | 'info:srw/diagnostic/1/2' => 'System temporarily unavailable',
22 | 'info:srw/diagnostic/1/3' => 'Authentication error',
23 | 'info:srw/diagnostic/1/4' => 'Unsupported operation',
24 | 'info:srw/diagnostic/1/5' => 'Unsupported version',
25 | 'info:srw/diagnostic/1/6' => 'Unsupported parameter value',
26 | 'info:srw/diagnostic/1/7' => 'Mandatory parameter not supplied',
27 | 'info:srw/diagnostic/1/8' => 'Unsupported parameter',
28 | 'info:srw/diagnostic/1/10' => 'Query syntax error',
29 | 'info:srw/diagnostic/1/12' => 'Too many characters in query',
30 | 'info:srw/diagnostic/1/13' => 'Invalid or unsupported use of parentheses',
31 | 'info:srw/diagnostic/1/14' => 'Invalid or unsupported use of quotes',
32 | 'info:srw/diagnostic/1/15' => 'Unsupported context set',
33 | 'info:srw/diagnostic/1/16' => 'Unsupported index',
34 | 'info:srw/diagnostic/1/18' => 'Unsupported combination of indexes',
35 | 'info:srw/diagnostic/1/19' => 'Unsupported relation',
36 | 'info:srw/diagnostic/1/20' => 'Unsupported relation modifier',
37 | 'info:srw/diagnostic/1/21' => 'Unsupported combination of relation modifers',
38 | 'info:srw/diagnostic/1/22' => 'Unsupported combination of relation and index',
39 | 'info:srw/diagnostic/1/23' => 'Too many characters in term',
40 | 'info:srw/diagnostic/1/24' => 'Unsupported combination of relation and term',
41 | 'info:srw/diagnostic/1/26' => 'Non special character escaped in term',
42 | 'info:srw/diagnostic/1/27' => 'Empty term unsupported',
43 | 'info:srw/diagnostic/1/28' => 'Masking character not supported',
44 | 'info:srw/diagnostic/1/29' => 'Masked words too short',
45 | 'info:srw/diagnostic/1/30' => 'Too many masking characters in term',
46 | 'info:srw/diagnostic/1/31' => 'Anchoring character not supported',
47 | 'info:srw/diagnostic/1/32' => 'Anchoring character in unsupported position',
48 | 'info:srw/diagnostic/1/33' => 'Combination of proximity/adjacency and masking characters not supported',
49 | 'info:srw/diagnostic/1/34' => 'Combination of proximity/adjacency and anchoring characters not supported',
50 | 'info:srw/diagnostic/1/35' => 'Term contains only stopwords',
51 | 'info:srw/diagnostic/1/36' => 'Term in invalid format for index or relatio',
52 | 'info:srw/diagnostic/1/37' => 'Unsupported boolean operator',
53 | 'info:srw/diagnostic/1/38' => 'Too many boolean operators in query',
54 | 'info:srw/diagnostic/1/39' => 'Proximity not supported',
55 | 'info:srw/diagnostic/1/40' => 'Unsupported proximity relation',
56 | 'info:srw/diagnostic/1/41' => 'Unsupported proximity distance',
57 | 'info:srw/diagnostic/1/42' => 'Unsupported proximity unit',
58 | 'info:srw/diagnostic/1/43' => 'Unsupported proximity ordering',
59 | 'info:srw/diagnostic/1/44' => 'Unsupported combination of proximity modifiers',
60 | 'info:srw/diagnostic/1/46' => 'Unsupported boolean modifier',
61 | 'info:srw/diagnostic/1/47' => 'Cannot process query; reason unknown',
62 | 'info:srw/diagnostic/1/48' => 'Query feature unsupported',
63 | 'info:srw/diagnostic/1/49' => 'Masking character in unsupported position',
64 | 'info:srw/diagnostic/1/50' => 'Result sets not supported',
65 | 'info:srw/diagnostic/1/51' => 'Result set does not exist',
66 | 'info:srw/diagnostic/1/52' => 'Result set temporarily unavailable',
67 | 'info:srw/diagnostic/1/53' => 'Result sets only supported for retrieval',
68 | 'info:srw/diagnostic/1/55' => 'Combination of result sets with search terms not supported',
69 | 'info:srw/diagnostic/1/58' => 'Result set created with unpredictable partial results available',
70 | 'info:srw/diagnostic/1/59' => 'Result set created with valid partial results available',
71 | 'info:srw/diagnostic/1/60' => 'Result set not created: too many matching records',
72 | 'info:srw/diagnostic/1/61' => 'First record position out of range',
73 | 'info:srw/diagnostic/1/64' => 'Record temporarily unavailable',
74 | 'info:srw/diagnostic/1/65' => 'Record does not exist',
75 | 'info:srw/diagnostic/1/66' => 'Unknown schema for retrieval',
76 | 'info:srw/diagnostic/1/67' => 'Record not available in this schema',
77 | 'info:srw/diagnostic/1/68' => 'Not authorised to send record',
78 | 'info:srw/diagnostic/1/69' => 'Not authorised to send record in this schema',
79 | 'info:srw/diagnostic/1/70' => 'Record too large to send',
80 | 'info:srw/diagnostic/1/71' => 'Unsupported record packing',
81 | 'info:srw/diagnostic/1/72' => 'XPath retrieval unsupported',
82 | 'info:srw/diagnostic/1/73' => 'XPath expression contains unsupported feature',
83 | 'info:srw/diagnostic/1/74' => 'Unable to evaluate XPath expression',
84 | 'info:srw/diagnostic/1/80' => 'Sort not supported',
85 | 'info:srw/diagnostic/1/82' => 'Unsupported sort sequence',
86 | 'info:srw/diagnostic/1/83' => 'Too many records to sort',
87 | 'info:srw/diagnostic/1/84' => 'Too many sort keys to sort',
88 | 'info:srw/diagnostic/1/86' => 'Cannot sort: incompatible record formats',
89 | 'info:srw/diagnostic/1/87' => 'Unsupported schema for sort',
90 | 'info:srw/diagnostic/1/88' => 'Unsupported path for sort',
91 | 'info:srw/diagnostic/1/89' => 'Path unsupported for schema',
92 | 'info:srw/diagnostic/1/90' => 'Unsupported direction',
93 | 'info:srw/diagnostic/1/91' => 'Unsupported case',
94 | 'info:srw/diagnostic/1/92' => 'Unsupported missing value action',
95 | 'info:srw/diagnostic/1/93' => 'Sort ended due to missing value',
96 | 'info:srw/diagnostic/1/94' => 'Sort spec included both in query and protocol: query prevails',
97 | 'info:srw/diagnostic/1/95' => 'Sort spec included both in query and protocol: protocol prevails',
98 | 'info:srw/diagnostic/1/96' => 'Sort spec included both in query and protocol: error',
99 | 'info:srw/diagnostic/1/110' => 'Stylesheets not supported',
100 | 'info:srw/diagnostic/1/120' => 'Response position out of range',
101 | 'info:srw/diagnostic/1/130' => 'Too many terms matched by masked query term',
102 | );
103 |
104 | /** @var string Raw XML response */
105 | protected string $rawResponse = '';
106 |
107 | /** @var QuiteSimpleXMLElement|null XML response */
108 | protected ?QuiteSimpleXMLElement $response = null;
109 |
110 | /** @var Client|null Reference to SRU client object */
111 | protected ?Client $client = null;
112 |
113 | /** @var string|null SRU protocol version */
114 | public ?string $version = null;
115 |
116 | /**
117 | * Create a new response
118 | *
119 | * @param string|null $text Raw XML response
120 | * @param Client|null $client SRU client reference (optional)
121 | * @param string|null $url
122 | */
123 | public function __construct(string $text = null, Client &$client = null, string $url = null)
124 | {
125 | $this->client = $client;
126 |
127 | if (!is_null($text)) {
128 | $this->initializeFromText($text, $url);
129 | }
130 | }
131 |
132 | protected function initializeFromText($text, $url)
133 | {
134 | // Fix missing namespace in Alma records:
135 | $text = str_replace('', '', $text);
136 |
137 | $this->rawResponse = $text;
138 |
139 | // Throws Danmichaelo\QuiteSimpleXMLElement\InvalidXMLException on invalid xml
140 | $this->response = QuiteSimpleXMLElement::make($text, self::$nsPrefixes);
141 |
142 | $this->version = $this->response->text('/srw:*/srw:version');
143 |
144 | $this->handleDiagnostic($url, $this->response->first('/srw:*/srw:diagnostics/d:diagnostic'));
145 | }
146 |
147 | protected function handleDiagnostic($url, QuiteSimpleXMLElement $node = null)
148 | {
149 | if (is_null($node)) {
150 | return;
151 | }
152 |
153 | // Only the 'uri' field is required, 'message' and 'details' are optional
154 | $uri = $node->text('d:uri');
155 | if (strlen($uri)) {
156 | $msg = $node->text('d:message');
157 | $details = $node->text('d:details');
158 | if (empty($msg)) {
159 | $msg = self::$errorMessages[$uri] ?? 'Unknown error';
160 | }
161 | if (!empty($details)) {
162 | $msg .= ' (' . $details . ')';
163 | }
164 | throw new Exceptions\SruErrorException($msg, $uri, $url);
165 | }
166 | }
167 |
168 | /**
169 | * Get the raw xml response
170 | *
171 | * @return string
172 | */
173 | public function asXml(): string
174 | {
175 | return $this->rawResponse;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/ResponseInterface.php:
--------------------------------------------------------------------------------
1 | response)) {
34 | return;
35 | }
36 |
37 | $this->numberOfRecords = (int) $this->response->text('/srw:searchRetrieveResponse/srw:numberOfRecords');
38 | $this->nextRecordPosition = (int) $this->response->text('/srw:searchRetrieveResponse/srw:nextRecordPosition') ?: null;
39 |
40 | // The server may echo the request back to the client along with the response
41 | $this->query = $this->response->text('/srw:searchRetrieveResponse/srw:echoedSearchRetrieveRequest/srw:query') ?: null;
42 |
43 | $this->records = [];
44 | foreach ($this->response->xpath('/srw:searchRetrieveResponse/srw:records/srw:record') as $record) {
45 | $this->records[] = new Record($record);
46 | }
47 | }
48 |
49 | /**
50 | * Request next batch of records in the result set, or return null if we're at the end of the set
51 | *
52 | * @return Response|null
53 | */
54 | public function next(): ?Response
55 | {
56 | if (is_null($this->client)) {
57 | throw new \Exception('No client reference passed to response');
58 | }
59 | if (is_null($this->query)) {
60 | throw new \Exception('No query available');
61 | }
62 | if (is_null($this->nextRecordPosition)) {
63 | return null;
64 | }
65 | return $this->client->search($this->query, $this->nextRecordPosition, count($this->records));
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/ClientTest.php:
--------------------------------------------------------------------------------
1 |
13 |
14 | ';
15 |
16 | protected $simple_explain_response = '
17 |
18 | ';
19 |
20 | public function testUrlTo()
21 | {
22 | $sru1 = new Client($this->url);
23 | $expectedUrl1 = $this->url . '?operation=searchRetrieve&version=1.1&recordSchema=marcxml&maximumRecords=10&query=isbn%3D123';
24 | $expectedUrl2 = $this->url . '?operation=searchRetrieve&version=1.1&recordSchema=marcxml&maximumRecords=50&query=isbn%3D123&startRecord=2';
25 | $expectedUrl5 = $this->url . '?operation=searchRetrieve&version=1.1&recordSchema=marcxml&maximumRecords=10&query=isbn%3D123&httpAccept=application%2Fxml';
26 |
27 | $sru3 = new Client($this->url, array('schema' => 'CUSTOMSCHEMA'));
28 | $expectedUrl3 = $this->url . '?operation=searchRetrieve&version=1.1&recordSchema=CUSTOMSCHEMA&maximumRecords=10&query=isbn%3D123';
29 |
30 | $sru4 = new Client($this->url, array('version' => '0.9'));
31 | $expectedUrl4 = $this->url . '?operation=searchRetrieve&version=0.9&recordSchema=marcxml&maximumRecords=10&query=isbn%3D123';
32 |
33 | $this->assertEquals($expectedUrl1, $sru1->urlTo('isbn=123'));
34 | $this->assertEquals($expectedUrl2, $sru1->urlTo('isbn=123', 2, 50));
35 | $this->assertEquals($expectedUrl3, $sru3->urlTo('isbn=123'));
36 | $this->assertEquals($expectedUrl4, $sru4->urlTo('isbn=123'));
37 | $this->assertEquals($expectedUrl5, $sru1->urlTo('isbn=123', 1, 10, array('httpAccept' => 'application/xml')));
38 | }
39 |
40 | public function testSearch()
41 | {
42 | $http = $this->httpMockWithResponses($this->simple_response);
43 | $sru = new Client($this->url, null, $http);
44 |
45 | $this->assertXmlStringEqualsXmlString(
46 | $this->simple_response,
47 | $sru->search('test')->asXml()
48 | );
49 | }
50 |
51 | public function testNext()
52 | {
53 | $cql = 'dc.title="Joda jada isjda"';
54 |
55 | $http = new MockHttp();
56 | $http->addResponse(
57 | (new HttpResponse())
58 | ->withBody(stream_for('
59 |
63 | 1.1
64 | 3
65 |
66 |
67 | marcxchange
68 | xml
69 | 1
70 | Record 1
71 |
72 |
73 | marcxchange
74 | xml
75 | 2
76 | Record 2
77 |
78 |
79 | 3
80 |
81 | searchRetrieve
82 | 1.1
83 | \' . $cql . \'
84 | 1
85 | 2
86 | marcxchange
87 |
88 |
89 | 2014-03-28T12:09:50Z
90 |
91 |
92 | ')
93 | )
94 | );
95 |
96 | $sru = new Client($this->url, null, $http);
97 | $response = $sru->search($cql);
98 | $this->assertCount(2, $response->records);
99 |
100 |
101 | $http->addResponse(
102 | (new HttpResponse())
103 | ->withBody(stream_for('
104 |
108 | 1.1
109 | 3
110 |
111 |
112 | marcxchange
113 | xml
114 | 3
115 | Record 3
116 |
117 |
118 |
119 | searchRetrieve
120 | 1.1
121 | ' . $cql . '
122 | 3
123 | 2
124 | marcxchange
125 |
126 |
127 | 2014-03-28T12:09:50Z
128 |
129 |
130 | '))
131 | );
132 |
133 | $response = $response->next();
134 | $this->assertCount(1, $response->records);
135 |
136 | $response = $response->next();
137 | $this->assertNull($response);
138 | }
139 |
140 | public function testHttpHeaders()
141 | {
142 | $sru1 = new Client($this->url, array(
143 | 'user-agent' => 'Blablabla/0.1',
144 | ));
145 |
146 | $opts = $sru1->headers;
147 |
148 | $this->assertEquals('application/xml', $opts['Accept']);
149 | $this->assertEquals('Blablabla/0.1', $opts['User-Agent']);
150 | }
151 |
152 | public function testRecords()
153 | {
154 | $http = $this->httpMockWithResponses($this->makeDummyResponse(1));
155 |
156 | $sru1 = new Client($this->url, [], $http);
157 | $r = $sru1->records('test', 1);
158 |
159 | $this->assertInstanceOf('Scriptotek\Sru\Records', $r);
160 | }
161 |
162 | public function testExplain()
163 | {
164 | $http = $this->httpMockWithResponses($this->simple_explain_response);
165 | $sru = new Client($this->url, null, $http);
166 | $exp = $sru->explain();
167 |
168 | $this->assertInstanceOf('Scriptotek\Sru\ExplainResponse', $exp);
169 |
170 | $this->assertXmlStringEqualsXmlString(
171 | $this->simple_explain_response,
172 | $exp->asXml()
173 | );
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/tests/ExplainResponseTest.php:
--------------------------------------------------------------------------------
1 |
10 |
11 | 1.2
12 |
13 | xml
14 | http://explain.z3950.org/dtd/2.0/
15 |
16 |
17 |
18 | sru.bibsys.no
19 | 80
20 | biblio
21 |
22 |
23 | BIBSYS Union Catalogue
24 | SRU access to the BIBSYS Union Catalogue.
25 | support@bibsys.no
26 | It is prohibited to use this service for bulk downloading of records.
27 |
28 | http://www.bibsys.no/images/logo/bibsys_logo_medium.jpg
29 |
30 |
31 |
32 | Sat Apr 26 16:47:43 CEST 2014
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Identifiserer en bibliografisk enhet
43 |
46 |
47 |
48 | An unambiguous reference to the resource within a given context.
49 |
52 |
53 |
54 | nlm nummer
55 |
58 |
59 |
60 | institusjonsspesifikke emneord
61 |
64 |
65 |
66 | en plassering
67 |
70 |
71 |
72 |
73 |
76 |
77 |
78 | intern
79 |
82 |
85 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | =
96 | marcxchange
97 | 10
98 | 50
99 |
100 |
101 |
102 |
103 | ');
104 |
105 | $this->assertEquals('1.2', $res->version);
106 | $this->assertEquals('sru.bibsys.no', $res->host);
107 | $this->assertEquals(80, $res->port);
108 | $this->assertEquals('biblio', $res->database->identifier);
109 | $this->assertEquals('BIBSYS Union Catalogue', $res->database->title);
110 | $this->assertEquals('SRU access to the BIBSYS Union Catalogue.', $res->database->description);
111 | $this->assertCount(7, $res->indexes);
112 | $this->assertFalse($res->indexes[0]->scan);
113 | $this->assertTrue($res->indexes[0]->sort);
114 | $this->assertTrue($res->indexes[0]->search);
115 | $this->assertEquals('Identifiserer en bibliografisk enhet', $res->indexes[0]->title);
116 | $this->assertEquals('bs.objektid', $res->indexes[0]->maps[0]);
117 | }
118 |
119 | public function testCanBeInitializedWithoutAnyData()
120 | {
121 | $res = new ExplainResponse;
122 | $this->assertNull($res->host);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/tests/RecordTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf('Scriptotek\Sru\Record', $record);
9 | $this->assertEquals(29, $record->position);
10 | }
11 |
12 | public function testToString() {
13 | $record = Record::make(29, 'Hello world');
14 |
15 | $this->assertEquals('Hello world', (string) $record);
16 | }
17 |
18 | public function testXmlToString() {
19 | $record = Record::make(29, 'world');
20 |
21 | $this->assertEquals('world', (string) $record);
22 | }
23 |
24 | public function testNamespacedXmlToString() {
25 | $record = Record::make(29, 'Test');
26 |
27 | $this->assertEquals('Test', (string) $record);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/RecordsTest.php:
--------------------------------------------------------------------------------
1 | makeDummyResponse($n);
11 | $http = $this->httpMockWithResponses([$response, $response]);
12 |
13 | $client = new Client($uri, [], $http);
14 | $records = new Records($cql, $client, 10);
15 | $this->assertEquals(8, $records->numberOfRecords());
16 | $records->rewind();
17 |
18 | $this->assertEquals(1, $records->key());
19 | $this->assertTrue($records->valid());
20 | $records->next();
21 | $records->next();
22 | $this->assertEquals(3, $records->key());
23 | $this->assertTrue($records->valid());
24 | $records->rewind();
25 | $this->assertEquals(1, $records->key());
26 | $this->assertTrue($records->valid());
27 |
28 | $i = 0;
29 | foreach ($records as $rec) {
30 | $i++;
31 | }
32 | $this->assertEquals($n, $i);
33 | }
34 |
35 | public function testRepeatSameResponse()
36 | {
37 | // Result set contains two records
38 | $response = $this->makeDummyResponse(2, array('maxRecords' => 1));
39 |
40 | $http = $this->httpMockWithResponses([$response, $response]);
41 | $uri = 'http://localhost';
42 | $cql = 'dummy';
43 |
44 | // Request only one record in each request
45 | $client = new Client($uri, [], $http);
46 | $rec = new Records($cql, $client, 1);
47 |
48 | // Jumping to position 2 should call fetchMore() and throw
49 | // an InvalidResponseException on getting the same response
50 | // as we got for position 1
51 | $this->expectException(\Scriptotek\Sru\Exceptions\InvalidResponseException::class);
52 | $rec->next();
53 | }
54 |
55 | public function testMultipleRequests()
56 | {
57 | $nrecs = 5;
58 |
59 | $responses = array(
60 | $this->makeDummyResponse($nrecs, array('startRecord' => 1, 'maxRecords' => 2)),
61 | $this->makeDummyResponse($nrecs, array('startRecord' => 3, 'maxRecords' => 2)),
62 | $this->makeDummyResponse($nrecs, array('startRecord' => 5, 'maxRecords' => 2))
63 | );
64 |
65 | $http = $this->httpMockWithResponses($responses);
66 | $uri = 'http://localhost';
67 | $cql = 'dummy';
68 |
69 | $client = new Client($uri, [], $http);
70 | $records = new Records($cql, $client, 10);
71 |
72 | $records->rewind();
73 | foreach (range(1, $nrecs) as $n) {
74 | $this->assertEquals($n, $records->key());
75 | $this->assertTrue($records->valid());
76 | $this->assertEquals($n, $records->current()->position);
77 | $records->next();
78 | }
79 | $this->assertFalse($records->valid());
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/SearchRetrieveResponseTest.php:
--------------------------------------------------------------------------------
1 |
10 |
14 | 1.1
15 | 1
16 |
17 |
18 | marcxchange
19 | xml
20 | 1
21 | Record 1
22 |
23 |
24 |
25 | searchRetrieve
26 | 1.1
27 | bs.avdelingsamling = "urealastr" AND bs.lokal-klass = "k C11?"
28 | 1
29 | 2
30 | marcxchange
31 |
32 |
33 | 2014-03-28T12:09:50Z
34 |
35 | ');
36 |
37 | $this->assertEquals('1.1', $res->version);
38 | $this->assertEquals(1, $res->numberOfRecords);
39 | $this->assertNull($res->nextRecordPosition);
40 |
41 | $this->assertCount(1, $res->records);
42 | $this->assertEquals(1, $res->records[0]->position);
43 | $this->assertEquals('marcxchange', $res->records[0]->schema);
44 | $this->assertEquals('xml', $res->records[0]->packing);
45 | $this->assertEquals('Record 1', $res->records[0]->data);
46 | }
47 |
48 | public function testMultipleRecordsResult()
49 | {
50 | $res = new SearchRetrieveResponse('
51 |
55 | 1.1
56 | 303
57 |
58 |
59 | marcxchange
60 | xml
61 | 1
62 | Record 1
63 |
64 |
65 | marcxchange
66 | xml
67 | 2
68 | Record 2
69 |
70 |
71 | 3
72 |
73 | searchRetrieve
74 | 1.1
75 | bs.avdelingsamling = "urealastr" AND bs.lokal-klass = "k C11?"
76 | 1
77 | 2
78 | marcxchange
79 |
80 |
81 | 2014-03-28T12:09:50Z
82 |
83 | ');
84 |
85 | $this->assertEquals('1.1', $res->version);
86 | $this->assertEquals(303, $res->numberOfRecords);
87 | $this->assertEquals(3, $res->nextRecordPosition);
88 |
89 | $this->assertCount(2, $res->records);
90 | $this->assertEquals(1, $res->records[0]->position);
91 | $this->assertEquals('marcxchange', $res->records[0]->schema);
92 | $this->assertEquals('xml', $res->records[0]->packing);
93 | $this->assertEquals('Record 1', $res->records[0]->data);
94 | }
95 |
96 | public function testErrorWithDetails()
97 | {
98 | $this->expectException(\Scriptotek\Sru\Exceptions\SruErrorException::class);
99 | $this->expectExceptionMessage("Unknown schema for retrieval (Invalid parameter: 'marcxml' for service: 'biblio'");
100 |
101 | $res = new SearchRetrieveResponse('
102 | 1.1
103 | 0
104 |
105 |
106 | info:srw/diagnostic/1/66
107 | Invalid parameter: \'marcxml\' for service: \'biblio\'
108 |
109 |
110 | ');
111 | }
112 |
113 | public function testErrorWithoutDetails()
114 | {
115 | $this->expectException(\Scriptotek\Sru\Exceptions\SruErrorException::class);
116 | $this->expectExceptionMessage("General system error");
117 |
118 | $res = new SearchRetrieveResponse('
119 | 1.1
120 | 0
121 |
122 |
123 | info:srw/diagnostic/1/1
124 |
125 |
126 | ');
127 | }
128 |
129 | public function testErrorWithCustomMessage()
130 | {
131 | $this->expectException(\Scriptotek\Sru\Exceptions\SruErrorException::class);
132 | $this->expectExceptionMessage("Too many boolean operators, the maximum is 10. Please try a less complex query. (10)");
133 |
134 | $res = new SearchRetrieveResponse('
135 | 1.1
136 | 0
137 |
138 |
139 | info:srw/diagnostic/1/10
140 | Too many boolean operators, the maximum is 10. Please try a less complex query.
141 | 10
142 |
143 |
144 | ');
145 | }
146 |
147 | // Should not throw error
148 | public function testDiagnosticsWithoutError()
149 | {
150 | $res = new SearchRetrieveResponse('
151 | 1.1
152 | 0
153 |
154 |
155 | ');
156 | $this->assertCount(0, $res->records);
157 | }
158 |
159 | public function testCanBeInitializedWithoutAnyData()
160 | {
161 | $res = new SearchRetrieveResponse;
162 | $this->assertNull($res->version);
163 | }
164 |
165 | }
166 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
10 | marcxchange
11 | xml
12 | {{position}}
13 | {{data}}
14 | ';
15 |
16 | protected $mainTpl = '
17 |
21 | 1.1
22 | {{numberOfRecords}}
23 |
24 | {{records}}
25 |
26 |
27 | searchRetrieve
28 | 1.1
29 | {{cql}}
30 | {{startRecord}}
31 | {{maxRecords}}
32 | marcxchange
33 |
34 |
35 | 2014-03-28T12:09:50Z
36 |
37 | ';
38 |
39 | /**
40 | * Get an item from an array using "dot" notation.
41 | * Source: http://laravel.com/api/source-function-array_get.html#226-251
42 | *
43 | * @param array $array
44 | * @param string $key
45 | * @param mixed $default
46 | * @return mixed
47 | */
48 | private function array_get($array, $key, $default = null)
49 | {
50 | if (is_null($key)) {
51 | return $array;
52 | }
53 |
54 | if (isset($array[$key])) {
55 | return $array[$key];
56 | }
57 |
58 | foreach (explode('.', $key) as $segment) {
59 | if (! is_array($array) or ! array_key_exists($segment, $array)) {
60 | return $default;
61 | }
62 |
63 | $array = $array[$segment];
64 | }
65 |
66 | return $array;
67 | }
68 |
69 | /**
70 | * numberOfRecords : Total number of records in response
71 | */
72 | public function makeDummyResponse($numberOfRecords = 10, $options = array())
73 | {
74 | // Request: CQL
75 | $cql = $this->array_get($options, 'cql', 'dummy');
76 |
77 | // Request: First record to fetch
78 | $startRecord = $this->array_get($options, 'startRecord', 1);
79 |
80 | // Request: Max number of records to return
81 | $maxRecords = $this->array_get($options, 'maxRecords', 10);
82 |
83 | $endRecord = $startRecord + min($maxRecords - 1, $numberOfRecords - $startRecord);
84 |
85 | $recordTpl = $this->recordTpl;
86 | $records = implode('', array_map(function ($n) use ($recordTpl) {
87 | return str_replace(
88 | array('{{position}}', '{{data}}'),
89 | array($n, 'RecordData #' . $n),
90 | $recordTpl
91 | );
92 | }, range($startRecord, $endRecord)));
93 |
94 | return str_replace(
95 | array('{{records}}', '{{cql}}', '{{startRecord}}', '{{maxRecords}}', '{{numberOfRecords}}'),
96 | array($records, $cql, $startRecord, $maxRecords, $numberOfRecords),
97 | $this->mainTpl
98 | );
99 | }
100 |
101 | /**
102 | * Returns a series of responses (no matter what request)
103 | */
104 | protected function httpMockWithResponses($bodies)
105 | {
106 | if (!is_array($bodies)) {
107 | $bodies = [$bodies];
108 | }
109 | $http = new MockHttp();
110 | foreach ($bodies as $body) {
111 | $http->addResponse(
112 | (new HttpResponse())
113 | ->withBody(stream_for($body))
114 | );
115 | }
116 |
117 | return $http;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------