cryonighter/formula-doctrine-bundle
Installation:
composer require cryonighter/formula-doctrine-bundle
Add the bundle to config/bundles.php:
return [
// ...
Cryonighter\FormulaDoctrineBundle\FormulaDoctrineBundle::class => ['all' => true],
];
First Use Case:
Add a computed field to an existing Doctrine entity (e.g., Customer):
#[ORM\Entity]
class Customer
{
#[Formula('(SELECT COUNT(*) FROM orders o WHERE o.customer_id = {this}.id)')]
public int $orderCount = 0;
}
Clear cache:
php bin/console cache:clear
Verify:
Fetch a Customer entity via Doctrine (e.g., in a controller or repository):
$customer = $entityManager->getRepository(Customer::class)->find($id);
// $customer->orderCount will now be populated automatically
DQL vs. Native SQL:
SELECT COUNT(o) FROM App\Entity\Order o WHERE o.customer = {this}).(SELECT COUNT(*) FROM orders o WHERE o.customer_id = {this}.id)).Integration with Repositories:
find(), findBy(), or getReference() calls. No manual hydration needed.$customers = $repository->findBy(['active' => true]);
// All formula fields (e.g., $customer->orderCount) are populated.
Combining with QueryBuilder:
QueryBuilder results unless explicitly selected. Use addSelect():
$query = $repository->createQueryBuilder('c')
->addSelect('c.orderCount') // Explicitly include formula field
->where('c.active = :active')
->setParameter('active', true);
Caching Strategies:
FormulaDriver (see Extension Points).Type Safety:
int, string, DateTime). The bundle handles basic type conversion.Symfony Forms:
$builder->remove('orderCount'); // Explicitly exclude from form
DataTransformer to handle computed values in forms.API Platform:
Doctrine Lifecycle Events:
loadClassMetadata and before postLoad. Use postLoad to react to computed values:
#[ORM\PostLoad]
public function postLoad(): void {
if ($this->orderCount > 10) {
$this->setVipStatus(true);
}
}
Testing:
FormulaDriver in unit tests to avoid hitting the database:
$driver = $this->createMock(FormulaDriver::class);
$driver->method('getValue')->willReturn(42);
$entityManager->getConnection()->getConfiguration()->setFormulaDriver($driver);
Parentheses in DQL:
// ❌ Wrong (extra parentheses)
#[Formula('(SELECT COUNT(o) FROM App\Entity\Order o WHERE o.customer = {this})')]
// ✅ Right (no parentheses for DQL)
#[Formula('SELECT COUNT(o) FROM App\Entity\Order o WHERE o.customer = {this}')]
N+1 Queries:
find() or findBy(). However, manual queries (e.g., createQueryBuilder()) may require explicit JOIN or addSelect():
// ❌ May cause N+1 if orderCount is accessed later
$query->where('c.id = :id');
// ✅ Safe (explicitly include formula)
$query->addSelect('c.orderCount');
Circular Dependencies:
#[Formula('{this}.fieldA + {this}.fieldB')] where fieldB is also a formula). This can lead to infinite recursion.Database-Specific SQL:
Case Sensitivity:
{this}.id) are case-sensitive. Use the exact property name defined in the entity.Enable SQL Logging:
# config/packages/doctrine.yaml
doctrine:
dbal:
logging: true
profiling: true
SELECT queries generated by the bundle.Check FormulaDriver:
FormulaDriver is registered:
php bin/console debug:container cryonighter.formula_doctrine.driver
FormulaDriver is overriding the default behavior.Metadata Cache:
php bin/console doctrine:cache:clear-metadata
Custom FormulaDriver:
# config/services.yaml
services:
App\Formula\CustomFormulaDriver:
decorates: 'cryonighter.formula_doctrine.driver'
arguments:
$decorated: '@.inner'
Cryonighter\FormulaDoctrine\Driver\FormulaDriverInterface.Event Subscribers:
onFlush or postLoad to modify formula behavior:
#[AsEventSubscriber]
class FormulaSubscriber implements EventSubscriber
{
public static function getSubscribedEvents(): array
{
return [
Events::postLoad,
];
}
public function postLoad(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof Customer && $entity->orderCount > 0) {
$entity->setHasOrders(true);
}
}
}
Custom SQL Walker:
cryonighter/formula-doctrine for details.Selective Loading:
DISTINCT or JOIN in formulas to avoid redundant calculations:
#[Formula('SELECT COUNT(DISTINCT o.id) FROM App\Entity\Order o WHERE o.customer = {this}')]
Indexing:
customer_id) in formula subqueries are indexed for performance.Materialized Views:
#[Formula('(SELECT order_count FROM customer_order_counts WHERE customer_id = {this}.id)')]
How can I help you explore Laravel packages today?