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);