Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHP version isolation helper for command line #1216

Merged
merged 30 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee6a17e
wip
NasirNobin Mar 21, 2022
794fbc0
wip
NasirNobin Mar 21, 2022
3d85b44
wip
NasirNobin Mar 21, 2022
b788079
wip
NasirNobin Mar 21, 2022
09f3d3e
wip
NasirNobin Mar 21, 2022
4f098a8
refactor with which-php command
NasirNobin Mar 22, 2022
1824ae4
wip
NasirNobin Mar 22, 2022
d9ebb47
tests added
NasirNobin Mar 22, 2022
245c68e
wip
NasirNobin Mar 22, 2022
720fed1
refactor code & tests
NasirNobin Mar 22, 2022
6b0c6f5
Merge branch 'laravel:master' into feature/valet-run
NasirNobin Mar 22, 2022
84f59b4
test readability
NasirNobin Mar 22, 2022
ca2924d
Self code review
NasirNobin Mar 22, 2022
faeee5e
Apply suggestions from code review
NasirNobin Mar 22, 2022
67ba790
Update cli/valet.php
NasirNobin Mar 22, 2022
605118d
Update tests/BrewTest.php
NasirNobin Mar 22, 2022
6f085bc
StyleCI Patch
NasirNobin Mar 22, 2022
fbf96b9
Apply suggestions from code review
NasirNobin Mar 23, 2022
6d3d191
[wip] Valet run/refactor with brew opt (#9)
NasirNobin Mar 23, 2022
23aebbe
StyleCI Patch
NasirNobin Mar 23, 2022
9a9f73f
Update cli/Valet/Brew.php
NasirNobin Mar 24, 2022
40cb21e
Apply suggestions from code review
NasirNobin Mar 29, 2022
1838945
Fix typo on arePhpVersionsEqual
NasirNobin Mar 29, 2022
dd8c8fc
add normalizePhpVersion inside getPhpExecutablePath
NasirNobin Mar 29, 2022
0202f77
remove static from normalizePhpVersion
NasirNobin Mar 29, 2022
d198d77
wip - refactor with valetphprc version (#10)
NasirNobin Mar 29, 2022
2acd133
StyleCI Patch
NasirNobin Mar 29, 2022
390f87e
Update cli/valet.php
NasirNobin Mar 30, 2022
6bc5024
Update cli/valet.php
NasirNobin Mar 30, 2022
804a924
StyleCI Patch
NasirNobin Mar 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 76 additions & 14 deletions cli/Valet/Brew.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,16 +251,7 @@ public function getParsedLinkedPhp()

$resolvedPath = $this->files->readLink(BREW_PREFIX.'/bin/php');

/**
* Typical homebrew path resolutions are like:
* "../Cellar/[email protected]/7.4.13/bin/php"
* or older styles:
* "../Cellar/php/7.4.9_2/bin/php
* "../Cellar/php55/bin/php.
*/
preg_match('~\w{3,}/(php)(@?\d\.?\d)?/(\d\.\d)?([_\d\.]*)?/?\w{3,}~', $resolvedPath, $matches);

return $matches;
return $this->parsePhpPath($resolvedPath);
}

/**
Expand Down Expand Up @@ -289,15 +280,51 @@ public function linkedPhp()

return $this->supportedPhpVersions()->first(
function ($version) use ($resolvedPhpVersion) {
$resolvedVersionNormalized = preg_replace('/[^\d]/', '', $resolvedPhpVersion);
$versionNormalized = preg_replace('/[^\d]/', '', $version);

return $resolvedVersionNormalized === $versionNormalized;
return $this->arePhpVersionsEqual($resolvedPhpVersion, $version);
}, function () use ($resolvedPhpVersion) {
throw new DomainException("Unable to determine linked PHP when parsing '$resolvedPhpVersion'");
});
}

/**
* Extract PHP executable path from PHP Version.
*
* @param string $phpVersion For example, "[email protected]"
* @return string
*/
public function getPhpExecutablePath($phpVersion = null)
{
if (! $phpVersion) {
return BREW_PREFIX.'/bin/php';
}

$phpVersion = PhpFpm::normalizePhpVersion($phpVersion);

// Check the default `/opt/homebrew/opt/[email protected]/bin/php` location first
if ($this->files->exists(BREW_PREFIX."/opt/{$phpVersion}/bin/php")) {
return BREW_PREFIX."/opt/{$phpVersion}/bin/php";
}

// Check the `/opt/homebrew/opt/php71/bin/php` location for older installations
$phpVersion = str_replace(['@', '.'], '', $phpVersion); // [email protected] to php81
if ($this->files->exists(BREW_PREFIX."/opt/{$phpVersion}/bin/php")) {
return BREW_PREFIX."/opt/{$phpVersion}/bin/php";
}

// Check if the default PHP is the version we are looking for
if ($this->files->isLink(BREW_PREFIX.'/opt/php')) {
$resolvedPath = $this->files->readLink(BREW_PREFIX.'/opt/php');
$matches = $this->parsePhpPath($resolvedPath);
$resolvedPhpVersion = $matches[3] ?: $matches[2];

if ($this->arePhpVersionsEqual($resolvedPhpVersion, $phpVersion)) {
return BREW_PREFIX.'/opt/php/bin/php';
}
}

return BREW_PREFIX.'/bin/php';
}

/**
* Restart the linked PHP-FPM Homebrew service.
*
Expand Down Expand Up @@ -463,4 +490,39 @@ function ($exitCode, $errorOutput) {
}
);
}

/**
* Parse homebrew PHP Path.
*
* @param string $resolvedPath
* @return array
*/
public function parsePhpPath($resolvedPath)
{
/**
* Typical homebrew path resolutions are like:
* "../Cellar/[email protected]/7.4.13/bin/php"
* or older styles:
* "../Cellar/php/7.4.9_2/bin/php
* "../Cellar/php55/bin/php.
*/
preg_match('~\w{3,}/(php)(@?\d\.?\d)?/(\d\.\d)?([_\d\.]*)?/?\w{3,}~', $resolvedPath, $matches);

return $matches;
}

/**
* Check if two PHP versions are equal.
*
* @param string $versionA
* @param string $versionB
* @return bool
*/
public function arePhpVersionsEqual($versionA, $versionB)
{
$versionANormalized = preg_replace('/[^\d]/', '', $versionA);
$versionBNormalized = preg_replace('/[^\d]/', '', $versionB);

return $versionANormalized === $versionBNormalized;
}
}
2 changes: 1 addition & 1 deletion cli/Valet/PhpFpm.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ public function symlinkPrimaryValetSock($phpVersion)
/**
* If passed php7.4, or php74, 7.4, or 74 formats, normalize to [email protected] format.
*/
public function normalizePhpVersion($version)
public static function normalizePhpVersion($version)
NasirNobin marked this conversation as resolved.
Show resolved Hide resolved
{
return preg_replace('/(?:php@?)?([0-9+])(?:.)?([0-9+])/i', 'php@$1.$2', $version);
}
Expand Down
37 changes: 37 additions & 0 deletions cli/valet.php
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,43 @@
table(['Path', 'PHP Version'], $sites->all());
})->descriptions('List all sites using isolated versions of PHP.');

