ottosmops/oai-pmh
Laravel/PHP package that adds an OAI-PMH 2.0 data-provider endpoint to Laravel applications, making repository and digital-library records harvestable through standard verbs like Identify, ListMetadataFormats, ListRecords, and GetRecord, with configurable metadata formats and sets.
composer require ottosmops/oai-pmh
php artisan vendor:publish --tag=oai-pmh-config
php artisan vendor:publish --tag=oai-pmh-views
Check the README for the exact publish tags if your install does not expose them.config/oai-pmh.php first. Set the repository identity values (base_url, repositoryName, adminEmail), enabled metadata formats, set definitions, deleted-record mode, and date granularity.OaiRepositoryContract and bind it in a service provider. The contract is deliberately small — three methods: get, getRecordForId, and getEarliestDatestamp. Start by wrapping an existing Eloquent model that represents published archive records.Identify, ListMetadataFormats, and a small ListRecords request before wiring up sets or custom XML.The first practical use case is exposing an existing Laravel model as oai_dc records so an external harvester can discover and harvest the collection without building the OAI-PMH protocol layer yourself. The package is battle-tested: it was extracted from the Anton archive-management system (built with the Gosteli Archive), so the protocol edge cases around sets, deletions, and resumption are already handled.
Repository adapter around Eloquent. Create a dedicated class such as EloquentOaiRepository that implements OaiRepositoryContract. Implement the three methods narrowly:
get — handle the query parameters (metadata prefix, datestamp range, set, pagination window) and return a page of records.getRecordForId — resolve a single record header plus metadata, or signal "not found" so the package can emit the correct OAI-PMH error.getEarliestDatestamp — return the oldest datestamp in the repository, used by Identify.Return records in the exact shape expected by the published views/contract, and keep ordering deterministic — usually datestamp plus identifier — so resumption tokens remain stable.
Keep protocol logic out of controllers. Bind the contract in AppServiceProvider and let the package route/controller call it. Your application code should only translate OAI-PMH filters into database queries and map models into OAI record headers and metadata payloads.
Metadata formats as views/transformers. For oai_dc, either return an array of Dublin Core fields expected by the default view or override the Blade view to render your model directly. If you support additional prefixes, add them in config and provide the corresponding XML view/transform. Validate each format against the OAI-PMH XSD.
Sets as query scopes. Map configured set specs to application scopes or relationships. Because the package supports set hierarchy with descendant inclusion, a parent set query should include records from child sets. Test that ListSets, ListIdentifiers?set=..., and ListRecords?set=... agree.
Deleted-record mode drives repository behaviour. Decide early whether you use no, persistent, or transient. If you return deleted records, include the deleted status in the record header and omit metadata. Ensure deleting or unpublishing a record updates its datestamp so harvesters see the change.
Resumption tokens need efficient pagination. Do not load the full collection in get. Use offsets, cursors, or chunked queries that match the contract's pagination parameters. Keep cursor and completeListSize accurate; harvesters rely on them for progress and retry behaviour.
Normalise dates at the boundary. Store/query in UTC and return datestamps in the granularity configured for the repository. If you use day granularity, make sure from/until semantics include the expected day boundaries; if you use second granularity, return full ISO-8601 UTC timestamps.
Cache stable responses. Identify, ListMetadataFormats, and ListSets rarely change but can be requested frequently by harvesters. Cache them at the application layer if your dataset is large or your endpoint is public.
Test with real OAI-PMH requests. Exercise all six verbs, each of the eight error codes (bad verb, bad argument, cannot dissect, bad resumption token, cannot open/resume, no records match, etc.), an invalid metadata prefix, an unknown identifier, an empty result set, and a multi-page ListRecords harvest. The package's XSD-conformance suite is useful, but also test your repository mapping with production-like data.
getEarliestDatestamp() is wrong, Identify will be wrong and some harvesters will behave unpredictably.completeListSize will break large harvests.config/oai-pmh.php, run php artisan config:clear in development. In production, deploy with config:cache and verify the published views are present.ListMetadataFormats should advertise only formats you actually render. If a harvester requests an unsupported prefix, let the package return the correct OAI-PMH error rather than throwing a raw exception.^1.0, and check the changelog for contract changes before assuming a minor bump is a drop-in.Identify/ListSets if the endpoint is public.How can I help you explore Laravel packages today?