Skip to content

Commit cbbdfb6

Browse files
aivispushpak1300
andauthored
[1.x] Add configurable PHP and Composer paths (#437)
* [Feature] Add configurable PHP and Composer paths * fix windows env tests * Use boost.commands.* prefix for php and composer commands * Add config options * adjust precedence based on the feedback * extend to npm * Add composer vendor bin prefix * wip Signed-off-by: Pushpak Chhajed <pushpak1300@gmail.com> * Refactor Signed-off-by: Pushpak Chhajed <pushpak1300@gmail.com> --------- Signed-off-by: Pushpak Chhajed <pushpak1300@gmail.com> Co-authored-by: Pushpak Chhajed <pushpak1300@gmail.com>
1 parent d26c455 commit cbbdfb6

File tree

6 files changed

+271
-18
lines changed

6 files changed

+271
-18
lines changed

config/boost.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,22 @@
2929

3030
'browser_logs_watcher' => env('BOOST_BROWSER_LOGS_WATCHER', true),
3131

32+
/*
33+
|--------------------------------------------------------------------------
34+
| Boost Executables Config
35+
|--------------------------------------------------------------------------
36+
|
37+
| The following options allow you to configure custom paths for the PHP,
38+
| Composer, and npm executables used by Boost. Leave empty to use defaults.
39+
| When configured, these take precedence over automatic detection.
40+
|
41+
*/
42+
43+
'executables' => [
44+
'php' => env('BOOST_PHP_EXECUTABLE'),
45+
'composer' => env('BOOST_COMPOSER_EXECUTABLE'),
46+
'npm' => env('BOOST_NPM_EXECUTABLE'),
47+
'vendor_bin' => env('BOOST_VENDOR_BIN_EXECUTABLE'),
48+
],
49+
3250
];

src/Install/CodeEnvironment/CodeEnvironment.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ public function useAbsolutePathForMcp(): bool
4040

4141
public function getPhpPath(bool $forceAbsolutePath = false): string
4242
{
43-
return ($this->useAbsolutePathForMcp() || $forceAbsolutePath) ? PHP_BINARY : 'php';
43+
$phpBinaryPath = config('boost.executables.php') ?? 'php';
44+
45+
if ($phpBinaryPath === 'php' && ($this->useAbsolutePathForMcp() || $forceAbsolutePath)) {
46+
return PHP_BINARY;
47+
}
48+
49+
return $phpBinaryPath;
4450
}
4551

4652
public function getArtisanPath(bool $forceAbsolutePath = false): string

src/Install/GuidelineAssist.php

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,19 +170,24 @@ public function supportsPintAgentFormatter(): bool
170170
return $this->roster->usesVersion(Packages::PINT, '1.27.0', '>=');
171171
}
172172

173-
public function nodePackageManager(): string
173+
protected function detectedNodePackageManager(): string
174174
{
175175
return ($this->roster->nodePackageManager() ?? NodePackageManager::NPM)->value;
176176
}
177177

178178
public function nodePackageManagerCommand(string $command): string
179179
{
180-
$manager = $this->nodePackageManager();
181-
$nodePackageManagerCommand = $this->config->usesSail
182-
? Sail::nodePackageManagerCommand($manager)
183-
: $manager;
180+
$npmExecutable = config('boost.executables.npm');
184181

185-
return "{$nodePackageManagerCommand} {$command}";
182+
if ($npmExecutable !== null) {
183+
return "{$npmExecutable} {$command}";
184+
}
185+
186+
if ($this->config->usesSail) {
187+
return Sail::nodePackageManagerCommand($this->detectedNodePackageManager())." {$command}";
188+
}
189+
190+
return "{$this->detectedNodePackageManager()} {$command}";
186191
}
187192

188193
public function artisanCommand(string $command): string
@@ -192,22 +197,42 @@ public function artisanCommand(string $command): string
192197

193198
public function composerCommand(string $command): string
194199
{
195-
$composerCommand = $this->config->usesSail
196-
? Sail::composerCommand()
197-
: 'composer';
200+
$composerExecutable = config('boost.executables.composer');
198201

199-
return "{$composerCommand} {$command}";
202+
if ($composerExecutable !== null) {
203+
return "{$composerExecutable} {$command}";
204+
}
205+
206+
if ($this->config->usesSail) {
207+
return Sail::composerCommand()." {$command}";
208+
}
209+
210+
return "composer {$command}";
200211
}
201212

