Skip to content

Commit b03cc49

Browse files
authored
Merge branch '5.x' into sentinel
2 parents 789627c + acb34f3 commit b03cc49

File tree

10 files changed

+347
-11
lines changed

10 files changed

+347
-11
lines changed

.github/workflows/tests.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
fail-fast: true
2525
matrix:
2626
php: [8.1, 8.2, 8.3, 8.4, 8.5]
27-
laravel: [10, 11, 12]
27+
laravel: [10, 11, 12, 13]
2828
include:
2929
- php: '8.0'
3030
laravel: 9
@@ -37,6 +37,10 @@ jobs:
3737
laravel: 11
3838
- php: 8.1
3939
laravel: 12
40+
- php: 8.1
41+
laravel: 13
42+
- php: 8.2
43+
laravel: 13
4044
- php: 8.4
4145
laravel: 10
4246
- php: 8.5

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Release Notes
22

3-
## [Unreleased](https://github.com/laravel/horizon/compare/v5.42.0...5.x)
3+
## [Unreleased](https://github.com/laravel/horizon/compare/v5.44.0...5.x)
4+
5+
## [v5.44.0](https://github.com/laravel/horizon/compare/v5.43.0...v5.44.0) - 2026-02-10
6+
7+
* [5.x] Add Node.js executable validation before starting file watcher by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/horizon/pull/1699
8+
* [5.x] Supports Laravel 13 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/horizon/pull/1700
9+
* [5.x] Fix incorrect `@return` tag in Supervisor property docblock by [@eranishojha](https://github.com/eranishojha) in https://github.com/laravel/horizon/pull/1701
10+
* [5.x] Guard `RedisStore::scan()` results against boolean failures by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/horizon/pull/1703
11+
12+
## [v5.43.0](https://github.com/laravel/horizon/compare/v5.42.0...v5.43.0) - 2026-01-15
13+
14+
* Add `horizon:listen` command by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/horizon/pull/1689
415

516
## [v5.42.0](https://github.com/laravel/horizon/compare/v5.41.0...v5.42.0) - 2026-01-06
617

bin/file-watcher.cjs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const chokidar = require('chokidar');
2+
3+
const paths = JSON.parse(process.argv[2]);
4+
const poll = process.argv[3] ? true : false;
5+
6+
// Chokidar removed support for glob in version 4...
7+
const chokidarPackagePath = require.resolve('chokidar').replace('index.js', 'package.json');
8+
const chokidarVersion4 = require(chokidarPackagePath).version.startsWith('4.');
9+
10+
const extractWildcardExtension = (path) => {
11+
// Match patterns like *.php, **/*.blade.php
12+
const match = path.match(/\/\*\.((\\w|\\.)+)$/);
13+
14+
return match ? match[1] : null;
15+
};
16+
17+
const getPathBeforeWildcard = (path) => {
18+
const match = path.match(/^(.*?)(\*|$)/);
19+
20+
return match ? match[1] : path;
21+
};
22+
23+
const extractedPaths = paths.map(path => {
24+
return { path: getPathBeforeWildcard(path), extension: extractWildcardExtension(path) };
25+
});
26+
27+
const watcherPaths = chokidarVersion4 ? extractedPaths.map(ep => ep.path) : paths;
28+
29+
const watcherIgnored = chokidarVersion4 ? (path, stats) => {
30+
if (! stats?.isFile()) {
31+
return false;
32+
}
33+
34+
const matchedPattern = extractedPaths.find(ep => path.startsWith(ep.path));
35+
36+
if (! matchedPattern) {
37+
return true;
38+
}
39+
40+
return matchedPattern.extension ? ! path.endsWith(`.${matchedPattern.extension}`) : false;
41+
} : undefined;
42+
43+
const watcher = chokidar.watch(watcherPaths, {
44+
ignoreInitial: true,
45+
usePolling: poll,
46+
ignored: watcherIgnored,
47+
});
48+
49+
watcher
50+
.on('add', () => console.log('File added...'))
51+
.on('change', () => console.log('File changed...'))
52+
.on('unlink', () => console.log('File deleted...'))
53+
.on('unlinkDir', () => console.log('Directory deleted...'));

composer.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@
1414
"ext-json": "*",
1515
"ext-pcntl": "*",
1616
"ext-posix": "*",
17-
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0",
18-
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0",
19-
"illuminate/support": "^9.21|^10.0|^11.0|^12.0",
17+
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0",
18+
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0",
19+
"illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0",
2020
"laravel/sentinel": "^1.0",
2121
"nesbot/carbon": "^2.17|^3.0",
2222
"ramsey/uuid": "^4.0",
23-
"symfony/console": "^6.0|^7.0",
24-
"symfony/error-handler": "^6.0|^7.0",
23+
"symfony/console": "^6.0|^7.0|^8.0",
24+
"symfony/error-handler": "^6.0|^7.0|^8.0",
2525
"symfony/polyfill-php83": "^1.28",
26-
"symfony/process": "^6.0|^7.0"
26+
"symfony/process": "^6.0|^7.0|^8.0"
2727
},
2828
"require-dev": {
2929
"mockery/mockery": "^1.0",
30-
"orchestra/testbench": "^7.56|^8.37|^9.16|^10.9",
30+
"orchestra/testbench": "^7.56|^8.37|^9.16|^10.9|^11.0",
3131
"phpstan/phpstan": "^1.10|^2.0",
3232
"predis/predis": "^1.1|^2.0|^3.0"
3333
},

