Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Luigis Box Bundle Laravel Package

answear/luigis-box-bundle

Symfony bundle for integrating Luigi's Box search and content updates. Configure public/private keys, hosts, timeouts, cache TTL, multiple or dynamic configs and custom headers, then send content updates and search requests through simple services.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package — there is no LuigisBoxServiceProvider, no facade, no extra.laravel.providers. Plan for one of two paths:

  1. Symfony-adjacent only — you use it in a Symfony service (e.g. a separate search/ingestion app) and Laravel talks to it over HTTP/queue. Lowest friction.
  2. Manual wiring in Laravel — the bundle's PHP classes are plain services around Guzzle; you copy ~5 service definitions from the bundle's Resources/config/*.xml into your own provider once. This is the realistic path for a Laravel monolith.

Minimal steps for path 2:

composer require answear/luigis-box-bundle

Check the version matrix first — 5.x requires PHP 8.4 + Symfony 7, 4.x is PHP 8.2 + Symfony 6/7, 3.x is Symfony 5.4/6. Pin deliberately.

Move the bundle config into config/services.php:

'luigis_box' => [
    'default_config' => 'main',
    'configs' => [
        'main' => [
            'host' => env('LUIGIS_BOX_HOST', 'https://live.luigisbox.com'),
            'publicKey' => env('LUIGIS_BOX_PUBLIC_KEY'),
            'privateKey' => env('LUIGIS_BOX_PRIVATE_KEY'),
            'connectionTimeout' => 4.0,
            'requestTimeout' => 10.0,
            'searchTimeout' => 6.0,
            'searchCacheTtl' => 0,
        ],
    ],
],

Bind the singletons in a provider — ConfigProvider first, then the request services that consume it:

$this->app->singleton(ConfigProvider::class, fn ($app) =>
    new ConfigProvider($app['config']->get('services.luigis_box'))
);
$this->app->singleton(RequestInterface::class, ...);        // content updates
$this->app->singleton(SearchRequestInterface::class, ...);  // search
$this->app->singleton(UpdateByQueryRequest::class, ...);

Where to look first: Service\ConfigProvider (all credentials/headers/state), Service\RequestInterface (content sync), Service\SearchRequestInterface (search), then ValueObject\* for the payload shapes and Response\* for what comes back. The README is the only doc; there is no published API reference beyond the Luigi's Box upstream docs.

First use case: push your catalog into Luigi's Box and read it back. Build a ContentUpdateCollection, call $request->contentUpdate($collection), assert $apiResponse->success, then wire a SearchUrlBuilder to a /search endpoint and return $response->hits.

Implementation Patterns

Catalog sync via model events + queues. Don't call the API inline from saved/deleted observers. Dispatch a job, build the collection inside the job, and keep batches well under the per-request item limit:

class SyncProductsToLuigisBox implements ShouldQueue
{
    public function handle(RequestInterface $request): void
    {
        Product::query()->chunkById(500, function ($products) use ($request) {
            $collection = new ContentUpdateCollection(
                $products->map(fn (Product $p) => new ContentUpdate(
                    $p->name,
                    "/p/{$p->slug}",
                    'product',
                    [
                        'title' => $p->name,
                        'price' => $p->price,
                        'brand' => $p->brand->name,
                        'category' => $p->category->path,
                        'availability' => $p->in_stock,
                    ]
                ))->all()
            );

            $response = $request->contentUpdate($collection);
            if (! $response->success) { /* log $response->errors, optionally retry */ }
        });
    }
}

Prefer partial updates for hot fields. PartialContentUpdate with just ['price' => ...] is much cheaper than a full document rewrite when only price/stock moves. Reserve ContentUpdate for new objects or full reindexes.

Availability is its own verb. changeAvailability(new ContentAvailability($url, $isAvailable)) accepts a single object or a ContentAvailabilityCollection — use the scalar form for one-off toggles in an admin action, the collection for a bulk stock import.

Removals need explicit urls. ContentRemoval($url, $type). When a product is deleted, enqueue the removal job before the row disappears (or capture url/type in the event payload), otherwise you lose the identifiers.

Search endpoint pattern. Build the SearchUrlBuilder from the incoming request, don't let the frontend dictate raw filters:

$builder = (new SearchUrlBuilder((int) $request->input('page', 1)))
    ->setQuery((string) $request->input('q'))
    ->addFilter('type', 'product');

foreach ((array) $request->input('brand', []) as $brand) {
    $builder->addFilter('brand', $brand);
}
foreach ((array) $request->input('prefer', []) as $p) {
    $builder->addPrefer('brand', $p);
}
if ($request->filled('sort')) {
    $builder->setSort($request->input('sort'), $request->input('dir', 'asc'));
}
if ($request->filled('client_id')) {
    $builder->setClientId($request->input('client_id')); // 5.2+: analytics attribution
}

$response = $searchRequest->search($builder);

