cryonighter/formula-doctrine-bundle
Install the Bundle
composer require cryonighter/formula-doctrine-bundle
Ensure Cryonighter\FormulaDoctrineBundle\CryonighterFormulaDoctrineBundle::class is registered in config/bundles.php.
First Use Case: Basic Formula Field
Add a computed field to an entity using the #[Formula] attribute:
#[ORM\Entity]
class Product
{
#[ORM\Id, ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
#[Formula('(SELECT AVG(rating) FROM reviews WHERE product_id = {this}.id)')]
public float $averageRating = 0.0;
}
Verify Functionality Fetch an entity and inspect the computed field:
$product = $entityManager->getRepository(Product::class)->find(1);
// $product->averageRating now contains the computed value.
Identify Computed Fields
#[Formula] for read-only properties derived from SQL (e.g., aggregations, joins, or subqueries).Leverage Placeholders
{this} to reference the current entity’s columns (e.g., {this}.id).#[Formula('SUM(o.quantity) FROM order_items o WHERE o.order_id = {this}.id')]
public int $totalOrderQuantity = 0;
Query Optimization
HYDRATE_ARRAY or custom hydration.Doctrine\ORM\QueryBuilder with addSelect() to explicitly include formulas:
$qb = $entityManager->createQueryBuilder()
->select('p', 'f1.averageRating AS HIDDEN averageRating') // HIDDEN prevents column naming conflicts
->from(Product::class, 'p')
->leftJoin('p.formulas', 'f1');
Caching Strategies
cryonighter_formula.doctrine.cache in config/packages/cryonighter_formula_doctrine.yaml to cache formula results (e.g., apcu or redis).
cryonighter_formula:
doctrine:
cache: true
cache_driver: apcu
null = no caching):
#[Formula('...', ttl: 3600)] // Cache for 1 hour
Integration with DTOs
#[ApiResource]
class ProductDto
{
public function getAverageRating(): float {
return $this->product->averageRating; // Assumes Product has #[Formula]
}
}
Testing
FormulaDoctrine\Formula\FormulaManager in tests to return deterministic values:
$formulaManager = $this->createMock(FormulaManager::class);
$formulaManager->method('getValue')->willReturn(4.5);
$entityManager->getConnection()->getConfiguration()->setFormulaManager($formulaManager);
Circular Dependencies
Formula A depends on Formula B which depends on Formula A) cause infinite recursion.Performance Overhead
EXPLAIN in your database.#[ORM\Index] on frequently filtered columns in formulas.Database Compatibility
#[Formula('CASE WHEN {dialect} = "mysql" THEN ... ELSE ... END')]
Lazy Loading Conflicts
find() without explicit SELECT.QueryBuilder or Criteria to ensure formulas are included:
$entityManager->createQueryBuilder()
->select('e', 'f1.averageRating AS HIDDEN averageRating')
->from(Entity::class, 'e')
->leftJoin('e.formulas', 'f1')
->getQuery()
->getResult();
Serialization Warnings
public float $averageRating = 0.0).Enable SQL Logging Configure Doctrine to log generated SQL with formulas:
# config/packages/dev/doctrine.yaml
doctrine:
dbal:
logging: true
profiling: true
FORMULA placeholders in queries.Inspect Metadata Dump the entity metadata to verify formula registration:
$metadata = $entityManager->getClassMetadata(Product::class);
print_r($metadata->formulaMappings); // Should list your #[Formula] attributes.
Override Formula Resolution
For debugging, override the FormulaManager to log or mock formula execution:
$entityManager->getConnection()->getConfiguration()->setFormulaManager(
new class implements FormulaManagerInterface {
public function getValue(Formula $formula, EntityManagerInterface $em, array $context): mixed {
error_log("Executing formula: " . $formula->getExpression());
return parent::getValue($formula, $em, $context);
}
}
);
Custom Formula Types Extend the bundle to support custom formula types (e.g., JavaScript-based computations):
// src/Formula/CustomFormula.php
class CustomFormula implements FormulaInterface {
public function getExpression(): string { ... }
public function getValue(EntityManagerInterface $em, array $context): mixed {
// Custom logic (e.g., call an external API).
}
}
Register the type in the bundle’s configuration.
Event Listeners Hook into formula resolution via Doctrine events:
$eventManager->addEventListener(ORM\Events::loadClassMetadata, function (LoadClassMetadataEvent $event) {
$metadata = $event->getClassMetadata();
if ($metadata->name === Product::class) {
$metadata->setFormula('custom_field', 'SELECT ...');
}
});
Database-Specific Dialects Override the SQL dialect to handle database-specific syntax:
# config/packages/cryonighter_formula_doctrine.yaml
cryonighter_formula:
doctrine:
dialect: App\Doctrine\CustomFormulaDialect
How can I help you explore Laravel packages today?