Skip to content

Commit e1b15a3

Browse files
committed
feat: ajout du hot reload
1 parent fb31ffd commit e1b15a3

7 files changed

Lines changed: 278 additions & 6 deletions

File tree

spec/support/application/app/Config/toolbar.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
*/
1111

1212
return [
13-
'collectors' => [],
14-
'collect_var_data' => true,
15-
'max_history' => 20,
16-
'view_path' => SYST_PATH . 'Debug' . DS . 'Toolbar' . DS . 'Views',
17-
'max_queries' => 100,
18-
'show_debugbar' => true,
13+
'collectors' => [],
14+
'collect_var_data' => true,
15+
'max_history' => 20,
16+
'view_path' => SYST_PATH . 'Debug' . DS . 'Toolbar' . DS . 'Views',
17+
'max_queries' => 100,
18+
'show_debugbar' => true,
19+
'watched_directories' => ['app'],
20+
'watched_extensions' => ['php', 'css', 'js', 'html', 'svg', 'json', 'env'],
1921
];
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Blitz PHP framework.
5+
*
6+
* (c) 2022 Dimitri Sitchet Tomkeu <devcode.dst@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
use BlitzPHP\Exceptions\FrameworkException;
13+
use BlitzPHP\HotReloader\DirectoryHasher;
14+
15+
use function Kahlan\expect;
16+
17+
describe('HotReloader / DirectoryHasher', function (): void {
18+
beforeAll(function (): void {
19+
$this->hasher = new DirectoryHasher();
20+
});
21+
22+
it('hashApp', function (): void {
23+
$results = $this->hasher->hashApp();
24+
25+
expect($results)->toBeA('array');
26+
expect($results)->toContainKey('app');
27+
});
28+
29+
it('Leve une exception si on essai de hasher un dossier invalide', function (): void {
30+
$path = $path = APP_PATH . 'Foo';
31+
32+
expect(fn() => $this->hasher->hashDirectory($path))
33+
->toThrow(FrameworkException::invalidDirectory($path));
34+
});
35+
36+
it('Chaque dossier a un hash unique', function (): void {
37+
$hash1 = $this->hasher->hashDirectory(APP_PATH);
38+
$hash2 = $this->hasher->hashDirectory(SYST_PATH);
39+
40+
expect($hash1)->not->toBe($hash2);
41+
});
42+
43+
it('Un meme dossier produira le meme hash', function (): void {
44+
$hash1 = $this->hasher->hashDirectory(APP_PATH);
45+
$hash2 = $this->hasher->hashDirectory(APP_PATH);
46+
47+
expect($hash1)->toBe($hash2);
48+
});
49+
50+
it ('hash', function (): void {
51+
$expected = md5(implode('', $this->hasher->hashApp()));
52+
53+
expect($expected)->toBe($this->hasher->hash());
54+
});
55+
});

src/Exceptions/FrameworkException.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public static function enabledZlibOutputCompression()
2828
return new static(lang('Core.enabledZlibOutputCompression'));
2929
}
3030

