scienta/doctrine-json-functions
Adds JSON function support to Doctrine ORM DQL by registering custom function nodes for multiple databases. Use MySQL/MariaDB, PostgreSQL, SQLite (json1), or SQL Server JSON functions directly in DQL with platform validation.
Installation:
composer require scienta/doctrine-json-functions
Register Functions (Symfony example):
# config/packages/doctrine.yaml
doctrine:
orm:
dql:
string_functions:
JSON_EXTRACT: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonExtract
JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains
First Use Case:
Query a JSON column in a Customer entity:
$query = $entityManager->createQuery(
'SELECT c FROM App\Entity\Customer c
WHERE JSON_UNQUOTE(JSON_EXTRACT(c.metadata, \'$.country\')) = :country'
)->setParameter('country', 'NL');
$results = $query->getResult();
JSON Extraction:
// Extract nested JSON value
$queryBuilder->andWhere('JSON_EXTRACT(c.data, \'$.user.profile.name\') = :name');
JSON Containment Checks:
// Check if a JSON array contains a value
$queryBuilder->andWhere('JSON_CONTAINS(c.tags, :tag) = 1');
PostgreSQL-Specific Queries:
// Use JSONB functions (note boolean comparison)
$queryBuilder->andWhere('JSONB_CONTAINS(c.settings, :setting) = true');
Aggregations:
// Group by JSON object keys
$queryBuilder->select('JSON_OBJECTAGG(c.id, c.name) as users')
->from('App\Entity\Customer', 'c');
$queryBuilder->andWhere('JSON_CONTAINS(c.data, :jsonValue)')
->setParameter('jsonValue', '{"key": "value"}');
Doctrine\DBAL\Platforms\AbstractPlatform to check the platform before writing queries:
if ($entityManager->getConnection()->getDatabasePlatform() === 'postgresql') {
$queryBuilder->andWhere('JSONB_EXISTS(c.data, \'$.key\') = true');
}
$qb = $entityManager->createQueryBuilder();
$qb->select('c')
->from('App\Entity\Customer', 'c')
->where($qb->expr()->eq(
'JSON_EXTRACT(c.data, \'$.status\')',
':status'
));
Boolean Functions in DQL:
JSONB_CONTAINS) must be compared explicitly with = true or = 1:
// ❌ Fails: "JSONB_CONTAINS(...) = true" is required
$qb->andWhere('JSONB_CONTAINS(c.data, :value)');
$qb->andWhere('JSONB_CONTAINS(c.data, :value) = true');
Path Syntax:
'$.key.subkey').JSON_GET_PATH for nested paths (avoids operator chaining issues):
// ✅ Works
$qb->andWhere('JSON_GET_PATH(c.data, \'$.key.subkey\') = :value');
SQLite Limitations:
json1 extension enabled. Test with:
SELECT sqlite_source('json1');
SQL Server Type Casting:
$qb->andWhere('CAST(JSON_VALUE(c.data, \'$.score\') AS DECIMAL(4,2)) > 100');
Platform Mismatch Errors:
Unsupported function "JSON_EXTRACT" on PostgreSQL, ensure you registered the correct platform-specific function (e.g., JsonGet for PostgreSQL).$platform = $entityManager->getConnection()->getDatabasePlatform();
if ($platform === 'postgresql') {
$config->addCustomStringFunction('JSON_GET', \Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonGet::class);
}
Query Validation:
Doctrine\ORM\Query\QueryException to debug invalid DQL:
try {
$query->getResult();
} catch (\Doctrine\ORM\Query\QueryException $e) {
echo $query->getSQL(); // Inspect the generated SQL
}
Adding Custom Functions:
Scienta\DoctrineJsonFunctions\Query\AST\Functions\AbstractFunction:
class CustomJsonFunction extends AbstractFunction {
public const FUNCTION_NAME = 'CUSTOM_JSON_FUNC';
public function parse(\Doctrine\ORM\Query\Parser $parser) { ... }
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { ... }
}
Platform-Specific Logic:
supportsFunction() in your custom function to restrict usage to specific platforms:
public function supportsFunction(\Doctrine\DBAL\Platforms\PlatformInterface $platform) {
return $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform;
}
Performance:
GENERATED ALWAYS AS or PostgreSQL GIN indexes). Example:
-- MySQL
ALTER TABLE customers ADD COLUMN country_index JSON GENERATED ALWAYS AS (JSON_EXTRACT(metadata, '$.country')) STORED;
CREATE INDEX idx_country ON customers(country_index);
How can I help you explore Laravel packages today?