fsi/data-indexer
A Laravel package for building searchable data indexes. Define indexes for your models and records, keep them in sync automatically, and run fast filtered queries without complex SQL. A simple, configurable API makes adding search and filtering to any Laravel app straightforward.
Start by reading src/ rather than the README — the package is tiny and the contract is the whole story. The core abstraction is an indexer that turns a domain object into two things:
Minimal setup:
composer require fsi/data-indexer:^1.0
Then look for the main interface (DataIndexerInterface / IndexerInterface depending on version) and the Doctrine implementation (DoctrineDataIndexer). The fastest first use case is wiring it into an FSi DataSource/DataGrid so grid rows have stable identifiers and filterable index values:
use FSi\Component\DataIndexer\DoctrineDataIndexer;
$indexer = new DoctrineDataIndexer($entityManager, MyEntity::class);
$index = $indexer->getIndex($entity); // ['id' => 1, 'name' => '...', ...]
$id = $indexer->getIdentifier($entity); // '1'
If the shipped Doctrine indexer does not map your fields the way you need, skip it and implement the interface directly — that is usually less work than fighting it.
Custom indexer per aggregate. Implement the interface once per entity/aggregate and keep the mapping explicit:
final class ProductIndexer implements DataIndexerInterface
{
public function getIndex($product): array
{
return [
'id' => $product->getId(),
'sku' => $product->getSku(),
'name' => $product->getName(),
'price' => $product->getPrice(),
'category' => $product->getCategory()?->getId(),
];
}
public function getIdentifier($product): string
{
return (string) $product->getId();
}
}
Doctrine metadata-driven indexing. Use DoctrineDataIndexer when the identifier is the mapped @Id and you want the component to read it via metadata instead of getters. This avoids proxy issues and works for entities whose getters are expensive or absent. Where you need extra index fields, subclass/decorate and merge your own array with the parent result.
Decoration and composition. Because the interface is small, decorate indexers to add cross-cutting behaviour: prefix keys (product.name), add a __type discriminator for heterogeneous collections, memoize results, or fall back to a default identifier when the primary key is null (unsaved entities). A composite indexer that dispatches on instanceof is a clean way to index mixed collections.
Integration with DataSource/DataGrid. The intended workflow is: build the indexer, hand it to the data source/grid layer, and let the grid use getIdentifier() for row keys and getIndex() for column values and filters. Register it as a service and inject it into the factory/grid definition rather than constructing it inline:
services:
App\Indexer\ProductIndexer:
tags: ['app.data_indexer']
There is no bundle autoconfiguration here, so plain service registration is expected. If you are on Symfony, add a small compiler pass or explicit service arguments; do not expect the package to discover your indexers. On Laravel, bind the indexer in a service provider or use contextual binding — there is no package service provider to lean on either.
Testing. The indexer is pure and trivially unit-testable with plain objects or Doctrine mocks. Assert the exact index array shape — downstream grids depend on those keys — and assert identifier stability across hydration states (new, managed, proxy, detached). 1.0.0 adds static analysis to the package itself, which is useful if you fork it, but it does not type your domain objects; keep asserting the concrete array shape in your own tests.
doctrine/common < 2.9. Constraint it as ^1.0 and check that your PHP, Symfony (if used) and Doctrine versions clear those floors before upgrading. It remains a small utility with effectively no dependents, so be prepared to fork — but target 1.0.0 rather than an abandoned dev branch.getIndex(Product $p)) — that narrows the parameter and triggers a declaration-compatibility error. Return types are safe; parameter types are not unless the interface declares them.implode('-', $ids)) and decoded consistently; keep the encoding in one place.get_class($object) returns the proxy subclass. Use Doctrine\Common\Util\ClassUtils::getClass() or Doctrine metadata when keying by class name — doctrine/common >= 2.9 still ships ClassUtils, so this remains the right tool on the new floor.$product->getCategory()->getId() still initializes the proxy in some setups) or fetch-join in the query that feeds the grid.DateTime (format? timezone?), booleans and enums appear in the index. Downstream filtering/sorting breaks quietly on inconsistent types. PHP 8 enums in particular need an explicit ->value (or ->name) mapping; they are not scalars.getId() can be null before flush; either exclude them from indexing or use a synthetic identifier, and make that behaviour explicit.getIndex() and getIdentifier() for one object first; the failure is almost always a mismatched key name or a non-scalar identifier, not the grid.How can I help you explore Laravel packages today?