From fa8220f639c8a750bfeab878e23be4997523ce11 Mon Sep 17 00:00:00 2001 From: Paul Pound Date: Wed, 25 Mar 2026 14:18:35 -0300 Subject: [PATCH] initial commit --- .gitignore | 1 + README.md | 279 +++++++++++++++++++++++++++++++++++ composer.json | 24 +++ phpunit.xml | 11 ++ src/InsertRequest.php | 266 +++++++++++++++++++++++++++++++++ src/InsertResponse.php | 61 ++++++++ src/RapidIllClient.php | 123 +++++++++++++++ src/RapidIllException.php | 7 + src/RequestType.php | 13 ++ tests/RapidIllClientTest.php | 160 ++++++++++++++++++++ 10 files changed, 945 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/InsertRequest.php create mode 100644 src/InsertResponse.php create mode 100644 src/RapidIllClient.php create mode 100644 src/RapidIllException.php create mode 100644 src/RequestType.php create mode 100644 tests/RapidIllClientTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..116bba5 --- /dev/null +++ b/README.md @@ -0,0 +1,279 @@ +# RapidILL Request + +A PHP library for creating InterLibrary Loan (ILL) requests using the [RapidILL](https://exlibrisgroup.com/products/rapidill-interlibrary-loan/) API `InsertRequest` method. + +Built on PHP's native `SoapClient` with no framework dependencies, making it easy to use in any PHP project including Drupal 10. + +## Requirements + +- PHP 8.1 or higher +- PHP SOAP extension (`ext-soap`) + +## Installation + +### Composer + +```bash +composer require rapidill/request +``` + +### Drupal 10 + +Add the package to your Drupal project's `composer.json` as a path repository if installing from a local copy: + +```json +{ + "repositories": [ + { + "type": "path", + "url": "/path/to/rapidILL-request" + } + ], + "require": { + "rapidill/request": "*" + } +} +``` + +Then run: + +```bash +composer require rapidill/request +``` + +## Quick Start + +```php +use RapidIll\RapidIllClient; +use RapidIll\InsertRequest; +use RapidIll\RequestType; + +// Create a client with your RapidILL credentials. +$client = new RapidIllClient( + username: 'your_username', + password: 'your_password', + rapidCode: 'YOUR_RAPID_CODE', + branchName: 'Main', +); + +// Build and submit a request. +$request = (new InsertRequest()) + ->setRequestType(RequestType::Article) + ->setJournalTitle('Nature') + ->setArticleTitle('A breakthrough in quantum computing') + ->setArticleAuthor('Smith, J.') + ->addIssn('0028-0836'); + +$response = $client->insertRequest($request); + +if ($response->isSuccessful) { + echo "Request submitted. Rapid ID: {$response->rapidRequestId}\n"; +} +``` + +## Examples + +### Request a journal article + +```php +use RapidIll\RapidIllClient; +use RapidIll\InsertRequest; +use RapidIll\RequestType; + +$client = new RapidIllClient( + username: 'your_username', + password: 'your_password', + rapidCode: 'YOUR_RAPID_CODE', + branchName: 'Main', +); + +$request = (new InsertRequest()) + ->setRequestType(RequestType::Article) + ->setJournalTitle('Nature') + ->setJournalYear('2024') + ->setJournalVolume('625') + ->setJournalIssue('7994') + ->setJournalMonth('January') + ->setArticleTitle('A breakthrough in quantum computing') + ->setArticleAuthor('Smith, J.') + ->setArticlePages('100-105') + ->addIssn('0028-0836') + ->addIssn('1476-4687') + ->setOclcNumber('1234567') + ->setPatronName('Jane Doe') + ->setPatronEmail('jane.doe@university.edu') + ->setPatronDepartment('Physics') + ->setXrefRequestId('ILL-2024-00042'); + +$response = $client->insertRequest($request); + +if ($response->isSuccessful) { + echo "Request ID: {$response->rapidRequestId}\n"; + echo "Match found: " . ($response->foundMatch ? 'yes' : 'no') . "\n"; + echo "Available holdings: {$response->numberOfAvailableHoldings}\n"; +} else { + echo "Request failed: {$response->verificationNote}\n"; +} +``` + +### Request a book + +```php +$request = (new InsertRequest()) + ->setRequestType(RequestType::Book) + ->setJournalTitle('Introduction to Algorithms') + ->setArticleAuthor('Cormen, Thomas H.') + ->addIsbn('978-0262033848') + ->setPublisher('MIT Press') + ->setEdition('3rd') + ->setPatronName('John Smith') + ->setPatronEmail('john.smith@university.edu'); + +$response = $client->insertRequest($request); +``` + +### Request a book chapter + +```php +$request = (new InsertRequest()) + ->setRequestType(RequestType::BookChapter) + ->setJournalTitle('The Oxford Handbook of Innovation') + ->setArticleTitle('Open Innovation') + ->setArticleAuthor('Chesbrough, Henry') + ->setArticlePages('191-213') + ->addIsbn('978-0199286805') + ->setPatronName('Alice Johnson') + ->setPatronEmail('alice@university.edu'); + +$response = $client->insertRequest($request); +``` + +### Check holdings only (without submitting a request) + +```php +$request = (new InsertRequest()) + ->setRequestType(RequestType::Article) + ->setHoldingsCheckOnly(true) + ->setJournalTitle('Science') + ->addIssn('0036-8075'); + +$response = $client->insertRequest($request); + +if ($response->isLocalHolding) { + echo "Item is available locally:\n"; + foreach ($response->localHoldings as $holding) { + echo " Branch: {$holding['branchName']}\n"; + echo " Location: {$holding['location']}\n"; + echo " Call Number: {$holding['callNumber']}\n"; + } +} else { + echo "Not held locally. {$response->numberOfAvailableHoldings} remote holdings available.\n"; +} +``` + +### Error handling + +```php +use RapidIll\RapidIllException; + +try { + $response = $client->insertRequest($request); +} catch (RapidIllException $e) { + // SOAP or connection error. + error_log('RapidILL error: ' . $e->getMessage()); +} +``` + +### Debugging SOAP requests + +The client records the raw SOAP XML when `trace` is enabled (the default): + +```php +$response = $client->insertRequest($request); + +// Inspect the XML that was sent and received. +echo $client->getLastRequest(); +echo $client->getLastResponse(); +``` + +## API Reference + +### `RapidIllClient` + +| Constructor Parameter | Type | Required | Description | +|---|---|---|---| +| `username` | string | yes | RapidILL API username | +| `password` | string | yes | RapidILL API password | +| `rapidCode` | string | yes | Your library's Rapid code | +| `branchName` | string | yes | Your library branch name | +| `wsdlUrl` | string | no | Override the default WSDL URL | +| `soapOptions` | array | no | Additional options passed to `SoapClient` | + +### `InsertRequest` + +All setters return `$this` for fluent chaining. + +| Method | Description | +|---|---| +| `setRequestType(RequestType)` | `Article`, `Book`, or `BookChapter` (default: `Article`) | +| `setHoldingsCheckOnly(bool)` | If true, only checks holdings without submitting | +| `setBlockLocalOnly(bool)` | Block request if only local holdings exist | +| `setInsertLocalHolding(bool)` | Insert a local holding record | +| `addIssn(string)` | Add an ISSN | +| `setIssns(array)` | Set all ISSNs at once | +| `addIsbn(string)` | Add an ISBN | +| `setIsbns(array)` | Set all ISBNs at once | +| `addLccn(string)` | Add an LCCN | +| `setLccns(array)` | Set all LCCNs at once | +| `setOclcNumber(string)` | OCLC number | +| `setJournalTitle(string)` | Journal or book title | +| `setJournalYear(string)` | Publication year | +| `setJournalVolume(string)` | Volume number | +| `setJournalIssue(string)` | Issue number | +| `setJournalMonth(string)` | Publication month | +| `setArticleTitle(string)` | Article or chapter title | +| `setArticleAuthor(string)` | Author name | +| `setArticlePages(string)` | Page range | +| `setEdition(string)` | Edition | +| `setPublisher(string)` | Publisher | +| `setPatronId(string)` | Patron identifier | +| `setPatronName(string)` | Patron full name | +| `setPatronDepartment(string)` | Patron department | +| `setPatronEmail(string)` | Patron email address | +| `setPatronPhone(string)` | Patron phone number | +| `setPatronNotes(string)` | Additional notes | +| `setXrefRequestId(string)` | Cross-reference ID from your ILL system | + +### `InsertResponse` + +All properties are `readonly`. + +| Property | Type | Description | +|---|---|---| +| `isSuccessful` | bool | Whether the API call succeeded | +| `foundMatch` | bool | Whether a lending match was found | +| `rapidRequestId` | int | The assigned Rapid request ID | +| `numberOfAvailableHoldings` | int | Count of available holdings | +| `isLocalHolding` | bool | Whether the item is held locally | +| `verificationNote` | ?string | Error or informational message | +| `matchingStandardNumber` | ?string | The matched ISSN/ISBN | +| `matchingStandardNumberType` | ?string | Type of the matched number | +| `duplicateRequestId` | ?string | ID if request is a duplicate | +| `localHoldings` | array | Array of local holding records | + +### `RequestType` (enum) + +- `RequestType::Article` +- `RequestType::Book` +- `RequestType::BookChapter` + +## Testing + +```bash +composer install +vendor/bin/phpunit +``` + +## License + +GPL-2.0-or-later diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..79e9746 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "rapidill/request", + "description": "PHP library for creating InterLibrary Loan requests via the RapidILL API", + "type": "library", + "license": "GPL-2.0-or-later", + "minimum-stability": "stable", + "require": { + "php": ">=8.1", + "ext-soap": "*" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "autoload": { + "psr-4": { + "RapidIll\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "RapidIll\\Tests\\": "tests/" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..1cf04a5 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests + + + diff --git a/src/InsertRequest.php b/src/InsertRequest.php new file mode 100644 index 0000000..6dadee4 --- /dev/null +++ b/src/InsertRequest.php @@ -0,0 +1,266 @@ +requestType = $type; + return $this; + } + + public function setHoldingsCheckOnly(bool $value): static + { + $this->holdingsCheckOnly = $value; + return $this; + } + + public function setBlockLocalOnly(bool $value): static + { + $this->blockLocalOnly = $value; + return $this; + } + + public function setInsertLocalHolding(bool $value): static + { + $this->insertLocalHolding = $value; + return $this; + } + + public function addIssn(string $issn): static + { + $this->issns[] = $issn; + return $this; + } + + public function setIssns(array $issns): static + { + $this->issns = $issns; + return $this; + } + + public function addIsbn(string $isbn): static + { + $this->isbns[] = $isbn; + return $this; + } + + public function setIsbns(array $isbns): static + { + $this->isbns = $isbns; + return $this; + } + + public function addLccn(string $lccn): static + { + $this->lccns[] = $lccn; + return $this; + } + + public function setLccns(array $lccns): static + { + $this->lccns = $lccns; + return $this; + } + + public function setOclcNumber(string $number): static + { + $this->oclcNumber = $number; + return $this; + } + + public function setJournalTitle(string $title): static + { + $this->journalTitle = $title; + return $this; + } + + public function setJournalYear(string $year): static + { + $this->journalYear = $year; + return $this; + } + + public function setJournalVolume(string $volume): static + { + $this->journalVolume = $volume; + return $this; + } + + public function setJournalIssue(string $issue): static + { + $this->journalIssue = $issue; + return $this; + } + + public function setJournalMonth(string $month): static + { + $this->journalMonth = $month; + return $this; + } + + public function setArticleTitle(string $title): static + { + $this->articleTitle = $title; + return $this; + } + + public function setArticleAuthor(string $author): static + { + $this->articleAuthor = $author; + return $this; + } + + public function setArticlePages(string $pages): static + { + $this->articlePages = $pages; + return $this; + } + + public function setEdition(string $edition): static + { + $this->edition = $edition; + return $this; + } + + public function setPublisher(string $publisher): static + { + $this->publisher = $publisher; + return $this; + } + + public function setPatronId(string $id): static + { + $this->patronId = $id; + return $this; + } + + public function setPatronName(string $name): static + { + $this->patronName = $name; + return $this; + } + + public function setPatronDepartment(string $department): static + { + $this->patronDepartment = $department; + return $this; + } + + public function setPatronEmail(string $email): static + { + $this->patronEmail = $email; + return $this; + } + + public function setPatronPhone(string $phone): static + { + $this->patronPhone = $phone; + return $this; + } + + public function setPatronNotes(string $notes): static + { + $this->patronNotes = $notes; + return $this; + } + + public function setXrefRequestId(string $id): static + { + $this->xrefRequestId = $id; + return $this; + } + + /** + * Convert to the array structure expected by the SOAP client. + */ + public function toSoapArray(): array + { + $data = [ + 'RapidRequestType' => $this->requestType->value, + 'IsHoldingsCheckOnly' => $this->holdingsCheckOnly, + 'DoBlockLocalOnly' => $this->blockLocalOnly, + 'DoInsertLocalHolding' => $this->insertLocalHolding, + ]; + + if ($this->issns) { + $data['SuggestedIssns'] = ['string' => $this->issns]; + } + if ($this->isbns) { + $data['SuggestedIsbns'] = ['string' => $this->isbns]; + } + if ($this->lccns) { + $data['SuggestedLccns'] = ['string' => $this->lccns]; + } + + $optional = [ + 'OclcNumber' => $this->oclcNumber, + 'PatronJournalTitle' => $this->journalTitle, + 'PatronJournalYear' => $this->journalYear, + 'JournalVol' => $this->journalVolume, + 'JournalIssue' => $this->journalIssue, + 'JournalMonth' => $this->journalMonth, + 'ArticleTitle' => $this->articleTitle, + 'ArticleAuthor' => $this->articleAuthor, + 'ArticlePages' => $this->articlePages, + 'Edition' => $this->edition, + 'Publisher' => $this->publisher, + 'PatronId' => $this->patronId, + 'PatronName' => $this->patronName, + 'PatronDepartment' => $this->patronDepartment, + 'PatronEmail' => $this->patronEmail, + 'PatronPhone' => $this->patronPhone, + 'PatronNotes' => $this->patronNotes, + 'XRefRequestId' => $this->xrefRequestId, + ]; + + foreach ($optional as $key => $value) { + if ($value !== null) { + $data[$key] = $value; + } + } + + return $data; + } +} diff --git a/src/InsertResponse.php b/src/InsertResponse.php new file mode 100644 index 0000000..dc6dcf7 --- /dev/null +++ b/src/InsertResponse.php @@ -0,0 +1,61 @@ +InsertRequestResult; + + $holdings = []; + if (isset($r->LocalHoldings->LocalHoldingItem)) { + $items = $r->LocalHoldings->LocalHoldingItem; + // Normalize single item to array. + if (!is_array($items)) { + $items = [$items]; + } + foreach ($items as $item) { + $holdings[] = [ + 'branchName' => $item->BranchName ?? null, + 'location' => $item->LibLocation ?? null, + 'callNumber' => $item->CallNumber ?? null, + 'redirectUrl' => $item->RapidRedirectUrl ?? null, + ]; + } + } + + return new static( + isSuccessful: (bool) ($r->IsSuccessful ?? false), + foundMatch: (bool) ($r->FoundMatch ?? false), + rapidRequestId: (int) ($r->RapidRequestId ?? 0), + numberOfAvailableHoldings: (int) ($r->NumberOfAvailableHoldings ?? 0), + isLocalHolding: (bool) ($r->IsLocalHolding ?? false), + verificationNote: $r->VerificationNote ?? null, + matchingStandardNumber: $r->MatchingStandardNumber ?? null, + matchingStandardNumberType: $r->MatchingStandardNumberType ?? null, + duplicateRequestId: $r->DuplicateRequestId ?? null, + localHoldings: $holdings, + ); + } +} diff --git a/src/RapidIllClient.php b/src/RapidIllClient.php new file mode 100644 index 0000000..8afd610 --- /dev/null +++ b/src/RapidIllClient.php @@ -0,0 +1,123 @@ +setRequestType(RequestType::Article) + * ->setJournalTitle('Nature') + * ->setArticleTitle('Example Article') + * ->setArticleAuthor('Smith, J.') + * ->addIssn('0028-0836'); + * + * $response = $client->insertRequest($request); + */ +class RapidIllClient +{ + private const WSDL_URL = 'https://rapid.exlibrisgroup.com/rapid5api/apiservice.asmx?wsdl'; + + private ?\SoapClient $soapClient = null; + + public function __construct( + private readonly string $username, + private readonly string $password, + private readonly string $rapidCode, + private readonly string $branchName, + private readonly ?string $wsdlUrl = null, + private readonly array $soapOptions = [], + ) { + } + + /** + * Submit an ILL request to RapidILL. + * + * @throws RapidIllException + */ + public function insertRequest(InsertRequest $request): InsertResponse + { + $params = $request->toSoapArray(); + + // Merge authentication fields. + $params['UserName'] = $this->username; + $params['Password'] = $this->password; + $params['RequestingRapidCode'] = $this->rapidCode; + $params['RequestingBranchName'] = $this->branchName; + + try { + $result = $this->getSoapClient()->InsertRequest(['req' => $params]); + return InsertResponse::fromSoapResponse($result); + } catch (\SoapFault $e) { + throw new RapidIllException( + 'RapidILL API error: ' . $e->getMessage(), + (int) $e->getCode(), + $e, + ); + } + } + + /** + * Get or create the SOAP client instance. + * + * @throws RapidIllException + */ + private function getSoapClient(): \SoapClient + { + if ($this->soapClient !== null) { + return $this->soapClient; + } + + $wsdl = $this->wsdlUrl ?? self::WSDL_URL; + + $options = array_merge([ + 'trace' => true, + 'exceptions' => true, + 'cache_wsdl' => WSDL_CACHE_BOTH, + ], $this->soapOptions); + + try { + $this->soapClient = new \SoapClient($wsdl, $options); + } catch (\SoapFault $e) { + throw new RapidIllException( + 'Failed to initialize RapidILL SOAP client: ' . $e->getMessage(), + (int) $e->getCode(), + $e, + ); + } + + return $this->soapClient; + } + + /** + * Replace the SOAP client (useful for testing). + */ + public function setSoapClient(\SoapClient $client): void + { + $this->soapClient = $client; + } + + /** + * Get the last SOAP request XML (requires trace=true). + */ + public function getLastRequest(): ?string + { + return $this->soapClient?->__getLastRequest(); + } + + /** + * Get the last SOAP response XML (requires trace=true). + */ + public function getLastResponse(): ?string + { + return $this->soapClient?->__getLastResponse(); + } +} diff --git a/src/RapidIllException.php b/src/RapidIllException.php new file mode 100644 index 0000000..1fa1c7c --- /dev/null +++ b/src/RapidIllException.php @@ -0,0 +1,7 @@ +setRequestType(RequestType::Article) + ->setJournalTitle('Nature') + ->setJournalYear('2024') + ->setJournalVolume('625') + ->setJournalIssue('7994') + ->setArticleTitle('A breakthrough in quantum computing') + ->setArticleAuthor('Smith, J.') + ->setArticlePages('100-105') + ->addIssn('0028-0836') + ->setPatronName('Jane Doe') + ->setPatronEmail('jane@example.edu'); + + $data = $request->toSoapArray(); + + $this->assertSame('Article', $data['RapidRequestType']); + $this->assertFalse($data['IsHoldingsCheckOnly']); + $this->assertSame('Nature', $data['PatronJournalTitle']); + $this->assertSame('2024', $data['PatronJournalYear']); + $this->assertSame('625', $data['JournalVol']); + $this->assertSame('7994', $data['JournalIssue']); + $this->assertSame('A breakthrough in quantum computing', $data['ArticleTitle']); + $this->assertSame('Smith, J.', $data['ArticleAuthor']); + $this->assertSame('100-105', $data['ArticlePages']); + $this->assertSame(['string' => ['0028-0836']], $data['SuggestedIssns']); + $this->assertSame('Jane Doe', $data['PatronName']); + $this->assertSame('jane@example.edu', $data['PatronEmail']); + } + + public function testInsertRequestOmitsNullFields(): void + { + $request = (new InsertRequest()) + ->setJournalTitle('Science'); + + $data = $request->toSoapArray(); + + $this->assertArrayHasKey('PatronJournalTitle', $data); + $this->assertArrayNotHasKey('ArticleTitle', $data); + $this->assertArrayNotHasKey('PatronEmail', $data); + $this->assertArrayNotHasKey('SuggestedIssns', $data); + } + + public function testInsertRequestMultipleIdentifiers(): void + { + $request = (new InsertRequest()) + ->addIssn('0028-0836') + ->addIssn('1476-4687') + ->addIsbn('978-0-123456-78-9') + ->setOclcNumber('12345678'); + + $data = $request->toSoapArray(); + + $this->assertSame(['string' => ['0028-0836', '1476-4687']], $data['SuggestedIssns']); + $this->assertSame(['string' => ['978-0-123456-78-9']], $data['SuggestedIsbns']); + $this->assertSame('12345678', $data['OclcNumber']); + } + + public function testInsertResponseFromSoapResponse(): void + { + $soapResult = (object) [ + 'InsertRequestResult' => (object) [ + 'IsSuccessful' => true, + 'FoundMatch' => true, + 'RapidRequestId' => 99999, + 'NumberOfAvailableHoldings' => 3, + 'IsLocalHolding' => false, + 'VerificationNote' => null, + 'MatchingStandardNumber' => '0028-0836', + 'MatchingStandardNumberType' => 'ISSN', + ], + ]; + + $response = InsertResponse::fromSoapResponse($soapResult); + + $this->assertTrue($response->isSuccessful); + $this->assertTrue($response->foundMatch); + $this->assertSame(99999, $response->rapidRequestId); + $this->assertSame(3, $response->numberOfAvailableHoldings); + $this->assertFalse($response->isLocalHolding); + $this->assertSame('0028-0836', $response->matchingStandardNumber); + $this->assertSame('ISSN', $response->matchingStandardNumberType); + } + + public function testInsertResponseWithLocalHoldings(): void + { + $soapResult = (object) [ + 'InsertRequestResult' => (object) [ + 'IsSuccessful' => true, + 'FoundMatch' => true, + 'RapidRequestId' => 12345, + 'NumberOfAvailableHoldings' => 1, + 'IsLocalHolding' => true, + 'LocalHoldings' => (object) [ + 'LocalHoldingItem' => (object) [ + 'BranchName' => 'Main Library', + 'LibLocation' => 'Stacks', + 'CallNumber' => 'Q1 .N2', + 'RapidRedirectUrl' => 'https://example.com/redirect', + ], + ], + ], + ]; + + $response = InsertResponse::fromSoapResponse($soapResult); + + $this->assertTrue($response->isLocalHolding); + $this->assertCount(1, $response->localHoldings); + $this->assertSame('Main Library', $response->localHoldings[0]['branchName']); + $this->assertSame('Q1 .N2', $response->localHoldings[0]['callNumber']); + } + + public function testClientWrapsSOAPFaultInException(): void + { + $mockSoap = $this->createMock(\SoapClient::class); + $mockSoap->method('__call') + ->willThrowException(new \SoapFault('Server', 'Authentication failed')); + + $client = new RapidIllClient('user', 'pass', 'CODE', 'Main'); + $client->setSoapClient($mockSoap); + + $this->expectException(RapidIllException::class); + $this->expectExceptionMessage('RapidILL API error: Authentication failed'); + + $client->insertRequest(new InsertRequest()); + } + + public function testBookRequest(): void + { + $request = (new InsertRequest()) + ->setRequestType(RequestType::Book) + ->setJournalTitle('Introduction to Algorithms') + ->addIsbn('978-0262033848') + ->setArticleAuthor('Cormen, T.H.') + ->setPublisher('MIT Press') + ->setEdition('3rd') + ->setPatronName('John Smith'); + + $data = $request->toSoapArray(); + + $this->assertSame('Book', $data['RapidRequestType']); + $this->assertSame('Introduction to Algorithms', $data['PatronJournalTitle']); + $this->assertSame('MIT Press', $data['Publisher']); + $this->assertSame('3rd', $data['Edition']); + } +}