/**
* Get the PHP executable path for a site.
*/
$app->command('which-php [site]', function ($site) {
$host = Site::host($site ?: getcwd()).'.'.Configuration::read()['tld'];
$phpVersion = Site::customPhpVersion($host);

if (! $phpVersion) {
$path = getcwd().'/.valetphprc';
NasirNobin marked this conversation as resolved.
Show resolved Hide resolved
if (file_exists($path)) {
$phpVersion = trim(file_get_contents($path));
}
}

return output(Brew::getPhpExecutablePath($phpVersion));
})->descriptions('Get the PHP executable path for a given site', [
'site' => 'The site to get the PHP executable path for',
]);

/**
* Proxy commands through to an isolated site's version of PHP.
*/
$app->command('php [command]', function ($command) {
warning('It looks like you are running `cli/valet.php` directly; please use the `valet` script in the project root instead.');
})->descriptions("Proxy PHP commands with isolated site's PHP executable", [
'command' => "Command to run with isolated site's PHP executable",
]);

/**
* Proxy commands through to an isolated site's version of Composer.
*/
$app->command('composer [command]', function ($command) {
warning('It looks like you are running `cli/valet.php` directly; please use the `valet` script in the project root instead.');
})->descriptions("Proxy Composer commands with isolated site's PHP executable", [
'command' => "Composer command to run with isolated site's PHP executable",
]);

/**
* Tail log file.
*/
Expand Down
65 changes: 65 additions & 0 deletions tests/BrewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,71 @@ public function test_restart_linked_php_will_pass_through_linked_php_formula_to_
$brewMock->restartLinkedPhp();
}

