EspnApiSymfonyBundle

EspnApiSymfonyBundle turns the raw ESPN NFL data exposed by espn-api-client into a fully persisted, relational dataset inside your Symfony application. It provides Doctrine entities for every ESPN resource, an asynchronous import pipeline built on Symfony Messenger, and a flexible control layer that lets you decide exactly which parts of the ESPN data tree get imported.

Where the client answers “give me this one ESPN resource as an object”, this bundle answers “keep my database in sync with ESPN”.

What it does

  • Entities — a Doctrine entity for every ESPN resource (seasons, teams, athletes, events, competitions, injuries, contracts, and more), with the relationships between them modeled explicitly.

  • Importers & converters — services that fetch a resource through the client, map it onto an entity, and connect it to its related entities.

  • Messages & handlers — one Symfony Messenger message per resource. The import is a cascade: importing a season dispatches messages for its weeks and teams, importing a team dispatches messages for its athletes, and so on.

  • Import control — a single nested-array structure decides which branches of the cascade run, so you can import everything, or just one team, or just live scores during a game.

  • Retry-aware error handling — transient ESPN failures are retried by Messenger; permanent failures (a missing parent, an unresolvable URL) are marked unrecoverable and dropped.

How it fits together

ESPN API
   │
   ▼
espn-api-client          ← fetches a resource, returns a DTO
   │
   ▼
Converter                ← maps DTO scalars onto an entity
   │
   ▼
Importer                 ← connects the entity to related entities
   │
   ▼
Message handler          ← persists the entity, dispatches child messages
   │
   ▼
Doctrine / your database

Every resource follows this same path. Once you understand it for one entity, you understand it for all of them.

A first import

The most common entry point dispatches two messages — one for the season tree and one for the (season-independent) positions tree. Each import message takes the ESPN $ref URL of the resource to import:

use HansPeterOrding\EspnApiClient\ApiClient\EspnApiClientInterface;
use HansPeterOrding\EspnApiSymfonyBundle\Message\EspnSync\ImportEspnSeasonMessage;
use HansPeterOrding\EspnApiSymfonyBundle\Message\EspnSync\ImportEspnPositionsMessage;
use Symfony\Component\Messenger\MessageBusInterface;

public function fullImport(MessageBusInterface $bus): void
{
    $seasonRef = EspnApiClientInterface::BASE_URI_SPORTS_CORE . 'seasons/2025';

    $bus->dispatch(new ImportEspnSeasonMessage($seasonRef));
    $bus->dispatch(new ImportEspnPositionsMessage());
}

That is the whole trigger. Everything else happens asynchronously as the cascade unfolds across your Messenger workers. You are never required to start here, though — see Import control for how to dispatch any single message in the chain to import just a slice of the data.