answear/econt-pickup-point-bundle
Econt pickup point integration for Symfony: fetch offices and cities from the Econt nomenclatures API via simple command classes. Install with Composer, add credentials in config, and query offices or cities in a few lines of PHP.
Install with Composer and let Symfony Flex register the bundle (it adds Answear\EcontBundle\AnswearEcontBundle::class => ['all' => true] to config/bundles.php). Then supply credentials:
# config/packages/answear_econt.yaml
answear_econt:
user: '%env(ECONT_USER)%'
password: '%env(ECONT_PASSWORD)%'
Those two values are handed to \Answear\EcontBundle\ConfigProvider, which is the only real "configuration surface" in the package — everything else is code.
Where to look first, in this order:
src/Command/GetCities.php and src/Command/GetOffices.php — these are the two public entry points, and both are plain constructor-injected services. Read their constructors to see exactly which collaborators (Guzzle client, ConfigProvider, serializer) you must supply if you ever instantiate them by hand.src/Request/GetCitiesRequest.php and src/Request/GetOfficesRequest.php — the only inputs. Note the cityId parameter on the offices request; its name was corrected in 3.0.0, so if you pin 2.x the wrong name is in play.src/Response/ — the DTOs you'll actually persist. OpeningHours is nullable (since 1.2.1), so build your schema accordingly.DependencyInjection/Configuration.php — confirms the exact config keys and any defaults the README omits (notably the Guzzle timeout added in 2.0.1).First use case — the one this package exists for — is a pickup-point selector: fetch cities once, let the user choose one, fetch that city's Econt offices, render them as a radio list, and store the chosen office code on the order.
use Answear\EcontBundle\Command\GetCities;
use Answear\EcontBundle\Command\GetOffices;
use Answear\EcontBundle\Request\GetCitiesRequest;
use Answear\EcontBundle\Request\GetOfficesRequest;
public function __construct(
private readonly GetCities $getCities,
private readonly GetOffices $getOffices,
) {}
$cities = $this->getCities->getCities(new GetCitiesRequest());
$offices = $this->getOffices->getOffices(new GetOfficesRequest($cityId));
One caveat before you start: despite the framing, this is not a Laravel package. It requires symfony/http-kernel and symfony/serializer, ships a Bundle class, and has no service provider. In a Laravel app you get nothing from Flex and nothing from the bundle registration — you'd be pulling two Symfony components in solely to construct three classes. For a Laravel codebase, treat this as reference source for the HTTP contract rather than something you install; for a Symfony codebase, the setup above is genuinely all there is.
Version floor for a new integration: ^4.2.1. That's the release that switched the API endpoint from http to https (see Gotchas) — there is no reason to adopt a version that sends Basic-auth credentials in cleartext.
Wrap the commands, don't sprinkle them. Both commands are stateless services with a single method each. Put them behind your own EcontLocations service that owns caching, retries, and mapping, and inject that into controllers/console commands. This gives you one seam to fake in tests and one place to change when (not if) the package's DTOs shift.
Cache nomenclatures hard. Cities and offices change on the order of days-to-weeks, not per request. Two viable shapes:
getCities() and getOffices() per city, upserts into econt_cities / econt_offices, and your UI reads the local tables. Fast, sortable, searchable, works when Econt is down.Cache::remember('econt.cities', now()->addDay(), fn () => $client->getCities(...)) plus a per-city offices key. Cheaper to build, but every cold key is a synchronous outbound call in a user's request path.Whichever you pick, never call getOffices() on page render without a cache in front of it.
Sync in chunks, not in one job. Offices are per-city and Econt has thousands of cities. Loop the city list in a queued job with a modest batch size and a RateLimited/WithoutOverlapping middleware, rather than one long-running job that will hit your worker timeout or Econt's throttling.
Map to your own model immediately. Don't let Answear\EcontBundle\Response\* objects leak past your service boundary. Extract the stable identifiers (office code/id, city id, address parts, lat/lng if present, nullable opening hours) into your own DTO/entity, and keep the package objects ephemeral. This is what makes version bumps survivable on a package with one star.
Credentials per environment. ConfigProvider gets whatever you put in answear_econt.user/password. Bind those to %env()% and give each environment its own values — a demo/sandbox pair alongside production means you don't want a hardcoded default anywhere. Basic authorization only arrived in 1.2.2, so confirm the version you install actually authenticates rather than silently returning empty nomenclatures.
Explicit timeouts and one retry. 2.0.1 added a Guzzle timeout; verify the default in Configuration.php is sane for you, and if you construct the client yourself (the Laravel case, or when you want a custom handler stack) set timeout and connect_timeout explicitly and attach a retry middleware for 5xx/connection errors only. Nomenclature GETs are idempotent, so retries are safe.
Test against a MockHandler. Inject a Guzzle client built on MockHandler with recorded fixtures; never let CI hit ee.econt.com. Because the commands take their dependencies via constructor, this is a two-line override in a Symfony test container.
Assert the scheme in your own integration test. One line — $this->assertStringStartsWith('https://', (string) $request->getUri()) on a MockHandler-captured request — locks in the 4.2.1 fix and will fail loudly if a future pin or a hand-built base URI ever regresses to http.
^8.4 and Symfony ^7.0.3 are hard floors. This is the single biggest adoption blocker — it won't install into a Symfony 6 or PHP 8.2/8.3 app without --ignore-platform-reqs, which is not a fix. On a typical Laravel 10/11 app it's an outright conflict. Check composer.json before promising anyone an integration timeline.user/password from ConfigProvider, anything older transmits credentials and payloads in cleartext. Pin ^4.2.1 (or explicitly configure an https:// base URI if you hand-build the client in Laravel), and if you're already on 4.1.x or earlier this is a good enough reason to bump on its own. If you own the Guzzle client, also verify you didn't copy an http:// base URI from an older snippet into your own config.AnswearEcontBundle is dead weight; you must hand-build ConfigProvider and the Guzzle client and new GetOffices(...) yourself. At that point, weigh whether the two Symfony component dependencies are worth it versus writing the ~50-line HTTP call.cityId naming changed in 3.0.0. The parameter was renamed to match the API docs. Pin ^4.2.1 and don't trust older blog snippets or 2.x examples.OpeningHours can be null. Made nullable in 1.2.1. Guard any rendering path — a pickup-point card that assumes an array will fatal on offices without published hours.getResponse() only exists from 4.1.0. It exposes the underlying response object. Convenient for debugging and for reaching fields the DTOs drop, but it hard-couples your code to the transport. Prefer the DTOs; reach for this only as a last resort, and isolate it in one method.src/ before trusting it, and wrap it behind your own interface so swapping it out is a one-file change. Note that the 4.1.0 → 4.2.1 hop spans an undocumented 4.2.0 as well as the https fix — read the diff between your pinned tag and the one you're moving to, since the notes only describe the final patch.ConfigProvider with your own, or skip the package for the raw endpoint. There is no event system, plugin hook, or configurable HTTP middleware.ext-json plus guzzlehttp/psr7: ^2.6 are hard requirements worth verifying in your lockfile when debugging odd transport errors.How can I help you explore Laravel packages today?