PHPackages                             cryonighter/formula-doctrine-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. cryonighter/formula-doctrine-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

cryonighter/formula-doctrine-bundle
===================================

Symfony bundle integrating Hibernate-style #\[Formula\] computed fields into Doctrine ORM entities

1.0.2(2w ago)036↓100%MITPHPPHP &gt;=8.2

Since May 1Pushed 2w agoCompare

[ Source](https://github.com/cryonighter/formula-doctrine-bundle)[ Packagist](https://packagist.org/packages/cryonighter/formula-doctrine-bundle)[ Docs](https://github.com/cryonighter/formula-doctrine-bundle)[ RSS](/packages/cryonighter-formula-doctrine-bundle/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (8)Versions (4)Used By (0)

Formula Doctrine Bundle
=======================

[](#formula-doctrine-bundle)

[![Latest Version on Packagist](https://camo.githubusercontent.com/36d66de7e19c42958ff71036ca29b3da27af96462834c44688377a212a1d49b3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6372796f6e6967687465722f666f726d756c612d646f637472696e652d62756e646c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cryonighter/formula-doctrine-bundle)[![Software License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE)[![Total Downloads](https://camo.githubusercontent.com/499cd0f788dd068443d621bf2bfc1b0b5a6f8b40c4ba6c0c6bdfe2ea1a778a0f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6372796f6e6967687465722f666f726d756c612d646f637472696e652d62756e646c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cryonighter/formula-doctrine-bundle)

Symfony bundle for integrating [`cryonighter/formula-doctrine`](https://github.com/cryonighter/formula-doctrine)into Symfony applications.

It enables Hibernate-style `#[Formula]` computed fields for Doctrine ORM entities and wires the required Doctrine metadata listeners, SQL walker configuration and DBAL middleware automatically through Symfony's dependency injection container.

Use it when you want read-only entity properties whose values are computed by SQL expressions, subqueries, aggregations or joins — without adding physical database columns and without introducing N+1 queries.

```
#[ORM\Entity]
class Customer
{
    #[Formula('(SELECT COUNT(*) FROM orders o WHERE o.customer_id = {this}.id)')]
    public int $orderCount = 0;
}
```

With this bundle installed, formula fields are populated automatically when entities are loaded through Doctrine in a Symfony application. The bundle keeps your entity code focused on the `#[Formula]` attributes while taking care of registering the integration services needed by [`cryonighter/formula-doctrine`](https://github.com/cryonighter/formula-doctrine).

Requirements
------------

[](#requirements)

- **PHP &gt;= 8.2.0** but the latest stable version of PHP is recommended

Install
-------

[](#install)

Via Composer

```
composer require cryonighter/formula-doctrine-bundle
```

The bundle will be automatically registered in `config/bundles.php`:

```
return [
    // ...
    Cryonighter\FormulaDoctrine\FormulaDoctrineBundle::class => ['all' => true],
];
```

### Bundle Registration Order

[](#bundle-registration-order)

If you use other bundles that extend Doctrine ORM with custom SQL walkers (e.g. Gedmo DoctrineExtensions, API Platform), register `FormulaDoctrineBundle`**last** in `config/bundles.php`:

```
php
return [
    // ... other bundles ...
    Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
    Cryonighter\FormulaDoctrine\FormulaDoctrineBundle::class => ['all' => true], // ← last
];

```

`FormulaDoctrineBundle` automatically detects and chains with any previously registered output walker, so both transformations are applied to every query.

If another bundle is registered after `FormulaDoctrineBundle` and also sets a custom output walker globally, you may need to manually call `FormulaDoctrineConfigurator::configure()` in your application's bundle.

Usage
-----

[](#usage)

### Basic example

[](#basic-example)

Add `#[Formula]` to any property on a Doctrine entity. The property **must not** be mapped with `#[ORM\Column]`.

```
use Cryonighter\FormulaDoctrine\Attribute\Formula;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'customers')]
class Customer
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    public int $id;

    #[ORM\Column]
    public string $name;

    #[Formula('(SELECT COUNT(*) FROM orders o WHERE o.customer_id = {this}.id)')]
    public int $orderCount = 0;

    #[Formula('(SELECT COALESCE(SUM(oi.price), 0) FROM order_items oi JOIN orders o ON oi.order_id = o.id WHERE o.customer_id = {this}.id)')]
    public float $totalRevenue = 0.0;

    #[Formula('(SELECT MAX(o.created_at) FROM orders o WHERE o.customer_id = {this}.id)')]
    public ?string $lastOrderDate = null;
}
```

### Fetching entities

[](#fetching-entities)

No changes to your query code are needed. Formula fields are populated automatically on every DQL `SELECT`:

```
$customers = $entityManager
    ->createQuery('SELECT c FROM App\Entity\Customer c')
    ->getResult();

foreach ($customers as $customer) {
    echo $customer->orderCount;    // populated from subquery
    echo $customer->totalRevenue;  // populated from subquery
}
```

A single SQL query is executed — no N+1:

```
SELECT c0_.id,
       c0_.name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c0_.id) AS orderCount,
       (SELECT COALESCE(SUM(...), 0) FROM ...) AS totalRevenue,
       (SELECT MAX(...) FROM ...) AS lastOrderDate
FROM customers c0_
```

### QueryBuilder

[](#querybuilder)

Works with `QueryBuilder` too:

```
$customers = $entityManager
    ->createQueryBuilder()
    ->select('c')
    ->from(Customer::class, 'c')
    ->where('c.name LIKE :name')
    ->setParameter('name', '%Acme%')
    ->getQuery()
    ->getResult();
```

And in the repositories too:

```
class CustomerRepository extends ServiceEntityRepository
{
    public function findTopCustomers(int $limit): array
    {
        return $this->createQueryBuilder('c')
            ->orderBy('c.id', 'ASC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
        // $result[0]->totalRevenue is populated automatically
    }
}
```

Methods find(), findBy(), findOneBy() and findAll() are also supported:

```
$customerRepository = $this->em->getRepository(Customer::class);

$customers = $customerRepository->findAll();

echo $customer[0]->orderCount;    // populated from subquery
echo $customer[0]->totalRevenue;  // populated from subquery
```

### Using formula fields in queries

[](#using-formula-fields-in-queries)

Formula fields can be used in `WHERE`, `ORDER BY`, `GROUP BY` and `HAVING` clauses just like regular entity properties:

#### WHERE clause

[](#where-clause)

Filter entities by computed values:

```
// DQL
$customers = $entityManager
    ->createQuery('SELECT c FROM App\Entity\Customer c WHERE c.orderCount > :minOrders')
    ->setParameter('minOrders', 5)
    ->getResult();

// QueryBuilder
$customers = $entityManager
    ->createQueryBuilder()
    ->select('c')
    ->from(Customer::class, 'c')
    ->where('c.totalRevenue >= :minRevenue')
    ->setParameter('minRevenue', 1000.0)
    ->getQuery()
    ->getResult();

// Repository findBy()
$customers = $customerRepository->findBy(['orderCount' => 10]);
```

#### ORDER BY clause

[](#order-by-clause)

Sort by formula fields:

```
// DQL
$customers = $entityManager
    ->createQuery('SELECT c FROM App\Entity\Customer c ORDER BY c.totalRevenue DESC')
    ->getResult();

// QueryBuilder
$customers = $entityManager
    ->createQueryBuilder()
    ->select('c')
    ->from(Customer::class, 'c')
    ->orderBy('c.orderCount', 'DESC')
    ->getQuery()
    ->getResult();

// Repository findBy() with ordering
$customers = $customerRepository->findBy(
    [],
    ['totalRevenue' => 'DESC']
);
```

#### GROUP BY and HAVING clauses

[](#group-by-and-having-clauses)

Aggregate and filter by computed values:

```
// Group customers by order count and filter groups
$result = $entityManager
    ->createQuery('
        SELECT c.orderCount, COUNT(c.id) as customerCount, AVG(c.totalRevenue) as avgRevenue
        FROM App\Entity\Customer c
        GROUP BY c.orderCount
        HAVING c.orderCount >= :minOrders AND COUNT(c.id) > :minCustomers
        ORDER BY c.orderCount DESC
    ')
    ->setParameter('minOrders', 3)
    ->setParameter('minCustomers', 1)
    ->getResult();

// Result example:
// [
//   ['orderCount' => 10, 'customerCount' => 5, 'avgRevenue' => 15000.50],
//   ['orderCount' => 7,  'customerCount' => 3, 'avgRevenue' => 8500.25],
//   ...
// ]
```

#### Combined example

[](#combined-example)

All clauses together in a single query:

```
$result = $entityManager
    ->createQuery('
        SELECT c.orderCount, COUNT(c.id) as total
        FROM App\Entity\Customer c
        WHERE c.totalRevenue > :minRevenue
        GROUP BY c.orderCount
        HAVING c.orderCount BETWEEN :minOrders AND :maxOrders
        ORDER BY c.orderCount DESC
    ')
    ->setParameter('minRevenue', 500.0)
    ->setParameter('minOrders', 2)
    ->setParameter('maxOrders', 10)
    ->getResult();
```

> **Note:** Formula fields work transparently in all query clauses. The SQL subquery is embedded only once per query, not per clause usage.

### Nullable fields

[](#nullable-fields)

If a formula can return `NULL` (e.g. `MAX` on an empty set), declare the property as nullable — the type is inferred automatically:

```
#[Formula('(SELECT MAX(o.total) FROM orders o WHERE o.customer_id = {this}.id)')]
public ?float $maxOrderTotal = null;
```

### The `{this}` placeholder

[](#the-this-placeholder)

Use `{this}` to reference the root entity's table alias in the SQL expression. It is resolved to the actual Doctrine-generated alias (e.g. `c0_`) at query time.

```
// {this} will become the real SQL alias, e.g. c0_
#[Formula('(SELECT COUNT(*) FROM orders o WHERE o.customer_id = {this}.id)')]
public int $orderCount = 0;
```

> **Do not** hardcode the table name directly — it will break when Doctrine generates a different alias.

### Custom SELECT alias

[](#custom-select-alias)

By default the SQL column alias matches the property name. Override it with the `alias` parameter:

```
#[Formula(
    sql: '(SELECT COUNT(*) FROM orders o WHERE o.customer_id = {this}.id)',
    alias: 'total_orders',
)]
public int $orderCount = 0;
```

> Use a custom alias only when you need to control the raw SQL column name, e.g. for compatibility with a specific reporting tool.

How it works
------------

[](#how-it-works)

You can read about this in the description of the base package [`cryonighter/formula-doctrine`](https://github.com/cryonighter/formula-doctrine#how-it-works).

Change log
----------

[](#change-log)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Testing
-------

[](#testing)

```
# All tests
./vendor/bin/phpunit

# Only unit
./vendor/bin/phpunit --testsuite Unit

# Only integration
./vendor/bin/phpunit --testsuite Integration

# Specific file
./vendor/bin/phpunit tests/Unit/DependencyInjection/FormulaDoctrineCompilerPassTest.php

# With coating (requires Xdebug or PCOV)
./vendor/bin/phpunit --coverage-text
```

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE\_OF\_CONDUCT](CODE_OF_CONDUCT.md) for details.

Security
--------

[](#security)

If you discover any security related issues, please email `cryonighter@yandex.ru` instead of using the issue tracker.

Credits
-------

[](#credits)

- [Andrey Reshetchenko](https://github.com/cryonighter)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance96

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~10 days

Total

3

Last Release

20d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/640fd5daf250ddc27926b04a4fa74e2d8239c777475a783f155a6c9d47e1df78?d=identicon)[cryonighter](/maintainers/cryonighter)

---

Top Contributors

[![cryonighter](https://avatars.githubusercontent.com/u/20028293?v=4)](https://github.com/cryonighter "cryonighter (8 commits)")

---

Tags

symfonyormdoctrineformulavirtualcomputedgeneratedhibernate

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/cryonighter-formula-doctrine-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/cryonighter-formula-doctrine-bundle/health.svg)](https://phpackages.com/packages/cryonighter-formula-doctrine-bundle)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
