Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Oai Bundle Laravel Package

subugoe/oai-bundle

Symfony bundle that adds an OAI-PMH 2.0 endpoint to your application, exposing repository records as Dublin Core and other metadata formats for harvesting by aggregators and discovery services. Includes configurable sets, resumption tokens, and standard error responses.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pin. There is a single 2023 release and no dependents, so treat the version as fixed infrastructure:

composer require subugoe/oai-bundle:"^1.0"

2. Register the bundle. Flex will usually add it to config/bundles.php; if not, add Subugoe\OaiBundle\SubugoeOaiBundle::class => ['all' => true].

3. Mount the endpoint. Import the bundle routing under a dedicated prefix so the OAI endpoint never collides with app routes:

# config/routes/oai.yaml
oai:
    resource: '@SubugoeOaiBundle/Resources/config/routing.xml'
    prefix: /oai

The exact resource name/format can differ between releases — check vendor/subugoe/oai-bundle/Resources/config/ before copying this.

4. Configure identity. Repository name, admin e-mail and absolute base URL live in config/packages/oai.yaml. The bundle's DependencyInjection/Configuration.php is the authoritative list of keys; read it first rather than trusting the README, which may lag the code.

5. Smoke-test the five verbs before writing any integration code:

B=https://example.test/oai
for v in Identify ListMetadataFormats ListSets ListIdentifiers ListRecords; do
  echo "== $v"; curl -s "$B?verb=$v" | xmllint --format - | head -30
done

Identify should return your repository name with no required arguments. If that works, routing and config are correct and everything else is data.

First real use case: expose an existing Doctrine entity — write a provider service that pages over it and maps rows to oai_dc, register it, then confirm ?verb=ListRecords&metadataPrefix=oai_dc returns headers plus <metadata>.

Implementation Patterns

Provider service per collection. The bundle delegates "give me records" to a service; the service returns a page plus a cursor/resumption token. Keep the provider thin: query, map to a metadata payload, hand back. Put all XML shaping in the bundle's serializer, not your Doctrine entity, so you can add formats later without touching persistence code.

Separate query object from mapper. For each record you need (a) a header: identifier, datestamp, setSpec, optional deleted, and (b) metadata for a given prefix. Mappers keyed by metadata prefix scale better than one god-method:

final class OaiDcMapper
{
    public function supports(string $prefix): bool { return $prefix === 'oai_dc'; }

    public function map(Item $item): array
    {
        return [
            'title'   => [$item->getTitle()],
            'creator' => $item->getAuthors(),
            'date'    => [$item->getIssued()->format('Y-m-d')],
            'identifier' => [$item->getLandingUrl()],
        ];
    }
}

Sets are a config concern, not a controller concern. Define setSpec/setName pairs in one place and resolve them into provider filters. Keep ListSets output and the set= query filter driven by the same array, otherwise harvesters will request sets you advertise but cannot serve.

Resumption tokens: prefer stateless. Encode (lastDatestamp, lastId, prefix, set) with an HMAC rather than storing rows; it survives cache flushes and multiple workers. If the bundle ships a DB/cache token store, check its TTL — a harvester that pauses overnight will fail on an expired token with badResumptionToken and restart from scratch.

Deletion by datestamp, not DELETE. Harvester correctness depends on seeing status="deleted" headers. Soft-delete in Doctrine, keep updated_at honest, and make sure your from/until filter runs off the deletion timestamp too.

Integration points: run your endpoint against a third-party harvester (Repository Explorer, oai-tool, or a plain scripted curl loop that follows resumption tokens to exhaustion). That loop is the only real conformance test; unit tests on your mappers will not catch protocol-level mistakes.

Gotchas and Tips

  • The base URL must be exact and absolute. It appears verbatim in Identify and in every error response; under a reverse proxy or Symfony CLI server it will silently be http://localhost. Set trusted proxies (framework.trusted_proxies, HEADER_X_FORWARDED_*) or the harvester rejects the response.
  • Return XML even on error. OAI-PMH requires <error code="..."> inside an OAI envelope; an HTML Symfony exception page, or the web debug toolbar appended to the body, breaks harvesters. Disable the toolbar for the endpoint path and make sure the response content type is text/xml; charset=utf-8.
  • Datestamps are UTC with a Z. YYYY-MM-DDThh:mm:ssZ, no local timezone, no microseconds. Mixing granularity (day vs second) between from, until, and your stored datestamp yields badArgument or silently empty result sets.
  • from/until/set/metadataPrefix argument validation is strict. Unknown arguments must return badArgument; missing metadataPrefix on ListRecords too. If you let the framework's parameter resolution swallow them, you get a 500 instead of a valid OAI error.
  • Don't expose database ids as identifier. Use a stable, opaque, URL-shaped identifier; ids change with re-imports and harvesters will treat every record as new.
  • Set specs are colon-delimited and hierarchical. collection:subcollection — ListSets should not advertise a child whose parent can't be filtered, and set= must accept any advertised spec.
  • Small project: 1 star, zero dependents, last release April 2023. Expect to read the source for anything undocumented, vendor/fork it if you need behavior changes, and check it works on your Symfony version before committing. AGPL-3.0 is a strong copyleft — confirm with whoever owns licensing that running it as a service endpoint fits your distribution model.
  • Extension points worth knowing: custom metadata formats are added by registering a new prefix + mapper service; the verb dispatch is in the controller, so behaviour overrides usually go through a service definition replacement or a fork rather than a config toggle. Use bin/console debug:router and debug:container --tag=... to find which services are actually wired before assuming a config key exists.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core