config/horizon.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,28 @@
227227
],
228228
],
229229
],
230+
231+
/*
232+
|--------------------------------------------------------------------------
233+
| File Watcher Configuration
234+
|--------------------------------------------------------------------------
235+
|
236+
| The following list of directories and files will be watched when using
237+
| the `horizon:listen` command. Whenever any directories or files are
238+
| changed, Horizon will automatically restart to apply all changes.
239+
|
240+
*/
241+
242+
'watch' => [
243+
'app',
244+
'bootstrap',
245+
'config/**/*.php',
246+
'database/**/*.php',
247+
'public/**/*.php',
248+
'resources/**/*.php',
249+
'routes',
250+
'composer.lock',
251+
'composer.json',
252+
'.env',
253+
],
230254
];

src/Console/ListenCommand.php

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
namespace Laravel\Horizon\Console;
4+
5+
use Illuminate\Console\Command;
6+
use InvalidArgumentException;
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
use Symfony\Component\Process\ExecutableFinder;
9+
use Symfony\Component\Process\Process;
10+
11+
#[AsCommand(name: 'horizon:listen')]
12+
class ListenCommand extends Command
13+
{
14+
/**
15+
* The name and signature of the console command.
16+
*
17+
* @var string
18+
*/
19+
protected $signature = 'horizon:listen
20+
{--environment= : The environment name}
21+
{--poll : Use polling for file watching}';
22+
23+
/**
24+
* The console command description.
25+
*
26+
* @var string
27+
*/
28+
protected $description = 'Run Horizon and automatically restart workers on file changes';
29+
30+
/**
31+
* The Horizon process instance.
32+
*
33+
* @var \Symfony\Component\Process\Process|null
34+
*/
35+
protected $horizonProcess;
36+
37+
/**
38+
* The file watcher process instance.
39+
*
40+
* @var \Symfony\Component\Process\Process|null
41+
*/
42+
protected $watcherProcess;
43+
44+
/**
45+
* Indicates if a termination signal has been received.
46+
*
47+
* @var int|null
48+
*/
49+
protected $trappedSignal = null;
50+
51+
/**
52+
* Execute the console command.
53+
*
54+
* @return int
55+
*/
56+
public function handle()
57+
{
58+
$this->components->info('Starting Horizon and watching for file changes...');
59+
60+
$this->watcherProcess = $this->startWatcher();
61+
62+
if ($this->watcherProcess->isTerminated()) {
63+
return $this->watcherFailed();
64+
}
65+
66+
if (! $this->startHorizon()) {
67+
return Command::FAILURE;
68+
}
69+
70+
$this->listenForChanges();
71+
72+
return Command::SUCCESS;
73+
}
74+
75+
/**
76+
* Start the file watcher process.
77+
*
78+
* @return \Symfony\Component\Process\Process
79+
*/
80+
protected function startWatcher()
81+
{
82+
if (empty($paths = config('horizon.watch'))) {
83+
throw new InvalidArgumentException(
84+
'List of directories / files to watch not found. Please update your "config/horizon.php" configuration file.',
85+
);
86+
}
87+
88+
$nodeExecutable = (new ExecutableFinder)->find('node');
89+
90+
if (! $nodeExecutable) {
91+
throw new InvalidArgumentException(
92+
'Node could not be found. Please ensure Node is installed and available in your system PATH.',
93+
);
94+
}
95+
96+
$process = new Process([
97+
$nodeExecutable,
98+
'file-watcher.cjs',
99+
json_encode(collect($paths)->map(fn ($path) => base_path($path))->values()->all()),
100+
$this->option('poll') ? '1' : '',
101+
], __DIR__.'/../../bin', ['NODE_PATH' => base_path('node_modules')], null, null);
102+
103+
$process->start();
104+
105+
sleep(1);
106+
107+
return $process;
108+
}
109+
110+
/**
111+
* Start the Horizon process.
112+
*
113+
* @return bool
114+
*/
115+
protected function startHorizon()
116+
{
117+
$command = 'php artisan horizon';
118+
119+
if ($environment = $this->option('environment')) {
120+
$command .= ' --environment='.$environment;
121+
}
122+
123+
$this->horizonProcess = Process::fromShellCommandline($command)
124+
->setTimeout(null);
125+
126+
$this->trap([SIGINT, SIGTERM, SIGQUIT], function ($signal) {
127+
$this->trappedSignal = $signal;
128+
129+
$this->horizonProcess->stop(signal: $signal);
130+
$this->horizonProcess->wait();
131+
132+
if ($this->watcherProcess) {
133+
$this->watcherProcess->stop();
134+
}
135+
});
136+
137+
$this->horizonProcess->start();
138+
139+
usleep(100000);
140+
141+
return ! $this->horizonProcess->isTerminated();
142+
}
143+
144+
/**
145+
* Listen for file changes and restart Horizon when detected.
146+
*
147+
* @return void
148+
*/
149+
protected function listenForChanges()
150+
{
151+
while (! $this->trappedSignal) {
152+
if ($this->watcherProcess->getIncrementalOutput()) {
153+
$this->restartHorizon();
154+
}
155+
156+
$this->output->write($this->horizonProcess->getIncrementalOutput());
157+
158+
if (! $this->horizonProcess->isRunning()) {
159+
break;
160+
}
161+
162+
usleep(500000);
163+
}
164+
}
165+
166+
/**
167+
* Restart the Horizon process.
168+
*
169+
* @return void
170+
*/
171+
protected function restartHorizon()
172+
{
173+
$this->components->info('File changed. Restarting Horizon...');
174+
175+
$this->horizonProcess->stop();
176+
$this->horizonProcess->wait();
177+
178+
$this->startHorizon();
179+
}
180+
181+
/**
182+
* Handle watcher process failure.
183+
*
184+
* @return int
185+
*/
186+
protected function watcherFailed()
187+
{
188+
$this->components->error(
189+
'Unable to start file watcher. Please ensure Node.js and the chokidar npm package are installed.',
190+
);
191+
192+
$this->output->writeln($this->watcherProcess->getErrorOutput());
193+
194+
return Command::FAILURE;
195+
}
196+
}

src/HorizonServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ protected function registerCommands()
128128
Console\HorizonCommand::class,
129129
Console\InstallCommand::class,
130130
Console\ListCommand::class,
131+
Console\ListenCommand::class,
131132
Console\PauseCommand::class,
132133
Console\PauseSupervisorCommand::class,
133134
Console\PublishCommand::class,

src/Repositories/RedisMetricsRepository.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,16 @@ public function clear()
396396
$cursor = null;
397397

398398
do {
399-
[$cursor, $keys] = $this->connection()->scan(
399+
$scanResult = $this->connection()->scan(
400400
$cursor ?? 0, ['match' => config('horizon.prefix').$pattern]
401401
);
402402

403+
if (! is_array($scanResult)) {
404+
break;
405+
}
406+
407+
[$cursor, $keys] = $scanResult;
408+
403409
foreach ($keys ?? [] as $key) {
404410
$this->forget(Str::after($key, config('horizon.prefix')));
405411
}

src/Supervisor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Supervisor implements Pausable, Restartable, Terminable
2222
/**
2323
* The name of this supervisor instance.
2424
*
25-
* @return string
25+
* @var string
2626
*/
2727
public $name;
2828

0 commit comments

Comments
 (0)