Skip to content

Multiple/Parallel PHP Version Support for Valet #1192

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

Merged
merged 11 commits into from
Mar 14, 2022
19 changes: 1 addition & 18 deletions cli/Valet/Nginx.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function installServer()
str_replace(
['VALET_HOME_PATH', 'VALET_SERVER_PATH', 'VALET_STATIC_PREFIX'],
[VALET_HOME_PATH, VALET_SERVER_PATH, VALET_STATIC_PREFIX],
$this->replaceLoopback($this->files->get(__DIR__.'/../stubs/valet.conf'))
$this->site->replaceLoopback($this->files->get(__DIR__.'/../stubs/valet.conf'))
)
);

Expand All @@ -90,23 +90,6 @@ public function installServer()
);
}

public function replaceLoopback($siteConf)
{
$loopback = $this->configuration->read()['loopback'];

if ($loopback === VALET_LOOPBACK) {
return $siteConf;
}

$str = '#listen VALET_LOOPBACK:80; # valet loopback';

return str_replace(
$str,
substr(str_replace('VALET_LOOPBACK', $loopback, $str), 1),
$siteConf
);
}

/**
* Install the Nginx configuration directory to the ~/.config/valet directory.
*
Expand Down
207 changes: 191 additions & 16 deletions cli/Valet/PhpFpm.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class PhpFpm
public $brew;
public $cli;
public $files;
public $config;
public $site;
public $nginx;

public $taps = [
'homebrew/homebrew-core',
Expand All @@ -21,13 +24,19 @@ class PhpFpm
* @param Brew $brew
* @param CommandLine $cli
* @param Filesystem $files
* @param Configuration $config
* @param Site $site
* @param Nginx $nginx
* @return void
*/
public function __construct(Brew $brew, CommandLine $cli, Filesystem $files)
public function __construct(Brew $brew, CommandLine $cli, Filesystem $files, Configuration $config, Site $site, Nginx $nginx)
{
$this->cli = $cli;
$this->brew = $brew;
$this->files = $files;
$this->config = $config;
$this->site = $site;
$this->nginx = $nginx;
}

/**
Expand Down Expand Up @@ -63,13 +72,14 @@ public function uninstall()
/**
* Update the PHP FPM configuration.
*
* @param string|null $phpVersion
* @return void
*/
public function updateConfiguration()
public function updateConfiguration($phpVersion = null)
{
info('Updating PHP configuration...');
info(sprintf('Updating PHP configuration%s...', $phpVersion ? ' for '.$phpVersion : ''));

$fpmConfigFile = $this->fpmConfigPath();
$fpmConfigFile = $this->fpmConfigPath($phpVersion);

$this->files->ensureDirExists(dirname($fpmConfigFile), user());

Expand All @@ -93,6 +103,11 @@ public function updateConfiguration()
$contents = preg_replace('/^;?listen\.group = .+$/m', 'listen.group = staff', $contents);
$contents = preg_replace('/^;?listen\.mode = .+$/m', 'listen.mode = 0777', $contents);
}

if ($phpVersion) {
$contents = str_replace('valet.sock', $this->fpmSockName($phpVersion), $contents);
}

$this->files->put($fpmConfigFile, $contents);

$contents = $this->files->get(__DIR__.'/../stubs/php-memory-limits.ini');
Expand All @@ -116,11 +131,12 @@ public function updateConfiguration()
/**
* Restart the PHP FPM process.
*
* @param string|null $phpVersion
* @return void
*/
public function restart()
public function restart($phpVersion = null)
{
$this->brew->restartLinkedPhp();
$this->brew->restartService($phpVersion ?: $this->utilizedPhpVersions());
}

