flexmodel/flexmodel-bundle
FlexModel Bundle offers a flexible model layer for Laravel/PHP applications, enabling dynamic attributes, relationships, and configurable schemas with less migration overhead. A practical fit for rapid prototyping and customizable data structures.
1. Install and enable the bundle
composer require flexmodel/flexmodel-bundle
This pulls in flexmodel/flexmodel (the actual model/adapter/indexer library) plus its adapters. If you're on Symfony 4+ Flex, the bundle should be auto-registered; on Symfony 3 add it manually:
// app/AppKernel.php
$bundles = [
// ...
new FlexModel\Bundle\FlexModelBundle(),
];
2. Read the bundle's Configuration.php first
The single most useful file in the repo is src/DependencyInjection/Configuration.php. It is the authoritative list of config keys, adapter/indexer types, and option names for the version you installed. The README is sparse and the package was archived in 2018, so treat the config tree as the real documentation.
vendor/bin/console config:dump-reference flex_model # exact root key: verify in Configuration.php
vendor/bin/console debug:container flex_model # see the registered manager/adapter services
3. Write the minimal config
# app/config/config.yml
flex_model:
adapter:
default:
type: memory # swap for dynamodb / redis / elasticsearch
indexer:
default:
type: memory
hydrator:
default:
type: reflection
The memory adapter/indexer combination is the correct starting point: it lets you build and unit-test your model classes with zero infrastructure, then flip one config value per environment.
4. First use case: a model that persists itself
Extend AbstractModel (which carries ModelManagerAwareTrait) and implement whatever indexable/adapter interface your version requires:
namespace App\Model;
use FlexModel\Model\AbstractModel;
use FlexModel\Model\IndexableInterface;
class Product extends AbstractModel implements IndexableInterface
{
private $id;
private $name;
private $price;
public function getId() { return $this->id; }
public function getName() { return $this->name; }
public function setName($name) { $this->name = $name; return $this; }
// getters/setters allow the ReflectionHydrator to round-trip the object
public function getIndexName(): string { return 'app'; }
public function getIndexType(): string { return 'product'; }
public function isIndexable(): bool { return true; }
public function toIndexableArray(): array { return ['name' => $this->name, 'price' => $this->price]; }
}
Then make models ModelManagerAware in the container — the bundle's compiler pass wires the manager into anything tagged/implementing the aware interface (confirm the tag name in the bundle's compiler pass):
services:
App\Model\Product:
tags: [flex_model.model_manager_aware]
From there, $product->save() / $product->delete() operate against the configured adapter, and the indexer is updated alongside — the whole point of the library is that the calling code never mentions DynamoDB or Elasticsearch.
Environment-split adapters. Keep memory in config_test.yml and the real backend in config_prod.yml. Because the manager is built from config, integration tests that exercise save()/find() run in milliseconds with no AWS or ES dependency. This is the highest-leverage pattern with the package and the main reason to adopt it.
One model = one index type, one table per model. FlexModel expects the adapter and indexer to be keyed by model. Decide your naming convention (getIndexName() returning a shared app-wide index, getIndexType() per model; table names derived from the class name or prefix) before writing models, because changing it later means re-provisioning DynamoDB tables and re-indexing Elasticsearch.
Repositories/services as the entry point. Don't let controllers touch models directly. Wrap lookup/query logic in a service that injects the model manager and returns hydrated models:
class ProductRepository
{
public function __construct(private ModelManager $modelManager) {}
public function find(string $id): ?Product
{
$adapter = $this->modelManager->getAdapter();
$data = $adapter->fetch(Product::class, ['id' => $id]);
return $data ? $this->modelManager->getHydrator()->hydrate(Product::class, $data) : null;
}
}
Search reads go through the indexer, not the adapter. For list/filter screens, use the indexer (Elasticsearch) and hydrate the returned IDs via the adapter, or hydrate directly from indexed fields. Mixing the two paths is normal, but be explicit in the repository about which is used.
Custom adapters are a plain service. To add a backend (or a decorator that logs/retries), implement ModelAdapterInterface (and ModelIndexerInterface for indexing) and register it, then reference it by name in flex_model.adapter.<name>. No bundle changes needed — this is the intended extension seam.
Tagged auto-wiring for models. When you have dozens of model classes, tag the whole App\Model\ namespace rather than listing each one:
services:
App\Model\:
resource: '../src/Model'
tags: [flex_model.model_manager_aware]
Hydrator choice per model. reflection (no setters required, slower), property_accessor (respects getters/setters, good when models have computed fields), and closure (fastest, but you hand-write the mapping) are all exposed via config. Start with reflection, and only switch if you profile a hot path.
ContainerAwareCommand, and autowiring behaviour have all drifted since. Forking it into a private repo is a realistic option.table_prefix, index_name, ES hosts, AWS region/version) are passed straight through to adapters. config:dump-reference is your friend; typos in adapter options surface as runtime exceptions inside the adapter, not as config validation errors.dynamodb-local container in dev. Missing-table errors look like generic adapter failures.index/type API. If you're on a modern ES cluster, pin ES 5/6 or plan on a custom indexer.isIndexable() returning false is silent. Models will save to the adapter and simply not appear in searches. If a record "exists but can't be found", check this method and toIndexableArray() before debugging the adapter.toIndexableArray() must be JSON-serialisable. Dates, value objects, and entities with cycles will break indexing. Flatten to scalars/ISO-8601 strings in that method.flex_model config. In tests, set dummy creds plus a local endpoint so the SDK client constructs.debug:container that the manager argument is present.flexmodel/flexmodel) is the part worth keeping, and the bundle is thin enough to replace with ~50 lines of your own service definitions if it stops booting.How can I help you explore Laravel packages today?