public function test_it_can_get_php_binary_path_from_php_version()
{
// Check the default `/opt/homebrew/opt/[email protected]/bin/php` location first
$brewMock = Mockery::mock(Brew::class, [
Mockery::mock(CommandLine::class),
$files = Mockery::mock(Filesystem::class),
])->makePartial();

$files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/[email protected]/bin/php')->andReturn(true);
$files->shouldNotReceive('exists')->with(BREW_PREFIX.'/opt/php@74/bin/php');
$this->assertEquals(BREW_PREFIX.'/opt/[email protected]/bin/php', $brewMock->getPhpExecutablePath('[email protected]'));

// Check the `/opt/homebrew/opt/php71/bin/php` location for older installations
$brewMock = Mockery::mock(Brew::class, [
Mockery::mock(CommandLine::class),
$files = Mockery::mock(Filesystem::class),
])->makePartial();

$files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/[email protected]/bin/php')->andReturn(false);
$files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php74/bin/php')->andReturn(true);
$this->assertEquals(BREW_PREFIX.'/opt/php74/bin/php', $brewMock->getPhpExecutablePath('[email protected]'));

// When the default PHP is the version we are looking for
$brewMock = Mockery::mock(Brew::class, [
Mockery::mock(CommandLine::class),
$files = Mockery::mock(Filesystem::class),
])->makePartial();

$files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/[email protected]/bin/php')->andReturn(false);
$files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php74/bin/php')->andReturn(false);
$files->shouldReceive('isLink')->with(BREW_PREFIX.'/opt/php')->andReturn(true);
$files->shouldReceive('readLink')->with(BREW_PREFIX.'/opt/php')->andReturn('../Cellar/[email protected]/7.4.13/bin/php');
$this->assertEquals(BREW_PREFIX.'/opt/php/bin/php', $brewMock->getPhpExecutablePath('[email protected]'));

// When the default PHP is not the version we are looking for
$brewMock = Mockery::mock(Brew::class, [
Mockery::mock(CommandLine::class),
$files = Mockery::mock(Filesystem::class),
])->makePartial();

$files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/[email protected]/bin/php')->andReturn(false);
$files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php74/bin/php')->andReturn(false);
$files->shouldReceive('isLink')->with(BREW_PREFIX.'/opt/php')->andReturn(true);
$files->shouldReceive('readLink')->with(BREW_PREFIX.'/opt/php')->andReturn('../Cellar/[email protected]/8.1.13/bin/php');
$this->assertEquals(BREW_PREFIX.'/bin/php', $brewMock->getPhpExecutablePath('[email protected]')); // Could not find a version, so retuned the default binary

// When no PHP Version is provided
$brewMock = Mockery::mock(Brew::class, [
Mockery::mock(CommandLine::class),
Mockery::mock(Filesystem::class),
])->makePartial();

$this->assertEquals(BREW_PREFIX.'/bin/php', $brewMock->getPhpExecutablePath(null));
}

public function test_it_can_compare_two_php_versions()
{
$this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php71', '[email protected]'));
$this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php71', 'php@71'));
$this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php71', '71'));

$this->assertFalse(resolve(Brew::class)->arePhpVersionsEqual('php71', 'php@70'));
$this->assertFalse(resolve(Brew::class)->arePhpVersionsEqual('php71', '72'));
}

/**
* Provider of php links and their expected split matches.
*
Expand Down
30 changes: 22 additions & 8 deletions valet
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ then
DIR=$(php -r "echo realpath('$DIR/../laravel/valet');")
fi

if [[ "$EUID" -ne 0 ]]
then
sudo USER="$USER" --preserve-env "$SOURCE" "$@"
exit
fi

# If the command is the "share" command we will need to resolve out any
# symbolic links for the site. Before starting Ngrok, we will fire a
# process to retrieve the live Ngrok tunnel URL in the background.
Expand Down Expand Up @@ -81,16 +75,36 @@ then
ARCH=$(uname -m)

if [[ $ARCH == 'arm64' ]]; then
sudo -u "$USER" "$DIR/bin/ngrok-arm" http "$HOST.$TLD:$PORT" -host-header=rewrite $PARAMS
"$DIR/bin/ngrok-arm" http "$HOST.$TLD:$PORT" -host-header=rewrite $PARAMS
else
sudo -u "$USER" "$DIR/bin/ngrok" http "$HOST.$TLD:$PORT" -host-header=rewrite $PARAMS
"$DIR/bin/ngrok" http "$HOST.$TLD:$PORT" -host-header=rewrite $PARAMS
mattstauffer marked this conversation as resolved.
Show resolved Hide resolved
fi

exit

# Proxy PHP commands to the "php" executable on the isolated site
elif [[ "$1" = "php" ]]
then
$(php "$DIR/cli/valet.php" which-php) "${@:2}"

exit

# Proxy Composer commands with the "php" executable on the isolated site
elif [[ "$1" = "composer" ]]
then
$(php "$DIR/cli/valet.php" which-php) $(which composer) "${@:2}"
NasirNobin marked this conversation as resolved.
Show resolved Hide resolved

exit

# Finally, for every other command we will just proxy into the PHP tool
# and let it handle the request. These are commands which can be run
# without sudo and don't require taking over terminals like Ngrok.
else
if [[ "$EUID" -ne 0 ]]
then
sudo USER="$USER" --preserve-env "$SOURCE" "$@"
exit
fi

php "$DIR/cli/valet.php" "$@"
fi