/**
Expand All @@ -139,13 +155,16 @@ public function stop()
/**
* Get the path to the FPM configuration file for the current PHP version.
*
* @param string|null $phpVersion
* @return string
*/
public function fpmConfigPath()
public function fpmConfigPath($phpVersion = null)
{
$version = $this->brew->linkedPhp();
if (! $phpVersion) {
$phpVersion = $this->brew->linkedPhp();
}

$versionNormalized = $this->normalizePhpVersion($version === 'php' ? Brew::LATEST_PHP_VERSION : $version);
$versionNormalized = $this->normalizePhpVersion($phpVersion === 'php' ? Brew::LATEST_PHP_VERSION : $phpVersion);
$versionNormalized = preg_replace('~[^\d\.]~', '', $versionNormalized);

return $versionNormalized === '5.6'
Expand All @@ -167,15 +186,62 @@ public function stopRunning()
);
}

/**
* Stop PHP, if a specific version isn't being used globally or by any sites.
*
* @param string|null $phpVersion
* @return void
*/
public function stopIfUnused($phpVersion)
{
if (! $phpVersion) {
return;
}

if (strpos($phpVersion, 'php') === false) {
$phpVersion = 'php'.$phpVersion;
}

$phpVersion = $this->normalizePhpVersion($phpVersion);

if (! in_array($phpVersion, $this->utilizedPhpVersions())) {
$this->brew->stopService($phpVersion);
}
}

