intaro/hstore-bundle
Symfony bundle that integrates PostgreSQL hstore with Doctrine, providing a custom DBAL type, schema support, and convenient entity mapping so you can store and query key-value data directly from your PHP applications.
intaro/hstore-bundle is a Symfony 2 bundle that registers a Doctrine DBAL type named hstore, letting a PostgreSQL hstore column hydrate into a PHP array on an entity. It is marked deprecated, last tagged in November 2014, has 5 stars and no dependents — so treat it as a reference implementation for how to map hstore in an ORM, not as something you install into a modern app. The transferable idea (a string→string key/value column on a model) is what you actually want; the wiring is Symfony-specific and will not load in Laravel.
Minimal steps (the package's own path, for reference):
composer require intaro/hstore-bundle (pin to the 2014 tag; expect conflicts with any DBAL ≥ 2.5 era constraint).AppKernel.php: new Intaro\HStoreBundle\IntaroHStoreBundle().CREATE EXTENSION IF NOT EXISTS hstore;.@ORM\Column(type="hstore") and declare the property as array.registerDoctrineTypeMapping('hstore', 'hstore') so schema tooling and hydration both understand the column.Minimal steps (Laravel equivalent):
// migration
DB::statement('CREATE EXTENSION IF NOT EXISTS hstore');
DB::statement('ALTER TABLE products ADD COLUMN attributes hstore');
// app/Casts/Hstore.php — implement CastsAttributes, see below
protected $casts = ['attributes' => \App\Casts\Hstore::class];
First use case: a per-row bag of arbitrary attributes — feature flags, translated labels, scrape metadata — where you want WHERE attributes @> 'color=>"red"' filtering without an EAV table or a jsonb column. If you control the schema and are on PostgreSQL 9.4+, reach for jsonb and a plain array cast first; hstore is a string-only map and earns its place only when you need the @> / ? / -> operators on flat string pairs or are joining against an existing hstore schema.
Entity mapping (Doctrine — the package's pattern). Keep the property initialized so hydration of NULL and empty maps stay distinguishable:
/** @ORM\Column(type="hstore", nullable=true) */
protected $attributes = [];
Filtering with native SQL. Doctrine DQL has no hstore operators, so drop to the connection and bind a literal — quoting is on you:
$rows = $em->getConnection()->fetchAllAssociative(
'SELECT * FROM product WHERE attributes @> :attrs',
['attrs' => 'color=>"dark red"'] // quote values containing spaces
);
Use ? for key existence, -> for value extraction, and @> for containment.
Index it or it will table-scan. GIN is the only index type that supports @>, ?, and ->:
CREATE INDEX products_attributes_gin ON products USING gin (attributes);
Laravel custom cast (the pattern you'll actually ship). A plain 'array' cast does not work — Eloquent runs json_decode, and the hstore text output is "a"=>"1", "b"=>"2", not JSON. Write a cast that parses and serializes the literal:
class Hstore implements CastsAttributes
{
public function get($model, $key, $value, $attributes): array
{
if ($value === null) return [];
preg_match_all('/"((?:[^"\\\\]|\\\\.)*)"=>"((?:[^"\\\\]|\\\\.)*)"/', $value, $m, PREG_SET_ORDER);
$out = [];
foreach ($m as [, $k, $v]) {
$out[stripcslashes($k)] = stripcslashes($v);
}
return $out;
}
public function set($model, $key, $value, $attributes): ?string
{
if ($value === null) return null;
$parts = [];
foreach ($value as $k => $v) {
$parts[] = '"'.addcslashes($k, '"\\').'"=>"'.addcslashes((string) $v, '"\\').'"';
}
return implode(', ', $parts); // no braces: that's array syntax, not hstore
}
}
Testing. In RefreshDatabase runs, create the extension in a dedicated first migration or a setUpBeforeClass statement; tests on a fresh database fail with type "hstore" does not exist otherwise. The postgres Docker image ships hstore in contrib, so no extra build is needed.
Serialization / API layer. Both patterns expose a plain associative array, so $model->attributes round-trips through json_encode unchanged. Keep the cast as the single boundary and never leak the raw literal string.
Type::getName(), which Doctrine DBAL 3 removed. Do not add it to a new build; copy the ~60-line type class if you truly need Doctrine support.AppKernel; it cannot be autoloaded usefully from a Laravel service provider. The DBAL type itself is portable, the bundle is not.Unknown database type hstore requested means the platform mapping was registered after schema introspection/doctrine:schema:validate ran. In the Symfony path this is bundle boot; in Laravel, if you use doctrine/dbal for ->change() migrations, you'll hit the same error and must register the mapping first.hstore on bind. If PostgreSQL complains column is of type hstore but expression is of type text, append ::hstore to the placeholder or set the parameter type explicitly.NULL vs empty map. convertToPHPValue in the original returns [] for null in some paths and null in others. Decide up front (the cast above returns [] only when the column is NULL) and be consistent, or whereNotNull('attributes') filters will surprise you." or \ must be backslash-escaped in both directions; a naive implode over k=>v will silently corrupt data. Always go through addcslashes/stripcslashes.jsonb unless you need the operators. jsonb gives you GIN, nesting, real types, and a native Eloquent array cast, with better tooling and no deprecated dependency. Migrating later is easy with hstore_to_jsonb(attributes).convertToDatabaseValue/convertToPHPValue to go through JSON; on the Laravel side, override get/set in the cast rather than reaching for raw DB::raw in every query.How can I help you explore Laravel packages today?