andydefer / laravel-cluster
A powerful cluster query parser and filter for Laravel with JSON column support
Requires
- php: ^8.1
- andydefer/laravel-actions: ^2.11.3
- laravel/framework: ^12.0|^13.0|^14.0|^15.0
Requires (Dev)
- laravel/pint: ^1.29
- laravel/tinker: ^2.11
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.23.3
- v0.23.2
- v0.23.1
- v0.23.0
- v0.22.0
- v0.21.0
- v0.20.5
- v0.20.4
- v0.20.3
- v0.20.2
- v0.20.1
- v0.20.0
- v0.19.4
- v0.19.3
- v0.19.2
- v0.19.1
- v0.19.0
- v0.18.0
- v0.17.1
- v0.17.0
- v0.16.0
- v0.15.0
- v0.14.0
- v0.13.0
- v0.12.0
- v0.11.0
- v0.10.0
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
- v0.5.1
- v0.5.0
- v0.4.1
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.0
This package is auto-updated.
Last update: 2026-09-10 14:52:39 UTC
README
Table des matières
- Installation
- Architecture du package
- Le moteur central : ClusterQuery
- Le service façade : ClusterService
- Structure des données : ClusterVO
- Eloquent Cast : ClusterCast
- La collection intelligente : ClusterVOCollection
- Filtrer des collections en mémoire
- Générer du SQL pour différents drivers
- Fonctions SQLite personnalisées
- Intégration avec Eloquent
- Les Macros Laravel
- Les fonctions SQL d'agrégation
- Les sous-conditions sur tableaux
- Les opérateurs EXISTS et NOT_EXISTS
- Les opérateurs LIKE et NOT_LIKE
- Les fonctions d'agrégation en mémoire
- Créer des fonctions personnalisées
- Parser et AST
- Référence des opérateurs
- Référence des méthodes de ClusterVOCollection
- Cas d'usage concrets
- Débogage et résolution des problèmes
- Performance et bonnes pratiques
1. Installation
1.1 Prérequis
- PHP 8.1 ou supérieur
- Laravel 10.x, 11.x, 12.x, 13.x, 14.x ou 15.x
1.2 Installation via Composer
composer require andydefer/laravel-cluster
1.3 Configuration
Le package s'enregistre automatiquement. Si vous utilisez une version de Laravel sans auto-discovery :
// config/app.php 'providers' => [ // ... AndyDefer\LaravelCluster\Providers\ClusterServiceProvider::class, ],
1.4 Structure du Service Provider
src/
├── Providers/
│ └── ClusterServiceProvider.php # Enregistrement des services
├── Utilities/
│ ├── ClusterMacroRegistrar.php # Enregistrement des macros
│ └── SqliteFunctionRegistrar.php # Fonctions SQLite personnalisées
├── Casts/
│ └── ClusterCast.php # Cast Eloquent
└── ...
1.5 Injection de dépendances
<?php namespace App\Services; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Registry\SqlFunctionRegistry; class UserFilterService { public function __construct( private readonly ClusterService $clusterService, private readonly ClusterQuery $clusterQuery, private readonly SqlFunctionRegistry $sqlRegistry ) {} public function filter(array $criteria) { // Utilisation de $this->clusterService... } }
2. Architecture du package
2.1 Flux de traitement d'une requête
use AndyDefer\LaravelCluster\Lexer; use AndyDefer\LaravelCluster\Parser; use AndyDefer\LaravelCluster\ClusterQuery; // 1. Vous écrivez une requête $query = 'status=active & age>25 & COUNT(addresses)>2'; // 2. Le Lexer tokenise l'expression $lexer = new Lexer(); $tokens = $lexer->tokenize($query); // 3. Le Parser construit l'AST $parser = new Parser(); $ast = $parser->parse($query); // 4. Le ClusterQuery exécute l'AST $clusterQuery = new ClusterQuery(); // Évaluation en mémoire $result = $clusterQuery->filter($collection, $query); // Génération SQL $sql = $clusterQuery->toSql('clusters', $query, DatabaseDriver::MYSQL); // Application Eloquent $clusterQuery->applyToEloquent($queryBuilder, 'clusters', $query, DatabaseDriver::MYSQL);
2.2 Les composants clés
use AndyDefer\LaravelCluster\Lexer; use AndyDefer\LaravelCluster\Parser; use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Registry\SqlFunctionRegistry; use AndyDefer\LaravelCluster\Registry\AggregateFunctionRegistry; // 1. Lexer - Tokenise une expression $tokens = (new Lexer())->tokenize('status=active'); // 2. Parser - Construit l'AST $ast = (new Parser())->parse('status=active'); // 3. ClusterQuery - Moteur central $engine = new ClusterQuery(); $filtered = $engine->filter($clusters, 'status=active'); // 4. ClusterService - Façade $service = new ClusterService($engine); $filtered = $service->filter($clusters, 'status=active'); // 5. Registres - Gestion des fonctions $sqlRegistry = new SqlFunctionRegistry(); $aggRegistry = new AggregateFunctionRegistry();
3. Le moteur central : ClusterQuery
ClusterQuery est le cœur du package. Il orchestre toutes les opérations.
3.1 Création
use AndyDefer\LaravelCluster\ClusterQuery; // Création simple $engine = new ClusterQuery(); // Via le conteneur Laravel $engine = app(ClusterQuery::class);
3.2 Parser une requête
use AndyDefer\LaravelCluster\ClusterQuery; $engine = new ClusterQuery(); // Parse une requête simple $ast = $engine->parse('status=active'); // Parse une requête complexe $ast = $engine->parse('status=active & (role=admin | role=doctor)'); // Parse une requête avec fonction SQL $ast = $engine->parse('COUNT(addresses) > 2'); // Parse une sous-condition $ast = $engine->parse('addresses[city=Kinshasa]');
3.3 Filtrer une collection en mémoire
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $engine = new ClusterQuery(); // Créer une collection $clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO(['status' => 'active', 'age' => 25])); $clusters->add(new ClusterVO(['status' => 'inactive', 'age' => 30])); $clusters->add(new ClusterVO(['status' => 'active', 'age' => 18])); // Filtrer $filtered = $engine->filter($clusters, 'status=active & age>=20'); // Résultat : 1 cluster (age=25)
3.4 Tester un cluster individuel
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $engine = new ClusterQuery(); $cluster = new ClusterVO([ 'status' => 'active', 'role' => 'admin', 'age' => 30, ]); // Test simple $matches = $engine->matches($cluster, 'status=active'); // true // Test avec AND $matches = $engine->matches($cluster, 'status=active & role=admin'); // true // Test avec OR $matches = $engine->matches($cluster, 'status=active | status=pending'); // true // Test avec fonction $matches = $engine->matches($cluster, 'age>25'); // true
3.5 Générer du SQL
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; $engine = new ClusterQuery(); // SQL pour MySQL $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::MYSQL); // JSON_EXTRACT(clusters, '$."status"') = 'active' // SQL pour SQLite $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::SQLITE); // json_extract(clusters, '$.status') = 'active' // SQL pour PostgreSQL $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::PGSQL); // clusters->>'status' = 'active' // SQL avec fonction $sql = $engine->toSql('clusters', 'COUNT(addresses) > 2', DatabaseDriver::SQLITE); // json_array_length(clusters, '$.addresses') > 2
3.6 Appliquer à Eloquent
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use App\Models\User; $engine = new ClusterQuery(); $query = User::query(); // Condition simple $engine->applyToEloquent($query, 'clusters', 'status=active', DatabaseDriver::MYSQL); // Conditions multiples $engine->applyToEloquent($query, 'clusters', 'status=active & role=admin', DatabaseDriver::MYSQL); // Sous-condition $engine->applyToEloquent($query, 'clusters', 'addresses[city=Kinshasa]', DatabaseDriver::MYSQL); // Fonction SQL $engine->applyToEloquent($query, 'clusters', 'COUNT(addresses) > 2', DatabaseDriver::MYSQL); $users = $query->get();
4. Le service façade : ClusterService
ClusterService est une façade qui délègue à ClusterQuery.
4.1 Création
use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\ClusterQuery; // Création manuelle $service = new ClusterService(new ClusterQuery()); // Via le conteneur $service = app(ClusterService::class);
4.2 Utilisation
use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $service = app(ClusterService::class); // 1. Parser $ast = $service->parse('status=active'); // 2. Filtrer une collection $clusters = new ClusterVOCollection(); $filtered = $service->filter($clusters, 'status=active'); // 3. Tester un cluster $cluster = new ClusterVO(['status' => 'active']); $matches = $service->matches($cluster, 'status=active'); // true // 4. Générer du SQL $sql = $service->toSql('clusters', 'status=active', DatabaseDriver::MYSQL); // 5. Appliquer à Eloquent $query = User::query(); $service->applyToEloquent($query, 'clusters', 'status=active', DatabaseDriver::MYSQL); $users = $query->get();
4.3 Exemple dans un service Laravel
<?php namespace App\Services; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use App\Models\Product; class ProductFilterService { public function __construct( private readonly ClusterService $clusterService ) {} public function filterProducts(array $filters): array { $query = Product::query(); // Construction de la requête $conditions = []; foreach ($filters as $key => $value) { if ($key === 'min_price') { $conditions[] = "price>=$value"; } elseif ($key === 'max_price') { $conditions[] = "price<=$value"; } elseif ($key === 'category') { $conditions[] = "category=$value"; } } $queryString = implode(' & ', $conditions); if (!empty($queryString)) { $this->clusterService->applyToEloquent( $query, 'attributes', $queryString, DatabaseDriver::MYSQL ); } return $query->get()->toArray(); } }
5. Structure des données : ClusterVO
ClusterVO est le conteneur qui aplatit automatiquement les données JSON pour un accès rapide.
5.1 Création d'un ClusterVO
use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $cluster = new ClusterVO([ 'id' => 1, 'name' => 'John Doe', 'age' => 30, 'is_active' => 'yes', 'address' => [ 'city' => 'Paris', 'country' => 'France', 'postal_code' => 75000, ], 'tags' => ['php', 'js', 'docker'], 'settings' => [ 'theme' => 'dark', 'notifications' => [ 'email' => 'yes', 'sms' => 'no', ], ], ]);
5.2 Accès aux données
// Accès simple $name = $cluster->get('name'); // 'John Doe' // Accès par notation pointée $city = $cluster->get('address.city'); // 'Paris' $email = $cluster->get('settings.notifications.email'); // 'yes' // Accès aux tableaux aplatis $hasPhp = $cluster->get('tags_php'); // 'yes' $hasJs = $cluster->get('tags_js'); // 'yes' // Vérification d'existence if ($cluster->has('address.city')) { echo "Ville définie"; } // Récupération de toutes les clés $keys = $cluster->keys(); // Récupération des données brutes $flatData = $cluster->toArray(); $nestedData = $cluster->getUnflattened()->toArray();
5.3 ArrayAccess
$cluster = new ClusterVO([ 'status' => 'active', 'role' => 'admin', 'user' => ['name' => 'John Doe'], ]); // Accès comme un tableau echo $cluster['status']; // 'active' echo $cluster['user.name']; // 'John Doe' // Vérification d'existence if (isset($cluster['user.email'])) { echo $cluster['user.email']; } // Le cluster est immutable try { $cluster['status'] = 'inactive'; // Lance une RuntimeException } catch (RuntimeException $e) { echo "ClusterVO is immutable"; }
5.4 Création simplifiée avec ClusterVOProxy
use AndyDefer\LaravelCluster\Proxies\ClusterVOProxy; // Création avec booléens PHP $cluster = ClusterVOProxy::make([ 'id' => 1, 'name' => 'John Doe', 'age' => 30, 'is_active' => true, // → 'yes' 'is_verified' => false, // → 'no' 'address' => [ 'city' => 'Paris', 'country' => 'France', 'postal_code' => 75000, ], 'tags' => ['php', 'js', 'docker'], 'settings' => [ 'theme' => 'dark', 'notifications' => [ 'email' => true, // → 'yes' 'sms' => false, // → 'no' ], ], ]); // Accès normalisé $cluster->get('is_active'); // 'yes' $cluster->get('is_verified'); // 'no' $cluster->get('settings.notifications.email'); // 'yes' $cluster->get('settings.notifications.sms'); // 'no'
6. Eloquent Cast : ClusterCast
6.1 Installation dans un modèle
<?php namespace App\Models; use AndyDefer\LaravelCluster\Casts\ClusterCast; use Illuminate\Database\Eloquent\Model; final class User extends Model { protected $casts = [ 'metadata' => ClusterCast::class, ]; }
6.2 Utilisation
// Création avec un tableau $user = User::create([ 'name' => 'John Doe', 'metadata' => [ 'status' => 'active', 'role' => 'admin', 'preferences' => [ 'theme' => 'dark', 'notifications' => 'yes', ], ], ]); // Lecture - automatiquement converti en ClusterVO $cluster = $user->metadata; // Accès comme un tableau (ArrayAccess) $status = $cluster['status']; // 'active' $theme = $cluster['preferences.theme']; // 'dark' // Accès via get() $role = $cluster->get('role'); // 'admin' // Mise à jour $user->metadata = [ 'status' => 'inactive', 'role' => 'doctor', ]; $user->save(); // Filtrage Eloquent avec whereCluster $activeAdmins = User::whereCluster('metadata', 'status=active & role=admin')->get();
7. La collection intelligente : ClusterVOCollection
7.1 Création
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; // Création vide $collection = new ClusterVOCollection(); // Ajout d'éléments $collection->add(new ClusterVO(['name' => 'John', 'status' => 'active'])); $collection->add(new ClusterVO(['name' => 'Jane', 'status' => 'inactive'])); $collection->add(new ClusterVO(['name' => 'Bob', 'status' => 'active']));
7.2 Filtres d'égalité
// where - Égalité $active = $collection->where('status', 'active'); // John, Bob // whereNot - Différent $notActive = $collection->whereNot('status', 'active'); // Jane // whereYes - Égal à 'yes' $verified = $collection->whereYes('verified'); // whereNo - Égal à 'no' $unverified = $collection->whereNo('verified'); // whereIn - Dans une liste $admins = $collection->whereIn('role', ['admin', 'super_admin']); // whereNotIn - Hors liste $nonAdmins = $collection->whereNotIn('role', ['admin', 'super_admin']);
7.3 Filtres numériques
// whereGreaterThan $adults = $collection->whereGreaterThan('age', 18); // whereGreaterThanOrEqual $seniors = $collection->whereGreaterThanOrEqual('age', 65); // whereLessThan $minors = $collection->whereLessThan('age', 18); // whereLessThanOrEqual $young = $collection->whereLessThanOrEqual('age', 25); // whereBetween $middleAged = $collection->whereBetween('age', 35, 50); // whereNotBetween $notMiddleAged = $collection->whereNotBetween('age', 35, 50);
7.4 Filtres d'existence
// whereHas - La clé existe $hasEmail = $collection->whereHas('email'); // whereMissing - La clé n'existe pas $noEmail = $collection->whereMissing('email'); // whereNull - La valeur est null $nullAge = $collection->whereNull('age'); // whereNotNull - La valeur n'est pas null $hasAge = $collection->whereNotNull('age');
7.5 Filtres sur chaînes
// whereContains - Contient une sous-chaîne $containsJohn = $collection->whereContains('name', 'John'); // whereStartsWith - Commence par $startsJ = $collection->whereStartsWith('name', 'J'); // whereEndsWith - Se termine par $endsDoe = $collection->whereEndsWith('name', 'Doe'); // whereLike - Alias de whereContains $likeJohn = $collection->whereLike('name', 'John'); // whereLikePattern - Motif LIKE SQL $pattern = $collection->whereLikePattern('name', '%john%'); // Contient $pattern = $collection->whereLikePattern('name', 'john%'); // Commence par $pattern = $collection->whereLikePattern('name', '%john'); // Se termine par
7.6 Filtres sur tableaux
// whereArrayContains - Le tableau contient une valeur $hasPhp = $collection->whereArrayContains('tags', 'php'); // whereArrayNotContains - Le tableau ne contient pas une valeur $noPhp = $collection->whereArrayNotContains('tags', 'php'); // whereArrayContainsAny - Le tableau contient au moins une valeur $hasPhpOrJs = $collection->whereArrayContainsAny('tags', ['php', 'js']); // whereArrayContainsAll - Le tableau contient toutes les valeurs $hasPhpAndJs = $collection->whereArrayContainsAll('tags', ['php', 'js']); // whereArraySize - Taille exacte $exactSize = $collection->whereArraySize('tags', 3); // whereArraySizeGreaterThan - Taille supérieure $moreThan2 = $collection->whereArraySizeGreaterThan('tags', 2); // whereArraySizeLessThan - Taille inférieure $lessThan2 = $collection->whereArraySizeLessThan('tags', 2); // whereArrayEmpty - Tableau vide $emptyTags = $collection->whereArrayEmpty('tags'); // whereArrayNotEmpty - Tableau non vide $hasTags = $collection->whereArrayNotEmpty('tags');
7.7 Opérateurs logiques
// AND - via chaînage $activeAdmins = $collection ->where('status', 'active') ->where('role', 'admin'); // OR - via orWhere $adminOrDoctor = $collection ->where('role', 'admin') ->orWhere('role', 'doctor'); // OR sur condition simple $activeOrPending = $collection ->where('status', 'active') ->orWhere('status', 'pending');
7.8 Filtres personnalisés
// whereClosure - Filtre personnalisé $complex = $collection->whereClosure(function (ClusterVO $cluster) { return $cluster->get('age') > 25 && $cluster->get('role') === 'admin'; }); // orWhereClosure - OR avec filtre personnalisé $result = $collection ->where('status', 'active') ->orWhereClosure(function (ClusterVO $cluster) { return $cluster->get('age') > 30 && $cluster->get('verified') === 'yes'; });
7.9 Requêtes complètes avec whereQuery
// whereQuery - Parse une requête textuelle $result = $collection->whereQuery('status=active & role=admin'); // Avec OR $result = $collection->whereQuery('status=active | status=pending'); // Avec parenthèses $result = $collection->whereQuery('(status=active | status=pending) & role=admin'); // Avec sous-condition $result = $collection->whereQuery('addresses[city=Kinshasa]'); // Avec fonction SQL $result = $collection->whereQuery('COUNT(addresses) > 2');
7.10 Récupération des résultats
// Récupérer tous les éléments $items = $collection->get(); // Premier élément correspondant $firstAdmin = $collection->firstWhere('role', 'admin'); // Compter les éléments $count = $collection->count(); // Itération foreach ($collection as $cluster) { echo $cluster->get('name') . "\n"; }
8. Filtrer des collections en mémoire
8.1 Exemple complet
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; use AndyDefer\LaravelCluster\ClusterQuery; $clusters = new ClusterVOCollection(); $clusters->add(new ClusterVO([ 'id' => 1, 'name' => 'John Doe', 'status' => 'active', 'role' => 'admin', 'age' => 30, 'tags' => ['php', 'js', 'docker'], 'addresses' => [ ['city' => 'Kinshasa'], ['city' => 'Paris'], ], ])); $clusters->add(new ClusterVO([ 'id' => 2, 'name' => 'Jane Smith', 'status' => 'inactive', 'role' => 'doctor', 'age' => 25, 'tags' => ['python', 'react'], 'addresses' => [ ['city' => 'Paris'], ], ])); $clusters->add(new ClusterVO([ 'id' => 3, 'name' => 'Bob Johnson', 'status' => 'active', 'role' => 'doctor', 'age' => 35, 'tags' => ['php', 'laravel', 'vuejs'], 'addresses' => [ ['city' => 'Kinshasa'], ['city' => 'London'], ['city' => 'Paris'], ], ])); // Filtrage avec ClusterQuery $engine = new ClusterQuery(); $filtered = $engine->filter($clusters, 'status=active & role=doctor'); // Bob Johnson uniquement // Filtrage avec ClusterVOCollection $filtered = $clusters ->where('status', 'active') ->where('role', 'doctor'); // Bob Johnson uniquement // Filtrage avec whereQuery $filtered = $clusters->whereQuery('status=active & role=doctor'); // Bob Johnson uniquement // Filtrage complexe $filtered = $clusters->whereQuery( 'status=active & (role=admin | role=doctor) & COUNT(addresses) > 2' ); // Bob Johnson uniquement
9. Générer du SQL pour différents drivers
9.1 Drivers supportés
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; $engine = new ClusterQuery(); // MySQL $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::MYSQL); // JSON_EXTRACT(clusters, '$."status"') = 'active' // SQLite $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::SQLITE); // json_extract(clusters, '$.status') = 'active' // PostgreSQL $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::PGSQL); // clusters->>'status' = 'active'
9.2 Conditions simples
// Égalité $sql = $engine->toSql('clusters', 'status=active', DatabaseDriver::MYSQL); // JSON_EXTRACT(clusters, '$."status"') = 'active' // Différent $sql = $engine->toSql('clusters', 'status!=inactive', DatabaseDriver::MYSQL); // JSON_EXTRACT(clusters, '$."status"') != 'inactive' // Supérieur $sql = $engine->toSql('clusters', 'age>25', DatabaseDriver::MYSQL); // CAST(JSON_EXTRACT(clusters, '$."age"') AS DECIMAL(10,2)) > 25 // Inférieur ou égal $sql = $engine->toSql('clusters', 'age<=25', DatabaseDriver::MYSQL); // CAST(JSON_EXTRACT(clusters, '$."age"') AS DECIMAL(10,2)) <= 25
9.3 Conditions avec AND/OR
// AND $sql = $engine->toSql('clusters', 'status=active & role=admin', DatabaseDriver::MYSQL); // (JSON_EXTRACT(clusters, '$."status"') = 'active' AND JSON_EXTRACT(clusters, '$."role"') = 'admin') // OR $sql = $engine->toSql('clusters', 'status=active | role=admin', DatabaseDriver::MYSQL); // (JSON_EXTRACT(clusters, '$."status"') = 'active' OR JSON_EXTRACT(clusters, '$."role"') = 'admin') // Mixte avec parenthèses $sql = $engine->toSql('clusters', '(status=active | status=pending) & role=admin', DatabaseDriver::MYSQL); // ((JSON_EXTRACT(clusters, '$."status"') = 'active' OR JSON_EXTRACT(clusters, '$."status"') = 'pending') AND JSON_EXTRACT(clusters, '$."role"') = 'admin')
9.4 Fonctions SQL
// COUNT $sql = $engine->toSql('clusters', 'COUNT(addresses) > 2', DatabaseDriver::SQLITE); // json_array_length(clusters, '$.addresses') > 2 // AVG $sql = $engine->toSql('clusters', 'AVG(scores) >= 85', DatabaseDriver::SQLITE); // AVG(CAST(json_extract(clusters, '$.scores') AS NUMERIC)) >= 85 // LENGTH $sql = $engine->toSql('clusters', 'LENGTH(name) > 5', DatabaseDriver::SQLITE); // LENGTH(json_extract(clusters, '$.name')) > 5 // JSON_LENGTH $sql = $engine->toSql('clusters', 'JSON_LENGTH(addresses) > 2', DatabaseDriver::SQLITE); // json_array_length(clusters, '$.addresses') > 2
9.5 Sous-conditions
// Sous-condition simple $sql = $engine->toSql('clusters', 'addresses[city=Kinshasa]', DatabaseDriver::SQLITE); // EXISTS (SELECT 1 FROM json_each(clusters, '$.addresses') WHERE LOWER(json_extract(value, '$.city')) = LOWER('Kinshasa')) // Sous-condition avec AND $sql = $engine->toSql('clusters', 'addresses[city=Kinshasa & country=RDC]', DatabaseDriver::SQLITE); // EXISTS (SELECT 1 FROM json_each(clusters, '$.addresses') WHERE LOWER(json_extract(value, '$.city')) = LOWER('Kinshasa') AND LOWER(json_extract(value, '$.country')) = LOWER('RDC'))
10. Fonctions SQLite personnalisées
10.1 Fonctions disponibles
| Fonction | Description | Exemple |
|---|---|---|
JSON_LENGTH |
Longueur d'un tableau JSON | JSON_LENGTH(clusters, '$.addresses') |
JSON_AVG |
Moyenne des valeurs numériques | JSON_AVG(clusters, '$.scores') |
JSON_SUM |
Somme des valeurs numériques | JSON_SUM(clusters, '$.prices') |
JSON_MIN |
Valeur minimale | JSON_MIN(clusters, '$.scores') |
JSON_MAX |
Valeur maximale | JSON_MAX(clusters, '$.scores') |
10.2 Utilisation
// Ces fonctions fonctionnent automatiquement en SQLite $users = User::whereRaw('JSON_LENGTH(clusters, \'$.addresses\') > 2')->get(); // Ou via whereCluster $users = User::whereCluster('clusters', 'COUNT(addresses) > 2')->get();
11. Intégration avec Eloquent
11.1 Utilisation de base
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use App\Models\User; $engine = new ClusterQuery(); $query = User::query(); $engine->applyToEloquent($query, 'clusters', 'status=active', DatabaseDriver::MYSQL); $users = $query->get(); // SELECT * FROM users WHERE JSON_EXTRACT(clusters, '$."status"') = 'active'
11.2 Conditions complexes
$query = User::query(); // AND $engine->applyToEloquent($query, 'clusters', 'status=active & role=admin', DatabaseDriver::MYSQL); // OR $engine->applyToEloquent($query, 'clusters', 'status=active | role=admin', DatabaseDriver::MYSQL); // Parenthèses $engine->applyToEloquent($query, 'clusters', '(status=active | status=pending) & role=admin', DatabaseDriver::MYSQL);
11.3 Combinaison avec Eloquent
$users = User::where('created_at', '>', now()->subDays(30)) ->whereCluster('clusters', 'status=active') ->whereCluster('clusters', 'role=admin') ->orderBy('name') ->get();
11.4 Sous-conditions Eloquent
$query = User::query(); // Utilisateurs avec une adresse à Kinshasa $engine->applyToEloquent($query, 'clusters', 'addresses[city=Kinshasa]', DatabaseDriver::MYSQL); // Utilisateurs avec une adresse à Kinshasa ET actifs $engine->applyToEloquent($query, 'clusters', 'status=active & addresses[city=Kinshasa]', DatabaseDriver::MYSQL); $users = $query->get();
11.5 Fonctions SQL Eloquent
$query = User::query(); // Utilisateurs avec plus de 2 adresses $engine->applyToEloquent($query, 'clusters', 'COUNT(addresses) > 2', DatabaseDriver::MYSQL); // Utilisateurs avec une moyenne de scores >= 85 $engine->applyToEloquent($query, 'clusters', 'AVG(scores) >= 85', DatabaseDriver::MYSQL); // Combinaison $engine->applyToEloquent($query, 'clusters', 'status=active & COUNT(addresses) > 1', DatabaseDriver::MYSQL); $users = $query->get();
12. Les Macros Laravel
12.1 Macro sur Eloquent Builder
use App\Models\User; // 1. Condition simple $users = User::whereCluster('clusters', 'status=active')->get(); // 2. Conditions multiples $users = User::whereCluster('clusters', 'status=active & role=admin')->get(); // 3. Sous-condition $users = User::whereCluster('clusters', 'addresses[city=Kinshasa]')->get(); // 4. Fonction SQL $users = User::whereCluster('clusters', 'COUNT(addresses) > 2')->get(); // 5. Combinaison avec Eloquent $users = User::where('created_at', '>', now()->subDays(30)) ->whereCluster('clusters', 'status=active') ->orderBy('name') ->get(); // 6. Chaînage $users = User::whereCluster('clusters', 'status=active') ->whereCluster('clusters', 'role=admin') ->get();
12.2 Macro sur Collection
use App\Models\User; $users = User::all(); // 1. Filtrage en mémoire $active = $users->whereCluster('clusters', 'status=active'); // 2. Chaînage $admins = $users ->whereCluster('clusters', 'status=active') ->whereCluster('clusters', 'role=admin'); // 3. Combinaison avec d'autres méthodes $names = $users ->whereCluster('clusters', 'status=active') ->pluck('name') ->toArray(); // 4. Sous-condition $kinshasaUsers = $users->whereCluster('clusters', 'addresses[city=Kinshasa]'); // 5. Fonction SQL $usersWithManyAddresses = $users->whereCluster('clusters', 'COUNT(addresses) > 2');
12.3 Détection automatique du driver
// Detection automatique (MySQL, PostgreSQL, SQLite) User::whereCluster('clusters', 'status=active')->get(); // SQL généré selon le driver configuré dans le fichier .env // DB_CONNECTION=mysql → JSON_EXTRACT // DB_CONNECTION=pgsql → ->> // DB_CONNECTION=sqlite → json_extract
13. Les fonctions SQL d'agrégation
13.1 Fonctions disponibles
| Fonction | Description | Exemple |
|---|---|---|
COUNT(path) |
Nombre d'éléments dans un tableau | COUNT(addresses) > 2 |
SUM(path) |
Somme des valeurs numériques | SUM(prices) > 500 |
AVG(path) |
Moyenne des valeurs numériques | AVG(scores) >= 85 |
MIN(path) |
Valeur minimale | MIN(scores) > 75 |
MAX(path) |
Valeur maximale | MAX(scores) < 95 |
LENGTH(path) |
Longueur d'une chaîne | LENGTH(name) > 5 |
JSON_LENGTH(path) |
Longueur d'un tableau JSON | JSON_LENGTH(addresses) > 2 |
13.2 Utilisation en mémoire
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $collection = new ClusterVOCollection(); $collection->add(new ClusterVO([ 'name' => 'John', 'addresses' => ['a', 'b', 'c'], 'scores' => [80, 90, 85], 'prices' => [100, 200, 300], ])); // COUNT - Compter les éléments $result = $collection->whereQuery('COUNT(addresses) > 2'); // SUM - Somme des valeurs $result = $collection->whereQuery('SUM(prices) > 500'); // AVG - Moyenne $result = $collection->whereQuery('AVG(scores) >= 85'); // MIN - Valeur minimale $result = $collection->whereQuery('MIN(scores) > 75'); // MAX - Valeur maximale $result = $collection->whereQuery('MAX(scores) < 95'); // LENGTH - Longueur d'une chaîne $result = $collection->whereQuery('LENGTH(name) > 5'); // JSON_LENGTH - Longueur d'un tableau JSON $result = $collection->whereQuery('JSON_LENGTH(addresses) > 2');
13.3 Utilisation avec Eloquent
use App\Models\User; // COUNT $users = User::whereCluster('clusters', 'COUNT(addresses) > 2')->get(); // SUM $users = User::whereCluster('clusters', 'SUM(prices) > 500')->get(); // AVG $users = User::whereCluster('clusters', 'AVG(scores) >= 85')->get(); // MIN $users = User::whereCluster('clusters', 'MIN(scores) > 75')->get(); // MAX $users = User::whereCluster('clusters', 'MAX(scores) < 95')->get(); // LENGTH $users = User::whereCluster('clusters', 'LENGTH(name) > 5')->get(); // JSON_LENGTH $users = User::whereCluster('clusters', 'JSON_LENGTH(addresses) > 2')->get();
13.4 SQL généré par driver
| Fonction | SQLite | MySQL | PostgreSQL |
|---|---|---|---|
COUNT |
json_array_length(clusters, '$.addresses') |
JSON_LENGTH(clusters, '$.addresses') |
jsonb_array_length(clusters->'addresses') |
SUM |
(SELECT SUM(json_extract(value, '$')) FROM json_each(clusters, '$.prices')) |
(SELECT SUM(JSON_EXTRACT(value, '$')) FROM JSON_TABLE(...)) |
(SELECT SUM((value->>'$')::numeric) FROM json_array_elements(...)) |
AVG |
(SELECT AVG(json_extract(value, '$')) FROM json_each(clusters, '$.scores')) |
(SELECT AVG(JSON_EXTRACT(value, '$')) FROM JSON_TABLE(...)) |
(SELECT AVG((value->>'$')::numeric) FROM json_array_elements(...)) |
MIN |
(SELECT MIN(json_extract(value, '$')) FROM json_each(clusters, '$.scores')) |
(SELECT MIN(JSON_EXTRACT(value, '$')) FROM JSON_TABLE(...)) |
(SELECT MIN((value->>'$')::numeric) FROM json_array_elements(...)) |
MAX |
(SELECT MAX(json_extract(value, '$')) FROM json_each(clusters, '$.scores')) |
(SELECT MAX(JSON_EXTRACT(value, '$')) FROM JSON_TABLE(...)) |
(SELECT MAX((value->>'$')::numeric) FROM json_array_elements(...)) |
LENGTH |
LENGTH(json_extract(clusters, '$.name')) |
LENGTH(JSON_EXTRACT(clusters, '$.name')) |
LENGTH(clusters->>'name') |
JSON_LENGTH |
json_array_length(clusters, '$.addresses') |
JSON_LENGTH(clusters, '$.addresses') |
jsonb_array_length(clusters->'addresses') |
14. Les sous-conditions sur tableaux
14.1 Syntaxe
// Syntaxe : path[condition] $query = 'addresses[city=Kinshasa]'; $query = 'addresses[city=Kinshasa & country=RDC]'; $query = 'addresses[city=Kinshasa | city=Paris]'; $query = 'addresses[city=~kin%]'; $query = 'addresses[#city]'; // NOT_EXISTS
14.2 Exemples en mémoire
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $collection = new ClusterVOCollection(); $collection->add(new ClusterVO([ 'name' => 'John', 'addresses' => [ ['city' => 'Kinshasa', 'country' => 'RDC'], ['city' => 'Paris', 'country' => 'France'], ], ])); $collection->add(new ClusterVO([ 'name' => 'Jane', 'addresses' => [ ['city' => 'Paris', 'country' => 'France'], ], ])); $collection->add(new ClusterVO([ 'name' => 'Bob', 'addresses' => [ ['city' => 'Kinshasa', 'country' => 'RDC'], ['city' => 'London', 'country' => 'UK'], ['city' => 'Paris', 'country' => 'France'], ], ])); // Condition simple $result = $collection->whereQuery('addresses[city=Kinshasa]'); // John, Bob // Condition avec AND $result = $collection->whereQuery('addresses[city=Kinshasa & country=RDC]'); // John, Bob // Condition avec OR $result = $collection->whereQuery('addresses[city=Kinshasa | city=Paris]'); // John, Jane, Bob // Condition avec LIKE $result = $collection->whereQuery('addresses[city=~kin%]'); // John, Bob // Condition avec NOT_LIKE $result = $collection->whereQuery('addresses[city!~kin%]'); // Jane, Bob (Bob a Paris et Londres) // EXISTS - Tableau non vide $result = $collection->whereQuery('addresses[]'); // John, Jane, Bob // NOT_EXISTS - Clé absente $result = $collection->whereQuery('addresses[#city]'); // Personne (tous ont city)
14.3 Exemples avec Eloquent
use App\Models\User; // Utilisateurs avec une adresse à Kinshasa $users = User::whereCluster('clusters', 'addresses[city=Kinshasa]')->get(); // Utilisateurs avec une adresse à Kinshasa ET actifs $users = User::whereCluster('clusters', 'status=active & addresses[city=Kinshasa]')->get(); // Utilisateurs avec une adresse à Kinshasa ou Paris $users = User::whereCluster('clusters', 'addresses[city=Kinshasa | city=Paris]')->get(); // Utilisateurs avec au moins une adresse $users = User::whereCluster('clusters', 'addresses[]')->get(); // Utilisateurs sans adresse $users = User::whereCluster('clusters', 'addresses[#city]')->get();
15. Les opérateurs EXISTS et NOT_EXISTS
15.1 EXISTS (*)
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $collection = new ClusterVOCollection(); $collection->add(new ClusterVO(['name' => 'John', 'email' => 'john@example.com'])); $collection->add(new ClusterVO(['name' => 'Jane'])); $collection->add(new ClusterVO(['name' => 'Bob', 'email' => 'bob@example.com'])); // EXISTS $result = $collection->whereQuery('*email'); // John, Bob // EXISTS avec condition $result = $collection->whereQuery('*email & name=John'); // John
15.2 NOT_EXISTS (#)
// NOT_EXISTS $result = $collection->whereQuery('#email'); // Jane // NOT_EXISTS avec condition $result = $collection->whereQuery('#email & name=Jane'); // Jane
15.3 Utilisation avec Eloquent
use App\Models\User; // Utilisateurs avec un email $users = User::whereCluster('clusters', '*email')->get(); // Utilisateurs sans email $users = User::whereCluster('clusters', '#email')->get(); // Utilisateurs avec un email et actifs $users = User::whereCluster('clusters', '*email & status=active')->get();
16. Les opérateurs LIKE et NOT_LIKE
16.1 LIKE (=~)
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $collection = new ClusterVOCollection(); $collection->add(new ClusterVO(['name' => 'John Doe'])); $collection->add(new ClusterVO(['name' => 'Jane Smith'])); $collection->add(new ClusterVO(['name' => 'Bob Johnson'])); // Contient "John" $result = $collection->whereQuery('name=~John'); // John Doe, Bob Johnson (contient John) // Commence par "J" $result = $collection->whereQuery('name=~J%'); // John Doe, Jane Smith // Se termine par "n" $result = $collection->whereQuery('name=~%n'); // Bob Johnson // Avec motif $result = $collection->whereQuery('name=~%John%'); // John Doe, Bob Johnson
16.2 NOT_LIKE (!~)
// Ne contient pas "John" $result = $collection->whereQuery('name!~John'); // Jane Smith // Ne commence pas par "J" $result = $collection->whereQuery('name!~J%'); // Bob Johnson // Ne se termine pas par "n" $result = $collection->whereQuery('name!~%n'); // John Doe, Jane Smith
16.3 Utilisation avec Eloquent
use App\Models\User; // Noms commençant par "J" $users = User::whereCluster('clusters', 'name=~J%')->get(); // Noms ne commençant pas par "J" $users = User::whereCluster('clusters', 'name!~J%')->get(); // Noms contenant "John" $users = User::whereCluster('clusters', 'name=~%John%')->get();
17. Les fonctions d'agrégation en mémoire
17.1 Présentation des fonctions
Les fonctions d'agrégation en mémoire permettent d'effectuer des calculs sur les données des clusters sans générer de SQL. Elles sont utilisées avec les méthodes whereAggregate(), matchesAggregate() et getAggregateValue() de ClusterVOCollection.
17.2 Liste des fonctions d'agrégation
| Fonction | Description | Signature | Retourne |
|---|---|---|---|
COUNT |
Compte les éléments d'un tableau ou les caractères d'une chaîne | COUNT(path) |
int |
AVG |
Calcule la moyenne des valeurs numériques | AVG(path) |
float |
SUM |
Calcule la somme des valeurs numériques | SUM(path) |
float |
MIN |
Trouve la valeur numérique minimale | MIN(path) |
float |
MAX |
Trouve la valeur numérique maximale | MAX(path) |
float |
LENGTH |
Calcule la longueur d'une chaîne ou le nombre d'éléments | LENGTH(path) |
int |
EXISTS |
Vérifie qu'un chemin existe et n'est pas vide | EXISTS(path) |
bool |
IS_EMPTY |
Détermine si une valeur est vide | IS_EMPTY(path) |
bool |
HAS |
Recherche une valeur dans un tableau | HAS(path, value) ou HAS(path, key, value) |
bool |
ALL |
Vérifie que tous les éléments satisfont une condition | ALL(path, key, expectedValue) |
bool |
MATCHES |
Recherche une valeur correspondant à une regex | MATCHES(path, regex) ou MATCHES(path, key, regex) |
bool |
GROUP |
Groupe des expressions pour la logique booléenne | GROUP({expr1} & {expr2}) |
bool |
17.3 Utilisation de whereAggregate
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $collection = new ClusterVOCollection(); $collection->add(new ClusterVO([ 'name' => 'John', 'addresses' => ['a', 'b', 'c'], 'scores' => [80, 90, 85], 'prices' => [100, 200, 300], 'tags' => ['php', 'javascript', 'docker'], 'addresses_detail' => [ ['city' => 'Kinshasa', 'country' => 'RDC'], ['city' => 'Paris', 'country' => 'France'], ], 'profile' => ['age' => 30, 'verified' => true], 'cart' => ['item1', 'item2'], ])); // COUNT - Compter les éléments $result = $collection->whereAggregate('{COUNT(addresses) > 2}'); // AVG - Moyenne $result = $collection->whereAggregate('{AVG(scores) >= 85}'); // SUM - Somme $result = $collection->whereAggregate('{SUM(prices) > 500}'); // MIN - Valeur minimale $result = $collection->whereAggregate('{MIN(scores) > 75}'); // MAX - Valeur maximale $result = $collection->whereAggregate('{MAX(scores) < 95}'); // LENGTH - Longueur $result = $collection->whereAggregate('{LENGTH(name) > 5}'); // EXISTS - Existence $result = $collection->whereAggregate('{EXISTS(profile)}'); // IS_EMPTY - Vide $result = $collection->whereAggregate('{IS_EMPTY(cart)}'); // HAS - Recherche simple $result = $collection->whereAggregate('{HAS(tags, "php")}'); // HAS - Recherche dans tableau d'objets $result = $collection->whereAggregate('{HAS(addresses_detail, city, "Kinshasa")}'); // ALL - Tous les éléments $result = $collection->whereAggregate('{ALL(addresses_detail, country, "RDC")}'); // MATCHES - Regex simple $result = $collection->whereAggregate('{MATCHES(tags, "/^ja.*/")}'); // MATCHES - Regex sur tableau d'objets $result = $collection->whereAggregate('{MATCHES(addresses_detail, city, "/^Kin.*/")}'); // GROUP - Combinaison complexe $result = $collection->whereAggregate( '{GROUP({COUNT(addresses) > 1} & {AVG(scores) >= 80} & {HAS(tags, "php")})}' );
17.4 Utilisation de whereAggregateDirect
// Exécution directe sans parsing $result = $collection->whereAggregateDirect('COUNT', ['addresses']); // Retourne les clusters avec COUNT(addresses) > 0 // Avec opérateur et valeur $result = $collection->whereAggregateDirect('COUNT', ['addresses'], '>', 2); // Retourne les clusters avec COUNT(addresses) > 2 $result = $collection->whereAggregateDirect('AVG', ['scores'], '>=', 85); $result = $collection->whereAggregateDirect('HAS', ['tags', 'php']); $result = $collection->whereAggregateDirect('EXISTS', ['profile']); $result = $collection->whereAggregateDirect('MATCHES', ['tags', '/^ja.*/']);
17.5 Évaluation sur un cluster spécifique
// matchesAggregate - Vérifier si un cluster correspond $cluster = $collection->first(); $matches = $collection->matchesAggregate($cluster, '{COUNT(addresses) > 2}'); // matchesAggregateDirect - Appel direct $matches = $collection->matchesAggregateDirect($cluster, 'COUNT', ['addresses']); // getAggregateValue - Obtenir la valeur $count = $collection->getAggregateValue($cluster, 'COUNT', ['addresses']); $avg = $collection->getAggregateValue($cluster, 'AVG', ['scores']); $hasPhp = $collection->getAggregateValue($cluster, 'HAS', ['tags', 'php']); $matches = $collection->getAggregateValue($cluster, 'MATCHES', ['tags', '/^ja.*/']);
17.6 Validation d'expressions
// validateAggregate - Vérifier la syntaxe $valid = $collection->validateAggregate('{COUNT(addresses) > 2}'); // true $valid = $collection->validateAggregate('{INVALID(addresses) > 2}'); // false // Validation avant utilisation if ($collection->validateAggregate($expression)) { $result = $collection->whereAggregate($expression); } else { // Gérer l'erreur }
17.7 Exemple complet avec toutes les fonctions
<?php declare(strict_types=1); use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $collection = new ClusterVOCollection(); $collection->add(new ClusterVO([ 'name' => 'John', 'addresses' => ['a', 'b', 'c'], 'scores' => [80, 90, 85], 'prices' => [100, 200, 300], 'tags' => ['php', 'javascript', 'docker'], 'addresses_detail' => [ ['city' => 'Kinshasa', 'country' => 'RDC'], ['city' => 'Paris', 'country' => 'France'], ], 'profile' => ['age' => 30, 'verified' => true], 'cart' => ['item1', 'item2'], ])); $collection->add(new ClusterVO([ 'name' => 'Jane', 'addresses' => ['a', 'b'], 'scores' => [70, 75, 80], 'prices' => [50, 75], 'tags' => ['python'], 'addresses_detail' => [ ['city' => 'Paris', 'country' => 'France'], ], 'profile' => ['age' => 25, 'verified' => false], 'cart' => [], ])); $collection->add(new ClusterVO([ 'name' => 'Bob', 'addresses' => ['a'], 'scores' => [95, 98, 92], 'prices' => [500, 600, 700], 'tags' => ['php', 'laravel', 'vuejs'], 'addresses_detail' => [ ['city' => 'Kinshasa', 'country' => 'RDC'], ['city' => 'London', 'country' => 'UK'], ['city' => 'Paris', 'country' => 'France'], ], 'profile' => ['age' => 35, 'verified' => true], 'cart' => ['item3'], ])); // 1. Filtrer avec COUNT $result = $collection->whereAggregate('{COUNT(addresses) > 2}'); // John, Bob // 2. Filtrer avec AVG $result = $collection->whereAggregate('{AVG(scores) >= 85}'); // John, Bob // 3. Filtrer avec SUM $result = $collection->whereAggregate('{SUM(prices) > 500}'); // John, Bob // 4. Filtrer avec MIN $result = $collection->whereAggregate('{MIN(scores) > 75}'); // John, Bob // 5. Filtrer avec MAX $result = $collection->whereAggregate('{MAX(scores) < 95}'); // John, Jane // 6. Filtrer avec EXISTS $result = $collection->whereAggregate('{EXISTS(profile)}'); // John, Jane, Bob // 7. Filtrer avec IS_EMPTY $result = $collection->whereAggregate('{IS_EMPTY(cart)}'); // Jane // 8. Filtrer avec HAS $result = $collection->whereAggregate('{HAS(tags, "php")}'); // John, Bob // 9. Filtrer avec ALL $result = $collection->whereAggregate('{ALL(addresses_detail, country, "RDC")}'); // John // 10. Filtrer avec MATCHES $result = $collection->whereAggregate('{MATCHES(tags, "/^ja.*/")}'); // John (javascript) // 11. Combinaison complexe avec GROUP $result = $collection->whereAggregate( '{GROUP({COUNT(addresses) > 1} & {AVG(scores) >= 80} & {HAS(tags, "php")})}' ); // John, Bob // 12. Utilisation directe $result = $collection->whereAggregateDirect('COUNT', ['addresses'], '>', 2); // 13. Tests individuels $cluster = $collection->first(); $matches = $collection->matchesAggregate($cluster, '{COUNT(addresses) > 2}'); // 14. Obtention de valeurs $count = $collection->getAggregateValue($cluster, 'COUNT', ['addresses']); $avg = $collection->getAggregateValue($cluster, 'AVG', ['scores']); $hasPhp = $collection->getAggregateValue($cluster, 'HAS', ['tags', 'php']); $matches = $collection->getAggregateValue($cluster, 'MATCHES', ['tags', '/^ja.*/']); echo "John: COUNT={$count}, AVG={$avg}, HAS_PHP=" . ($hasPhp ? 'yes' : 'no') . ", MATCHES=" . ($matches ? 'yes' : 'no') . "\n"; // John: COUNT=3, AVG=85, HAS_PHP=yes, MATCHES=yes // 15. Validation d'expression $valid = $collection->validateAggregate('{COUNT(addresses) > 2}'); // true $invalid = $collection->validateAggregate('{INVALID(addresses) > 2}'); // false
18. Créer des fonctions personnalisées
18.1 Fonction d'agrégation personnalisée
<?php namespace App\Cluster\Functions; use AndyDefer\LaravelCluster\Functions\AbstractAggregateFunction; class DoubleCountFunction extends AbstractAggregateFunction { public function execute(array $data, array $args): int { $path = $args[0] ?? null; $value = $this->resolveArg($data, $path); if (is_array($value)) { return count($value) * 2; } if (is_string($value)) { return strlen($value) * 2; } return 0; } public function getName(): string { return 'DOUBLE_COUNT'; } public function getDefaultValue(): mixed { return 0; } public function getReturnType(): string { return 'int'; } public function returnsBoolean(): bool { return false; } public function getMinArgs(): int { return 1; } public function getMaxArgs(): int { return 1; } public function validateArgs(array $args): bool { return count($args) === 1; } }
18.2 Enregistrement de la fonction
use AndyDefer\LaravelCluster\Registry\AggregateFunctionRegistry; use App\Cluster\Functions\DoubleCountFunction; $registry = app(AggregateFunctionRegistry::class); $registry->register(new DoubleCountFunction()); // Utilisation $result = $collection->whereAggregate('{DOUBLE_COUNT(addresses) > 4}');
18.3 Fonction SQL personnalisée
<?php namespace App\Cluster\SqlFunctions; use AndyDefer\LaravelCluster\SqlFunctions\AbstractSqlFunction; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; class CustomFunction extends AbstractSqlFunction { public function getName(): string { return 'CUSTOM'; } public function toSql(string $column, string $path, DatabaseDriver $driver): string { return match ($driver) { DatabaseDriver::SQLITE => sprintf( "CUSTOM_FN(json_extract(%s, '$.%s'))", $column, $path ), DatabaseDriver::MYSQL => sprintf( "CUSTOM_FN(JSON_EXTRACT(%s, '$.%s'))", $column, $path ), DatabaseDriver::PGSQL => sprintf( "CUSTOM_FN(%s->>'%s')", $column, $path ), }; } public function getReturnType(): string { return 'int'; } public function execute(mixed $value): mixed { // Logique en mémoire return is_array($value) ? count($value) * 2 : 0; } }
18.4 Enregistrement SQL personnalisé
use AndyDefer\LaravelCluster\Registry\SqlFunctionRegistry; use App\Cluster\SqlFunctions\CustomFunction; $registry = app(SqlFunctionRegistry::class); $registry->register(new CustomFunction()); // Utilisation $result = $collection->whereAggregate('{CUSTOM(addresses) > 4}'); // ou $query = User::whereCluster('clusters', 'CUSTOM(addresses) > 4');
19. Parser et AST
19.1 Structure de l'AST
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Nodes\ConditionNode; use AndyDefer\LaravelCluster\Nodes\GroupNode; use AndyDefer\LaravelCluster\Nodes\FunctionNode; use AndyDefer\LaravelCluster\Nodes\SubConditionNode; $engine = new ClusterQuery(); // ConditionNode - Condition simple $ast = $engine->parse('status=active'); var_dump($ast instanceof ConditionNode); // true // GroupNode - Groupe logique (AND/OR) $ast = $engine->parse('status=active & role=admin'); var_dump($ast instanceof GroupNode); // true // FunctionNode - Fonction SQL $ast = $engine->parse('COUNT(addresses) > 2'); var_dump($ast instanceof FunctionNode); // true // SubConditionNode - Sous-condition $ast = $engine->parse('addresses[city=Kinshasa]'); var_dump($ast instanceof SubConditionNode); // true
19.2 Manipulation de l'AST
// ConditionNode $ast = $engine->parse('status=active'); echo $ast->getKey(); // 'status' echo $ast->getOperator(); // ComparisonOperator::EQUAL echo $ast->getValue(); // 'active' // GroupNode $ast = $engine->parse('status=active & role=admin'); echo $ast->getOperator(); // LogicalOperator::AND $children = $ast->getChildren(); // [ConditionNode, ConditionNode] // FunctionNode $ast = $engine->parse('COUNT(addresses) > 2'); $children = $ast->getChildren(); // [] // SubConditionNode $ast = $engine->parse('addresses[city=Kinshasa]'); echo $ast->getPath(); // 'addresses' $condition = $ast->getCondition(); // ConditionNode // Évaluation manuelle de l'AST $cluster = new ClusterVO(['status' => 'active']); $result = $ast->evaluate($cluster); // true
19.3 Génération SQL depuis l'AST
use AndyDefer\LaravelCluster\Enums\DatabaseDriver; $ast = $engine->parse('status=active'); $sql = $ast->toSql('clusters', DatabaseDriver::MYSQL); // JSON_EXTRACT(clusters, '$."status"') = 'active' // Application Eloquent depuis l'AST $query = User::query(); $ast->toEloquent($query, 'clusters', DatabaseDriver::MYSQL); $users = $query->get();
19.4 Cache du Parser
$ast1 = $engine->parse('status=active'); $ast2 = $engine->parse('status=active'); // $ast1 et $ast2 sont la même instance $ast3 = $engine->parse('status=inactive'); // $ast3 est une nouvelle instance
20. Référence des opérateurs
20.1 Opérateurs de comparaison
| Opérateur | Description | Exemple |
|---|---|---|
= |
Égalité | status=active |
!= |
Différent | status!=inactive |
< |
Inférieur | age<18 |
> |
Supérieur | age>18 |
<= |
Inférieur ou égal | age<=18 |
>= |
Supérieur ou égal | age>=18 |
=~ |
LIKE (insensible à la casse) | name=~John% |
!~ |
NOT LIKE (insensible à la casse) | name!~John% |
20.2 Opérateurs logiques
| Opérateur | Description | Exemple |
|---|---|---|
& ou AND |
ET logique | status=active & role=admin |
| ou OR |
OU logique | `status=active |
! ou NOT |
Négation | !deleted |
20.3 Opérateurs spéciaux
| Opérateur | Description | Exemple |
|---|---|---|
* |
EXISTS - La clé existe | *email |
# |
NOT_EXISTS - La clé est absente | #deleted_at |
20.4 Parenthèses
// Priorité des opérateurs $query = '(status=active | status=pending) & role=admin'; // (status=active OU status=pending) ET role=admin
21. Référence des méthodes de ClusterVOCollection
21.1 Filtres d'égalité
// where - Égalité simple $collection->where(string $key, mixed $value): self // whereNot - Différent $collection->whereNot(string $key, mixed $value): self // whereYes - Égal à 'yes' $collection->whereYes(string $key): self // whereNo - Égal à 'no' $collection->whereNo(string $key): self // orWhere - OR logique $collection->orWhere(string $key, mixed $value): self // whereIn - Dans une liste $collection->whereIn(string $key, array $values): self // whereNotIn - Hors liste $collection->whereNotIn(string $key, array $values): self
21.2 Filtres d'existence
// whereHas - La clé existe $collection->whereHas(string $key): self // whereMissing - La clé n'existe pas $collection->whereMissing(string $key): self // whereNull - La valeur est null $collection->whereNull(string $key): self // whereNotNull - La valeur n'est pas null $collection->whereNotNull(string $key): self
21.3 Filtres numériques
// whereGreaterThan $collection->whereGreaterThan(string $key, int|float $value): self // whereGreaterThanOrEqual $collection->whereGreaterThanOrEqual(string $key, int|float $value): self // whereLessThan $collection->whereLessThan(string $key, int|float $value): self // whereLessThanOrEqual $collection->whereLessThanOrEqual(string $key, int|float $value): self // whereBetween $collection->whereBetween(string $key, mixed $min, mixed $max): self // whereNotBetween $collection->whereNotBetween(string $key, mixed $min, mixed $max): self
21.4 Filtres sur chaînes
// whereContains - Contient une sous-chaîne $collection->whereContains(string $key, string $search): self // whereStartsWith - Commence par $collection->whereStartsWith(string $key, string $prefix): self // whereEndsWith - Se termine par $collection->whereEndsWith(string $key, string $suffix): self // whereLike - Alias de whereContains $collection->whereLike(string $key, string $search): self // whereLikePattern - Motif LIKE SQL $collection->whereLikePattern(string $key, string $pattern): self // whereNotLike - Négation de whereLike $collection->whereNotLike(string $key, string $search): self // whereNotLikePattern - Négation de whereLikePattern $collection->whereNotLikePattern(string $key, string $pattern): self
21.5 Filtres sur tableaux
// whereArrayContains - Le tableau contient une valeur $collection->whereArrayContains(string $key, mixed $value): self // whereArrayNotContains - Le tableau ne contient pas une valeur $collection->whereArrayNotContains(string $key, mixed $value): self // whereArrayContainsAny - Le tableau contient au moins une valeur $collection->whereArrayContainsAny(string $key, array $values): self // whereArrayContainsAll - Le tableau contient toutes les valeurs $collection->whereArrayContainsAll(string $key, array $values): self // whereArraySize - Taille exacte $collection->whereArraySize(string $key, int $size): self // whereArraySizeGreaterThan - Taille supérieure $collection->whereArraySizeGreaterThan(string $key, int $size): self // whereArraySizeLessThan - Taille inférieure $collection->whereArraySizeLessThan(string $key, int $size): self // whereArrayEmpty - Tableau vide $collection->whereArrayEmpty(string $key): self // whereArrayNotEmpty - Tableau non vide $collection->whereArrayNotEmpty(string $key): self
21.6 Filtres personnalisés
// whereClosure - Filtre personnalisé $collection->whereClosure(Closure $callback): self // orWhereClosure - OR avec filtre personnalisé $collection->orWhereClosure(Closure $callback): self
21.7 Requêtes textuelles
// whereQuery - Parse une requête textuelle $collection->whereQuery(string $query): self
21.8 Agrégations en mémoire
// whereAggregate - Expression d'agrégation $collection->whereAggregate(string $expression): self // whereAggregateDirect - Appel direct $collection->whereAggregateDirect(string $functionName, array $args = []): self $collection->whereAggregateDirect(string $functionName, array $args, string $operator, mixed $value): self // matchesAggregate - Vérifier un cluster $collection->matchesAggregate(ClusterVO $cluster, string $expression): bool // matchesAggregateDirect - Vérifier un cluster (direct) $collection->matchesAggregateDirect(ClusterVO $cluster, string $functionName, array $args = []): bool // getAggregateValue - Obtenir une valeur $collection->getAggregateValue(ClusterVO $cluster, string $functionName, array $args = []): mixed // validateAggregate - Valider une expression $collection->validateAggregate(string $expression): bool
21.9 Récupération
// get - Tous les éléments $collection->get(): array // firstWhere - Premier élément correspondant $collection->firstWhere(string $key, mixed $value): ?ClusterVO
22. Cas d'usage concrets
22.1 Filtrage de clients B2B
<?php namespace App\Services; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use App\Models\Customer; class CustomerFilterService { public function __construct( private readonly ClusterService $clusterService ) {} public function findCustomers(array $criteria): array { $conditions = []; if (isset($criteria['min_revenue'])) { $conditions[] = "revenue >= " . $criteria['min_revenue']; } if (isset($criteria['industry'])) { $conditions[] = "industry=" . $criteria['industry']; } if (isset($criteria['country'])) { $conditions[] = "country=" . $criteria['country']; } if (isset($criteria['is_active'])) { $conditions[] = "active=" . ($criteria['is_active'] ? 'yes' : 'no'); } if (isset($criteria['has_contract'])) { $conditions[] = "*contract_signed"; } $queryString = implode(' & ', $conditions); $query = Customer::query(); if (!empty($queryString)) { $this->clusterService->applyToEloquent( $query, 'company_data', $queryString, DatabaseDriver::MYSQL ); } return $query->get()->toArray(); } }
22.2 Filtrage de produits e-commerce
<?php namespace App\Services; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use App\Models\Product; class ProductSearchService { public function __construct( private readonly ClusterService $clusterService ) {} public function searchProducts(array $filters): array { $query = Product::query(); $conditions = []; // Catégories - avec OR if (!empty($filters['categories'])) { $categoryConditions = []; foreach ($filters['categories'] as $category) { $categoryConditions[] = "categories_{$category}=yes"; } $conditions[] = '(' . implode(' OR ', $categoryConditions) . ')'; } // Tags - avec AND if (!empty($filters['tags'])) { $tagConditions = []; foreach ($filters['tags'] as $tag) { $tagConditions[] = "tags_{$tag}=yes"; } $conditions[] = '(' . implode(' AND ', $tagConditions) . ')'; } // Prix if (isset($filters['min_price'])) { $conditions[] = "price >= " . $filters['min_price']; } if (isset($filters['max_price'])) { $conditions[] = "price <= " . $filters['max_price']; } // Disponibilité if (isset($filters['in_stock'])) { $conditions[] = "in_stock=" . ($filters['in_stock'] ? 'yes' : 'no'); } // Promotion if (isset($filters['on_promotion'])) { $conditions[] = "promotion=" . ($filters['on_promotion'] ? 'yes' : 'no'); } if (!empty($conditions)) { $queryString = implode(' & ', $conditions); $this->clusterService->applyToEloquent( $query, 'product_attributes', $queryString, DatabaseDriver::MYSQL ); } return $query->get()->toArray(); } }
22.3 Filtrage d'utilisateurs avec compétences
<?php namespace App\Services; use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; use AndyDefer\LaravelCluster\Services\ClusterService; class DeveloperFilterService { public function __construct( private readonly ClusterService $clusterService ) {} public function findDevelopers(array $candidates, array $criteria): array { $collection = new ClusterVOCollection(); foreach ($candidates as $candidate) { $collection->add(new ClusterVO($candidate)); } // Filtrer par compétences if (!empty($criteria['required_skills'])) { foreach ($criteria['required_skills'] as $skill) { $collection = $collection->whereArrayContains('skills', $skill); } } // Filtrer par compétences optionnelles (OR) if (!empty($criteria['optional_skills'])) { $result = $collection; foreach ($criteria['optional_skills'] as $skill) { $result = $result->whereArrayContains('skills', $skill); } // Union des résultats $collection = $collection->orWhereQuery( '(' . implode(' OR ', array_map( fn($s) => "skills_{$s}=yes", $criteria['optional_skills'] )) . ')' ); } // Années d'expérience if (isset($criteria['min_experience'])) { $collection = $collection->whereGreaterThanOrEqual('experience', $criteria['min_experience']); } // Localisation if (isset($criteria['city'])) { $collection = $collection->where('city', $criteria['city']); } // Disponibilité if (isset($criteria['available'])) { $collection = $collection->where('available', $criteria['available'] ? 'yes' : 'no'); } return $collection->get(); } }
22.4 API REST avec filtrage dynamique
<?php namespace App\Http\Controllers\Api; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; use App\Models\Resource; class ResourceController extends Controller { public function __construct( private readonly ClusterService $clusterService ) {} public function index(Request $request) { $query = Resource::query(); $filter = $request->get('filter'); $search = $request->get('search'); $sort = $request->get('sort', 'created_at'); $order = $request->get('order', 'desc'); // Filtrage avancé if ($filter) { $this->clusterService->applyToEloquent( $query, 'metadata', $filter, DatabaseDriver::MYSQL ); } // Recherche textuelle if ($search) { $query->where('name', 'LIKE', "%{$search}%"); } $query->orderBy($sort, $order); return $query->paginate(20); } }
22.5 Filtrage en mémoire pour export
<?php namespace App\Services; use AndyDefer\LaravelCluster\Services\ClusterService; use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; class DataExportService { public function __construct( private readonly ClusterService $clusterService ) {} public function exportFilteredData(array $sourceData, string $filter, array $columns = []): array { // Conversion en ClusterVOCollection $collection = new ClusterVOCollection(); foreach ($sourceData as $item) { $collection->add(new ClusterVO($item)); } // Filtrage $filtered = $this->clusterService->filter($collection, $filter); // Extraction des colonnes spécifiques if (empty($columns)) { return $filtered->toArray(); } $result = []; foreach ($filtered as $cluster) { $row = []; foreach ($columns as $column) { $row[$column] = $cluster->get($column, null); } $result[] = $row; } return $result; } }
23. Débogage et résolution des problèmes
23.1 Vérifier la syntaxe d'une requête
use AndyDefer\LaravelCluster\ClusterQuery; $engine = new ClusterQuery(); try { $ast = $engine->parse('status=active & role=admin'); // Requête valide } catch (\RuntimeException $e) { echo "Erreur de syntaxe: " . $e->getMessage(); }
23.2 Valider une expression d'agrégation
use AndyDefer\LaravelCluster\Collections\ClusterVOCollection; $collection = new ClusterVOCollection(); $valid = $collection->validateAggregate('{COUNT(addresses) > 2}'); // true $valid = $collection->validateAggregate('{INVALID(addresses) > 2}'); // false if (!$valid) { // L'expression est invalide }
23.3 Afficher le SQL généré
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\Enums\DatabaseDriver; $engine = new ClusterQuery(); // Afficher le SQL généré pour débogage $sql = $engine->toSql('clusters', 'status=active & role=admin', DatabaseDriver::MYSQL); dd($sql); // Voir le SQL exact // Avec une sous-condition $sql = $engine->toSql('clusters', 'addresses[city=Kinshasa]', DatabaseDriver::SQLITE); dd($sql); // Avec une fonction SQL $sql = $engine->toSql('clusters', 'COUNT(addresses) > 2', DatabaseDriver::SQLITE); dd($sql);
23.4 Tester une requête sur un cluster spécifique
use AndyDefer\LaravelCluster\ClusterQuery; use AndyDefer\LaravelCluster\ValueObjects\ClusterVO; $engine = new ClusterQuery(); $cluster = new ClusterVO([ 'status' => 'active', 'role' => 'admin', 'age' => 30, ]); // Tester différentes requêtes $tests = [ 'status=active' => $engine->matches($cluster, 'status=active'), 'role=admin' => $engine->matches($cluster, 'role=admin'), 'status=active & role=admin' => $engine->matches($cluster, 'status=active & role=admin'), 'age>25' => $engine->matches($cluster, 'age>25'), 'age>35' => $engine->matches($cluster, 'age>35'), ]; foreach ($tests as $query => $result) { echo "$query: " . ($result ? '✅ true' : '❌ false') . "\n"; }
23.5 Problèmes courants
| Problème | Cause | Solution |
|---|---|---|
| Syntax error | Requête mal formée | Vérifier les parenthèses et les opérateurs |
| Valeurs entre guillemets | Guillemets dans la requête | Supprimer les guillemets : status=active |
| Fonction inconnue | Fonction non enregistrée | Ajouter la fonction au registre |
| Tableau vide attendu | whereArrayEmpty sur tableau non vide |
Vérifier la structure du tableau |
| Driver non supporté | Driver inconnu | Utiliser MySQL, PostgreSQL ou SQLite |
| Résultat inattendu | Comportement de l'opérateur | Vérifier la casse des valeurs |
24. Performance et bonnes pratiques
24.1 Performance en mémoire
// ❌ À éviter - Filtrer plusieurs fois $filtered = $collection->where('status', 'active'); $filtered = $filtered->where('role', 'admin'); $filtered = $filtered->where('age', '>', '25'); // ✅ Recommandé - Chaînage direct $filtered = $collection ->where('status', 'active') ->where('role', 'admin') ->whereGreaterThan('age', 25); // ✅ Recommandé - Une seule requête $filtered = $collection->whereQuery('status=active & role=admin & age>25');
24.2 Performance en base de données
// ❌ À éviter - Utiliser les fonctions SQL sur des colonnes non indexées $users = User::whereCluster('clusters', 'COUNT(addresses) > 2')->get(); // ✅ Recommandé - Indexer les colonnes JSON utilisées // Dans la migration : DB::statement('ALTER TABLE users ADD INDEX idx_clusters_status ((JSON_EXTRACT(clusters, "$.status")))'); // ✅ Recommandé - Utiliser des conditions simples sur les colonnes indexées $users = User::whereCluster('clusters', 'status=active')->get();
24.3 Optimisation des requêtes
// ✅ Recommandé - Limiter les résultats avant de filtrer $users = User::take(100) ->whereCluster('clusters', 'status=active') ->get(); // ✅ Recommandé - Utiliser select pour ne récupérer que les colonnes nécessaires $users = User::select('id', 'name', 'clusters') ->whereCluster('clusters', 'status=active') ->get(); // ✅ Recommandé - Utiliser pagination $users = User::whereCluster('clusters', 'status=active') ->paginate(20);
24.4 Bonnes pratiques
// 1. Utiliser les alias pour améliorer la lisibilité $service = app(ClusterService::class); $service->applyToEloquent($query, 'metadata', 'status=active', DatabaseDriver::MYSQL); // 2. Valider les expressions avant de les utiliser $collection = new ClusterVOCollection(); if ($collection->validateAggregate($expression)) { $result = $collection->whereAggregate($expression); } // 3. Utiliser les macros Laravel pour plus de clarté $users = User::whereCluster('clusters', 'status=active')->get(); // 4. Préférer whereQuery pour les requêtes complexes $result = $collection->whereQuery('status=active & COUNT(addresses) > 2'); // 5. Utiliser les fonctions d'agrégation pour les calculs complexes $result = $collection->whereAggregate('{AVG(scores) >= 85}');
24.5 Conservation de la mémoire
// ❌ À éviter - Travailler sur de très grandes collections en mémoire $allUsers = User::all(); $filtered = $allUsers->whereCluster('clusters', 'status=active'); // ✅ Recommandé - Utiliser le filtrage en base de données $filtered = User::whereCluster('clusters', 'status=active')->get(); // ✅ Recommandé - Utiliser le streaming si nécessaire User::whereCluster('clusters', 'status=active')->chunk(100, function ($users) { foreach ($users as $user) { // Traitement par lots } });
25. Licence
MIT © Andy Defer