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:
EspnTeamEspnSeasonGroupEspnInjuryEspnPosition(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:
EspnCompetitorandEspnOfficialstore their ordering in adisplayOrderproperty mapped to adisplay_ordercolumn.
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;
}
Read next
Import control — choosing which entities to import
Import chain — the full message catalog