andydefer / laravel-cluster
A powerful cluster query parser and filter for Laravel with JSON column support
Requires
- php: ^8.1
- andydefer/laravel-actions: ^2.10.6
- 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
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 (Arbre Syntaxique Abstrait)
- 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
Le package est organisé de manière modulaire :
src/
├── Providers/
│ └── ClusterServiceProvider.php # Enregistrement des services
├── Utilities/
│ ├── ClusterMacroRegistrar.php # Enregistrement des macros
│ └── SqliteFunctionRegistrar.php # Fonctions SQLite personnalisées
├── Casts/
│ └── ClusterCast.php # Cast Eloquent
└── ...
Le ClusterServiceProvider :
- Enregistre les services dans le conteneur
- Enregistre les fonctions SQLite via
SqliteFunctionRegistrar - Enregistre les macros via
ClusterMacroRegistrar
// ClusterServiceProvider::boot() public function boot(): void { SqliteFunctionRegistrar::register(); // Fonctions SQLite ClusterMacroRegistrar::register(); // Macros Laravel }
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
Voici comment une requête textuelle est transformée en action concrè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); // Tokens: [status, =, active, &, age, >, 25, &, COUNT, (, addresses, ), >, 2] // 3. Le Parser construit l'AST $parser = new Parser(); $ast = $parser->parse($query); // AST: GroupNode(AND) // ├── ConditionNode(status, =, active) // ├── ConditionNode(age, >, 25) // └── FunctionNode(COUNT, addresses, >, 2) // 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'); // Retourne une TokenRecordCollection // 2. Parser - Construit l'AST $ast = (new Parser())->parse('status=active'); // Retourne un ConditionNode // 3. ClusterQuery - Moteur central $engine = new ClusterQuery(); $ast = $engine->parse('status=active'); $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. Il est utile pour l'injection de dépendances dans les services Laravel.
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(); // ... ajout des clusters ... $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(); // ['id', 'name', 'age', 'is_active', 'address.city', 'address.country', ...] // Récupération des données brutes $flatData = $cluster->toArray(); $nestedData = $cluster->getUnflattened()->toArray();
5.3 ArrayAccess (Accès comme un tableau)
ClusterVO implémente ArrayAccess, ce qui permet d'accéder aux données avec la syntaxe des tableaux :
$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 - les modifications sont bloquées try { $cluster['status'] = 'inactive'; // Lance une RuntimeException } catch (RuntimeException $e) { echo "ClusterVO is immutable"; }
5.4 Cas d'utilisation
// 1. Dans une collection $collection = new ClusterVOCollection(); $collection->add($cluster); // 2. Dans un service class UserService { public function processUser(array $userData): void { $cluster = new ClusterVO($userData); if ($cluster->get('status') === 'active') { // Traitement pour les utilisateurs actifs } if ($cluster->has('address.city')) { $city = $cluster->get('address.city'); // Traitement selon la ville } } } // 3. Dans une validation $cluster = new ClusterVO($input); if ($cluster->get('age') >= 18 && $cluster->get('verified') === 'yes') { // Valider l'utilisateur }
5.5 Création simplifiée avec ClusterVOProxy
ClusterVOProxy est un proxy qui simplifie la création de ClusterVO en normalisant automatiquement les valeurs booléennes. Il convertit récursivement les booléens PHP (true/false) et les chaînes booléennes ('true'/'false') en 'yes'/'no', évitant ainsi les exceptions de ClusterVO.
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' ], ], ]); // Création avec chaînes booléennes $cluster = ClusterVOProxy::make([ 'active' => 'true', // → 'yes' 'verified' => 'false', // → 'no' 'status' => 'active', // Préservé ]); // 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'
Avantages du proxy
- Normalisation automatique : Plus besoin de convertir manuellement les booléens
- Support récursif : Traite les structures profondément imbriquées
- Préservation des valeurs : Les chaînes 'yes'/'no' sont conservées
- Validation intégrée : Hérite de la validation de
ClusterVO
Utilisation dans un modèle
class Doctor extends Model { public function getIndexableCluster(): ClusterVO { return ClusterVOProxy::make([ 'status' => $this->is_active, 'verified' => $this->email_verified_at !== null, 'has_patients' => $this->patients()->exists(), 'profile' => $this->profile ? [ 'is_verified' => $this->profile->is_verified, 'is_accepting' => $this->profile->is_accepting_new_patients, 'years_experience' => $this->profile->years_of_experience, ] : null, 'specialties' => $this->specialties->pluck('name')->toArray(), ]); } }
ArrayAccess avec le proxy
$cluster = ClusterVOProxy::make([ 'active' => true, 'verified' => false, ]); echo $cluster['active']; // 'yes' echo $cluster['verified']; // 'no' // Vérification d'existence if (isset($cluster['active'])) { // La clé existe } // Immutable - modification bloquée try { $cluster['active'] = 'inactive'; } catch (RuntimeException $e) { echo "ClusterVO is immutable"; }
6. Eloquent Cast : ClusterCast
Le package fournit un cast Eloquent ClusterCast qui permet d'utiliser ClusterVO directement dans vos modèles Laravel.
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' // Vérification d'existence if (isset($cluster['preferences.notifications'])) { // ... } // Mise à jour $user->metadata = [ 'status' => 'inactive', 'role' => 'doctor', ]; $user->save(); // Le cast est immutable - pour modifier une valeur spécifique $data = $user->metadata->toArray(); $data['status'] = 'pending'; $user->metadata = $data; $user->save(); // Filtrage Eloquent avec whereCluster $activeAdmins = User::whereCluster('metadata', 'status=active & role=admin')->get();
6.3 Avantages
- Transparence : Les données sont automatiquement converties en ClusterVO
- ArrayAccess : Accès natif comme un tableau
$model->metadata['key'] - Validation : Les données sont validées par ClusterVO à l'écriture
- Compatibilité : Fonctionne avec toutes les méthodes du package (whereCluster, etc.)
7. La collection intelligente : ClusterVOCollection
ClusterVOCollection offre une API fluide pour filtrer des clusters.
7.1 Création d'une collection
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
8.2 Chaînage de filtres
// Chaînage avec méthodes de collection $result = $clusters ->where('status', 'active') ->whereGreaterThan('age', 25) ->whereArrayContains('tags', 'php'); // Chaînage avec whereQuery $result = $clusters ->whereQuery('status=active') ->whereQuery('age>25') ->whereQuery('tags_php=yes'); // Mélange des deux $result = $clusters ->where('status', 'active') ->whereQuery('age>25') ->whereArrayContains('tags', 'php');
8.3 Conservation des clés
// Les clés originales sont conservées $filtered = $clusters->where('status', 'active'); $filtered->keys(); // [0, 2] (si les indices 0 et 2 correspondent) // Pour récupérer un tableau indexé normal $array = $filtered->values()->toArray();
9. Générer du SQL pour différents drivers
9.1 Drivers supportés
Le package génère du SQL adapté à chaque driver de base de données :
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
Pour assurer la compatibilité entre les drivers, le package enregistre automatiquement des fonctions SQLite qui imitent les fonctionnalités natives de MySQL et PostgreSQL.
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();
10.3 Enregistrement
Les fonctions sont enregistrées uniquement si le driver est SQLite, via SqliteFunctionRegistrar :
// src/Utilities/SqliteFunctionRegistrar.php SqliteFunctionRegistrar::register();
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
Le package ajoute automatiquement deux macros : whereCluster sur Builder et Collection.
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
La macro whereCluster détecte automatiquement le driver de la connexion :
// 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
Les fonctions SQL permettent d'effectuer des opérations de calcul et d'analyse sur les données JSON directement au niveau de la base de données. Elles génèrent du SQL adapté à chaque driver (SQLite, MySQL, PostgreSQL) et s'exécutent aussi bien en mémoire que dans les requêtes Eloquent.
13.1 Fonctions d'agrrégation
Les fonctions d'agrégation effectuent des calculs sur des ensembles de valeurs (tableaux JSON) et retournent un résultat unique.
| 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 |
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');
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(); // Combinaison avec d'autres conditions $users = User::whereCluster('clusters', 'status=active & COUNT(addresses) > 1')->get();
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') |
13.2 Fonctions booléennes
Les fonctions booléennes retournent true ou false et peuvent être utilisées avec des opérateurs de comparaison (=, !=, etc.).
| Fonction | Description | Exemple |
|---|---|---|
CONTAINS(path, value) |
Vérifie si un tableau contient une valeur | CONTAINS(languages, fr) |
REGEXP(path, pattern) |
Vérifie si une chaîne correspond à une regex | REGEXP(name, "^John.*") |
13.2.1 Fonction CONTAINS
La fonction CONTAINS vérifie si un tableau JSON contient une valeur spécifique.
Syntaxe :
CONTAINS(path, value)
Paramètres :
path: Le chemin vers le tableau dans les données JSONvalue: La valeur à rechercher dans le tableau
Utilisation en mémoire :
$filtered = $collection->whereQuery('CONTAINS(languages, fr)'); // Retourne les clusters dont le tableau 'languages' contient 'fr' // Avec opérateur de comparaison $filtered = $collection->whereQuery('CONTAINS(languages, fr) = true'); $filtered = $collection->whereQuery('CONTAINS(languages, fr) = false'); // Combinaison avec AND $filtered = $collection->whereQuery('CONTAINS(languages, fr) & status=active'); // Combinaison avec OR $filtered = $collection->whereQuery('CONTAINS(languages, fr) | CONTAINS(languages, en)');
Utilisation avec Eloquent :
use App\Models\User; // Utilisateurs qui parlent français $users = User::whereCluster('clusters', 'CONTAINS(languages, fr)')->get(); // Utilisateurs qui parlent français ET anglais $users = User::whereCluster('clusters', 'CONTAINS(languages, fr) & CONTAINS(languages, en)')->get(); // Utilisateurs qui parlent français OU anglais $users = User::whereCluster('clusters', 'CONTAINS(languages, fr) | CONTAINS(languages, en)')->get(); // Utilisateurs qui parlent français ET sont actifs $users = User::whereCluster('clusters', 'CONTAINS(languages, fr) & status=active')->get(); // Utilisateurs qui parlent français = true $users = User::whereCluster('clusters', 'CONTAINS(languages, fr) = true')->get(); // Utilisateurs qui parlent français = false (ne parlent pas français) $users = User::whereCluster('clusters', 'CONTAINS(languages, fr) = false')->get();
SQL généré :
| Driver | SQL |
|---|---|
| SQLite | EXISTS (SELECT 1 FROM json_each(clusters, '$.languages') WHERE value = 'fr') |
| MySQL | JSON_SEARCH(clusters, 'one', 'fr', NULL, '$."languages"') IS NOT NULL |
| PostgreSQL | EXISTS (SELECT 1 FROM json_array_elements_text(clusters->'languages') AS elem WHERE elem = 'fr') |
Exemples de requêtes complexes avec CONTAINS :
// Docteurs qui parlent français ET ont plus de 5 ans d'expérience $results = $collection->whereQuery('CONTAINS(languages, fr) & experience > 5'); // Produits avec le tag "promo" OU "new" $results = $collection->whereQuery('CONTAINS(tags, promo) | CONTAINS(tags, new)'); // Utilisateurs qui parlent français ET sont vérifiés $results = $collection->whereQuery('CONTAINS(languages, fr) & verified=yes');
13.2.2 Fonction REGEXP
La fonction REGEXP vérifie si une chaîne correspond à une expression régulière.
Syntaxe :
REGEXP(path, pattern)
Paramètres :
path: Le chemin vers la chaîne dans les données JSONpattern: L'expression régulière à tester
Utilisation en mémoire :
// Noms commençant par "John" $filtered = $collection->whereQuery('REGEXP(name, "^John.*")'); // Noms contenant "Smith" $filtered = $collection->whereQuery('REGEXP(name, "Smith")'); // Noms se terminant par "son" $filtered = $collection->whereQuery('REGEXP(name, "son$")'); // Noms commençant par "J" et contenant "e" $filtered = $collection->whereQuery('REGEXP(name, "^J.*e.*")'); // Combinaison avec d'autres conditions $filtered = $collection->whereQuery('REGEXP(name, "^John.*") & status=active');
Utilisation avec Eloquent :
use App\Models\User; // Noms commençant par "John" $users = User::whereCluster('clusters', 'REGEXP(name, "^John.*")')->get(); // Noms contenant "Smith" $users = User::whereCluster('clusters', 'REGEXP(name, "Smith")')->get(); // Noms se terminant par "son" $users = User::whereCluster('clusters', 'REGEXP(name, "son$")')->get(); // Noms commençant par "J" et contenant "e" $users = User::whereCluster('clusters', 'REGEXP(name, "^J.*e.*")')->get(); // Combinaison avec d'autres conditions $users = User::whereCluster('clusters', 'REGEXP(name, "^John.*") & status=active')->get(); // Utilisateurs avec email Gmail $users = User::whereCluster('clusters', 'REGEXP(email, ".*@gmail\.com$")')->get(); // Utilisateurs avec un nom contenant des lettres uniquement $users = User::whereCluster('clusters', 'REGEXP(name, "^[A-Za-z]+$")')->get();
SQL généré :
| Driver | SQL |
|---|---|
| SQLite | json_extract(clusters, '$.name') REGEXP '^John.*' |
| MySQL | JSON_EXTRACT(clusters, '$.name') REGEXP '^John.*' |
| PostgreSQL | clusters->>'name' ~ '^John.*' |
Note sur SQLite : La fonction REGEXP n'est pas native dans SQLite. Le package l'enregistre automatiquement via SqliteFunctionRegistrar. Si vous utilisez SQLite, assurez-vous que la fonction est bien enregistrée.
13.3 Combinaison de fonctions
Les fonctions SQL peuvent être combinées entre elles et avec d'autres conditions :
// Combinaison de fonctions d'agrégation $result = $collection->whereQuery('COUNT(addresses) > 2 & AVG(scores) >= 85 & SUM(prices) > 400'); // Combinaison de fonctions booléennes $result = $collection->whereQuery('CONTAINS(languages, fr) & REGEXP(name, "^J.*")'); // Combinaison de fonctions d'agrégation et booléennes $result = $collection->whereQuery('CONTAINS(languages, fr) & COUNT(addresses) > 1 & AVG(scores) >= 85'); // Avec des conditions classiques $result = $collection->whereQuery('COUNT(addresses) > 2 & status=active & verified=yes'); // Avec des parenthèses pour la priorité $result = $collection->whereQuery('(COUNT(addresses) > 2 | SUM(prices) > 500) & status=active');
13.4 Comparaison des fonctions
| Fonction | Type | Arguments | Retourne | Utilisation typique |
|---|---|---|---|---|
COUNT |
Agrégation | 1 | int |
Nombre d'éléments |
SUM |
Agrégation | 1 | float |
Somme des valeurs |
AVG |
Agrégation | 1 | float |
Moyenne des valeurs |
MIN |
Agrégation | 1 | float |
Valeur minimale |
MAX |
Agrégation | 1 | float |
Valeur maximale |
LENGTH |
Agrégation | 1 | int |
Longueur d'une chaîne |
JSON_LENGTH |
Agrégation | 1 | int |
Longueur d'un tableau |
CONTAINS |
Booléenne | 2 | bool |
Présence dans un tableau |
REGEXP |
Booléenne | 2 | bool |
Correspondance regex |
13.5 Bonnes pratiques
// ✅ Utiliser des index pour les champs JSON fréquemment filtrés // Migration : DB::statement('ALTER TABLE users ADD INDEX idx_clusters_status ((JSON_EXTRACT(clusters, "$.status")))'); // ✅ Préférer les fonctions booléennes pour les vérifications d'existence $users = User::whereCluster('clusters', 'CONTAINS(languages, fr)')->get(); // ✅ Combiner les fonctions avec des conditions classiques pour des filtres précis $users = User::whereCluster('clusters', 'status=active & COUNT(addresses) > 1')->get(); // ❌ Éviter les fonctions d'agrégation sur de grandes collections en mémoire $allUsers = User::all(); // ❌ $filtered = $allUsers->whereCluster('clusters', 'COUNT(addresses) > 2'); // ❌ // ✅ Utiliser le filtrage en base de données pour les grandes collections $filtered = User::whereCluster('clusters', 'COUNT(addresses) > 2')->get(); // ✅
14. Les sous-conditions sur tableaux
Les sous-conditions permettent de filtrer sur des tableaux d'objets.
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();
14.4 Chemins imbriqués
// Structure : settings.notifications.email $result = $collection->whereQuery('settings.notifications[email=yes]'); // John, Bob (email=yes) // Structure : settings.notifications[email=yes & sms=no] $result = $collection->whereQuery('settings.notifications[email=yes & sms=no]'); // John uniquement
15. Les opérateurs EXISTS et NOT_EXISTS
15.1 EXISTS (*)
Vérifie si une clé existe dans les données.
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 (#)
Vérifie si une clé est absente.
// 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 (=~)
Recherche insensible à la casse.
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 (!~)
Exclusion insensible à la casse.
// 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 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], ])); // COUNT $result = $collection->whereAggregate('{COUNT(addresses) > 2}'); // AVG $result = $collection->whereAggregate('{AVG(scores) >= 85}'); // Combinaison $result = $collection->whereAggregate('{COUNT(addresses) > 1} & {AVG(scores) >= 85}');
17.2 Utilisation de whereAggregateDirect
// Exécution directe sans parsing $result = $collection->whereAggregateDirect('COUNT', ['addresses']); // John (3 > 0) $result = $collection->whereAggregateDirect('EXISTS', ['addresses']); // John (addresses existe)
17.3 É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']);
17.4 Validation d'expressions
// validateAggregate - Vérifier la syntaxe $valid = $collection->validateAggregate('{COUNT(addresses) > 2}'); // true $valid = $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 (Arbre Syntaxique Abstrait)
19.1 Structure de l'AST
L'AST est composé de différents types de nœuds :
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
Le parser met en cache les résultats pour les requêtes identiques :
$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
// whereAggregate - Expression d'agrégation $collection->whereAggregate(string $expression): self // whereAggregateDirect - Appel direct $collection->whereAggregateDirect(string $functionName, array $args = []): 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(); } } // Utilisation $service = new CustomerFilterService(app(ClusterService::class)); $customers = $service->findCustomers([ 'min_revenue' => 1000000, 'industry' => 'technology', 'country' => 'France', 'is_active' => true, 'has_contract' => true, ]);
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(); } } // Utilisation $service = new ProductSearchService(app(ClusterService::class)); $products = $service->searchProducts([ 'categories' => ['electronics', 'computers'], 'tags' => ['new', 'best-seller'], 'min_price' => 500, 'max_price' => 2000, 'in_stock' => true, 'on_promotion' => true, ]);
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(); } } // Utilisation $service = new DeveloperFilterService(app(ClusterService::class)); $developers = $service->findDevelopers($candidates, [ 'required_skills' => ['php', 'laravel'], 'optional_skills' => ['docker', 'vuejs'], 'min_experience' => 3, 'city' => 'Paris', 'available' => true, ]);
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); } } // Exemples d'appels API // GET /api/resources?filter=status=active AND category=documents // GET /api/resources?filter=(status=active | status=pending) & tags_php=yes // GET /api/resources?filter=COUNT(addresses) > 2 // GET /api/resources?search=John&filter=role=admin
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; } } // Utilisation $service = new DataExportService(app(ClusterService::class)); $data = $service->exportFilteredData( $sourceData, 'status=active & COUNT(addresses) > 1', ['name', 'email', 'status', 'age'] );
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