gally/gally-standard
Gally Standard is the baseline Laravel package for Gally projects, bundling standard configuration, conventions, and core building blocks so teams can bootstrap a Gally-powered application quickly. It provides a clean, extensible foundation for custom modules and long-term maintenance.
First, reset expectations: gally/gally-standard is not a Laravel package. It is a Symfony application / distribution metapackage that boots a full searchandising service (Elasticsearch/OpenSearch-backed catalog, search API, rule engine, admin UI). There is no service provider, no facade, no config/gally.php to publish. Your integration surface is HTTP — a GraphQL API and a set of CLI commands — plus, optionally, the Symfony extension points if you self-host a fork.
Minimal path to first value:
composer require gally/gally-standard inside your Laravel composer.json — you'll drag Symfony runtime deps and kernel bootstrapping into a Laravel request lifecycle.config/ — the whole product lives in YAML here: sources, metadata, search config, rule definitions, scopes/stores./graphql) — introspect the schema (__schema) before writing any query; query/mutation names shift between releases./admin) — the fastest way to understand what a "source," "metadata," "rule," and "scope" actually are before automating them.bin/console gally:index..., plus the Elasticsearch index-creation command) to push documents in.Do not start by writing merchandiser rules. Start by getting a clean index and a search query returning sensible facets — rules are meaningless until the underlying documents and mappings are right.
Since v2.3.1, metadata definitions carry is_system and is_internal flags. Read those before modelling your attribute set: they distinguish platform-managed attributes from ones merchandisers are meant to touch, and they affect what shows up in the admin UI. Treat them as an editorial boundary, not just a UI toggle — decide per-attribute up front whether it is a merchandiser-facing facet or internal plumbing, because flipping the flag later is a metadata config change with reindex implications.
Wrap the HTTP surface in one Laravel service. A single GallyClient built on Illuminate\Support\Facades\Http (with a base URI, bearer token, and a sane timeout) beats scattering Http::post('/graphql') calls around:
final class GallyClient
{
public function __construct(private readonly array $cfg) {}
public function query(string $gql, array $vars = []): array
{
$res = Http::baseUrl($this->cfg['url'])
->withToken($this->cfg['token'])
->timeout($this->cfg['timeout'] ?? 3)
->retry(2, 100)
->post('/graphql', ['query' => $gql, 'variables' => $vars]);
// Gally returns 200 with an `errors` key for validation problems — check both.
return $res->throw()->json();
}
}
Treat GraphQL errors as first-class: a malformed filter usually comes back as a 200 with a populated errors array, so $response->successful() is not sufficient.
Hydration pattern (the one you'll use most). Gally returns search documents, not your models. Fetch SKUs from Gally, then batch-hydrate:
$hits = $gally->query($searchQuery, ['search' => $term])['data']['...']['items'];
$skus = array_column($hits, 'sku');
$products = Product::whereIn('sku', $skus)->get()->keyBy('sku');
// Re-order by the ranked SKU list — do not trust DB ordering.
Re-sort by Gally's returned order; that ordering is the merchandising value. Cache the Gally response (short TTL, keyed by normalized query + filters + scope) and cache the hydration separately so a rule change only invalidates the first layer.
Catalog ingestion as a scheduled job. Keep Gally in sync from Laravel rather than making your product tables the direct source:
gally_synced_at column and a dead-letter table for documents that failed mapping — mapping failures are silent-ish and you will need to replay them.Metadata flags as a modelling tool. Use is_system for attributes Gally populates/handles itself and is_internal for attributes your integration needs but merchandisers shouldn't see (internal scoring signals, sync bookkeeping, cost/margin fields used in boosts). This keeps the admin UI legible as your attribute count grows, and reduces the chance a merchandiser edits a field your queries depend on. Audit existing metadata for attributes that should have been flagged internal — retrofitting is a config change plus reindex, not a live toggle.
Scopes for multi-store / multi-locale. Model each store view as a Gally scope. This is the cleanest way to get per-locale relevance, per-store rules, and per-store facet sets without duplicating queries in Laravel. Resolve the scope from your request context (locale + store) in middleware and inject it as a variable into every query.
Merchandising rules as config, reviewed like code. Rules (boost / pin / exclude / re-sort, gated by conditions) are YAML. Keep them in Git next to the app, deploy them via a build step, and test with the preview mode before publishing — otherwise "why is this product #1?" becomes an archaeology exercise.
Graceful degradation. Search should never take down the storefront. Wrap calls in a circuit breaker; on failure, fall back to a LIKE-based query or a cached result set. Log the Gally request ID/correlation if the API exposes one.
When you need custom logic, you're writing Symfony, not Laravel: decorators/resolvers registered in the Gally app's container. Budget for maintaining a fork; if you can express the need as a pre/post-processing step in Laravel, do that instead. Note that v2.3.1 moves the distribution to Symfony 7.4 — if you maintain a fork, rebase your decorators and adapt your DI config before pulling the tag, and re-check any code that relied on config value denormalization or generated OpenAPI docs.
artisan commands, no Laravel config publishing, no queued-job integration, no Eloquent. Everything you "integrate" is HTTP + YAML + a separate container. If your team expects a drop-in package, this is the single biggest expectation gap.is_internal/is_system on the attribute you're filtering by — an internal attribute may not be exposed where you assume. Verify the document exists in the index for that scope before touching scoring.gally:cache:clear-style) and wire your deploy to run it.How can I help you explore Laravel packages today?