stinger-soft/doctrine-commons
A shared Doctrine commons library for PHP projects, providing reusable persistence utilities and foundational components that help standardize entity, repository, and database setup across Stinger Soft applications.
This is a small utility package for Doctrine ORM: it ships custom DQL functions and a handful of helper functions. Treat it as a toolbox you reach for when native DQL can't express what the query needs, not as a framework.
Install and wire it up
composer require stinger-soft/doctrine-commons
In a Laravel app using laravel-doctrine/orm, the integration point is config/doctrine.php. DQL functions are registered by category, so the exact key depends on what each function returns:
// config/doctrine.php
'custom_string_functions' => [
'FIELD' => StingerSoft\DoctrineCommons\DQL\FieldFunction::class,
'FIND_IN_SET' => StingerSoft\DoctrineCommons\DQL\FindInSetFunction::class,
],
'custom_numeric_functions' => [
// ...
],
'custom_datetime_functions' => [
// ...
],
If you're on plain Doctrine (Symfony-style or standalone), do the same registration in the Configuration object:
$config->addCustomStringFunction('FIELD', FieldFunction::class);
Where to look first
src/ — skim the directory listing; the DQL function classes are the bulk of the value. Each class declares getName() and getSql().composer.json — check the Doctrine ORM version constraint. This tells you immediately whether it will work with your installed doctrine/orm major version, which is the #1 compatibility risk.First use case
The canonical reason to install this is ordering by a fixed list of IDs or slugs without raw SQL:
$qb->addSelect('FIELD(p.status, :statuses) AS HIDDEN sortOrder')
->setParameter('statuses', ['pending', 'active', 'archived'])
->orderBy('sortOrder', 'ASC');
Or FIND_IN_SET for filtering against a comma-separated column when you've inherited a schema you can't normalize:
$qb->andWhere('FIND_IN_SET(:tag, p.tags) > 0')
->setParameter('tag', 'php');
If neither of those applies to your codebase, you probably don't need the package — verify the function list before committing to the dependency.
Register once, use everywhere
The registration is global to the EntityManager, so any repository, query builder, or Paginator on that connection sees the functions. Register in a service provider or config bootstrap, never per-query.
Repository + QueryBuilder pattern
Keep the function usage inside repository methods rather than in controllers. The DQL string is platform-coupled (see Gotchas), so isolating it makes a future swap tractable.
class ProductRepository
{
public function findOrderedByStatus(array $statusOrder): array
{
return $this->createQueryBuilder('p')
->addSelect('FIELD(p.status, :order) AS HIDDEN sort')
->setParameter('order', $statusOrder)
->orderBy('sort', 'ASC')
->addOrderBy('p.name', 'ASC')
->getQuery()
->getResult();
}
}
Note the HIDDEN keyword — without it the scalar aliased column becomes part of the result shape and breaks getResult() hydration into entities.
Parameterized only
DQL custom functions receive AST\Node arguments and translate them to SQL verbatim. Anything you inline into the DQL string is injected into SQL. Always bind list values as parameters; Doctrine expands array parameters correctly for functions that accept variable arity, but verify that behavior for the specific function — not all custom functions implement variadic handling.
Pagination with Paginator
The Doctrine Paginator runs a count query derived from your DQL. Functions in SELECT are usually fine; functions in ORDER BY can occasionally confuse the count-query rewrite. If pagination returns wrong totals, run the getCountQuery variant manually and compare. Pattern:
$paginator = new Paginator($qb->getQuery(), $fetchJoinCollection = false);
$total = count($paginator);
Composition with native DQL
These helpers pair well with CASE WHEN, COALESCE, and IDENTITY() because they fill gaps native DQL leaves open (ordering, string matching). Reach for native constructs first; use the package only where DQL genuinely has no equivalent.
Helper functions in entities
If the package includes PHP-side helpers (string normalization, comparison utilities), use them in entity lifecycle callbacks or value objects rather than duplicating logic in services. Keep them out of the query path — mixing PHP helpers and DQL functions with similar names is a common source of confusion.
Testing
Unit-test repository methods against a real database (SQLite in-memory is cheap but will not support MySQL-specific functions like FIELD/FIND_IN_SET). Either:
The second option is more work but keeps the test suite fast.
Version drift is the main risk
doctrine/orm 2.x → 3.x changed the FunctionNode API and removed some legacy behavior. With zero stars and zero dependents, this package has no community pressure keeping it current. Check the doctrine/orm constraint in composer.json and pin the package version explicitly. If you're on ORM 3 and the package targets 2.x, expect to fork and patch the getSql() signatures.
Platform-specific SQL
FIELD() and FIND_IN_SET() are MySQL/MariaDB functions. The DQL parser accepts them everywhere because it delegates to the custom function, but the generated SQL will fail on PostgreSQL or SQLite. If your app runs more than one engine (common with SQLite in tests), this is the failure mode you'll hit first. Options: guard per-platform, or register alternate DQL function classes per platform and choose at bootstrap.
Registration category mismatches
Doctrine dispatches parsing by the function's return type category. Registering a numeric-returning function under custom_string_functions produces a parse error or a silently wrong AST. Match the category to what the function actually returns, and confirm the class's intended category (NumericFunction vs StringPrimary return type) by reading its source — the README may not say.
Clear the metadata cache
After changing DQL function registration, clear the Doctrine metadata/query cache (php artisan doctrine:clear:metadata:cache, doctrine:clear:query:cache, or your equivalent). Stale parser state produces bewildering "unknown function" errors that persist across restarts of the request cycle.
Arity errors surface late
Custom functions typically validate argument count inside getSql() rather than during DQL parsing. A wrong number of arguments fails at query execution, not at build time — so it can slip past tests that never execute that branch. Add an execution test for each function you depend on.
HIDDEN and hydration
Any aliased scalar in SELECT needs HIDDEN if you're hydrating entities. Forgetting it yields arrays or EntityManager hydration exceptions. For array hydration, drop HIDDEN and use the alias deliberately.
Escape hatch: write your own function
The extension point is straightforward — extend Doctrine\ORM\Query\AST\Functions\FunctionNode, implement getSql(Query $query, Parser $parser), register it. For a one- or two-function need, that's 30 lines and you own the maintenance. Given the package's low activity, this is often the more defensible choice; import the package when you need several functions at once, or treat its source as a reference implementation to copy from.
Vendor risk discipline
With 0 stars and 0 dependents, assume no downstream bug reports and no security review. Read the actual source of every function class you register (they're small), confirm it does what the name says, and keep a note in your repo about what you rely on. That way, if you need to vendor a fork, you know exactly the surface area.
How can I help you explore Laravel packages today?