From f1755eb1b55cda7d1a121f2ff3a1095c752704cc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 11 Oct 2026 18:20:26 +0100 Subject: [PATCH 1/5] fix: bug lors de la persistances des donnees avec le DatabaseHandler --- src/Handlers/DatabaseHandler.php | 187 +++++++++++++++++++++---------- 1 file changed, 126 insertions(+), 61 deletions(-) diff --git a/src/Handlers/DatabaseHandler.php b/src/Handlers/DatabaseHandler.php index 8b0cbfd..66f9844 100644 --- a/src/Handlers/DatabaseHandler.php +++ b/src/Handlers/DatabaseHandler.php @@ -14,7 +14,6 @@ use BlitzPHP\Database\Builder\BaseBuilder; use BlitzPHP\Database\Connection\BaseConnection; use BlitzPHP\Database\Exceptions\DatabaseException; -use BlitzPHP\Utilities\DateTime\Date; use RuntimeException; /** @@ -89,6 +88,7 @@ public function get(string $file, string $property, ?string $context = null): mi public function set(string $file, string $property, mixed $value = null, ?string $context = null): void { if ($this->deferWrites) { + $this->hydrate($context); $this->markPending($file, $property, $value, $context); } else { $this->persist($file, $property, $value, $context); @@ -136,21 +136,21 @@ public function setMany(array $settings, ?string $context = null): void */ private function persist(string $file, string $property, mixed $value, ?string $context): void { - $time = Date::now()->format('Y-m-d H:i:s'); + $time = date('Y-m-d H:i:s'); $type = gettype($value); $prepared = $this->prepareValue($value); // S'il a été stocké, nous devons le mettre à jour if ($this->has($file, $property, $context)) { - $result = $this->builder()->where('file', $file)->where('key', $property); + $query = $this->builder()->where('file', $file)->where('key', $property); if ($context === null) { - $result = $result->whereNull('context'); + $query->whereNull('context'); } else { - $result = $result->where('context', $context); + $query->where('context', $context); } - $result = $result->update([ + $result = $query->update([ 'value' => $prepared, 'type' => $type, 'context' => $context, @@ -325,73 +325,135 @@ public function persistPendingProperties(): void } try { - $this->db->beginTransaction(); + $this->persistRows($upserts, $deletes); - // Gérer les mises à jour avec insertion : récupérer les enregistrements existants correspondant à nos données en attente - if ($upserts !== []) { - // Construire une requête pour récupérer uniquement les enregistrements dont nous avons besoin - $builder = $this->buildOrWhereConditions($upserts, 'file', 'key', 'context'); + $this->pendingProperties = []; + } catch (DatabaseException|RuntimeException $e) { + logger()->error('Échec de la persistance des propriétés en attente : ' . $e->getMessage()); - $existing = $builder->clone()->result('array'); + $this->pendingProperties = []; + } + } - // Créez une carte des enregistrements existants pour faciliter la recherche - $existingMap = []; + /** + * Prépare les lignes de la base de données en vue de leur persistance. + * + * @param list $settings + * + * @return list + */ + private function prepareUpsertRows(array $settings, ?string $context): array + { + $time = date('Y-m-d H:i:s'); + $rows = []; - foreach ($existing as $row) { - $key = $this->buildCompositeKey($row['file'], $row['key'], $row['context']); - $existingMap[$key] = $row['id']; - } + foreach ($settings as $setting) { + $rows[] = [ + 'file' => $setting['file'], + 'key' => $setting['property'], + 'value' => $this->prepareValue($setting['value']), + 'type' => gettype($setting['value']), + 'context' => $context, + 'created_at' => $time, + 'updated_at' => $time, + ]; + } - // Distinguer les insertions des mises à jour - $inserts = []; - $updates = []; - - foreach ($upserts as $row) { - $key = $this->buildCompositeKey($row['file'], $row['key'], $row['context']); - - if (isset($existingMap[$key])) { - // L'enregistrement existe - se préparer à la mise à jour - $updates[] = [ - 'id' => $existingMap[$key], - 'value' => $row['value'], - 'type' => $row['type'], - 'updated_at' => $row['updated_at'], - ]; - } else { - // Nouvel enregistrement - préparation à l'insertion - $inserts[] = $row; - } - } + return $rows; + } - // Insérer de nouveaux enregistrements par lots - if ($inserts !== []) { - $builder->bulkInsert($inserts); - } + /** + * Prépare les lignes de la base de données en vue de leur suppression définitive. + * + * @param list $settings + * + * @return list + */ + private function prepareDeleteRows(array $settings, ?string $context): array + { + $rows = []; - // Mise à jour groupée des enregistrements existants - if ($updates !== []) { - $builder->bulkUpdate($updates, 'id'); - } - } + foreach ($settings as $setting) { + $rows[] = [ + 'file' => $setting['file'], + 'key' => $setting['property'], + 'context' => $context, + ]; + } + + return $rows; + } + + /** + * Persiste les lignes préparées dans la base de données. + * + * @param list $upserts + * @param list $deletes + */ + private function persistRows(array $upserts, array $deletes): void + { + if ($upserts === [] && $deletes === []) { + return; + } + + $this->db->beginTransaction(); - // Supprimer en bloc toutes les opérations de suppression - if ($deletes !== []) { - $builder = $this->buildOrWhereConditions($deletes, 'file', 'key', 'context'); + // Gérer les mises à jour avec insertion : récupérer les enregistrements existants correspondant à nos données en attente + if ($upserts !== []) { + // Construire une requête pour récupérer uniquement les enregistrements dont nous avons besoin + $builder = $this->buildOrWhereConditions($upserts, 'file', 'key', 'context'); - $builder->delete(); + $existing = $builder->clone()->result('array'); + + // Créez une carte des enregistrements existants pour faciliter la recherche + $existingMap = []; + + foreach ($existing as $row) { + $key = $this->buildCompositeKey($row['file'], $row['key'], $row['context']); + $existingMap[$key] = $row['id']; } - $this->db->commit(); + // Distinguer les insertions des mises à jour + $inserts = []; + $updates = []; + + foreach ($upserts as $row) { + $key = $this->buildCompositeKey($row['file'], $row['key'], $row['context']); + + if (isset($existingMap[$key])) { + // L'enregistrement existe - se préparer à la mise à jour + $updates[] = [ + 'id' => $existingMap[$key], + 'value' => $row['value'], + 'type' => $row['type'], + 'updated_at' => $row['updated_at'], + ]; + } else { + // Nouvel enregistrement - préparation à l'insertion + $inserts[] = $row; + } + } - if ($this->db->transStatus() === false) { - logger()->error("Impossible d'enregistrer les propriétés en attente dans la base de données."); + // Insérer de nouveaux enregistrements par lots + if ($inserts !== []) { + $builder->bulkInsert($inserts); } - $this->pendingProperties = []; - } catch (DatabaseException $e) { - logger()->error('Échec de la persistance des propriétés en attente : ' . $e->getMessage()); + // Mise à jour groupée des enregistrements existants + if ($updates !== []) { + $builder->bulkUpdate($updates, 'id'); + } + } - $this->pendingProperties = []; + // Supprimer en bloc toutes les opérations de suppression + if ($deletes !== []) { + $this->buildOrWhereConditions($deletes, 'file', 'key', 'context')->delete(); + } + + $this->db->commit(); + + if ($this->db->transStatus() === false) { + logger()->error("Impossible d'enregistrer les propriétés en attente dans la base de données."); } } @@ -412,9 +474,12 @@ private function buildOrWhereConditions(array $rows, string $fileKey, string $ke foreach ($rows as $row) { $builder->orWhere(function ($q) use ($row, $fileKey, $keyKey, $contextKey) { - $q->where($fileKey, $row[$fileKey]) - ->where($keyKey, $row[$keyKey]) - ->where($contextKey, $row[$contextKey]); + $q->where([$fileKey => $row[$fileKey], $keyKey => $row[$keyKey]]); + if (null === $row[$contextKey]) { + $q->whereNull($contextKey); + } else { + $q->where($contextKey, $row[$contextKey]); + } }); } From 4607bb48639b515db9ff7defe219b7b8e38fbe9a Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 11 Oct 2026 18:21:48 +0100 Subject: [PATCH 2/5] fix: bug du verouillage des fichier avec le gestionnaire FileHandler --- src/Handlers/FileHandler.php | 143 +++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 25 deletions(-) diff --git a/src/Handlers/FileHandler.php b/src/Handlers/FileHandler.php index 0bb95df..e818d26 100644 --- a/src/Handlers/FileHandler.php +++ b/src/Handlers/FileHandler.php @@ -12,6 +12,7 @@ namespace BlitzPHP\Parametres\Handlers; use RuntimeException; +use Throwable; /** * Fournit une persistance basée sur les fichiers pour les paramètres. @@ -169,7 +170,7 @@ public function forget(string $file, string $property, ?string $context = null): } /** - * Supprime plusieurs enregistrements du stockage persistant, + * Supprime plusieurs enregistrements du stockage persistant, * s'ils existent, ainsi que du cache local. * * @param list $settings @@ -301,7 +302,7 @@ private function loadFromFile(string $file, ?string $context): void } // Utiliser include pour obtenir le tableau de données - $data = include $filePath; + $data = @include $filePath; if (! is_array($data)) { throw new RuntimeException('Le fichier de paramètres ne retourne pas un tableau : ' . $filePath); @@ -321,6 +322,9 @@ private function loadFromFile(string $file, ?string $context): void * Persiste les changements de propriétés spécifiques sur le disque. * Utilisé à la fois pour les écritures immédiates et différées. * + * La lecture et l'écriture se font via le MÊME handle verrouillé, + * ce qui évite les conflits de verrou sur Windows (Permission denied). + * * @throws RuntimeException En cas d'échec d'écriture du fichier */ private function persist(string $file, ?string $context, array $changes): void @@ -334,39 +338,128 @@ private function persist(string $file, ?string $context, array $changes): void throw new RuntimeException('Impossible de créer le répertoire : ' . $directory); } - $currentData = []; - if (file_exists($filePath)) { - $currentData = include $filePath; + // Ouvrir/créer le fichier pour le verrouillage + $lockHandle = fopen($filePath, 'c+b'); + + if ($lockHandle === false) { + throw new RuntimeException('Impossible d\'ouvrir le fichier pour le verrouillage: ' . $filePath); + } + + try { + // Acquérir un verrou exclusif + if (! flock($lockHandle, LOCK_EX)) { + throw new RuntimeException('Impossible d\'acquérir le verrou sur le fichier: ' . $filePath); + } + + // Vider le cache des statistiques de fichier pour obtenir la taille actuelle + clearstatcache(true, $filePath); + + // Lire le contenu actuel via le handle déjà ouvert (pas de include) + // hack blitzphp personnel pour eviter les problemes de verou sous windows + $currentData = $this->readDataFromHandle($lockHandle); + + // Appliquer tous les changements en attente + foreach ($changes as $change) { + if ($change['delete']) { + // Supprimer explicitement cette propriété + unset($currentData[$change['property']]); + } else { + // Définir ou mettre à jour cette propriété + $currentData[$change['property']] = [ + 'value' => $change['value'], + 'type' => gettype($change['value']), + ]; + } + } + + // Générer le contenu du fichier PHP + $content = ' $change['value'], - 'type' => gettype($change['value']), - ]; + @chmod($filePath, 0644); + } + + /** + * Lit les données depuis un handle de fichier PHP déjà verrouillé. + * + * Évite d'utiliser `include` qui ouvrirait un second handle, + * ce qui provoquerait un conflit de verrou sur Windows + * (Permission denied: Read of 4096 bytes failed with errno=13). + * + * @param resource $handle + * + * @return array + */ + private function readDataFromHandle($handle): array + { + $content = ''; + rewind($handle); + + while (! feof($handle)) { + $chunk = fread($handle, 8192); + + if ($chunk === false) { + break; } + + $content .= $chunk; + } + + if (trim($content) === '') { + return []; } - // Générer le contenu du fichier PHP - $content = 'evaluatePhpReturn($content); + + return is_array($data) ? $data : []; + } - // Écrire le fichier - if (file_put_contents($filePath, $content, LOCK_EX) === false) { - throw new RuntimeException('Impossible d\'écrire le fichier de paramètres : ' . $filePath); + /** + * Évalue un contenu PHP de la forme ` Date: Sun, 11 Oct 2026 18:22:31 +0100 Subject: [PATCH 3/5] fix: corrections diverses --- composer.json | 4 ++-- src/Handlers/ArrayHandler.php | 6 ++++++ src/Handlers/JsonHandler.php | 8 ++++++-- src/Parametres.php | 4 ++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 0661e50..0f9071e 100644 --- a/composer.json +++ b/composer.json @@ -19,8 +19,8 @@ }, "require-dev": { "blitz-php/coding-standard": "^1.6", - "blitz-php/database": "^1.0.0-rc", - "blitz-php/framework": "^1.0.0-rc", + "blitz-php/database": "^1.3.5-rc", + "blitz-php/framework": "^1.0.13-rc", "kahlan/kahlan": "^6.1.0", "phpstan/phpstan": "^2.1.42" }, diff --git a/src/Handlers/ArrayHandler.php b/src/Handlers/ArrayHandler.php index 65b2cce..c37b1e5 100644 --- a/src/Handlers/ArrayHandler.php +++ b/src/Handlers/ArrayHandler.php @@ -160,8 +160,14 @@ protected function setStored(string $file, string $property, mixed $value, ?stri protected function forgetStored(string $file, string $property, ?string $context): void { if ($context === null) { + if (! isset($this->general[$file])) { + return; + } Arr::forget($this->general[$file], $property); } else { + if (! isset($this->contexts[$context][$file])) { + return; + } Arr::forget($this->contexts[$context][$file], $property); } } diff --git a/src/Handlers/JsonHandler.php b/src/Handlers/JsonHandler.php index 9a7b2b7..3f235e6 100644 --- a/src/Handlers/JsonHandler.php +++ b/src/Handlers/JsonHandler.php @@ -164,7 +164,7 @@ public function setMany(array $settings, ?string $context = null): void */ public function forget(string $file, string $property, ?string $context = null): void { - $this->hydrate($file); + $this->hydrate($context); // Suppression du stockage local $this->forgetStored($file, $property, $context); @@ -289,7 +289,7 @@ private function hydrate(?string $context = null): void $this->hydrated[] = null; $items = $data->whereNull('context'); } else { - // Si le général n'a pas été hydraté, on l'hydrate donc. + // Si le général n'a pas été hydraté, on l'hydrate en même temps. if (! in_array(null, $this->hydrated, true)) { $this->hydrated[] = null; $items = $data->whereNull('context')->merge($data->where('context', $context)); @@ -320,6 +320,10 @@ private function hydrate(?string $context = null): void */ private function persistChanges(array $changes): void { + if ($changes === []) { + return; + } + // Acquérir un verrou exclusif pour éviter les conflits d'écriture $lockHandle = fopen($this->file, 'c+b'); diff --git a/src/Parametres.php b/src/Parametres.php index 63ba056..633e7bf 100644 --- a/src/Parametres.php +++ b/src/Parametres.php @@ -148,7 +148,7 @@ public function setMany(array $settings, ?string $context = null): void */ public function forget(string $key, ?string $context = null): void { - [$file, , $dotProperty] = $this->prepareFileAndProperty($key); + [$file, , , $dotProperty] = $this->prepareFileAndProperty($key); foreach ($this->getWriteHandlers() as $handler) { $handler->forget($file, $dotProperty, $context); @@ -170,7 +170,7 @@ public function forgetMany(array $keys, ?string $context = null): void $prepared = []; foreach ($keys as $key) { - [$file, , $dotProperty] = $this->prepareFileAndProperty($key); + [$file, , , $dotProperty] = $this->prepareFileAndProperty($key); $prepared[$file . '::' . $dotProperty] = [ 'file' => $file, From e0423074faa3b4e3b526d044ed23554cced08088 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 11 Oct 2026 18:24:31 +0100 Subject: [PATCH 4/5] Fix styling --- src/Handlers/FileHandler.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Handlers/FileHandler.php b/src/Handlers/FileHandler.php index e818d26..c62f0eb 100644 --- a/src/Handlers/FileHandler.php +++ b/src/Handlers/FileHandler.php @@ -451,9 +451,7 @@ private function evaluatePhpReturn(string $content): mixed try { // Évaluation isolée dans une closure statique // (aucune variable du scope courant n'est accessible) - $evaluate = static function () use ($content) { - return eval($content . ';'); - }; + $evaluate = static fn () => eval($content . ';'); return $evaluate(); } catch (Throwable $e) { From 84f48687f09bd2f0f447cffeec2cdb0da27cae9e Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 11 Oct 2026 18:39:07 +0100 Subject: [PATCH 5/5] chore: mise a jour github actions --- .github/workflows/run-tests.yml | 2 +- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-phpstan.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 7778cdf..f2ffa7d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false matrix: operating-system: [ubuntu-latest] - php-versions: ['8.1', '8.2'] + php-versions: ['8.2', '8.3'] name: Test with PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} steps: - name: Checkout diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index c84d406..8487773 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -26,8 +26,8 @@ jobs: fail-fast: false matrix: php-version: - - '8.1' - '8.2' + - '8.3' steps: - name: Checkout diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index a04d542..1cded8d 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -42,7 +42,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.2' extensions: intl coverage: none