/**
* Use a specific version of php.
*
* @param $version
* @param $force
* @return string
* @param string $version
* @param bool $force
* @param string|null $directory
* @return string|void
*/
public function useVersion($version, $force = false)
public function useVersion($version, $force = false, $directory = null)
{
if ($directory) {
$site = $this->site->getSiteUrl($directory);

if (! $site) {
throw new DomainException(
sprintf(
"The [%s] site could not be found in Valet's site list.",
$directory
)
);
}

if ($version == 'default') { // Remove isolation for this site
$oldCustomPhpVersion = $this->site->customPhpVersion($site); // Example output: "74"
$this->site->removeIsolation($site);
$this->stopIfUnused($oldCustomPhpVersion);
$this->nginx->restart();
info(sprintf('The site [%s] is now using the default PHP version.', $site));

return;
}
}

$version = $this->validateRequestedVersion($version);

try {
Expand All @@ -192,8 +258,31 @@ public function useVersion($version, $force = false)
$this->brew->ensureInstalled($version, [], $this->taps);
}

// Unlink the current php if there is one
if ($directory) {
$oldCustomPhpVersion = $this->site->customPhpVersion($site); // Example output: "74"
$this->cli->quietly('sudo rm '.VALET_HOME_PATH.'/'.$this->fpmSockName($version));
$this->updateConfiguration($version);
$this->site->installSiteConfig($site, $this->fpmSockName($version), $version);

$this->stopIfUnused($oldCustomPhpVersion);
$this->restart($version);
$this->nginx->restart();
info(sprintf('The site [%s] is now using %s.', $site, $version));

return;
}

// Unlink the current global PHP if there is one installed
if ($this->brew->hasLinkedPhp()) {
$linkedPhp = $this->brew->linkedPhp();

// Update the old FPM to keep running, using a custom sock file, so existing isolated sites aren't broken
$this->updateConfiguration($linkedPhp);

// Update existing custom Nginx config files; if they're using the old or new PHP version,
// update them to the new correct sock file location
$this->updateConfigurationForGlobalUpdate($version, $linkedPhp);

$currentVersion = $this->brew->getLinkedPhpFormula();
info(sprintf('Unlinking current version: %s', $currentVersion));
$this->brew->unlink($currentVersion);
Expand All @@ -206,12 +295,19 @@ public function useVersion($version, $force = false)

// remove any orphaned valet.sock files that PHP didn't clean up due to version conflicts
$this->files->unlink(VALET_HOME_PATH.'/valet.sock');
$this->cli->quietly('sudo rm '.VALET_HOME_PATH.'/valet.sock');
$this->cli->quietly('sudo rm '.VALET_HOME_PATH.'/valet*.sock');

// ensure configuration is correct and start the linked version
$this->install();

return $version === 'php' ? $this->brew->determineAliasedVersion($version) : $version;
$newVersion = $version === 'php' ? $this->brew->determineAliasedVersion($version) : $version;

$this->nginx->restart();

info(sprintf('Valet is now using %s.', $newVersion).PHP_EOL);
info('Note that you might need to run <comment>composer global update</comment> if your PHP version change affects the dependencies of global packages required by Composer.');

return $newVersion;
}

/**
Expand Down Expand Up @@ -257,4 +353,83 @@ public function validateRequestedVersion($version)

return $version;
}

/**
* Get FPM sock file name for a given PHP version.
*
* @param string|null $phpVersion
* @return string
*/
public function fpmSockName($phpVersion = null)
{
$versionInteger = preg_replace('~[^\d]~', '', $phpVersion);

return "valet{$versionInteger}.sock";
}

/**
* Update all existing Nginx files when running a global PHP version update.
* If a given file is pointing to `valet.sock`, it's targeting the old global PHP version;
* update it to point to the new custom sock file for that version.
* If a given file is pointing the custom sock file for the new global version, that new
* version will now be hosted at `valet.sock`, so update the config file to point to that instead.
*
* @param string $newPhpVersion
* @param string $oldPhpVersion
* @return void
*/
public function updateConfigurationForGlobalUpdate($newPhpVersion, $oldPhpVersion)
{
collect($this->files->scandir(VALET_HOME_PATH.'/Nginx'))
->reject(function ($file) {
return starts_with($file, '.');
})
->each(function ($file) use ($newPhpVersion, $oldPhpVersion) {
$content = $this->files->get(VALET_HOME_PATH.'/Nginx/'.$file);

if (! starts_with($content, '# Valet isolated PHP version')) {
return;
}

if (strpos($content, $this->fpmSockName($newPhpVersion)) !== false) {
info(sprintf('Updating site %s to keep using version: %s', $file, $newPhpVersion));
$this->files->put(VALET_HOME_PATH.'/Nginx/'.$file, str_replace($this->fpmSockName($newPhpVersion), 'valet.sock', $content));
} elseif (strpos($content, 'valet.sock') !== false) {
info(sprintf('Updating site %s to keep using version: %s', $file, $oldPhpVersion));
$this->files->put(VALET_HOME_PATH.'/Nginx/'.$file, str_replace('valet.sock', $this->fpmSockName($oldPhpVersion), $content));
}
});
}

/**
* Get a list including the global PHP version and allPHP versions currently serving "isolated sites" (sites with
* custom Nginx configs pointing them to a specific PHP version).
*
* @return array
*/
public function utilizedPhpVersions()
{
$fpmSockFiles = $this->brew->supportedPhpVersions()->map(function ($version) {
return $this->fpmSockName($this->normalizePhpVersion($version));
})->unique();

return collect($this->files->scandir(VALET_HOME_PATH.'/Nginx'))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential future refactor here to:

  • Make it clearer that this map and then the later filter is really rejecting any without a custom valet sock file
  • Stop looking for future instances of any given sock file (e.g. valet74.sock) once you find it in one site config

->reject(function ($file) {
return starts_with($file, '.');
})
->map(function ($file) use ($fpmSockFiles) {
$content = $this->files->get(VALET_HOME_PATH.'/Nginx/'.$file);

// Get the normalized PHP version for this config file, if it's defined
foreach ($fpmSockFiles as $sock) {
if (strpos($content, $sock) !== false) {
// Extract the PHP version number from a custom .sock path;
// for example, "valet74.sock" will output "php74"
$phpVersion = 'php'.str_replace(['valet', '.sock'], '', $sock);

return $this->normalizePhpVersion($phpVersion); // Example output [email protected]
}
}
})->merge([$this->brew->getLinkedPhpFormula()])->filter()->unique()->values()->toArray();
}
}
Loading