202213
public function binCommand(string $command): string
203214
{
204-
return $this->config->usesSail
205-
? Sail::binCommand().$command
206-
: "vendor/bin/{$command}";
215+
$vendorBinPrefix = config('boost.executables.vendor_bin');
216+
217+
if ($vendorBinPrefix !== null) {
218+
return "{$vendorBinPrefix}{$command}";
219+
}
220+
221+
if ($this->config->usesSail) {
222+
return Sail::binCommand().$command;
223+
}
224+
225+
return "vendor/bin/{$command}";
207226
}
208227

209228
public function artisan(): string
210229
{
230+
$phpExecutable = config('boost.executables.php');
231+
232+
if ($phpExecutable !== null) {
233+
return "{$phpExecutable} artisan";
234+
}
235+
211236
return $this->config->usesSail
212237
? Sail::artisanCommand()
213238
: 'php artisan';

tests/Feature/Install/CodeEnvironment/CodeEnvironmentPathResolutionTest.php

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Laravel\Boost\Install\Detection\DetectionStrategyFactory;
88

99
test('PhpStorm returns absolute PHP_BINARY path', function (): void {
10+
config(['boost.executables.php' => null]);
1011
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
1112
$phpStorm = new PhpStorm($strategyFactory);
1213

@@ -25,20 +26,49 @@
2526
});
2627

2728
test('Cursor returns relative php string', function (): void {
29+
config(['boost.executables.php' => null]);
2830
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
2931
$cursor = new Cursor($strategyFactory);
3032

3133
expect($cursor->getPhpPath())->toBe('php');
3234
});
3335

36+
test('Cursor uses configured default_php_bin when not forcing absolute path', function (): void {
37+
config(['boost.executables.php' => '/custom/path/to/php']);
38+
39+
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
40+
$cursor = new Cursor($strategyFactory);
41+
42+
expect($cursor->getPhpPath())->toBe('/custom/path/to/php');
43+
});
44+
45+
test('Cursor uses config even when forceAbsolutePath is true', function (): void {
46+
config(['boost.executables.php' => '/custom/path/to/php']);
47+
48+
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
49+
$cursor = new Cursor($strategyFactory);
50+
51+
expect($cursor->getPhpPath(true))->toBe('/custom/path/to/php');
52+
});
53+
54+
test('Cursor uses PHP_BINARY when forceAbsolutePath is true and config is empty', function (): void {
55+
config(['boost.executables.php' => null]);
56+
57+
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
58+
$cursor = new Cursor($strategyFactory);
59+
60+
expect($cursor->getPhpPath(true))->toBe(PHP_BINARY);
61+
});
62+
3463
test('Cursor returns relative artisan path', function (): void {
3564
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
3665
$cursor = new Cursor($strategyFactory);
3766

3867
expect($cursor->getArtisanPath())->toBe('artisan');
3968
});
4069

41-
test('CodeEnvironment returns absolute paths when forceAbsolutePath is true', function (): void {
70+
test('CodeEnvironment returns absolute paths when forceAbsolutePath is true and config is empty', function (): void {
71+
config(['boost.executables.php' => null]);
4272
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
4373
$cursor = new Cursor($strategyFactory);
4474

@@ -47,7 +77,8 @@
4777
->not->toBe('artisan');
4878
});
4979

50-
test('CodeEnvironment maintains relative paths when forceAbsolutePath is false', function (): void {
80+
test('CodeEnvironment maintains relative paths when forceAbsolutePath is false and config is empty', function (): void {
81+
config(['boost.executables.php' => null]);
5182
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
5283
$cursor = new Cursor($strategyFactory);
5384

@@ -56,6 +87,7 @@
5687
});
5788

5889
test('PhpStorm paths remain absolute regardless of forceAbsolutePath parameter', function (): void {
90+
config(['boost.executables.php' => null]);
5991
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
6092
$phpStorm = new PhpStorm($strategyFactory);
6193

@@ -69,3 +101,13 @@
69101

70102
expect($phpStorm->getArtisanPath(false))->toBe($artisanPath);
71103
});
104+
105+
test('PhpStorm uses config when configured', function (): void {
106+
config(['boost.executables.php' => '/custom/php']);
107+
$strategyFactory = Mockery::mock(DetectionStrategyFactory::class);
108+
$phpStorm = new PhpStorm($strategyFactory);
109+
110+
// Config takes precedence over useAbsolutePathForMcp
111+
expect($phpStorm->getPhpPath(true))->toBe('/custom/php');
112+
expect($phpStorm->getPhpPath(false))->toBe('/custom/php');
113+
});

tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,12 +406,14 @@ public function mcpConfigPath(): string
406406
->and($capturedContent)->toBe(fixtureContent('mcp-expected.json5'));
407407
});
408408

409-
test('getPhpPath uses absolute paths when forceAbsolutePath is true', function (): void {
409+
test('getPhpPath uses absolute paths when forceAbsolutePath is true and config is empty', function (): void {
410+
config(['boost.executables.php' => null]);
410411
$environment = new TestCodeEnvironment($this->strategyFactory);
411412
expect($environment->getPhpPath(true))->toBe(PHP_BINARY);
412413
});
413414

414-
test('getPhpPath maintains default behavior when forceAbsolutePath is false', function (): void {
415+
test('getPhpPath maintains default behavior when forceAbsolutePath is false and config is empty', function (): void {
416+
config(['boost.executables.php' => null]);
415417
$environment = new TestCodeEnvironment($this->strategyFactory);
416418
expect($environment->getPhpPath(false))->toBe('php');
417419
});
@@ -425,3 +427,31 @@ public function mcpConfigPath(): string
425427
$environment = new TestCodeEnvironment($this->strategyFactory);
426428
expect($environment->getArtisanPath(false))->toBe('artisan');
427429
});
430+
431+
test('getPhpPath uses configured default_php_bin from config', function (): void {
432+
config(['boost.executables.php' => '/usr/local/bin/php8.3']);
433+
434+
$environment = new TestCodeEnvironment($this->strategyFactory);
435+
expect($environment->getPhpPath(false))->toBe('/usr/local/bin/php8.3');
436+
});
437+
438+
test('getPhpPath returns php when config is set to php', function (): void {
439+
config(['boost.executables.php' => 'php']);
440+
441+
$environment = new TestCodeEnvironment($this->strategyFactory);
442+
expect($environment->getPhpPath(false))->toBe('php');
443+
});
444+
445+
test('getPhpPath uses config even when forceAbsolutePath is true', function (): void {
446+
config(['boost.executables.php' => '/usr/local/bin/php8.3']);
447+
448+
$environment = new TestCodeEnvironment($this->strategyFactory);
449+
expect($environment->getPhpPath(true))->toBe('/usr/local/bin/php8.3');
450+
});
451+
452+
test('getPhpPath uses PHP_BINARY when forceAbsolutePath is true and config is empty', function (): void {
453+
config(['boost.executables.php' => null]);
454+
455+
$environment = new TestCodeEnvironment($this->strategyFactory);
456+
expect($environment->getPhpPath(true))->toBe(PHP_BINARY);
457+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Boost\Install\GuidelineAssist;
6+
use Laravel\Boost\Install\GuidelineConfig;
7+
use Laravel\Boost\Install\Sail;
8+
use Laravel\Roster\Roster;
9+
10+
beforeEach(function (): void {
11+
$this->roster = Mockery::mock(Roster::class);
12+
$this->roster->shouldReceive('nodePackageManager')->andReturn(null);
13+
$this->roster->shouldReceive('usesVersion')->andReturn(false);
14+
15+
$this->config = new GuidelineConfig;
16+
});
17+
18+
test('php executable falls back to Sail when no config is set', function (): void {
19+
config(['boost.executables.php' => null]);
20+
$this->config->usesSail = true;
21+
22+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
23+
$assist->shouldAllowMockingProtectedMethods();
24+
$assist->shouldReceive('discover')->andReturn([]);
25+
26+
expect($assist->artisan())->toBe(Sail::artisanCommand());
27+
});
28+
29+
test('php executable config takes precedence over Sail', function (): void {
30+
config(['boost.executables.php' => '/usr/local/bin/php8.3']);
31+
$this->config->usesSail = true;
32+
33+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
34+
$assist->shouldAllowMockingProtectedMethods();
35+
$assist->shouldReceive('discover')->andReturn([]);
36+
37+
expect($assist->artisan())->toBe('/usr/local/bin/php8.3 artisan');
38+
});
39+
40+
test('composer executable falls back to Sail when no config is set', function (): void {
41+
config(['boost.executables.composer' => null]);
42+
$this->config->usesSail = true;
43+
44+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
45+
$assist->shouldAllowMockingProtectedMethods();
46+
$assist->shouldReceive('discover')->andReturn([]);
47+
48+
$defaultSailComposer = Sail::composerCommand();
49+
50+
expect($assist->composerCommand('install'))->toBe("{$defaultSailComposer} install");
51+
});
52+
53+
test('composer executable config takes precedence over Sail', function (): void {
54+
config(['boost.executables.composer' => '/usr/local/bin/composer2']);
55+
$this->config->usesSail = true;
56+
57+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
58+
$assist->shouldAllowMockingProtectedMethods();
59+
$assist->shouldReceive('discover')->andReturn([]);
60+
61+
expect($assist->composerCommand('install'))->toBe('/usr/local/bin/composer2 install');
62+
});
63+
64+
test('npm executable falls back to Sail when no config is set', function (): void {
65+
config(['boost.executables.npm' => null]);
66+
$this->config->usesSail = true;
67+
68+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
69+
$assist->shouldAllowMockingProtectedMethods();
70+
$assist->shouldReceive('discover')->andReturn([]);
71+
72+
$expectedCommand = Sail::nodePackageManagerCommand('npm');
73+
74+
expect($assist->nodePackageManagerCommand('install'))->toBe("{$expectedCommand} install");
75+
});
76+
77+
test('npm executable config takes precedence over Sail', function (): void {
78+
config(['boost.executables.npm' => '/usr/local/bin/yarn']);
79+
$this->config->usesSail = true;
80+
81+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
82+
$assist->shouldAllowMockingProtectedMethods();
83+
$assist->shouldReceive('discover')->andReturn([]);
84+
85+
expect($assist->nodePackageManagerCommand('install'))->toBe('/usr/local/bin/yarn install');
86+
});
87+
88+
test('npm executable falls back to npm when no config and no Sail', function (): void {
89+
config(['boost.executables.npm' => null]);
90+
$this->config->usesSail = false;
91+
92+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
93+
$assist->shouldAllowMockingProtectedMethods();
94+
$assist->shouldReceive('discover')->andReturn([]);
95+
96+
expect($assist->nodePackageManagerCommand('install'))->toBe('npm install');
97+
});
98+
99+
test('vendor bin prefix falls back to Sail when no config is set', function (): void {
100+
config(['boost.executables.vendor_bin' => null]);
101+
$this->config->usesSail = true;
102+
103+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
104+
$assist->shouldAllowMockingProtectedMethods();
105+
$assist->shouldReceive('discover')->andReturn([]);
106+
107+
$expectedPrefix = Sail::binCommand();
108+
109+
expect($assist->binCommand('pint'))->toBe("{$expectedPrefix}pint");
110+
});
111+
112+
test('vendor bin prefix config takes precedence over Sail', function (): void {
113+
config(['boost.executables.vendor_bin' => '/custom/path/']);
114+
$this->config->usesSail = true;
115+
116+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
117+
$assist->shouldAllowMockingProtectedMethods();
118+
$assist->shouldReceive('discover')->andReturn([]);
119+
120+
expect($assist->binCommand('pint'))->toBe('/custom/path/pint');
121+
});
122+
123+
test('vendor bin prefix falls back to vendor/bin when no config and no Sail', function (): void {
124+
config(['boost.executables.vendor_bin' => null]);
125+
$this->config->usesSail = false;
126+
127+
$assist = Mockery::mock(GuidelineAssist::class, [$this->roster, $this->config])->makePartial();
128+
$assist->shouldAllowMockingProtectedMethods();
129+
$assist->shouldReceive('discover')->andReturn([]);
130+
131+
expect($assist->binCommand('pint'))->toBe('vendor/bin/pint');
132+
});

0 commit comments

Comments
 (0)