Jacek Kowalski
4 hours ago 6b9278a5d57091fce636dbb640b380cfb990db6c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php
declare(strict_types=1);
 
namespace App\Lookup;
 
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
 
/**
 * Looks books up in the Polish National Library catalogue (https://data.bn.org.pl).
 */
class NationalLibraryLookup implements BookLookupInterface {
    private const int LIMIT = 10;
 
    public function __construct(private readonly HttpClientInterface $bnClient) {
    }
 
    public function findByStandardNumber(string $number): array {
        return $this->fetch(['isbnIssn' => str_replace(['-', ' '], '', $number)]);
    }
 
    public function findByDescription(string $title, string $author, string $publisher): array {
        return $this->fetch(array_filter(
            ['title' => $title, 'author' => $author, 'publisher' => $publisher],
            static fn(string $value): bool => $value !== '',
        ));
    }
 
    /**
     * @param array<string, string> $query
     *
     * @return list<BookCandidate>
     */
    private function fetch(array $query): array {
        try {
            $response = $this->bnClient->request('GET', '/api/networks/bibs.json', [
                'query' => $query + ['limit' => self::LIMIT, 'kind' => 'książka'],
            ]);
            $records = $response->toArray()['bibs'] ?? [];
        } catch (ExceptionInterface $exception) {
            throw new LookupFailedException('The catalogue request failed.', previous: $exception);
        }
 
        return array_map(self::toCandidate(...), $records);
    }
 
    /**
     * @param array<string, mixed> $record
     */
    private static function toCandidate(array $record): BookCandidate {
        $number = preg_replace('/[^\dX]/', '', strtoupper((string)($record['isbnIssn'] ?? ''))) ?: null;
        $isIssn = $number !== null && \strlen($number) === 8;
        $year = (string)($record['publicationYear'] ?? '');
 
        return new BookCandidate(
            title: trim((string)($record['title'] ?? ''), " \t\n\r/:;,."),
            author: self::firstPerson((string)($record['author'] ?? '')),
            language: self::blankToNull((string)($record['language'] ?? '')),
            publisher: self::singlePublisher((string)($record['publisher'] ?? '')),
            publicationLoc: self::blankToNull(trim(explode(':', (string)($record['placeOfPublication'] ?? ''))[0])),
            publicationYear: preg_match('/\d{4}/', $year, $match) === 1 ? (int)$match[0] : null,
            isbn: $isIssn ? null : $number,
            issn: $isIssn ? $number : null,
        );
    }
 
    /**
     * "MacLean, Alistair (1922-1987) Ginalski, Robert ..." becomes "Alistair MacLean".
     */
    private static function firstPerson(string $authors): ?string {
        $first = preg_split('/\s*\(\d{4}/', $authors, 2)[0];
        $parts = array_map(trim(...), explode(',', rtrim($first, " .\t\n\r")));
 
        return self::blankToNull(\count($parts) === 2 ? $parts[1] . ' ' . $parts[0] : implode(', ', $parts));
    }
 
    private static function singlePublisher(string $publisher): ?string {
        $publisher = trim($publisher, " \t\n\r\",;:.");
 
        return self::blankToNull(preg_replace('/^(.+)\s+\1$/u', '$1', $publisher) ?? $publisher);
    }
 
    private static function blankToNull(string $value): ?string {
        return trim($value) === '' ? null : trim($value);
    }
}