diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b4ed6cf0e..6fd9c7b03 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- php: [5.6, '7.0', 7.1, 7.2, 7.3, 7.4, '8.0', 8.1]
+ php: ['7.0', 7.1, 7.2, 7.3, 7.4, '8.0', 8.1]
name: PHP ${{ matrix.php }}
diff --git a/.gitignore b/.gitignore
index 737bb2c64..8277f6c32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ composer.lock
error.log
.idea
.phpunit.result.cache
+tests/conf.d
diff --git a/cli/Valet/Brew.php b/cli/Valet/Brew.php
index a6a481e57..d9f02731f 100644
--- a/cli/Valet/Brew.php
+++ b/cli/Valet/Brew.php
@@ -15,12 +15,10 @@ class Brew
'php@7.2',
'php@7.1',
'php@7.0',
- 'php@5.6',
'php73',
'php72',
'php71',
'php70',
- 'php56',
];
const LATEST_PHP_VERSION = 'php@8.1';
diff --git a/cli/Valet/Nginx.php b/cli/Valet/Nginx.php
index 8f84c014d..ea4f8b127 100644
--- a/cli/Valet/Nginx.php
+++ b/cli/Valet/Nginx.php
@@ -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'))
)
);
@@ -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.
*
@@ -194,4 +177,17 @@ public function uninstall()
$this->brew->uninstallFormula('nginx nginx-full');
$this->cli->quietly('rm -rf '.BREW_PREFIX.'/etc/nginx '.BREW_PREFIX.'/var/log/nginx');
}
+
+ /**
+ * Return a list of all sites with explicit Nginx configurations.
+ *
+ * @return \Illuminate\Support\Collection
+ */
+ public function configuredSites()
+ {
+ return collect($this->files->scandir(VALET_HOME_PATH.'/Nginx'))
+ ->reject(function ($file) {
+ return starts_with($file, '.');
+ });
+ }
}
diff --git a/cli/Valet/PhpFpm.php b/cli/Valet/PhpFpm.php
index bfe7c7b45..b9403b293 100644
--- a/cli/Valet/PhpFpm.php
+++ b/cli/Valet/PhpFpm.php
@@ -9,6 +9,9 @@ class PhpFpm
public $brew;
public $cli;
public $files;
+ public $config;
+ public $site;
+ public $nginx;
public $taps = [
'homebrew/homebrew-core',
@@ -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;
}
/**
@@ -43,9 +52,16 @@ public function install()
$this->files->ensureDirExists(VALET_HOME_PATH.'/Log', user());
- $this->updateConfiguration();
+ $phpVersion = $this->brew->linkedPhp();
+ $this->createConfigurationFiles($phpVersion);
+
+ // Remove old valet.sock
+ $this->files->unlink(VALET_HOME_PATH.'/valet.sock');
+ $this->cli->quietly('sudo rm '.VALET_HOME_PATH.'/valet.sock');
$this->restart();
+
+ $this->symlinkPrimaryValetSock($phpVersion);
}
/**
@@ -61,66 +77,59 @@ public function uninstall()
}
/**
- * Update the PHP FPM configuration.
+ * Create (or re-create) the PHP FPM configuration files.
+ *
+ * Writes FPM config file, pointing to the correct .sock file, and log and ini files.
*
+ * @param string $phpVersion
* @return void
*/
- public function updateConfiguration()
+ public function createConfigurationFiles($phpVersion)
{
- info('Updating PHP configuration...');
+ info("Updating PHP configuration for {$phpVersion}...");
- $fpmConfigFile = $this->fpmConfigPath();
+ $fpmConfigFile = $this->fpmConfigPath($phpVersion);
$this->files->ensureDirExists(dirname($fpmConfigFile), user());
- // rename (to disable) old FPM Pool configuration, regardless of whether it's a default config or one customized by an older Valet version
- $oldFile = dirname($fpmConfigFile).'/www.conf';
- if (file_exists($oldFile)) {
- rename($oldFile, $oldFile.'-backup');
- }
-
- if (false === strpos($fpmConfigFile, '5.6')) {
- // since PHP 7 we can simply drop in a valet-specific fpm pool config, and not touch the default config
- $contents = $this->files->get(__DIR__.'/../stubs/etc-phpfpm-valet.conf');
- $contents = str_replace(['VALET_USER', 'VALET_HOME_PATH'], [user(), VALET_HOME_PATH], $contents);
- } else {
- // for PHP 5 we must do a direct edit of the fpm pool config to switch it to Valet's needs
- $contents = $this->files->get($fpmConfigFile);
- $contents = preg_replace('/^user = .+$/m', 'user = '.user(), $contents);
- $contents = preg_replace('/^group = .+$/m', 'group = staff', $contents);
- $contents = preg_replace('/^listen = .+$/m', 'listen = '.VALET_HOME_PATH.'/valet.sock', $contents);
- $contents = preg_replace('/^;?listen\.owner = .+$/m', 'listen.owner = '.user(), $contents);
- $contents = preg_replace('/^;?listen\.group = .+$/m', 'listen.group = staff', $contents);
- $contents = preg_replace('/^;?listen\.mode = .+$/m', 'listen.mode = 0777', $contents);
- }
+ // Create FPM Config File from stub
+ $contents = str_replace(
+ ['VALET_USER', 'VALET_HOME_PATH', 'valet.sock'],
+ [user(), VALET_HOME_PATH, self::fpmSockName($phpVersion)],
+ $this->files->get(__DIR__.'/../stubs/etc-phpfpm-valet.conf')
+ );
$this->files->put($fpmConfigFile, $contents);
- $contents = $this->files->get(__DIR__.'/../stubs/php-memory-limits.ini');
- $destFile = dirname($fpmConfigFile);
- $destFile = str_replace('/php-fpm.d', '', $destFile);
- $destFile .= '/conf.d/php-memory-limits.ini';
- $this->files->ensureDirExists(dirname($destFile), user());
- $this->files->putAsUser($destFile, $contents);
-
- $contents = $this->files->get(__DIR__.'/../stubs/etc-phpfpm-error_log.ini');
- $contents = str_replace(['VALET_USER', 'VALET_HOME_PATH'], [user(), VALET_HOME_PATH], $contents);
- $destFile = dirname($fpmConfigFile);
- $destFile = str_replace('/php-fpm.d', '', $destFile);
- $destFile .= '/conf.d/error_log.ini';
- $this->files->ensureDirExists(dirname($destFile), user());
- $this->files->putAsUser($destFile, $contents);
+ // Create other config files from stubs
+ $destDir = dirname(dirname($fpmConfigFile)).'/conf.d';
+ $this->files->ensureDirExists($destDir, user());
+
+ $this->files->putAsUser(
+ $destDir.'/php-memory-limits.ini',
+ $this->files->get(__DIR__.'/../stubs/php-memory-limits.ini')
+ );
+
+ $contents = str_replace(
+ ['VALET_USER', 'VALET_HOME_PATH'],
+ [user(), VALET_HOME_PATH],
+ $this->files->get(__DIR__.'/../stubs/etc-phpfpm-error_log.ini')
+ );
+ $this->files->putAsUser($destDir.'/error_log.ini', $contents);
+
+ // Create log directory and file
$this->files->ensureDirExists(VALET_HOME_PATH.'/Log', user());
$this->files->touch(VALET_HOME_PATH.'/Log/php-fpm.log', user());
}
/**
- * Restart the PHP FPM process.
+ * Restart the PHP FPM process (if one specified) or processes (if none specified).
*
+ * @param string|null $phpVersion
* @return void
*/
- public function restart()
+ public function restart($phpVersion = null)
{
- $this->brew->restartLinkedPhp();
+ $this->brew->restartService($phpVersion ?: $this->utilizedPhpVersions());
}
/**
@@ -139,22 +148,23 @@ 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'
- ? BREW_PREFIX.'/etc/php/5.6/php-fpm.conf'
- : BREW_PREFIX."/etc/php/${versionNormalized}/php-fpm.d/valet-fpm.conf";
+ return BREW_PREFIX."/etc/php/${versionNormalized}/php-fpm.d/valet-fpm.conf";
}
/**
- * Only stop running php services.
+ * Stop only the running php services.
*/
public function stopRunning()
{
@@ -168,11 +178,94 @@ public function stopRunning()
}
/**
- * Use a specific version of php.
+ * Stop a given PHP version, if that specific version isn't being used globally or by any sites.
*
- * @param $version
- * @param $force
- * @return string
+ * @param string|null $phpVersion
+ * @return void
+ */
+ public function stopIfUnused($phpVersion = null)
+ {
+ if (! $phpVersion) {
+ return;
+ }
+
+ $phpVersion = $this->normalizePhpVersion($phpVersion);
+
+ if (! in_array($phpVersion, $this->utilizedPhpVersions())) {
+ $this->brew->stopService($phpVersion);
+ }
+ }
+
+ /**
+ * Isolate a given directory to use a specific version of PHP.
+ *
+ * @param string $directory
+ * @param string $version
+ * @return void
+ */
+ public function isolateDirectory($directory, $version)
+ {
+ if (! $site = $this->site->getSiteUrl($directory)) {
+ throw new DomainException("The [{$directory}] site could not be found in Valet's site list.");
+ }
+
+ $version = $this->validateRequestedVersion($version);
+
+ $this->brew->ensureInstalled($version, [], $this->taps);
+
+ $oldCustomPhpVersion = $this->site->customPhpVersion($site); // Example output: "74"
+ $this->createConfigurationFiles($version);
+
+ $this->site->isolate($site, $version);
+
+ $this->stopIfUnused($oldCustomPhpVersion);
+ $this->restart($version);
+ $this->nginx->restart();
+
+ info(sprintf('The site [%s] is now using %s.', $site, $version));
+ }
+
+ /**
+ * Remove PHP version isolation for a given directory.
+ *
+ * @param string $directory
+ * @return void
+ */
+ public function unIsolateDirectory($directory)
+ {
+ if (! $site = $this->site->getSiteUrl($directory)) {
+ throw new DomainException("The [{$directory}] site could not be found in Valet's site list.");
+ }
+
+ $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));
+ }
+
+ /**
+ * List all directories with PHP isolation configured.
+ *
+ * @return \Illuminate\Support\Collection
+ */
+ public function isolatedDirectories()
+ {
+ return $this->nginx->configuredSites()->filter(function ($item) {
+ return strpos($this->files->get(VALET_HOME_PATH.'/Nginx/'.$item), ISOLATED_PHP_VERSION) !== false;
+ })->map(function ($item) {
+ return ['url' => $item, 'version' => $this->normalizePhpVersion($this->site->customPhpVersion($item))];
+ });
+ }
+
+ /**
+ * Use a specific version of PHP globally.
+ *
+ * @param string $version
+ * @param bool $force
+ * @return string|void
*/
public function useVersion($version, $force = false)
{
@@ -187,12 +280,9 @@ public function useVersion($version, $force = false)
} catch (DomainException $e) { /* ignore thrown exception when no linked php is found */
}
- if (! $this->brew->installed($version)) {
- // Install the relevant formula if not already installed
- $this->brew->ensureInstalled($version, [], $this->taps);
- }
+ $this->brew->ensureInstalled($version, [], $this->taps);
- // Unlink the current php if there is one
+ // Unlink the current global PHP if there is one installed
if ($this->brew->hasLinkedPhp()) {
$currentVersion = $this->brew->getLinkedPhpFormula();
info(sprintf('Unlinking current version: %s', $currentVersion));
@@ -204,28 +294,41 @@ public function useVersion($version, $force = false)
$this->stopRunning();
- // 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');
-
- // 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 composer global update if your PHP version change affects the dependencies of global packages required by Composer.');
+
+ return $newVersion;
}
/**
- * If passed php7.4 or php74 formats, normalize to php@7.4 format.
+ * Symlink (Capistrano-style) a given Valet.sock file to be the primary valet.sock.
+ *
+ * @param string $phpVersion
+ * @return void
+ */
+ public function symlinkPrimaryValetSock($phpVersion)
+ {
+ $this->files->symlinkAsUser(VALET_HOME_PATH.'/'.$this->fpmSockName($phpVersion), VALET_HOME_PATH.'/valet.sock');
+ }
+
+ /**
+ * If passed php7.4, or php74, 7.4, or 74 formats, normalize to php@7.4 format.
*/
public function normalizePhpVersion($version)
{
- return preg_replace('/(php)([0-9+])(?:.)?([0-9+])/i', '$1@$2.$3', $version);
+ return preg_replace('/(?:php@?)?([0-9+])(?:.)?([0-9+])/i', 'php@$1.$2', $version);
}
/**
* Validate the requested version to be sure we can support it.
*
- * @param $version
+ * @param string $version
* @return string
*/
public function validateRequestedVersion($version)
@@ -233,12 +336,7 @@ public function validateRequestedVersion($version)
$version = $this->normalizePhpVersion($version);
if (! $this->brew->supportedPhpVersions()->contains($version)) {
- throw new DomainException(
- sprintf(
- 'Valet doesn\'t support PHP version: %s (try something like \'php@7.3\' instead)',
- $version
- )
- );
+ throw new DomainException("Valet doesn't support PHP version: {$version} (try something like 'php@7.3' instead)");
}
if (strpos($aliasedVersion = $this->brew->determineAliasedVersion($version), '@')) {
@@ -246,10 +344,6 @@ public function validateRequestedVersion($version)
}
if ($version === 'php') {
- if (strpos($aliasedVersion = $this->brew->determineAliasedVersion($version), '@')) {
- return $aliasedVersion;
- }
-
if ($this->brew->hasInstalledPhp()) {
throw new DomainException('Brew is already using PHP '.PHP_VERSION.' as \'php\' in Homebrew. To use another version, please specify. eg: php@7.3');
}
@@ -257,4 +351,42 @@ public function validateRequestedVersion($version)
return $version;
}
+
+ /**
+ * Get FPM sock file name for a given PHP version.
+ *
+ * @param string|null $phpVersion
+ * @return string
+ */
+ public static function fpmSockName($phpVersion = null)
+ {
+ $versionInteger = preg_replace('~[^\d]~', '', $phpVersion);
+
+ return "valet{$versionInteger}.sock";
+ }
+
+ /**
+ * 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 self::fpmSockName($this->normalizePhpVersion($version));
+ })->unique();
+
+ return $this->nginx->configuredSites()->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 and normalize it to, e.g., "php@7.4"
+ return $this->normalizePhpVersion(str_replace(['valet', '.sock'], '', $sock));
+ }
+ }
+ })->merge([$this->brew->getLinkedPhpFormula()])->filter()->unique()->values()->toArray();
+ }
}
diff --git a/cli/Valet/Site.php b/cli/Valet/Site.php
index b7f530be5..660f68bac 100644
--- a/cli/Valet/Site.php
+++ b/cli/Valet/Site.php
@@ -190,6 +190,29 @@ public function proxies()
return $proxies;
}
+ /**
+ * Get the site URL from a directory if it's a valid Valet site.
+ *
+ * @param string $directory
+ * @return string|false
+ */
+ public function getSiteUrl($directory)
+ {
+ $tld = $this->config->read()['tld'];
+
+ if ($directory == '.' || $directory == './') { // Allow user to use dot as current dir's site `--site=.`
+ $directory = $this->host(getcwd());
+ }
+
+ $directory = str_replace('.'.$tld, '', $directory); // Remove .tld from sitename if it was provided
+
+ if (! $this->parked()->merge($this->links())->where('site', $directory)->count() > 0) {
+ return false; // Invalid directory provided
+ }
+
+ return $directory.'.'.$tld;
+ }
+
/**
* Identify whether a site is for a proxy by reading the host name from its config file.
*
@@ -474,6 +497,9 @@ public function secured()
*/
public function secure($url, $siteConf = null, $certificateExpireInDays = 396, $caExpireInYears = 20)
{
+ // Extract in order to later preserve custom PHP version config when securing
+ $phpVersion = $this->customPhpVersion($url);
+
$this->unsecure($url);
$this->files->ensureDirExists($this->caPath(), user());
@@ -487,9 +513,14 @@ public function secure($url, $siteConf = null, $certificateExpireInDays = 396, $
$this->createCa($caExpireInDate->format('%a'));
$this->createCertificate($url, $certificateExpireInDays);
- $this->files->putAsUser(
- $this->nginxPath($url), $this->buildSecureNginxServer($url, $siteConf)
- );
+ $siteConf = $this->buildSecureNginxServer($url, $siteConf);
+
+ // If the user had isolated the PHP version for this site, swap out .sock file
+ if ($phpVersion) {
+ $siteConf = $this->replaceSockFile($siteConf, $phpVersion);
+ }
+
+ $this->files->putAsUser($this->nginxPath($url), $siteConf);
}
/**
@@ -665,6 +696,50 @@ public function buildSecureNginxServer($url, $siteConf = null)
);
}
+ /**
+ * Create new nginx config or modify existing nginx config to isolate this site
+ * to a custom version of PHP.
+ *
+ * @param string $valetSite
+ * @param string $phpVersion
+ * @return void
+ */
+ public function isolate($valetSite, $phpVersion)
+ {
+ if ($this->files->exists($this->nginxPath($valetSite))) {
+ // Modify the existing config if it exists (likely because it's secured)
+ $siteConf = $this->files->get($this->nginxPath($valetSite));
+ $siteConf = $this->replaceSockFile($siteConf, $phpVersion);
+ } else {
+ $siteConf = str_replace(
+ ['VALET_HOME_PATH', 'VALET_SERVER_PATH', 'VALET_STATIC_PREFIX', 'VALET_SITE', 'VALET_PHP_FPM_SOCKET', 'VALET_ISOLATED_PHP_VERSION'],
+ [VALET_HOME_PATH, VALET_SERVER_PATH, VALET_STATIC_PREFIX, $valetSite, PhpFpm::fpmSockName($phpVersion), $phpVersion],
+ $this->replaceLoopback($this->files->get(__DIR__.'/../stubs/site.valet.conf'))
+ );
+ }
+
+ $this->files->putAsUser($this->nginxPath($valetSite), $siteConf);
+ }
+
+ /**
+ * Remove PHP Version isolation from a specific site.
+ *
+ * @param string $valetSite
+ * @return void
+ */
+ public function removeIsolation($valetSite)
+ {
+ // If a site has an SSL certificate, we need to keep its custom config file, but we can
+ // just re-generate it without defining a custom `valet.sock` file
+ if ($this->files->exists($this->certificatesPath($valetSite, 'crt'))) {
+ $siteConf = $this->buildSecureNginxServer($valetSite);
+ $this->files->putAsUser($this->nginxPath($valetSite), $siteConf);
+ } else {
+ // When site doesn't have SSL, we can remove the custom nginx config file to remove isolation
+ $this->files->unlink($this->nginxPath($valetSite));
+ }
+ }
+
/**
* Unsecure the given URL so that it will use HTTP again.
*
@@ -673,6 +748,9 @@ public function buildSecureNginxServer($url, $siteConf = null)
*/
public function unsecure($url)
{
+ // Extract in order to later preserve custom PHP version config when unsecuring. Example output: "74"
+ $phpVersion = $this->customPhpVersion($url);
+
if ($this->files->exists($this->certificatesPath($url, 'crt'))) {
$this->files->unlink($this->nginxPath($url));
@@ -688,6 +766,11 @@ public function unsecure($url)
'sudo security find-certificate -e "%s%s" -a -Z | grep SHA-1 | sudo awk \'{system("security delete-certificate -Z \'$NF\' /Library/Keychains/System.keychain")}\'',
$url, '@laravel.valet'
));
+
+ // If the user had isolated the PHP version for this site, swap out .sock file
+ if ($phpVersion) {
+ $this->isolate($url, $phpVersion);
+ }
}
public function unsecureAll()
@@ -964,4 +1047,63 @@ public function certificatesPath($url = null, $extension = null)
return $this->valetHomePath().'/Certificates'.$url.$extension;
}
+
+ /**
+ * Replace Loopback configuration line in Valet site configuration file contents.
+ *
+ * @param string $siteConf
+ * @return string
+ */
+ public function replaceLoopback($siteConf)
+ {
+ $loopback = $this->config->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
+ );
+ }
+
+ /**
+ * Extract PHP version of exising nginx conifg.
+ *
+ * @param string $url
+ * @return string|void
+ */
+ public function customPhpVersion($url)
+ {
+ if ($this->files->exists($this->nginxPath($url))) {
+ $siteConf = $this->files->get($this->nginxPath($url));
+
+ if (starts_with($siteConf, '# '.ISOLATED_PHP_VERSION)) {
+ $firstLine = explode(PHP_EOL, $siteConf)[0];
+
+ return preg_replace("/[^\d]*/", '', $firstLine); // Example output: "74" or "81"
+ }
+ }
+ }
+
+ /**
+ * Replace .sock file in an Nginx site configuration file contents.
+ *
+ * @param string $siteConf
+ * @param string $phpVersion
+ * @return string
+ */
+ public function replaceSockFile($siteConf, $phpVersion)
+ {
+ $sockFile = PhpFpm::fpmSockName($phpVersion);
+
+ $siteConf = preg_replace('/valet[0-9]*.sock/', $sockFile, $siteConf);
+ $siteConf = preg_replace('/# '.ISOLATED_PHP_VERSION.'.*\n/', '', $siteConf); // Remove ISOLATED_PHP_VERSION line from config
+
+ return '# '.ISOLATED_PHP_VERSION.'='.$phpVersion.PHP_EOL.$siteConf;
+ }
}
diff --git a/cli/includes/compatibility.php b/cli/includes/compatibility.php
index 4c2dc2a5d..86e57fe74 100644
--- a/cli/includes/compatibility.php
+++ b/cli/includes/compatibility.php
@@ -16,8 +16,8 @@
exit(1);
}
-if (version_compare(PHP_VERSION, '5.6.0', '<')) {
- echo 'Valet requires PHP 5.6 or later.';
+if (version_compare(PHP_VERSION, '7.0', '<')) {
+ echo 'Valet requires PHP 7.0 or later.';
exit(1);
}
diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php
index 1daf74fec..352d867fb 100644
--- a/cli/includes/helpers.php
+++ b/cli/includes/helpers.php
@@ -23,6 +23,8 @@
define('BREW_PREFIX', (new CommandLine())->runAsUser('printf $(brew --prefix)'));
+define('ISOLATED_PHP_VERSION', 'ISOLATED_PHP_VERSION');
+
/**
* Output the given text to the console.
*
diff --git a/cli/stubs/site.valet.conf b/cli/stubs/site.valet.conf
new file mode 100644
index 000000000..9fb0a7939
--- /dev/null
+++ b/cli/stubs/site.valet.conf
@@ -0,0 +1,40 @@
+# ISOLATED_PHP_VERSION=VALET_ISOLATED_PHP_VERSION
+server {
+ listen 127.0.0.1:80;
+ server_name VALET_SITE www.VALET_SITE *.VALET_SITE;
+ #listen VALET_LOOPBACK:80; # valet loopback
+ root /;
+ charset utf-8;
+ client_max_body_size 128M;
+
+ location /VALET_STATIC_PREFIX/ {
+ internal;
+ alias /;
+ try_files $uri $uri/;
+ }
+
+ location / {
+ rewrite ^ "VALET_SERVER_PATH" last;
+ }
+
+ location = /favicon.ico { access_log off; log_not_found off; }
+ location = /robots.txt { access_log off; log_not_found off; }
+
+ access_log off;
+ error_log "VALET_HOME_PATH/Log/nginx-error.log";
+
+ error_page 404 "VALET_SERVER_PATH";
+
+ location ~ [^/]\.php(/|$) {
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass "unix:VALET_HOME_PATH/VALET_PHP_FPM_SOCKET";
+ fastcgi_index "VALET_SERVER_PATH";
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME "VALET_SERVER_PATH";
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+ }
+
+ location ~ /\.ht {
+ deny all;
+ }
+}
diff --git a/cli/valet.php b/cli/valet.php
index 2477a8099..e009188e3 100755
--- a/cli/valet.php
+++ b/cli/valet.php
@@ -502,35 +502,54 @@
]);
/**
- * Allow the user to change the version of php valet uses.
+ * Allow the user to change the version of php Valet uses.
*/
$app->command('use [phpVersion] [--force]', function ($phpVersion, $force) {
if (! $phpVersion) {
$path = getcwd().'/.valetphprc';
$linkedVersion = Brew::linkedPhp();
if (! file_exists($path)) {
- return info(sprintf('Valet is using %s.', $linkedVersion));
+ return info("Valet is using {$linkedVersion}.");
}
$phpVersion = trim(file_get_contents($path));
- info('Found \''.$path.'\' specifying version: '.$phpVersion);
+ info("Found '{$path}' specifying version: {$phpVersion}");
if ($linkedVersion == $phpVersion) {
- return info(sprintf('Valet is already using %s.', $linkedVersion));
+ return info("Valet is already using {$linkedVersion}.");
}
}
- PhpFpm::validateRequestedVersion($phpVersion);
-
- $newVersion = PhpFpm::useVersion($phpVersion, $force);
+ PhpFpm::useVersion($phpVersion, $force);
+ })->descriptions('Change the version of PHP used by Valet', [
+ 'phpVersion' => 'The PHP version you want to use, e.g php@7.3',
+ ]);
- Nginx::restart();
- info(sprintf('Valet is now using %s.', $newVersion).PHP_EOL);
- info('Note that you might need to run composer global update if your PHP version change affects the dependencies of global packages required by Composer.');
- })->descriptions('Change the version of PHP used by valet', [
+ /**
+ * Allow the user to change the version of PHP Valet uses to serve the current site.
+ */
+ $app->command('isolate [phpVersion] ', function ($phpVersion) {
+ PhpFpm::isolateDirectory(basename(getcwd()), $phpVersion);
+ })->descriptions('Change the version of PHP used by Valet to serve the current working directory', [
'phpVersion' => 'The PHP version you want to use, e.g php@7.3',
]);
+ /**
+ * Allow the user to un-do specifying the version of PHP Valet uses to serve the current site.
+ */
+ $app->command('unisolate', function () {
+ PhpFpm::unIsolateDirectory(basename(getcwd()));
+ })->descriptions('Stop customizing the version of PHP used by Valet to serve the current working directory');
+
+ /**
+ * List isolated sites.
+ */
+ $app->command('isolated', function () {
+ $sites = PhpFpm::isolatedDirectories();
+
+ table(['Path', 'PHP Version'], $sites->all());
+ })->descriptions('List all sites using isolated versions of PHP.');
+
/**
* Tail log file.
*/
diff --git a/composer.json b/composer.json
index 1c870f32b..2cdc7a1b6 100644
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,7 @@
}
},
"require": {
- "php": "^5.6|^7.0|^8.0",
+ "php": "^7.0|^8.0",
"illuminate/container": "~5.1|^6.0|^7.0|^8.0|^9.0",
"mnapoli/silly": "^1.0",
"symfony/process": "^3.0|^4.0|^5.0|^6.0",
diff --git a/tests/BrewTest.php b/tests/BrewTest.php
index a5f15304d..f2886277f 100644
--- a/tests/BrewTest.php
+++ b/tests/BrewTest.php
@@ -93,24 +93,16 @@ public function test_has_installed_php_indicates_if_php_is_installed_via_brew()
$brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]);
$brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@7.0']));
$this->assertTrue($brew->hasInstalledPhp());
-
- $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]);
- $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@5.6']));
- $this->assertTrue($brew->hasInstalledPhp());
-
- $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]);
- $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php56']));
- $this->assertTrue($brew->hasInstalledPhp());
}
public function test_tap_taps_the_given_homebrew_repository()
{
$cli = Mockery::mock(CommandLine::class);
+ $cli->shouldReceive('passthru')->once()->with('sudo -u "'.user().'" brew tap php@8.0');
$cli->shouldReceive('passthru')->once()->with('sudo -u "'.user().'" brew tap php@7.1');
$cli->shouldReceive('passthru')->once()->with('sudo -u "'.user().'" brew tap php@7.0');
- $cli->shouldReceive('passthru')->once()->with('sudo -u "'.user().'" brew tap php@5.6');
swap(CommandLine::class, $cli);
- resolve(Brew::class)->tap('php@7.1', 'php@7.0', 'php@5.6');
+ resolve(Brew::class)->tap('php@8.0', 'php@7.1', 'php@7.0');
}
public function test_restart_restarts_the_service_using_homebrew_services()
@@ -162,10 +154,6 @@ public function test_linked_php_returns_linked_php_formula_name()
$files = Mockery::mock(Filesystem::class);
$files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php72/7.2.9_2/test');
$this->assertSame('php@7.2', $getBrewMock($files)->linkedPhp());
-
- $files = Mockery::mock(Filesystem::class);
- $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php56/test');
- $this->assertSame('php@5.6', $getBrewMock($files)->linkedPhp());
}
public function test_linked_php_throws_exception_if_no_php_link()
@@ -460,15 +448,15 @@ public function supportedPhpLinkPathProvider()
'php74',
],
[
- '/test/path/php56/test',
+ '/test/path/php71/test',
[
- 'path/php56/test',
+ 'path/php71/test',
'php',
- '56',
+ '71',
'',
'',
],
- 'php56',
+ 'php71',
],
];
}
diff --git a/tests/NginxTest.php b/tests/NginxTest.php
index e2f780511..f05c39580 100644
--- a/tests/NginxTest.php
+++ b/tests/NginxTest.php
@@ -86,4 +86,26 @@ public function test_install_nginx_directories_rewrites_secure_nginx_files()
$site->shouldHaveReceived('resecureForNewConfiguration', [$data, $data]);
}
+
+ public function test_it_gets_configured_sites()
+ {
+ $files = Mockery::mock(Filesystem::class);
+
+ $files->shouldReceive('scandir')
+ ->once()
+ ->with(VALET_HOME_PATH.'/Nginx')
+ ->andReturn(['.gitkeep', 'isolated-site-71.test', 'isolated-site-72.test', 'isolated-site-73.test']);
+
+ swap(Filesystem::class, $files);
+ swap(Configuration::class, $config = Mockery::spy(Configuration::class, ['read' => ['tld' => 'test', 'loopback' => VALET_LOOPBACK]]));
+ swap(Site::class, Mockery::mock(Site::class));
+
+ $nginx = resolve(Nginx::class);
+ $output = $nginx->configuredSites();
+
+ $this->assertEquals(
+ ['isolated-site-71.test', 'isolated-site-72.test', 'isolated-site-73.test'],
+ $output->values()->all()
+ );
+ }
}
diff --git a/tests/PhpFpmTest.php b/tests/PhpFpmTest.php
index 4bd59e2e5..68f807473 100644
--- a/tests/PhpFpmTest.php
+++ b/tests/PhpFpmTest.php
@@ -3,9 +3,12 @@
use Illuminate\Container\Container;
use Valet\Brew;
use Valet\CommandLine;
+use Valet\Configuration;
use Valet\Filesystem;
+use Valet\Nginx;
use Valet\PhpFpm;
use function Valet\resolve;
+use Valet\Site;
use function Valet\swap;
use function Valet\user;
@@ -32,11 +35,174 @@ public function test_fpm_is_configured_with_the_correct_user_group_and_port()
copy(__DIR__.'/files/fpm.conf', __DIR__.'/output/fpm.conf');
mkdir(__DIR__.'/output/conf.d');
copy(__DIR__.'/files/php-memory-limits.ini', __DIR__.'/output/conf.d/php-memory-limits.ini');
- resolve(StubForUpdatingFpmConfigFiles::class)->updateConfiguration();
+
+ resolve(StubForUpdatingFpmConfigFiles::class)->createConfigurationFiles('php@7.2');
$contents = file_get_contents(__DIR__.'/output/fpm.conf');
$this->assertStringContainsString(sprintf("\nuser = %s", user()), $contents);
$this->assertStringContainsString("\ngroup = staff", $contents);
- $this->assertStringContainsString("\nlisten = ".VALET_HOME_PATH.'/valet.sock', $contents);
+ $this->assertStringContainsString("\nlisten = ".VALET_HOME_PATH.'/valet72.sock', $contents);
+ }
+
+ public function test_it_can_generate_sock_file_name_from_php_version()
+ {
+ $this->assertEquals('valet72.sock', resolve(PhpFpm::class)->fpmSockName('php@7.2'));
+ $this->assertEquals('valet72.sock', resolve(PhpFpm::class)->fpmSockName('php@72'));
+ $this->assertEquals('valet72.sock', resolve(PhpFpm::class)->fpmSockName('php72'));
+ $this->assertEquals('valet72.sock', resolve(PhpFpm::class)->fpmSockName('72'));
+ }
+
+ public function test_it_normalizes_php_versions()
+ {
+ $this->assertEquals('php@8.1', resolve(PhpFpm::class)->normalizePhpVersion('php@8.1'));
+ $this->assertEquals('php@8.1', resolve(PhpFpm::class)->normalizePhpVersion('php8.1'));
+ $this->assertEquals('php@8.1', resolve(PhpFpm::class)->normalizePhpVersion('php81'));
+ $this->assertEquals('php@8.1', resolve(PhpFpm::class)->normalizePhpVersion('8.1'));
+ $this->assertEquals('php@8.1', resolve(PhpFpm::class)->normalizePhpVersion('81'));
+ }
+
+ public function test_utilized_php_versions()
+ {
+ $fileSystemMock = Mockery::mock(Filesystem::class);
+ $brewMock = Mockery::mock(Brew::class);
+ $nginxMock = Mockery::mock(Nginx::class);
+
+ $phpFpmMock = Mockery::mock(PhpFpm::class, [
+ $brewMock,
+ Mockery::mock(CommandLine::class),
+ $fileSystemMock,
+ resolve(Configuration::class),
+ Mockery::mock(Site::class),
+ $nginxMock,
+ ])->makePartial();
+
+ swap(PhpFpm::class, $phpFpmMock);
+
+ $brewMock->shouldReceive('supportedPhpVersions')->andReturn(collect([
+ 'php@7.1',
+ 'php@7.2',
+ 'php@7.3',
+ 'php@7.4',
+ ]));
+
+ $brewMock->shouldReceive('getLinkedPhpFormula')->andReturn('php@7.3');
+
+ $nginxMock->shouldReceive('configuredSites')
+ ->once()
+ ->andReturn(collect(['isolated-site-71.test', 'isolated-site-72.test', 'isolated-site-73.test']));
+
+ $sites = [
+ [
+ 'site' => 'isolated-site-71.test',
+ 'conf' => '# '.ISOLATED_PHP_VERSION.'=71'.PHP_EOL.'valet71.sock',
+ ],
+ [
+ 'site' => 'isolated-site-72.test',
+ 'conf' => '# '.ISOLATED_PHP_VERSION.'=php@7.2'.PHP_EOL.'valet72.sock',
+ ],
+ [
+ 'site' => 'isolated-site-73.test',
+ 'conf' => '# '.ISOLATED_PHP_VERSION.'=73'.PHP_EOL.'valet.sock',
+ ],
+ ];
+
+ foreach ($sites as $site) {
+ $fileSystemMock->shouldReceive('get')->once()->with(VALET_HOME_PATH.'/Nginx/'.$site['site'])->andReturn($site['conf']);
+ }
+
+ $this->assertEquals(['php@7.1', 'php@7.2', 'php@7.3'], resolve(PhpFpm::class)->utilizedPhpVersions());
+ }
+
+ public function test_it_lists_isolated_directories()
+ {
+ $fileSystemMock = Mockery::mock(Filesystem::class);
+ $nginxMock = Mockery::mock(Nginx::class);
+ $site = Mockery::mock(Site::class);
+
+ $phpFpmMock = Mockery::mock(PhpFpm::class, [
+ Mockery::mock(Brew::class),
+ Mockery::mock(CommandLine::class),
+ $fileSystemMock,
+ resolve(Configuration::class),
+ $site,
+ $nginxMock,
+ ])->makePartial();
+
+ swap(PhpFpm::class, $phpFpmMock);
+
+ $nginxMock->shouldReceive('configuredSites')
+ ->once()
+ ->andReturn(collect(['isolated-site-71.test', 'isolated-site-72.test', 'not-isolated-site.test']));
+
+ $site->shouldReceive('customPhpVersion')->with('isolated-site-71.test')->andReturn('71');
+ $site->shouldReceive('customPhpVersion')->with('isolated-site-72.test')->andReturn('72');
+ $site->shouldReceive('normalizePhpVersion')->with('71')->andReturn('php@7.1');
+ $site->shouldReceive('normalizePhpVersion')->with('72')->andReturn('php@7.2');
+
+ $sites = [
+ [
+ 'site' => 'isolated-site-71.test',
+ 'conf' => '# '.ISOLATED_PHP_VERSION.'=71'.PHP_EOL.'valet71.sock',
+ ],
+ [
+ 'site' => 'isolated-site-72.test',
+ 'conf' => '# '.ISOLATED_PHP_VERSION.'=php@7.2'.PHP_EOL.'valet72.sock',
+ ],
+ [
+ 'site' => 'not-isolated-site.test',
+ 'conf' => 'This one is not isolated',
+ ],
+ ];
+
+ foreach ($sites as $site) {
+ $fileSystemMock->shouldReceive('get')->once()->with(VALET_HOME_PATH.'/Nginx/'.$site['site'])->andReturn($site['conf']);
+ }
+
+ $this->assertEquals([
+ [
+ 'url' => 'isolated-site-71.test',
+ 'version' => 'php@7.1',
+ ],
+ [
+ 'url' => 'isolated-site-72.test',
+ 'version' => 'php@7.2',
+ ],
+ ], resolve(PhpFpm::class)->isolatedDirectories()->toArray());
+ }
+
+ public function test_stop_unused_php_versions()
+ {
+ $brewMock = Mockery::mock(Brew::class);
+
+ $phpFpmMock = Mockery::mock(PhpFpm::class, [
+ $brewMock,
+ Mockery::mock(CommandLine::class),
+ Mockery::mock(Filesystem::class),
+ resolve(Configuration::class),
+ Mockery::mock(Site::class),
+ Mockery::mock(Nginx::class),
+ ])->makePartial();
+
+ swap(PhpFpm::class, $phpFpmMock);
+
+ $phpFpmMock->shouldReceive('utilizedPhpVersions')->andReturn([
+ 'php@7.1',
+ 'php@7.2',
+ ]);
+
+ // Would do nothing
+ resolve(PhpFpm::class)->stopIfUnused(null);
+
+ // This currently-un-used PHP version should be stopped
+ $brewMock->shouldReceive('stopService')->times(3)->with('php@7.3');
+ resolve(PhpFpm::class)->stopIfUnused('73');
+ resolve(PhpFpm::class)->stopIfUnused('php73');
+ resolve(PhpFpm::class)->stopIfUnused('php@7.3');
+
+ // These currently-used PHP versions should not be stopped
+ $brewMock->shouldNotReceive('stopService')->with('php@7.1');
+ $brewMock->shouldNotReceive('stopService')->with('php@7.2');
+ resolve(PhpFpm::class)->stopIfUnused('php@7.1');
+ resolve(PhpFpm::class)->stopIfUnused('php@7.2');
}
public function test_stopRunning_will_pass_filtered_result_of_getRunningServices_to_stopService()
@@ -46,7 +212,7 @@ public function test_stopRunning_will_pass_filtered_result_of_getRunningServices
->andReturn(collect([
'php7.2',
'php@7.3',
- 'php56',
+ 'php71',
'php',
'nginx',
'somethingelse',
@@ -54,7 +220,7 @@ public function test_stopRunning_will_pass_filtered_result_of_getRunningServices
$brewMock->shouldReceive('stopService')->once()->with([
'php7.2',
'php@7.3',
- 'php56',
+ 'php71',
'php',
]);
@@ -65,17 +231,25 @@ public function test_stopRunning_will_pass_filtered_result_of_getRunningServices
public function test_use_version_will_convert_passed_php_version()
{
$brewMock = Mockery::mock(Brew::class);
+ $nginxMock = Mockery::mock(Nginx::class);
+ $siteMock = Mockery::mock(Site::class);
+ $filesystem = Mockery::mock(Filesystem::class);
+ $cli = Mockery::mock(CommandLine::class);
+
$phpFpmMock = Mockery::mock(PhpFpm::class, [
$brewMock,
- resolve(CommandLine::class),
- resolve(Filesystem::class),
+ $cli,
+ $filesystem,
+ resolve(Configuration::class),
+ $siteMock,
+ $nginxMock,
])->makePartial();
$phpFpmMock->shouldReceive('install');
$brewMock->shouldReceive('supportedPhpVersions')->twice()->andReturn(collect([
'php@7.2',
- 'php@5.6',
+ 'php@7.1',
]));
$brewMock->shouldReceive('hasLinkedPhp')->andReturn(false);
$brewMock->shouldReceive('ensureInstalled')->with('php@7.2', [], $phpFpmMock->taps);
@@ -86,6 +260,12 @@ public function test_use_version_will_convert_passed_php_version()
$brewMock->shouldReceive('getAllRunningServices')->andReturn(collect());
$brewMock->shouldReceive('stopService');
+ $nginxMock->shouldReceive('restart');
+
+ $filesystem->shouldReceive('unlink')->with(VALET_HOME_PATH.'/valet.sock');
+
+ $cli->shouldReceive('quietly')->with('sudo rm '.VALET_HOME_PATH.'/valet.sock');
+
// Test both non prefixed and prefixed
$this->assertSame('php@7.2', $phpFpmMock->useVersion('php7.2'));
$this->assertSame('php@7.2', $phpFpmMock->useVersion('php72'));
@@ -109,18 +289,27 @@ public function test_use_version_will_throw_if_version_not_supported()
public function test_use_version_if_already_linked_php_will_unlink_before_installing()
{
$brewMock = Mockery::mock(Brew::class);
+ $nginxMock = Mockery::mock(Nginx::class);
+ $siteMock = Mockery::mock(Site::class);
+
$phpFpmMock = Mockery::mock(PhpFpm::class, [
$brewMock,
resolve(CommandLine::class),
resolve(Filesystem::class),
+ resolve(Configuration::class),
+ $siteMock,
+ $nginxMock,
])->makePartial();
+
$phpFpmMock->shouldReceive('install');
$brewMock->shouldReceive('supportedPhpVersions')->andReturn(collect([
'php@7.2',
- 'php@5.6',
+ 'php@7.1',
]));
+
$brewMock->shouldReceive('hasLinkedPhp')->andReturn(true);
+ $brewMock->shouldReceive('linkedPhp')->andReturn('php@7.1');
$brewMock->shouldReceive('getLinkedPhpFormula')->andReturn('php@7.1');
$brewMock->shouldReceive('unlink')->with('php@7.1');
$brewMock->shouldReceive('ensureInstalled')->with('php@7.2', [], $phpFpmMock->taps);
@@ -131,14 +320,104 @@ public function test_use_version_if_already_linked_php_will_unlink_before_instal
$brewMock->shouldReceive('getAllRunningServices')->andReturn(collect());
$brewMock->shouldReceive('stopService');
- // Test both non prefixed and prefixed
+ $nginxMock->shouldReceive('restart');
+
$this->assertSame('php@7.2', $phpFpmMock->useVersion('php@7.2'));
}
+
+ public function test_isolate_will_isolate_a_site()
+ {
+ $brewMock = Mockery::mock(Brew::class);
+ $nginxMock = Mockery::mock(Nginx::class);
+ $siteMock = Mockery::mock(Site::class);
+
+ $phpFpmMock = Mockery::mock(PhpFpm::class, [
+ $brewMock,
+ resolve(CommandLine::class),
+ resolve(Filesystem::class),
+ resolve(Configuration::class),
+ $siteMock,
+ $nginxMock,
+ ])->makePartial();
+
+ $brewMock->shouldReceive('supportedPhpVersions')->andReturn(collect([
+ 'php@7.2',
+ 'php@7.1',
+ ]));
+
+ $brewMock->shouldReceive('ensureInstalled')->with('php@7.2', [], $phpFpmMock->taps);
+ $brewMock->shouldReceive('installed')->with('php@7.2');
+ $brewMock->shouldReceive('determineAliasedVersion')->with('php@7.2')->andReturn('php@7.2');
+ // $brewMock->shouldReceive('linkedPhp')->once();
+
+ $siteMock->shouldReceive('getSiteUrl')->with('test')->andReturn('test.test');
+ $siteMock->shouldReceive('isolate')->withArgs(['test.test', 'php@7.2']);
+ $siteMock->shouldReceive('customPhpVersion')->with('test.test')->andReturn('72');
+
+ $phpFpmMock->shouldReceive('stopIfUnused')->with('72')->once();
+ $phpFpmMock->shouldReceive('createConfigurationFiles')->with('php@7.2')->once();
+ $phpFpmMock->shouldReceive('restart')->with('php@7.2')->once();
+
+ $nginxMock->shouldReceive('restart');
+
+ // These should only run when doing global PHP switches
+ $brewMock->shouldNotReceive('stopService');
+ $brewMock->shouldNotReceive('link');
+ $brewMock->shouldNotReceive('unlink');
+ $phpFpmMock->shouldNotReceive('stopRunning');
+ $phpFpmMock->shouldNotReceive('install');
+
+ $this->assertSame(null, $phpFpmMock->isolateDirectory('test', 'php@7.2'));
+ }
+
+ public function test_un_isolate_will_remove_isolation_for_a_site()
+ {
+ $nginxMock = Mockery::mock(Nginx::class);
+ $siteMock = Mockery::mock(Site::class);
+
+ $phpFpmMock = Mockery::mock(PhpFpm::class, [
+ Mockery::mock(Brew::class),
+ resolve(CommandLine::class),
+ resolve(Filesystem::class),
+ resolve(Configuration::class),
+ $siteMock,
+ $nginxMock,
+ ])->makePartial();
+
+ $siteMock->shouldReceive('getSiteUrl')->with('test')->andReturn('test.test');
+ $siteMock->shouldReceive('customPhpVersion')->with('test.test')->andReturn('74');
+ $siteMock->shouldReceive('removeIsolation')->with('test.test')->once();
+ $phpFpmMock->shouldReceive('stopIfUnused')->with('74');
+ $nginxMock->shouldReceive('restart');
+
+ $this->assertSame(null, $phpFpmMock->unIsolateDirectory('test'));
+ }
+
+ public function test_isolate_will_throw_if_site_is_not_parked_or_linked()
+ {
+ $siteMock = Mockery::mock(Site::class);
+
+ $phpFpmMock = Mockery::mock(PhpFpm::class, [
+ Mockery::mock(Brew::class),
+ resolve(CommandLine::class),
+ resolve(Filesystem::class),
+ resolve(Configuration::class),
+ $siteMock,
+ Mockery::mock(Nginx::class),
+ ])->makePartial();
+
+ $this->expectException(DomainException::class);
+ $this->expectExceptionMessage("The [test] site could not be found in Valet's site list.");
+
+ $siteMock->shouldReceive('getSiteUrl');
+
+ $this->assertSame(null, $phpFpmMock->isolateDirectory('test', 'php@8.1'));
+ }
}
class StubForUpdatingFpmConfigFiles extends PhpFpm
{
- public function fpmConfigPath()
+ public function fpmConfigPath($phpVersion = null)
{
return __DIR__.'/output/fpm.conf';
}
diff --git a/tests/SiteTest.php b/tests/SiteTest.php
index 0d09c4b8b..e8e03d41e 100644
--- a/tests/SiteTest.php
+++ b/tests/SiteTest.php
@@ -527,6 +527,261 @@ public function test_remove_proxy()
$this->assertEquals([], $site->proxies()->all());
}
+ public function test_gets_site_url_from_directory()
+ {
+ $config = Mockery::mock(Configuration::class);
+
+ swap(Configuration::class, $config);
+
+ $siteMock = Mockery::mock(Site::class, [
+ resolve(Configuration::class),
+ resolve(CommandLine::class),
+ resolve(Filesystem::class),
+ ])->makePartial();
+
+ swap(Site::class, $siteMock);
+
+ $config->shouldReceive('read')
+ ->andReturn(['tld' => 'test', 'loopback' => VALET_LOOPBACK, 'paths' => []]);
+
+ $siteMock->shouldReceive('parked')
+ ->andReturn(collect([
+ 'site1' => [
+ 'site' => 'site1',
+ 'secured' => '',
+ 'url' => 'http://site1.test',
+ 'path' => '/Users/name/code/site1',
+ ],
+ ]));
+
+ $siteMock->shouldReceive('links')->andReturn(collect([
+ 'site2' => [
+ 'site' => 'site2',
+ 'secured' => 'X',
+ 'url' => 'http://site2.test',
+ 'path' => '/Users/name/code/site2',
+ ],
+ ]));
+
+ $siteMock->shouldReceive('host')->andReturn('site1');
+
+ $site = resolve(Site::class);
+
+ $this->assertEquals('site1.test', $site->getSiteUrl('.'));
+ $this->assertEquals('site1.test', $site->getSiteUrl('./'));
+
+ $this->assertEquals('site1.test', $site->getSiteUrl('site1'));
+ $this->assertEquals('site1.test', $site->getSiteUrl('site1.test'));
+
+ $this->assertEquals('site2.test', $site->getSiteUrl('site2'));
+ $this->assertEquals('site2.test', $site->getSiteUrl('site2.test'));
+
+ $this->assertEquals(false, $site->getSiteUrl('site3'));
+ $this->assertEquals(false, $site->getSiteUrl('site3.test'));
+ }
+
+ public function test_isolation_will_persist_when_adding_ssl_certificate()
+ {
+ $files = Mockery::mock(Filesystem::class);
+ $config = Mockery::mock(Configuration::class);
+
+ $siteMock = Mockery::mock(Site::class, [
+ $config,
+ Mockery::mock(CommandLine::class),
+ $files,
+ ])->makePartial();
+
+ swap(Site::class, $siteMock);
+
+ $siteMock->shouldReceive('unsecure');
+ $files->shouldReceive('ensureDirExists');
+ $files->shouldReceive('putAsUser');
+ $siteMock->shouldReceive('createCa');
+ $siteMock->shouldReceive('createCertificate');
+ $siteMock->shouldReceive('buildSecureNginxServer');
+
+ // If site has an isolated PHP version for the site, it would replace .sock file
+ $siteMock->shouldReceive('customPhpVersion')->with('site1.test')->andReturn('73')->once();
+ $siteMock->shouldReceive('replaceSockFile')->withArgs([Mockery::any(), '73'])->once();
+ resolve(Site::class)->secure('site1.test');
+
+ // For sites without an isolated PHP version, nothing should be replaced
+ $siteMock->shouldReceive('customPhpVersion')->with('site2.test')->andReturn(null)->once();
+ $siteMock->shouldNotReceive('replaceSockFile');
+ resolve(Site::class)->secure('site2.test');
+ }
+
+ public function test_isolation_will_persist_when_removing_ssl_certificate()
+ {
+ $files = Mockery::mock(Filesystem::class);
+ $config = Mockery::mock(Configuration::class);
+ $cli = Mockery::mock(CommandLine::class);
+
+ $siteMock = Mockery::mock(Site::class, [
+ $config,
+ $cli,
+ $files,
+ ])->makePartial();
+
+ swap(Site::class, $siteMock);
+
+ $cli->shouldReceive('run');
+ $files->shouldReceive('exists')->andReturn(false);
+
+ // If a site has an isolated PHP version, there should still be a custom nginx site config
+ $siteMock->shouldReceive('customPhpVersion')->with('site1.test')->andReturn('73')->once();
+ $siteMock->shouldReceive('isolate')->withArgs(['site1.test', '73'])->once();
+ resolve(Site::class)->unsecure('site1.test');
+
+ // If a site doesn't have an isolated PHP version, there should no longer be a custom nginx site config
+ $siteMock->shouldReceive('customPhpVersion')->with('site2.test')->andReturn(null)->once();
+ $siteMock->shouldNotReceive('isolate');
+ resolve(Site::class)->unsecure('site2.test');
+ }
+
+ public function test_can_install_nginx_site_config_for_specific_php_version()
+ {
+ $files = Mockery::mock(Filesystem::class);
+ $config = Mockery::mock(Configuration::class);
+
+ $siteMock = Mockery::mock(Site::class, [
+ $config,
+ resolve(CommandLine::class),
+ $files,
+ ])->makePartial();
+
+ $config->shouldReceive('read')
+ ->andReturn(['tld' => 'test', 'loopback' => VALET_LOOPBACK]);
+
+ // If Nginx config exists for the site, modify exising config
+ $files->shouldReceive('exists')->once()->with($siteMock->nginxPath('site1.test'))->andReturn(true);
+
+ $files->shouldReceive('get')
+ ->once()
+ ->with($siteMock->nginxPath('site1.test'))
+ ->andReturn('# '.ISOLATED_PHP_VERSION.'=php@7.4'.PHP_EOL.'server { fastcgi_pass: valet74.sock }');
+
+ $files->shouldReceive('putAsUser')
+ ->once()
+ ->withArgs([
+ $siteMock->nginxPath('site1.test'),
+ '# '.ISOLATED_PHP_VERSION.'=php@8.0'.PHP_EOL.'server { fastcgi_pass: valet80.sock }',
+ ]);
+
+ $siteMock->isolate('site1.test', 'php@8.0');
+
+ // When no Nginx file exists, it will create a new config file from the template
+ $files->shouldReceive('exists')->once()->with($siteMock->nginxPath('site2.test'))->andReturn(false);
+ $files->shouldReceive('get')
+ ->once()
+ ->with(dirname(__DIR__).'/cli/Valet/../stubs/site.valet.conf')
+ ->andReturn(file_get_contents(__DIR__.'/../cli/stubs/site.valet.conf'));
+
+ $files->shouldReceive('putAsUser')
+ ->once()
+ ->withArgs([
+ $siteMock->nginxPath('site2.test'),
+ Mockery::on(function ($argument) {
+ return preg_match('/^# '.ISOLATED_PHP_VERSION.'=php@8.0/', $argument)
+ && preg_match('#fastcgi_pass "unix:.*/valet80.sock#', $argument)
+ && strpos($argument, 'server_name site2.test www.site2.test *.site2.test;') !== false;
+ }),
+ ]);
+
+ $siteMock->isolate('site2.test', 'php@8.0');
+ }
+
+ public function test_it_removes_isolation()
+ {
+ $files = Mockery::mock(Filesystem::class);
+
+ $siteMock = Mockery::mock(Site::class, [
+ resolve(Configuration::class),
+ resolve(CommandLine::class),
+ $files,
+ ])->makePartial();
+
+ swap(Site::class, $siteMock);
+
+ // SSL Site
+ $files->shouldReceive('exists')->once()->with($siteMock->certificatesPath('site1.test', 'crt'))->andReturn(true);
+ $files->shouldReceive('putAsUser')->withArgs([$siteMock->nginxPath('site1.test'), Mockery::any()])->once();
+ $siteMock->shouldReceive('buildSecureNginxServer')->once()->with('site1.test');
+ resolve(Site::class)->removeIsolation('site1.test');
+
+ // Non-SSL Site
+ $files->shouldReceive('exists')->once()->with($siteMock->certificatesPath('site2.test', 'crt'))->andReturn(false);
+ $files->shouldReceive('unlink')->with($siteMock->nginxPath('site2.test'))->once();
+ $siteMock->shouldNotReceive('buildSecureNginxServer')->with('site2.test');
+ resolve(Site::class)->removeIsolation('site2.test');
+ }
+
+ public function test_retrieves_custom_php_version_from_nginx_config()
+ {
+ $files = Mockery::mock(Filesystem::class);
+
+ $siteMock = Mockery::mock(Site::class, [
+ resolve(Configuration::class),
+ resolve(CommandLine::class),
+ $files,
+ ])->makePartial();
+
+ swap(Site::class, $siteMock);
+
+ // Site with isolated PHP version
+ $files->shouldReceive('exists')->once()->with($siteMock->nginxPath('site1.test'))->andReturn(true);
+ $files->shouldReceive('get')
+ ->once()
+ ->with($siteMock->nginxPath('site1.test'))
+ ->andReturn('# '.ISOLATED_PHP_VERSION.'=php@7.4');
+ $this->assertEquals('74', resolve(Site::class)->customPhpVersion('site1.test'));
+
+ // Site without any custom nginx config
+ $files->shouldReceive('exists')->once()->with($siteMock->nginxPath('site2.test'))->andReturn(false);
+ $files->shouldNotReceive('get')->with($siteMock->nginxPath('site2.test'));
+ $this->assertEquals(null, resolve(Site::class)->customPhpVersion('site2.test'));
+
+ // Site with a custom nginx config, but doesn't have the header
+ $files->shouldReceive('exists')->once()->with($siteMock->nginxPath('site3.test'))->andReturn(true);
+ $files->shouldReceive('get')
+ ->once()
+ ->with($siteMock->nginxPath('site3.test'))
+ ->andReturn('server { }');
+ $this->assertEquals(null, resolve(Site::class)->customPhpVersion('site3.test'));
+ }
+
+ public function test_replace_sock_file_in_nginx_config()
+ {
+ $site = resolve(Site::class);
+
+ // When switching to php71, valet71.sock should be replaced with valet.sock;
+ // isolation header should be prepended
+ $this->assertEquals(
+ '# '.ISOLATED_PHP_VERSION.'=71'.PHP_EOL.'server { fastcgi_pass: valet71.sock }',
+ $site->replaceSockFile('server { fastcgi_pass: valet71.sock }', '71')
+ );
+
+ // When switching to php72, valet.sock should be replaced with valet72.sock
+ $this->assertEquals(
+ '# '.ISOLATED_PHP_VERSION.'=72'.PHP_EOL.'server { fastcgi_pass: valet72.sock }',
+ $site->replaceSockFile('server { fastcgi_pass: valet.sock }', '72')
+ );
+
+ // When switching to php73 from php72, valet72.sock should be replaced with valet73.sock;
+ // isolation header should be updated to php@7.3
+ $this->assertEquals(
+ '# '.ISOLATED_PHP_VERSION.'=73'.PHP_EOL.'server { fastcgi_pass: valet73.sock }',
+ $site->replaceSockFile('# '.ISOLATED_PHP_VERSION.'=72'.PHP_EOL.'server { fastcgi_pass: valet72.sock }', '73')
+ );
+
+ // When switching to php72 from php74, valet72.sock should be replaced with valet74.sock;
+ // isolation header should be updated to php@7.4
+ $this->assertEquals(
+ '# '.ISOLATED_PHP_VERSION.'=php@7.4'.PHP_EOL.'server { fastcgi_pass: valet74.sock }',
+ $site->replaceSockFile('# '.ISOLATED_PHP_VERSION.'=72'.PHP_EOL.'server { fastcgi_pass: valet.sock }', 'php@7.4')
+ );
+ }
+
public function test_it_returns_secured_sites()
{
$files = Mockery::mock(Filesystem::class);