Return $response->hits, $response->facets, $response->totalHits, $response->correctedQuery. Hit::$attributes is the field bag you pushed during indexing — hydrate thin view models from it rather than re-querying Eloquent per hit.

Facets are the merchandising surface. Facet::$name, $type, $values map directly to your filter sidebar. Dynamic facets (3.1+) let you request facet sets contextually; keep the facet names aligned with the field names you index, or the sidebar silently comes back empty.

Multi-store / multi-tenant via named configs. Define configs.store_de and configs.store_pl, and switch with $configProvider->setConfig('store_de') at the start of the request/job. For per-tenant credentials loaded at runtime (not in services.php), use addConfig('tenant_42', new ConfigDTO(...)).

Bulk re-merchandising with update-by-query. Use UpdateByQuery\Search($types, $fields) + UpdateByQuery\Update($updateFields), keep the $jobId, then poll getStatus($jobId) until $response->completed is true and read okCount/errorsCount. Do the polling in a scheduled command with backoff, not in a web request.

Enable search caching for read-heavy pages. searchCacheTtl (seconds, hard cap 300 — that's Luigi's Box's limit) kills repeated identical queries on category pages. Leave it at 0 for personalized/prefer-heavy queries where staleness hurts.

Test against the interfaces. Resolve SearchRequestInterface from the container in your feature tests and swap it for a fake returning a hand-built SearchResponse. Same for RequestInterface around sync jobs — you should never hit the live API in CI.

Gotchas and Tips

  • It is not a Laravel package. No auto-discovery. If you skip the manual bindings you get "Target [ConfigProvider] is not instantiable" — that's the wiring, not the library.
  • ConfigProvider is mutable, shared state. setConfig(), setHeader(), resetHeaders() mutate the singleton. In Octane, queue workers, or any long-lived process, a header or config set for one job leaks into the next. Always resetHeaders() and re-setConfig() at the top of each job, or resolve a fresh provider per unit of work.
  • Custom headers are search-only. setHeader() feeds search requests (2.2+); don't assume content-update calls carry them. getRequestHeaders() is deprecated — use getAuthorizationHeaders() if you need the auth ones.
  • Timeouts are split three ways. connectionTimeout/requestTimeout govern content pushes; searchTimeout (default 6s) governs search; recommendations got their own timeouts in 4.2. A search that "hangs" while indexing is fast usually means you tuned only requestTimeout. Set searchTimeout below your PHP-FPM/queue timeout.
  • ApiResponse::$success === false on partial success. This is the #1 silent-data-loss trap: 480 of 500 docs index, $success is false, $okCount is 480, $errors holds 20 ApiResponseError objects with $url, $type, $reason, $causedBy. Log $errors keyed by $url and re-queue only those urls. Never branch on $success alone for "did anything work".
  • Exception taxonomy matters for retry logic.
    • TooManyRequestsException::$retryAfterSeconds → release the job with that delay.
    • TooManyItemsException::$limit → split the collection and retry; don't retry the same payload.
    • ServiceUnavailableException → back off and retry.
    • BadRequestException exposes $request/$response → the payload is wrong; retrying is pointless, fix the mapping.
    • MalformedResponseException exposes $response → API contract drift; log it, it's a canary. Catch them separately; a blanket catch (\Throwable) around content updates will spin forever on BadRequestException.
  • The first ContentUpdate argument is not always the title. If $fields contains a title key, that wins. Index a title field when your display name differs from the indexed title, and don't be surprised when the constructor arg seems ignored.
  • searchCacheTtl above 300 is not honored — Luigi's Box caps it. Validate the config at boot rather than discovering it in production.
  • Nullable query hits (5.1+). SearchResponse::$query can be null (e.g. filtered-only browsing with no q). Type-hint/handle null in your controllers and templates; older code that assumed a string will break on a version bump.
  • guid in responses (5.3+) and clientId in search (5.2+). Use clientId to attribute traffic in Luigi's Box analytics, and guid to correlate a response hit back to your own aggregate if you push internal ids as a field.
  • Debugging is on you. Guzzle is used under the hood but there's no documented injection point for a handler stack, so Laravel's HTTP facade and Telescope won't capture these calls. Log ApiResponse::$rawResponse on failures, log the exception's $request/$response payloads, and wrap the request services in a small decorator that times and logs each verb. That decorator is also your cleanest extension point — bind your decorator to RequestInterface and keep the bundle class as the inner call.
  • Extension points are thin. You get the ValueObject constructors, the ConfigProvider mutation API, and interface-level substitution. There is no event system, no pluggable serializer, no retry middleware. If you need richer behavior, decorate at the interface.
  • Adoption risk. 7 stars, 0 dependents, a single-maintainer cadence driven by the vendor's own tickets (ANS-*). Treat it as "vendor-internal bundle published for reuse" — pin the minor version, read every release note (several are BC breaks on PHP/Symfony floors), and keep your API surface behind your own interfaces so swapping to a hand-rolled Guzzle client is a day's work, not a rewrite.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log