florianajir/datanova-bundle
Symfony bundle that integrates DataNova into PHP applications, providing configuration, service wiring, and commands to manage DataNova-powered data workflows with minimal setup. Ideal for Symfony projects that need a clean, idiomatic integration.
Read this first: it is a Symfony bundle, not a Laravel package. There is no Illuminate\Support\ServiceProvider, no facade, no package auto-discovery, and no config/datanova.php you can vendor:publish. composer require florianajir/datanova-bundle in a Laravel app will pull in symfony/dependency-injection, symfony/http-kernel and friends, and the bundle's DI extension will simply never be loaded. So "getting started" means deciding which half of the bundle you actually want.
The bundle is a thin Symfony DI wrapper around a PHP client for La Poste's DataNOVA open-data API (French address search, geocoding, postal/parcel reference data). The valuable part is the underlying HTTP client and the request/response mapping; the bundle is glue. For a Laravel app, depend on the client library, not the bundle.
What 0.4.1 changed: one commit, one PR — "Refactor controller for SF5." The Symfony controller was adjusted to work against Symfony 5. That is it. No new endpoints, no DI extension, no config tree changes, no Laravel-friendly entry point. If you were already consuming the underlying client directly, 0.4.1 is a no-op for you; if you were pinning an older Guzzle because of a Symfony 4 constraint, this release may loosen the install path slightly, but the architecture answer below is unchanged.
Minimal path:
composer.json and note the require block — that names the actual DataNOVA client package and its Guzzle constraint. That client is what you install.DependencyInjection/Configuration.php in the bundle. It enumerates every config key the bundle supports (API key/token, base URI, timeout, etc.). This is your authoritative list of options — copy them into config/services.php.AppServiceProvider::register() as a singleton bound to an interface, reading config('services.datanova.*').Where to look when confused: Resources/config/services.xml (or .yaml) for the exact service ids the bundle registers, and the README for a usage snippet. Between those two files you can reconstruct 90% of the bundle's behaviour in about an hour. Skim Controller/ too — after 0.4.1 that's the most recently touched code and shows the intended call shape, but remember it's a Symfony controller: it will never be routed in a Laravel app, so treat it as a usage example, not an integration point.
Config + binding. Mirror the bundle's config keys under config/services.php:
'datanova' => [
'key' => env('DATANOVA_KEY'),
'base_uri' => env('DATANOVA_BASE_URI', 'https://api.laposte.fr/datanova/'),
'timeout' => (int) env('DATANOVA_TIMEOUT', 5),
],
Bind a single configured client instance in AppServiceProvider:
$this->app->singleton(DataNovaClient::class, fn () => new DataNovaClient(
config('services.datanova.key'),
config('services.datanova.base_uri'),
config('services.datanova.timeout'),
));
Wrap it, don't sprinkle it. Put an app/Services/DataNova/AddressLookup.php around the client that returns your own DTOs (AddressResult), not vendor arrays. The bundle's response objects (if any) are Symfony-flavoured and will leak into your controllers otherwise. This wrapper is also your seam for tests and for the day you replace the client.
Cache aggressively — it's read-only reference data. Address and postal lookups are idempotent and expensive relative to a Redis hit:
return Cache::remember(
'datanova:addr:'.sha1(mb_strtolower(trim($q))),
now()->addHours(12),
fn () => $this->client->searchAddress($q),
);
Key on the normalized query (lowercased, accent-stripped, whitespace-collapsed) or you'll get 40% cache misses from Rue de l'Église vs rue de l eglise.
Autocomplete endpoint. Debounce on the client (250–400ms), return a capped list (5–8 results), and validate with a FormRequest (min:3, max:120). Never proxy arbitrary user strings straight through — you're paying per call and burning quota on one-character queries.
Bulk enrichment via jobs. For CSV imports of customer addresses, dispatch one job per row (or one per chunk) so retries and backoff are per-record. Add RateLimiter::for('datanova', fn ($job) => Limit::perMinute(60)) and release() on 429. Don't loop the API inside an HTTP request; a 500-row import will time out.
Artisan command instead of the bundle's console command. If the bundle ships a Symfony console command, it won't register in Laravel. Reimplement it as php artisan datanova:warm-cache or similar — it's usually 30 lines calling your wrapper. Same applies to the controller touched in 0.4.1: rewrite that action as a Laravel controller method that delegates to your wrapper; do not attempt to route or instantiate the Symfony controller.
Testing. If you swap in Laravel's Http client underneath, Http::fake() gives you clean fixtures. If you keep the Guzzle-based vendor client, inject a GuzzleHttp\Handler\MockHandler into a test double instead — far less painful than booting the bundle. Either way, a golden fixture set built from a handful of real French addresses (with accents, St, and CEDEX) is the most valuable test asset you'll write.
0.4.1 signals maintenance, but the shape of the package hasn't moved. One controller refactor for Symfony 5 is good news — the repo isn't abandoned — but it does nothing for Laravel. The bundle still has no ServiceProvider, no auto-discovery, no config publish, and no DI extension Laravel will ever load. Don't interpret a new tag as "it's Laravel-compatible now."
The version still buys you a Symfony stack you don't want. Even after the SF5 refactor, check composer why-not florianajir/datanova-bundle before committing. Symfony 5 requires PHP 7.2.5+ and runs on PHP 8, so the install may succeed on modern Laravel where it previously failed — but the transitive symfony/dependency-injection / http-kernel pull remains dead weight, and a pinned Guzzle 6 can still conflict. The pragmatic answer is unchanged: copy the two or three classes you need into app/Support/DataNova, keep the MIT header, and drop the dependency.
No DI extension = silent no-op. If you do install the bundle and services aren't resolving, it's not misconfiguration — Laravel never instantiates DataNovaBundle or its Extension. There is nothing to debug; the wiring doesn't exist. Bind manually. The 0.4.1 controller refactor is a red herring here: it's a Symfony service definition, invisible to Laravel.
API key leakage in logs. DataNOVA's auth model commonly puts the key in a header or query param. If it's a query param, Guzzle/Laravel debug logs and exception messages will contain it verbatim. Redact it in any Log:: call and never enable debug => true in production.
Errors are 4xx with French prose bodies, not structured JSON. Wrap calls in try/catch, map 401 → ConfigurationException, 429 → RateLimited, 5xx → UpstreamException, and give your own users a generic message. Don't surface La Poste's error text to end users.
Accents, CEDEX, and abbreviations. French address data is unforgiving: É vs E, St vs Saint, CEDEX 9 suffixes. Normalize input (strip accents via iconv/Str::ascii, uppercase, collapse spaces) before both caching and sending. Expect the API to match some variants and not others; surface "did you mean" in the UI rather than retrying blindly.
Quotas are real. The free tier is small and per-key. Budget it: cache first, debounce input, never call on page load, and add a RateLimiter guard. A single misconfigured autocomplete can exhaust a monthly quota in an afternoon.
Don't cache forever. Street renames and postal mergers happen; 12–24h TTL on lookups is fine, but no forever() on address resolution.
Extension points are your own code. The bundle exposes almost nothing worth extending (a couple of services, one config tree), and 0.4.1 didn't add any. The real extension surface is the underlying client's HTTP middleware — add a logging/retry middleware there and you get observability and resilience without touching vendor code. Forking the bundle itself is cheap given its size; just vendor the classes you need under a namespace you control and add a // adapted from florianajir/datanova-bundle (MIT) header. Note the version you copied from (0.4.1) in that comment — with a repo this small and this sparsely released, "which commit did we fork?" is a question you will ask again.
MIT licence means the port is legally trivial. Preserve the notice, copy what you need, and keep a note in your README about the origin — it makes the next developer's git blame far less confusing.
How can I help you explore Laravel packages today?