31+
public static function invalidDirectory(string $path)
32+
{
33+
return new static(lang('Core.invalidDirectory', [$path]));
34+
}
35+
3136
public static function invalidFile(string $path)
3237
{
3338
return new static(lang('Core.invalidFile', [$path]));
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Blitz PHP framework.
5+
*
6+
* (c) 2022 Dimitri Sitchet Tomkeu <devcode.dst@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace BlitzPHP\HotReloader;
13+
14+
use BlitzPHP\Exceptions\FrameworkException;
15+
use FilesystemIterator;
16+
use RecursiveDirectoryIterator;
17+
use RecursiveIteratorIterator;
18+
19+
/**
20+
* @internal
21+
*
22+
* @credit <a href="https://codeigniter.com">CodeIgniter 4.6 - CodeIgniter\HotReloader\DirectoryHasher</a>
23+
*/
24+
final class DirectoryHasher
25+
{
26+
/**
27+
* Génère une valeur MD5 de tous les répertoires surveillés par le rechargeur à chaud,
28+
* comme défini dans le fichier app/Config/toolbar.php.
29+
*
30+
* Il s'agit de l'empreinte actuelle de l'application.
31+
*/
32+
public function hash(): string
33+
{
34+
return md5(implode('', $this->hashApp()));
35+
}
36+
37+
/**
38+
* Génère un tableau de hachages md5 pour tous les répertoires surveillés par le Hot Reloader,
39+
* comme défini dans app/Config/toolbar.php.
40+
*/
41+
public function hashApp(): array
42+
{
43+
$hashes = [];
44+
45+
$watchedDirectories = config('toolbar.watched_directories', []);
46+
47+
foreach ($watchedDirectories as $directory) {
48+
if (is_dir(ROOTPATH . $directory)) {
49+
$hashes[$directory] = $this->hashDirectory(ROOTPATH . $directory);
50+
}
51+
}
52+
53+
return array_unique(array_filter($hashes));
54+
}
55+
56+
/**
57+
* Génère un hachage MD5 d'un répertoire donné et de tous ses fichiers * qui correspondent aux extensions surveillées
58+
* définies dans app/Config/toolbar.php.
59+
*/
60+
public function hashDirectory(string $path): string
61+
{
62+
if (! is_dir($path)) {
63+
throw FrameworkException::invalidDirectory($path);
64+
}
65+
66+
$directory = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
67+
$filter = new IteratorFilter($directory);
68+
$iterator = new RecursiveIteratorIterator($filter);
69+
70+
$hashes = [];
71+
72+
foreach ($iterator as $file) {
73+
if ($file->isFile()) {
74+
$hashes[] = md5_file($file->getRealPath());
75+
}
76+
}
77+
78+
return md5(implode('', $hashes));
79+
}
80+
}

src/HotReloader/HotReloader.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Blitz PHP framework.
5+
*
6+
* (c) 2022 Dimitri Sitchet Tomkeu <devcode.dst@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace BlitzPHP\HotReloader;
13+
14+
/**
15+
* @internal
16+
*
17+
* @credit <a href="https://codeigniter.com">CodeIgniter 4.6 - CodeIgniter\HotReloader\HotReloader</a>
18+
*/
19+
final class HotReloader
20+
{
21+
public function run(): void
22+
{
23+
if (session_status() === PHP_SESSION_ACTIVE) {
24+
session_write_close();
25+
}
26+
27+
ini_set('zlib.output_compression', 'Off');
28+
29+
header('Cache-Control: no-store');
30+
header('Content-Type: text/event-stream');
31+
header('Access-Control-Allow-Methods: GET');
32+
33+
ob_end_clean();
34+
set_time_limit(0);
35+
36+
$hasher = new DirectoryHasher();
37+
$appHash = $hasher->hash();
38+
39+
while (true) {
40+
if (connection_status() !== CONNECTION_NORMAL || connection_aborted() === 1) {
41+
break;
42+
}
43+
44+
$currentHash = $hasher->hash();
45+
46+
// Si le hachage a changé, demandez au navigateur de se recharger.
47+
if ($currentHash !== $appHash) {
48+
$appHash = $currentHash;
49+
50+
$this->sendEvent('reload', ['time' => date('Y-m-d H:i:s')]);
51+
break;
52+
}
53+
54+
if (mt_rand(1, 10) > 8) {
55+
$this->sendEvent('ping', ['time' => date('Y-m-d H:i:s')]);
56+
}
57+
58+
sleep(1);
59+
}
60+
}
61+
62+
/**
63+
* Envoyer un événement au navigateur.
64+
*/
65+
private function sendEvent(string $event, array $data): void
66+
{
67+
echo "event: {$event}\n";
68+
echo 'data: ' . json_encode($data) . "\n\n";
69+
70+
ob_flush();
71+
flush();
72+
}
73+
}

src/HotReloader/IteratorFilter.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Blitz PHP framework.
5+
*
6+
* (c) 2022 Dimitri Sitchet Tomkeu <devcode.dst@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace BlitzPHP\HotReloader;
13+
14+
use RecursiveFilterIterator;
15+
use RecursiveIterator;
16+
17+
/**
18+
* @internal
19+
*
20+
* @psalm-suppress MissingTemplateParam
21+
*
22+
* @credit <a href="https://codeigniter.com">CodeIgniter 4.6 - CodeIgniter\HotReloader\IteratorFilter</a>
23+
*/
24+
final class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator
25+
{
26+
private array $watchedExtensions = [];
27+
28+
public function __construct(RecursiveIterator $iterator)
29+
{
30+
parent::__construct($iterator);
31+
32+
$this->watchedExtensions = config('toolbar.watched_extensions', []);
33+
}
34+
35+
/**
36+
* Appliquer des filtres aux fichiers dans l'itérateur.
37+
*/
38+
public function accept(): bool
39+
{
40+
if (! $this->current()->isFile()) {
41+
return true;
42+
}
43+
44+
$filename = $this->current()->getFilename();
45+
46+
// Ignorer les fichiers et répertoires cachés.
47+
if ($filename[0] === '.') {
48+
return false;
49+
}
50+
51+
// Ne consommez que les fichiers qui vous intéressent.
52+
$ext = trim(strtolower($this->current()->getExtension()), '. ');
53+
54+
return in_array($ext, $this->watchedExtensions, true);
55+
}
56+
}

src/Translations/en/Core.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
return [
1414
'copyError' => 'An error was encountered while attempting to replace the file ({0}). Please make sure your file directory is writable.',
1515
'enabledZlibOutputCompression' => 'Your zlib.output_compression ini directive is turned on. This will not work well with output buffers.',
16+
'invalidDirectory' => 'Directory does not exist: "{0}"',
1617
'invalidFile' => 'Invalid file: {0}',
1718
'invalidPhpVersion' => 'Your PHP version must be {0} or higher to run CodeIgniter. Current version: {1}',
1819
'missingExtension' => 'The framework needs the following extension(s) installed and loaded: {0}.',

0 commit comments

Comments
 (0)