Extending
The client is intentionally small and open. This page shows the three most common ways developers extend it: adding a new endpoint, adding a new DTO, and customizing serialization.
Adding a new endpoint
Every endpoint extends AbstractEndpoint, which provides two helpers:
uri(string $path, array $attributes = [], string $baseUri = ...)builds aUriInterfacefrom a path template and optional query parameters.extractRefs(array $data)pulls the$reflinks out of a list envelope’sitemsarray.
A minimal endpoint looks like this:
namespace HansPeterOrding\EspnApiClient\ApiClient\Endpoints;
use HansPeterOrding\EspnApiClient\Dto\EspnSeason;
class EspnSeasons extends AbstractEndpoint
{
const URL_TEMPLATE_SEASONS = 'seasons';
const URL_TEMPLATE_SEASON = 'seasons/%d';
public function get(int $year): ?EspnSeason
{
$url = $this->uri(sprintf(self::URL_TEMPLATE_SEASON, $year));
return $this->espnApiClient->get($url, EspnSeason::class);
}
public function listRefs(): array
{
$url = $this->uri(self::URL_TEMPLATE_SEASONS, ['limit' => 100]);
$data = $this->espnApiClient->decodeJson($url);
return $this->extractRefs($data);
}
}
To make a child endpoint reachable, add an accessor method that constructs it with the same client instance:
public function teams(): EspnTeams
{
return new EspnTeams($this->espnApiClient);
}
To expose a brand-new top-level endpoint, add an accessor to EspnApiClient
(and its interface):
public function myResource(): EspnMyResource
{
return new EspnMyResource($this);
}
Building URLs
uri() accepts query parameters as an array. List parameters are
automatically collapsed into the key[]= form ESPN expects:
// seasons?limit=100
$this->uri('seasons', ['limit' => 100]);
// a path on the "site" API instead of the default "sports core" API
$this->uri('teams', [], EspnApiClientInterface::BASE_URI_SITE);
Adding a new DTO
A DTO is a plain class with nullable private properties and fluent getters/setters. Follow the existing convention so the serializer and the reference normalizer behave predictably:
namespace HansPeterOrding\EspnApiClient\Dto;
final class EspnMyResource
{
private ?string $id = null;
private ?string $displayName = null;
// A link to another resource — note the "Reference" suffix
private ?string $venueReference = null;
public function getId(): ?string { return $this->id; }
public function setId(?string $id): static { $this->id = $id; return $this; }
public function getDisplayName(): ?string { return $this->displayName; }
public function setDisplayName(?string $v): static { $this->displayName = $v; return $this; }
public function getVenueReference(): ?string { return $this->venueReference; }
public function setVenueReference(?string $v): static { $this->venueReference = $v; return $this; }
}
Remember the rules from References:
A linked sub-object named
venue→ propertyvenueReference.An array of linked sub-objects named
venue→ propertyvenueReferences(typedarray).
Numeric-as-string fields
If ESPN delivers a field as a JSON number but you want to store it as a string, disable type enforcement for that property so the serializer accepts the number:
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
#[Context(denormalizationContext: [
AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true,
])]
private ?string $value = null;
This is the same mechanism the built-in DTOs use for fields like weight,
clock and value.
Customizing serialization
The client accepts any Symfony\Component\Serializer\SerializerInterface,
so you control exactly which normalizers and encoders are in play. To add
behavior — a custom date handler, an extra normalizer, metadata-driven name
conversion — build the serializer with the normalizers you need and pass it to
EspnApiClient. The client only ever calls deserialize, decode and
denormalize on it, so any standards-compliant serializer configuration
works.
Read next
Contributing to EspnApiClient — how to contribute changes upstream