PHPackages                             cadabra/php - 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. cadabra/php

ActiveLibrary[Database &amp; ORM](/categories/database)

cadabra/php
===========

PHP client and Symfony bundle for Cadabra query cache with Doctrine DBAL middleware

v0.2.3(6mo ago)03MITPHPPHP &gt;=8.2CI passing

Since Oct 27Pushed 6mo agoCompare

[ Source](https://github.com/SebastiaanWouters/cadabra-php)[ Packagist](https://packagist.org/packages/cadabra/php)[ RSS](/packages/cadabra-php/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (14)Versions (9)Used By (0)

Cadabra PHP Client &amp; Symfony Bundle
=======================================

[](#cadabra-php-client--symfony-bundle)

**Opt-in query caching** for Symfony applications with Doctrine ORM. Intercepts at DBAL level for transparent integration.

> **Note**: This package requires a running [Cadabra server](https://github.com/SebastiaanWouters/cadabra). The server handles SQL normalization, cache key generation, and invalidation logic.

Why This Works
--------------

[](#why-this-works)

This bundle intercepts database queries **after SQL generation but before execution** (DBAL `Statement::execute` level). It caches **raw database arrays before ORM hydration**, allowing Doctrine to hydrate entities normally. All Doctrine features work: UnitOfWork, lazy loading, lifecycle events, etc.

Key Design Principles
---------------------

[](#key-design-principles)

**1. Opt-In Caching**: Queries are **NOT cached by default**. You explicitly mark queries for caching using `->useCadabraCache()` or the `/* CADABRA:USE */` comment. This keeps cache size low and gives you full control.

**2. No Logic Duplication**: This client sends raw SQL to the Cadabra server without any normalization. The server handles all SQL normalization, cache key generation, and invalidation logic. This ensures consistent behavior across all clients (PHP, TypeScript, etc.).

**3. Automatic Invalidation**: Write queries (INSERT/UPDATE/DELETE) **always** trigger invalidation - no configuration needed. The server intelligently determines which cache entries to invalidate.

### The Architecture

[](#the-architecture)

```
User Code
   ↓
Doctrine Repository (find/createQueryBuilder/etc.)
   ↓
Doctrine ORM (generates DQL)
   ↓
Doctrine DBAL (converts to SQL: "SELECT t0.id FROM users t0 WHERE t0.id = ?")
   ↓
🎯 CadabraMiddleware (intercepts here)
   ├─ Check for /* CADABRA:USE */ comment
   ├─ If NO comment → Execute directly (no caching)
   ├─ If YES → Send RAW SQL to Cadabra server for analysis
   ├─ Server normalizes (t0 → u) and generates cache key fingerprint
   ├─ Check server cache by fingerprint
   ├─ Return CachedResult if hit
   └─ Execute & register with server if miss
   ↓
Database (on cache miss or when not using Cadabra)
   ↓
Doctrine ORM (hydrates entities from cached arrays)
   ↓
Your Entity Objects

```

Installation
------------

[](#installation)

```
composer require cadabra/php
```

Quick Start
-----------

[](#quick-start)

### 1. Register the Bundle

[](#1-register-the-bundle)

```
// config/bundles.php
return [
    // ...
    Cadabra\SymfonyBundle\CadabraBundle::class => ['all' => true],
];
```

### 2. Configure Cadabra

[](#2-configure-cadabra)

```
# config/packages/cadabra.yaml
cadabra:
    service_url: '%env(CADABRA_SERVICE_URL)%'  # Required
    prefix: '%env(APP_ENV)%_myapp'              # Optional, default: 'cadabra'
```

### 3. Set Environment Variables

[](#3-set-environment-variables)

```
# .env
CADABRA_SERVICE_URL=http://localhost:6942
```

### 4. Integrate CadabraQueryBuilder (Recommended)

[](#4-integrate-cadabraquerybuilder-recommended)

Create a base repository class that returns `CadabraQueryBuilder` instead of the default `QueryBuilder`:

```
// src/Repository/CadabraRepository.php
namespace App\Repository;

use Cadabra\SymfonyBundle\ORM\CadabraQueryBuilder;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;

abstract class CadabraRepository extends ServiceEntityRepository
{
    public function createQueryBuilder($alias, $indexBy = null): CadabraQueryBuilder
    {
        return (new CadabraQueryBuilder($this->getEntityManager()))
            ->select($alias)
            ->from($this->getEntityName(), $alias, $indexBy);
    }
}
```

Then extend it in your repositories:

```
// src/Repository/UserRepository.php
namespace App\Repository;

use App\Entity\User;

class UserRepository extends CadabraRepository
{
    // Now createQueryBuilder() returns CadabraQueryBuilder
    // which has ->useCadabraCache() method available
}
```

### 5. Mark Queries for Caching

[](#5-mark-queries-for-caching)

```
// Enable caching for specific queries
$users = $repository->createQueryBuilder('u')
    ->where('u.status = :status')
    ->setParameter('status', 'active')
    ->useCadabraCache()  // ← Opt-in to caching
    ->getQuery()
    ->getResult();
```

Usage
-----

[](#usage)

### Opt-In Caching with QueryBuilder

[](#opt-in-caching-with-querybuilder)

**Method 1: Using CadabraQueryBuilder (Recommended)**

```
// After integrating CadabraRepository (see Quick Start)
public function findActiveUsers(): array
{
    return $this->createQueryBuilder('u')
        ->where('u.status = :status')
        ->setParameter('status', 'active')
        ->useCadabraCache()  // ← Enable caching
        ->getQuery()
        ->getResult();
}

// Queries without ->useCadabraCache() are NOT cached
public function findUserForUpdate(int $id): ?User
{
    return $this->createQueryBuilder('u')
        ->where('u.id = :id')
        ->setParameter('id', $id)
        // No ->useCadabraCache() = no caching (good for transactions)
        ->getQuery()
        ->getOneOrNullResult();
}
```

**Method 2: Using Trait in Custom QueryBuilder**

If you already have a custom QueryBuilder:

```
namespace App\ORM;

use Cadabra\SymfonyBundle\ORM\CadabraQueryBuilderTrait;
use Doctrine\ORM\QueryBuilder;

class MyCustomQueryBuilder extends QueryBuilder
{
    use CadabraQueryBuilderTrait;

    // Your custom methods here
}
```

**Method 3: Using Magic Comment (Raw SQL)**

```
// Cache this query
$sql = '/* CADABRA:USE */ SELECT * FROM users WHERE status = ?';
$stmt = $conn->prepare($sql);
$result = $stmt->execute(['active']);

// Don't cache this query (default behavior)
$sql = 'SELECT * FROM users WHERE status = ?';
$stmt = $conn->prepare($sql);
$result = $stmt->execute(['active']);
```

### Automatic Invalidation (All Write Queries)

[](#automatic-invalidation-all-write-queries)

**All write queries trigger automatic invalidation** - no opt-in required:

```
// INSERT - automatically triggers invalidation
$user = new User();
$user->setName('John');
$em->persist($user);
$em->flush();  // ← Server invalidates relevant cache entries

// UPDATE - automatically triggers invalidation
$user->setEmail('new@example.com');
$em->flush();  // ← Server invalidates cache entries for this user

// DELETE - automatically triggers invalidation
$em->remove($user);
$em->flush();  // ← Server invalidates cache entries for this user
```

The Cadabra server intelligently determines which cache entries to invalidate based on:

- Tables affected
- Rows modified
- Columns changed

### Example: Cache Hit Flow

[](#example-cache-hit-flow)

```
// First call - cache MISS
$user = $repository->createQueryBuilder('u')
    ->where('u.email = :email')
    ->setParameter('email', 'john@example.com')
    ->useCadabraCache()
    ->getQuery()
    ->getOneOrNullResult();

// Doctrine generates SQL: "SELECT t0.id, t0.name, t0.email FROM users t0 WHERE t0.email = ?"
// CadabraMiddleware sees /* CADABRA:USE */ comment
// → Sends RAW SQL to server
// → Server normalizes and generates fingerprint
// → Cache MISS
// → Executes real query, returns: [['id' => 10, 'name' => 'John', 'email' => 'john@example.com']]
// → Registers with server for caching and invalidation tracking
// → Returns CachedResult to Doctrine
// → Doctrine hydrates to User entity

// Second call - cache HIT
$user = $repository->createQueryBuilder('u')
    ->where('u.email = :email')
    ->setParameter('email', 'john@example.com')
    ->useCadabraCache()
    ->getQuery()
    ->getOneOrNullResult();

// → Sends RAW SQL to server
// → Server recognizes same fingerprint
// → Cache HIT - returns cached array directly
// → Doctrine hydrates from cached data
// → Result: User entity (no database query executed!)
```

Features
--------

[](#features)

### ✅ Lazy Loading Works

[](#-lazy-loading-works)

```
$user = $repo->createQueryBuilder('u')
    ->where('u.id = :id')
    ->setParameter('id', 10)
    ->useCadabraCache()
    ->getQuery()
    ->getOneOrNullResult();  // Cached

$orders = $user->getOrders();  // Lazy load - NEW query, can also be cached if marked
```

### ✅ Transactions Work

[](#-transactions-work)

```
$em->beginTransaction();
try {
    $user->setEmail('new@example.com');
    $em->flush();  // Invalidation triggered
    $em->commit();
} catch (\Exception $e) {
    $em->rollback();  // No invalidation on rollback
}
```

### ✅ All Doctrine Features Work

[](#-all-doctrine-features-work)

- ✅ UnitOfWork change tracking
- ✅ Lifecycle events (PrePersist, PostLoad, etc.)
- ✅ Entity listeners
- ✅ Proxy objects for lazy loading
- ✅ Cascade operations
- ✅ Orphan removal
- ✅ Doctrine's second-level cache (independent layer)

Configuration
-------------

[](#configuration)

### Available Settings

[](#available-settings)

**`service_url`** (required) URL of the Cadabra server. The server handles SQL normalization, cache key generation, and cache storage.

**`prefix`** (optional, default: `'cadabra'`) Cache key prefix/namespace. Use different prefixes for different environments or applications sharing the same Cadabra server.

```
# config/packages/cadabra.yaml
cadabra:
    service_url: 'http://localhost:6942'
    prefix: 'prod_myapp'  # Different prefix per environment
```

### Cache Storage

[](#cache-storage)

Cache is stored **on the Cadabra server**, not locally. This provides:

- **Shared cache** across multiple app servers
- **Centralized invalidation** - one server writes, all servers' cache updated
- **No local memory overhead** - cache lives on dedicated server
- **Persistent cache** - survives app restarts

When to Use Caching
-------------------

[](#when-to-use-caching)

### ✅ Good Candidates for Caching

[](#-good-candidates-for-caching)

Mark these queries with `->useCadabraCache()`:

- **Read-heavy queries**: Product catalogs, user profiles, category lists
- **Expensive queries**: Complex JOINs, aggregations, GROUP BY
- **Frequently accessed data**: Homepage content, navigation menus
- **Paginated lists**: Search results, product listings
- **Static-ish data**: Settings, configurations, rarely updated content

### ❌ Don't Cache These

[](#-dont-cache-these)

Leave these queries without `->useCadabraCache()`:

- **Financial transactions**: Require real-time accuracy
- **Queries with FOR UPDATE locks**: Transaction-sensitive
- **Audit logs**: Frequently changing, must be current
- **Real-time data**: Stock prices, live scores
- **One-time queries**: Reports, exports
- **Development/debugging**: When you need to see immediate changes

Advanced Usage
--------------

[](#advanced-usage)

### Alternative: Use Trait in Custom QueryBuilder

[](#alternative-use-trait-in-custom-querybuilder)

If you have an existing custom QueryBuilder and can't change repositories:

```
namespace App\ORM;

use Cadabra\SymfonyBundle\ORM\CadabraQueryBuilderTrait;
use Doctrine\ORM\QueryBuilder;

class AppQueryBuilder extends QueryBuilder
{
    use CadabraQueryBuilderTrait;

    // Your existing methods here
}
```

Then configure your EntityManager to use it:

```
# config/packages/doctrine.yaml
doctrine:
    orm:
        query_builder_class: App\ORM\AppQueryBuilder
```

### Manual Cache Control

[](#manual-cache-control)

```
use Cadabra\Client\CadabraClient;

class CacheService
{
    public function __construct(private CadabraClient $client) {}

    public function clearTableCache(string $table): void
    {
        // Manually clear cache for a specific table
        $this->client->clearTable($table);
    }

    public function getStats(): array
    {
        return $this->client->getStats();
    }
}
```

### Monitoring &amp; Debugging

[](#monitoring--debugging)

Enable debug logging to see cache hits/misses:

```
# config/packages/monolog.yaml
monolog:
    handlers:
        cadabra:
            type: stream
            path: '%kernel.logs_dir%/cadabra.log'
            level: debug
            channels: ['cadabra']
```

Log output:

```
[2024-01-15 10:23:45] cadabra.DEBUG: Cache HIT {"sql":"SELECT...","fingerprint":"abc123"}
[2024-01-15 10:23:46] cadabra.DEBUG: Cache MISS {"sql":"SELECT...","fingerprint":"def456"}
[2024-01-15 10:23:47] cadabra.DEBUG: Invalidation queued {"sql":"UPDATE users..."}

```

Performance
-----------

[](#performance)

### Typical Results

[](#typical-results)

With opt-in caching on appropriate queries:

- **Cache hit rate**: 80-95% for marked queries
- **Response time improvement**: 2-5x faster for cached queries
- **Database load reduction**: 60-80% fewer queries on cached operations

### Overhead

[](#overhead)

- **Cache miss**: +2-5ms (server analysis + caching)
- **Cache hit**: +0.5-1ms (much faster than database)
- **Invalidation**: Async, zero overhead on writes

### Benchmark Results

[](#benchmark-results)

From integration tests (50 iterations):

Query TypeCold (No Cache)With CacheSpeedupSimple lookup1.35ms634μs2.1xJOIN with pagination648μs153μs4.2xPrice range filter6.01ms1.74ms3.5xGROUP BY aggregate539μs144μs3.7xComplex aggregate719μs151μs4.8x**Average speedup: 2.8x faster**

Server Setup
------------

[](#server-setup)

### Using Docker (Recommended)

[](#using-docker-recommended)

```
docker pull ghcr.io/sebastiaanwouters/cadabra:latest
docker run -d -p 6942:6942 --name cadabra-server \
  ghcr.io/sebastiaanwouters/cadabra:latest
```

### Verify Server is Running

[](#verify-server-is-running)

```
curl http://localhost:6942/health
# Should return: {"status":"ok"}
```

### From Source

[](#from-source)

```
git clone https://github.com/SebastiaanWouters/cadabra
cd cadabra
# See repository README for setup instructions
```

Development
-----------

[](#development)

### Running Tests

[](#running-tests)

```
# Run all tests
composer test

# Run only unit tests
composer test:unit

# Run integration tests (requires Cadabra server)
cd symfony-test-app
vendor/bin/phpunit

# Check code style
composer cs:check

# Auto-fix code style
composer cs:fix

# Run all checks
composer check
```

### Testing in Your Application

[](#testing-in-your-application)

**Option 1: Disable Cadabra in tests** (queries execute directly):

```
# config/packages/test/cadabra.yaml
cadabra:
    service_url: 'http://localhost:6942'  # Point to test server
```

**Option 2: Mock the Cadabra client**:

```
// In your test
$mockClient = $this->createMock(CadabraClient::class);
$mockClient->method('get')->willReturn(['id' => 1, 'name' => 'Test']);
```

**Option 3: Use in-memory test database** (fastest, most isolated):

```
# config/packages/test/doctrine.yaml
doctrine:
    dbal:
        url: 'sqlite:///:memory:'
```

Troubleshooting
---------------

[](#troubleshooting)

### Queries Not Being Cached

[](#queries-not-being-cached)

**Check 1**: Did you add `->useCadabraCache()`?

```
// ❌ NOT cached (missing ->useCadabraCache())
$users = $repo->createQueryBuilder('u')
    ->where('u.status = :status')
    ->getQuery()
    ->getResult();

// ✅ Cached
$users = $repo->createQueryBuilder('u')
    ->where('u.status = :status')
    ->useCadabraCache()  // ← Added
    ->getQuery()
    ->getResult();
```

**Check 2**: Is CadabraQueryBuilder being used?

```
// Verify your repository extends CadabraRepository
class UserRepository extends CadabraRepository  // ← Must extend this
{
    // ...
}
```

**Check 3**: Is the server running?

```
curl http://localhost:6942/health
```

**Check 4**: Enable debug logging to see what's happening:

```
# config/packages/monolog.yaml
monolog:
    handlers:
        main:
            level: debug
```

### Stale Data After Updates

[](#stale-data-after-updates)

If you see stale data after writes:

1. **Check write query executed**: Updates/deletes should trigger invalidation automatically
2. **Check server logs**: Look for invalidation messages
3. **Manually clear cache**: ```
    $this->cadabraClient->clearTable('users');
    ```

### Performance Issues

[](#performance-issues)

If caching makes queries slower:

1. **Check network latency** to Cadabra server
2. **Verify server health**: `curl http://localhost:6942/health`
3. **Consider query complexity**: Very simple queries might be faster without caching
4. **Use caching selectively**: Only mark expensive queries with `->useCadabraCache()`

Production Checklist
--------------------

[](#production-checklist)

- Cadabra service running and healthy
- Prefix set to environment-specific value: `prod_myapp`
- Only expensive/frequently-accessed queries marked with `->useCadabraCache()`
- Monitoring and logging configured
- Cache stats monitored (hit rate, performance)
- Load testing performed with caching enabled

How This Differs from Doctrine Cache
------------------------------------

[](#how-this-differs-from-doctrine-cache)

FeatureDoctrine Result CacheCadabra**Caching Strategy**Manual opt-in per queryManual opt-in per query**Interception Level**Result set (after hydration)DBAL (before hydration)**Invalidation**Manual/TTL onlyAutomatic on all writes**Granularity**Query-basedRow/column-aware**Storage**Local (per server)Centralized server**Normalization**NoneServer-side SQL normalization**Multi-server**Each server has own cacheShared cache across serversLicense
-------

[](#license)

MIT

Links
-----

[](#links)

- [Cadabra Server](https://github.com/SebastiaanWouters/cadabra)
- [Package on Packagist](https://packagist.org/packages/cadabra/php)
- [Report Issues](https://github.com/SebastiaanWouters/cadabra-php/issues)

###  Health Score

32

—

LowBetter than 72% of packages

Maintenance67

Regular maintenance activity

Popularity3

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 87.5% 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 ~0 days

Total

7

Last Release

203d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/c7cc634778558883084db4c89daefa1b871ed907358651baa49a771005734dff?d=identicon)[SebastiaanWouters](/maintainers/SebastiaanWouters)

---

Top Contributors

[![SebastiaanWouters](https://avatars.githubusercontent.com/u/24827662?v=4)](https://github.com/SebastiaanWouters "SebastiaanWouters (14 commits)")[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (2 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/cadabra-php/health.svg)

```
[![Health](https://phpackages.com/badges/cadabra-php/health.svg)](https://phpackages.com/packages/cadabra-php)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M651](/packages/sylius-sylius)[shopware/platform

The Shopware e-commerce core

3.3k1.5M3](/packages/shopware-platform)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.3M152](/packages/sulu-sulu)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

81733.7k](/packages/flow-php-flow)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.2M386](/packages/shopware-core)[sonata-project/entity-audit-bundle

Audit for Doctrine Entities

644989.8k1](/packages/sonata-project-entity-audit-bundle)

PHPackages © 2026

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