survos/loc-bundle

Symfony bundle client for the Library of Congress JSON API (loc.gov) — search, collections, items, and raw-resource extraction.

Maintainers

Package info

github.com/survos/loc-bundle

Type:symfony-bundle

pkg:composer/survos/loc-bundle

Transparency log

Fund package maintenance!

kbond

Statistics

Installs: 27

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.19.1 2026-08-08 17:10 UTC

This package is auto-updated.

Last update: 2026-08-08 17:11:37 UTC


README

Symfony bundle client for the Library of Congress JSON API (loc.gov) — search, collection, and item lookup, with raw responses always preserved alongside tolerant DTOs.

No API key is required for read access. LOC exposes JSON on ordinary loc.gov pages by adding fo=json to the query string; this bundle handles that automatically.

This is a Phase 1 implementation (see survos/mono#23): the API client, DI wiring, and basic console commands. Deliberately not included yet, per the issue's own phasing:

  • recursive resource/asset extraction beyond the raw resources array (Phase 2)
  • response caching, Symfony RateLimiter integration, retries, streamed asset downloads with resume/checksums (Phase 3)
  • Messenger-based harvesting (Phase 4)

Item::$resources and Item::$raw already carry everything a resource walker would need — adding one is additive, not a breaking change to this phase.

Fetch strategy (cache/retry) — caching done, retry still open

Corrects an earlier version of this note: caching is already done (survos/mono@ce974f98, 2026-08-04), just not via PersistentFetcher like most other bundles. LocClient's $httpClient is survos_loc.caching_client — a Symfony RFC 9111 CachingHttpClient (fetch-bundle's CachingHttpClientFactory, cache_enabled: true by default) wrapping the scoped loc.client. LOC sends real Cache-Control/ETag headers (24h on item/collection JSON, up to 1 year on tile.loc.gov static files), so this obeys the origin's own freshness window — capped at cache_max_ttl (default 86400s) even if LOC sends a longer max-age — rather than PersistentFetcher's app-controlled "cache forever until forgotten" model. That's the more correct choice here, not a lesser one: LOC already tells us exactly how long each response is good for.

Still genuinely missing (this bundle's own Phase 3, survos/mono#23):

  • Retry/backoff on transient failures and 5xx — no retry_failed is configured on the loc.client scoped client (see prependExtension). This is the one item worth doing now, independent of the rest: add retry_failed: {enabled: true, http_codes: [500, 502, 503, 504]} to the loc.client scoped-client config, the same mechanism (not PersistentFetcher) other bundles used before this sweep. Deliberately exclude 429 from that list — LOC's 429 already carries Retry-After and is surfaced as RateLimitException precisely so a caller (a future Phase 4 Messenger consumer) can reschedule with the server-specified delay; auto-retrying 429 at the HTTP-client layer would silently swallow that signal.
  • Symfony RateLimiter integration — proactive throttling of outgoing requests, unrelated to fetch-bundle entirely (symfony/rate-limiter, not an HTTP client concern). Only worth adding if LOC's own etiquette guidance documents a request-rate ceiling to stay under.
  • Streamed asset downloads with resume/checksums — not needed yet; LocClient only fetches JSON today. Once Phase 2 (resource/asset extraction) adds fetching the actual media files LOC items link to, that's ChunkDownloader (see geonames-bundle's GeoAuthorityCommand for the pattern), not PersistentFetcher — same reasoning as geonames-bundle's own large-file downloads.

RateLimitException/LocApiException/InvalidJsonException remain part of this bundle's tested public API (tests/Client/LocClientTest.php) and are unaffected by any of the above.

Installation

composer require survos/loc-bundle

Configuration

# config/packages/survos_loc.yaml
survos_loc:
    base_url: 'https://www.loc.gov'
    user_agent: 'Museado LOC Harvester/0.1 (+https://museado.org)'
    timeout: 30

All three keys are optional; the defaults above (with a generic Survos LocBundle/0.1 user agent) work out of the box.

Usage

use Survos\LocBundle\Client\LocClientInterface;

final readonly class ImportService
{
    public function __construct(private LocClientInterface $locClient) {}

    public function import(): void
    {
        foreach ($this->locClient->iterateCollection('voices-remembering-slavery') as $result) {
            $item = $this->locClient->getItemDto($result->id);

            foreach ($item->resources as $resource) {
                // raw LOC resource array — walk/extract per-file URLs as needed (Phase 2)
            }
        }
    }
}
  • search(array $query): array / searchPage(array $query): SearchPage/search/.
  • getCollection(string $collection, array $query = []): array / getCollectionPage(...)/collections/{slug}/.
  • iterateCollection(string $collection, array $query = []): iterable<SearchResult> — lazily follows pagination.next, fetching one page at a time.
  • getItem(string $idOrUrl, array $query = []): array / getItemDto(...): Item — accepts a bare item id or a canonical loc.gov/item/{id}/ URL.
  • getJsonUrl(string $url): array — fetches an arbitrary fo=json URL discovered in a previous response; restricted to www.loc.gov / tile.loc.gov / memory.loc.gov.

Every DTO (SearchResult, Item, SearchPage, CollectionPage, Pagination) keeps its source array on ->raw — unknown/future LOC fields never cause a hydration failure.

Console commands

bin/console loc:search "oral history" --limit=10
bin/console loc:collection:list voices-remembering-slavery --limit=20
bin/console loc:item:get afc1950037_afs09990a --resources

Test collections

Useful real collections for manual testing (see the issue for why each is interesting):

Downloading vs. reuse

Successfully fetching or downloading an asset through this bundle says nothing about whether it is legally reusable. Rights fields on LOC responses (item.rights, item.rights_advisory, etc.) are preserved unchanged on Item::$raw — nothing in this bundle collapses them into a boolean.