splash/staci
Staci is a Laravel package from Splash for turning your app into fast static HTML. It works with existing routes and Blade views to generate or cache pages, reducing server load and simplifying deployment to static hosting.
Confirm what you're actually installing first. splash/staci is a Splash connector bundle, so it is an adapter, not a standalone address library. Before writing code, open the repo and check three files:
composer.json — the require block tells you the supported Laravel/PHP range and, critically, which Splash core package (splash/connectors or similar) it depends on. That core package defines the interfaces your application code should actually depend on.config/staci.php (or whatever the published config is named) — this is where the embedded dataset path/connection and any credentials live.src/ — look for a ServiceProvider, a Connector class, and a Console/ directory. The console commands are usually the fastest way to understand the intended lifecycle (import → query).Typical install:
composer require splash/staci
php artisan vendor:publish --tag=staci-config
Then read the config before running anything. "Embedded" almost always means the dataset lives locally (a SQLite file, a MySQL dump, or a bundled binary) rather than being called over HTTP, so there is usually a one-off import/install step:
php artisan staci:install # or staci:import / staci:sync — check `php artisan list | grep staci`
Run that on a scratch database first. It is usually the slowest and most failure-prone part of the whole integration.
First use case — address lookup/validation. Start with the narrowest thing that proves the wiring works:
use Splash\Staci\Facades\Staci; // verify the real facade/alias in src/
$result = Staci::search('10 rue de la Paix, 75002');
If there is no facade, resolve the connector from the container (app(StaciConnector::class)) or inject it — the service provider is auto-discovered, so it should already be bound.
Once a single lookup returns data, you have validated config, dataset, and autoloading in one shot. Only then build the HTTP layer.
Depend on the interface, not the connector. The whole point of a Splash connector bundle is swappability — embedded STACI for dev/CI and a remote or newer dataset in production. Type-hint against whatever interface from the Splash core the connector implements, and bind the concrete class in a service provider:
// AppServiceProvider::register()
$this->app->bind(AddressLookup::class, fn () => app(StaciConnector::class));
Now a test double is a one-line swap, and you can migrate connectors later without touching controllers.
Wrap it in a repository with a cache layer. Embedded lookups are fast but not free, and address data is extremely cacheable:
class AddressRepository
{
public function __construct(private AddressLookup $staci) {}
public function findByPostalCode(string $zip): Collection
{
return Cache::remember("staci:zip:{$zip}", now()->addDay(), fn () =>
$this->staci->search(['postal_code' => $zip])
);
}
}
Invalidate by tag or by a version key that you bump after each dataset import (staci:v3:zip:75002). Tag-based flushing is cleaner if your cache driver supports it — check, because file and database drivers silently do not.
Autocomplete endpoint. The embedded connector is usually a synchronous, low-latency call, which makes it a good fit for a debounced typeahead. Keep the controller thin, throttle the route, and always cap the result count:
Route::get('/addresses', fn (Request $request) => AddressResource::collection(
$repo->search($request->string('q'), limit: 8)
))->middleware('throttle:60,1');
Validation rule. A custom rule that confirms a postal-code/city pair exists is a high-value, low-risk first integration:
Validator::extend('staci_address', fn ($attr, $value) => $repo->exists($value));
Fail open here (log and pass) if the connector throws — you do not want an address dataset hiccup to block checkout.
Deployment pattern. Separate "import the dataset" from "deploy the code." The import is long-running and should run as a release step or a queued job, not inside a migration or a service provider boot(). Idempotent command + a data_version marker in the config or DB is the pattern to aim for.
php artisan config:cache after publishing but before setting env vars will freeze defaults. Publish, set env, then cache.search('') will exhaust memory_limit. Always filter and paginate at the query level; never ->all() on a full table.--chunk. Wrap it in try/finally in your deploy script so a partial import fails the deploy loudly rather than leaving a half-populated table served to users.é/e, Saint-/St-, and leading zeros in department codes. Normalize input (lowercase, strip diacritics) on both write and read paths, and test "1 rue de l'Église" vs "1 rue de l'Eglise" explicitly. If the connector's search() is a simple LIKE, this is on you, not the package.DB::transaction() spans it. Never assume the connector participates in your app's transaction.debug. When results look wrong, the question is always "bad input, bad dataset version, or connector bug" — the log answers it in one look.StaciManager/factory-style class before you subclass anything. Splash bundles typically let you register drivers or override the container binding; that is the sanctioned way to add caching or retries, and it survives composer update far better than patching src/.composer.json, read the connector's tests as the de facto documentation, and be prepared to vendor-patch. Verify the dataset's licensing/update cadence before you build a production feature on it — embedded address data goes stale.How can I help you explore Laravel packages today?