Entities

The bundle ships a Doctrine entity for every ESPN resource it imports, plus a set of embeddable value objects. All entities live in HansPeterOrding\EspnApiSymfonyBundle\Entity and all tables are prefixed easb_espn_.

Common conventions

Every persisted entity follows the same conventions, so once you have read one you can predict the rest.

espnId

ESPN’s own identifier is stored in a string column espnId — separate from the entity’s database primary key. Look-ups during import match on espnId (and, where a resource is season-scoped, on the related season), which is what makes re-imports idempotent.

Primary keys

Primary keys are bigint identity columns generated by the database. They are internal to your application and have no meaning in the ESPN API.

Nullable by default

Almost every column is nullable, mirroring the client’s DTO philosophy: ESPN is inconsistent about which fields are present, so the bundle never forces a value to exist.

Timestamps

Every entity uses SyncTimestampsTrait, which adds createdAt and lastSyncedAt columns maintained through Doctrine lifecycle callbacks. These let you tell when a row was first imported and when it was last refreshed.

$team->getCreatedAt();      // first import
$team->getLastSyncedAt();   // most recent refresh

Enums

Fixed vocabularies (injury status, competitor home/away, competition status type, …) are modeled as native PHP enums and stored with Doctrine’s enumType. This gives you type-safe values rather than free-form strings.

Reference strings

The {name}Reference convention from the client carries through to the entities. A reference column stores the ESPN $ref URL of a related resource. Importers use these strings to look up and connect the related entity; you can also read them directly if you only need the link.

Free-form text

Fields that can hold long, unbounded text — note headlines and bodies, injury comments — are mapped as TEXT columns rather than VARCHAR(255). This matters because ESPN occasionally “misuses” some feeds (notably injuries) to carry long status updates and news.

Embeddable value objects

Some structures are genuinely part of their parent resource rather than links to a separate resource. These are mapped as Doctrine embeddables and stored inline with a column prefix:

Embeddables are mapped inline because they have no independent identity in the ESPN API — they only ever exist as part of their parent.

Key relationships

The associations below are worth calling out because they encode deliberate design decisions.

Season scoping

Most resources are season-scoped: an athlete in 2025 is a different row from the same person in 2026, because ESPN models them as distinct season-bound resources. A handful of resources are season-independent and unique by espnId alone:

  • EspnTeam

  • EspnSeasonGroup

  • EspnInjury

  • EspnPosition (league-level, with a self-referencing parent)

Team ↔ Franchise (one-to-one)

A franchise maps to exactly one team and vice versa, modeled as a bidirectional OneToOne. EspnTeam owns the foreign key (inversedBy); setting the team on a franchise also sets the franchise on the team via a recursion-guarded setter.

Venue ↔ Teams / Franchises (one-to-many)

A venue can host several teams and franchises (e.g. a shared stadium), so EspnVenue holds OneToMany collections back to both, with the foreign key on the owning EspnTeam / EspnFranchise side.

Injury ↔ Athletes (many-to-many)

Because the same real-world injury is referenced by the season-scoped athlete records of multiple seasons, an injury is connected to athletes through a ManyToMany join table (easb_espn_injury_to_espn_athlete). The injury itself is season-independent; deleting a healed injury removes it for every season at once.

CompetitionStatus ↔ Competition (one-to-one)

The live status of a game (clock, period, status type) is its own entity with a OneToOne back to the competition, rather than an embeddable. This is deliberate: status changes constantly during a game, so isolating it lets you refresh just the status without touching the competition row. See Import chain.

Reserved words

Two columns would collide with SQL reserved words and are therefore mapped to renamed columns:

  • EspnCompetitor and EspnOfficial store their ordering in a displayOrder property mapped to a display_order column.

If you query these tables directly, use the display_order column name.

Inspecting the model

Because the entities are standard Doctrine entities, all the usual tooling works:

# show the mapping for one entity
$ php bin/console doctrine:mapping:info

# validate every mapping and its database sync state
$ php bin/console doctrine:schema:validate

And you query them through their repositories like any other Doctrine entity:

use HansPeterOrding\EspnApiSymfonyBundle\Entity\EspnTeam;

$team = $entityManager
    ->getRepository(EspnTeam::class)
    ->findOneBy(['espnId' => '12']);

foreach ($team->getAthletes() as $athlete) {
    echo $athlete->getFullName() . PHP_EOL;
}