diff --git a/.gitattributes b/.gitattributes index 48773144d..304656f5f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,5 +16,3 @@ phpunit.xml export-ignore CHANGELOG.md export-ignore phpunit.xml.dist export-ignore UPGRADE.md export-ignore - -/bin/ngrok -diff diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 790615b50..3f82dffdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.2, 7.3, 7.4, '8.0', 8.1, 8.2] + php: ['8.0', 8.1, 8.2] name: PHP ${{ matrix.php }} diff --git a/bin/ngrok b/bin/ngrok deleted file mode 100755 index dbb675eed..000000000 Binary files a/bin/ngrok and /dev/null differ diff --git a/cli/Valet/Brew.php b/cli/Valet/Brew.php index eab55efd6..2acf68b9f 100644 --- a/cli/Valet/Brew.php +++ b/cli/Valet/Brew.php @@ -3,6 +3,7 @@ namespace Valet; use DomainException; +use Illuminate\Support\Collection; use PhpFpm; class Brew @@ -16,58 +17,45 @@ class Brew 'php@7.3', 'php@7.2', 'php@7.1', - 'php@7.0', - 'php73', - 'php72', - 'php71', - 'php70', ]; const BREW_DISABLE_AUTO_CLEANUP = 'HOMEBREW_NO_INSTALL_CLEANUP=1'; const LATEST_PHP_VERSION = 'php@8.1'; - public $cli; - - public $files; - - /** - * Create a new Brew instance. - * - * @return void - */ - public function __construct(CommandLine $cli, Filesystem $files) + public function __construct(public CommandLine $cli, public Filesystem $files) { - $this->cli = $cli; - $this->files = $files; } /** * Ensure the formula exists in the current Homebrew configuration. - * - * @param string $formula - * @return bool */ - public function installed($formula) + public function installed(string $formula): bool { - $result = $this->cli->runAsUser("brew info $formula --json"); + $result = $this->cli->runAsUser("brew info $formula --json=v2"); // should be a json response, but if not installed then "Error: No available formula ..." if (starts_with($result, 'Error: No')) { return false; } - $details = json_decode($result); + $details = json_decode($result, true); + + if (! empty($details['formulae'])) { + return ! empty($details['formulae'][0]['installed']); + } + + if (! empty($details['casks'])) { + return ! is_null($details['casks'][0]['installed']); + } - return ! empty($details[0]->installed); + return false; } /** * Determine if a compatible PHP version is Homebrewed. - * - * @return bool */ - public function hasInstalledPhp() + public function hasInstalledPhp(): bool { $installed = $this->installedPhpFormulae()->first(function ($formula) { return $this->supportedPhpVersions()->contains($formula); @@ -78,15 +66,16 @@ public function hasInstalledPhp() /** * Get a list of supported PHP versions. - * - * @return \Illuminate\Support\Collection */ - public function supportedPhpVersions() + public function supportedPhpVersions(): Collection { return collect(static::SUPPORTED_PHP_VERSIONS); } - public function installedPhpFormulae() + /** + * Get a list of installed PHP formulae. + */ + public function installedPhpFormulae(): Collection { return collect( explode(PHP_EOL, $this->cli->runAsUser('brew list --formula | grep php')) @@ -96,7 +85,7 @@ public function installedPhpFormulae() /** * Get the aliased formula version from Homebrew. */ - public function determineAliasedVersion($formula) + public function determineAliasedVersion($formula): string { $details = json_decode($this->cli->runAsUser("brew info $formula --json")); @@ -109,10 +98,8 @@ public function determineAliasedVersion($formula) /** * Determine if a compatible nginx version is Homebrewed. - * - * @return bool */ - public function hasInstalledNginx() + public function hasInstalledNginx(): bool { return $this->installed('nginx') || $this->installed('nginx-full'); @@ -120,23 +107,16 @@ public function hasInstalledNginx() /** * Return name of the nginx service installed via Homebrew. - * - * @return string */ - public function nginxServiceName() + public function nginxServiceName(): string { return $this->installed('nginx-full') ? 'nginx-full' : 'nginx'; } /** * Ensure that the given formula is installed. - * - * @param string $formula - * @param array $options - * @param array $taps - * @return void */ - public function ensureInstalled($formula, $options = [], $taps = []) + public function ensureInstalled(string $formula, array $options = [], array $taps = []): void { if (! $this->installed($formula)) { $this->installOrFail($formula, $options, $taps); @@ -145,13 +125,8 @@ public function ensureInstalled($formula, $options = [], $taps = []) /** * Install the given formula and throw an exception on failure. - * - * @param string $formula - * @param array $options - * @param array $taps - * @return void */ - public function installOrFail($formula, $options = [], $taps = []) + public function installOrFail(string $formula, array $options = [], array $taps = []): void { info("Installing {$formula}..."); @@ -173,11 +148,8 @@ public function installOrFail($formula, $options = [], $taps = []) /** * Tap the given formulas. - * - * @param dynamic[string] $formula - * @return void */ - public function tap($formulas) + public function tap($formulas): void { $formulas = is_array($formulas) ? $formulas : func_get_args(); @@ -189,7 +161,7 @@ public function tap($formulas) /** * Restart the given Homebrew services. */ - public function restartService($services) + public function restartService($services): void { $services = is_array($services) ? $services : func_get_args(); @@ -210,7 +182,7 @@ public function restartService($services) /** * Stop the given Homebrew services. */ - public function stopService($services) + public function stopService($services): void { $services = is_array($services) ? $services : func_get_args(); @@ -242,20 +214,16 @@ public function stopService($services) /** * Determine if php is currently linked. - * - * @return bool */ - public function hasLinkedPhp() + public function hasLinkedPhp(): bool { return $this->files->isLink(BREW_PREFIX.'/bin/php'); } /** * Get the linked php parsed. - * - * @return mixed */ - public function getParsedLinkedPhp() + public function getParsedLinkedPhp(): array { if (! $this->hasLinkedPhp()) { throw new DomainException('Homebrew PHP appears not to be linked. Please run [valet use php@X.Y]'); @@ -270,10 +238,8 @@ public function getParsedLinkedPhp() * Gets the currently linked formula by identifying the symlink in the hombrew bin directory. * Different to ->linkedPhp() in that this will just get the linked directory name, * whether that is php, php74 or php@7.4. - * - * @return string */ - public function getLinkedPhpFormula() + public function getLinkedPhpFormula(): string { $matches = $this->getParsedLinkedPhp(); @@ -282,10 +248,8 @@ public function getLinkedPhpFormula() /** * Determine which version of PHP is linked in Homebrew. - * - * @return string */ - public function linkedPhp() + public function linkedPhp(): string { $matches = $this->getParsedLinkedPhp(); $resolvedPhpVersion = $matches[3] ?: $matches[2]; @@ -301,10 +265,9 @@ function ($version) use ($resolvedPhpVersion) { /** * Extract PHP executable path from PHP Version. * - * @param string $phpVersion For example, "php@8.1" - * @return string + * @param string|null $phpVersion For example, "php@8.1" */ - public function getPhpExecutablePath($phpVersion = null) + public function getPhpExecutablePath(?string $phpVersion = null): string { if (! $phpVersion) { return BREW_PREFIX.'/bin/php'; @@ -339,20 +302,16 @@ public function getPhpExecutablePath($phpVersion = null) /** * Restart the linked PHP-FPM Homebrew service. - * - * @return void */ - public function restartLinkedPhp() + public function restartLinkedPhp(): void { $this->restartService($this->getLinkedPhpFormula()); } /** * Create the "sudoers.d" entry for running Brew. - * - * @return void */ - public function createSudoersEntry() + public function createSudoersEntry(): void { $this->files->ensureDirExists('/etc/sudoers.d'); @@ -362,21 +321,16 @@ public function createSudoersEntry() /** * Remove the "sudoers.d" entry for running Brew. - * - * @return void */ - public function removeSudoersEntry() + public function removeSudoersEntry(): void { $this->cli->quietly('rm /etc/sudoers.d/brew'); } /** * Link passed formula. - * - * @param bool $force - * @return string */ - public function link($formula, $force = false) + public function link(string $formula, bool $force = false): string { return $this->cli->runAsUser( sprintf('brew link %s%s', $formula, $force ? ' --force' : ''), @@ -390,10 +344,8 @@ function ($exitCode, $errorOutput) use ($formula) { /** * Unlink passed formula. - * - * @return string */ - public function unlink($formula) + public function unlink(string $formula): string { return $this->cli->runAsUser( sprintf('brew unlink %s', $formula), @@ -407,10 +359,8 @@ function ($exitCode, $errorOutput) use ($formula) { /** * Get all the currently running brew services. - * - * @return \Illuminate\Support\Collection */ - public function getAllRunningServices() + public function getAllRunningServices(): Collection { return $this->getRunningServicesAsRoot() ->concat($this->getRunningServicesAsUser()) @@ -420,10 +370,8 @@ public function getAllRunningServices() /** * Get the currently running brew services as root. * i.e. /Library/LaunchDaemons (started at boot). - * - * @return \Illuminate\Support\Collection */ - public function getRunningServicesAsRoot() + public function getRunningServicesAsRoot(): Collection { return $this->getRunningServices(); } @@ -431,21 +379,16 @@ public function getRunningServicesAsRoot() /** * Get the currently running brew services. * i.e. ~/Library/LaunchAgents (started at login). - * - * @return \Illuminate\Support\Collection */ - public function getRunningServicesAsUser() + public function getRunningServicesAsUser(): Collection { return $this->getRunningServices(true); } /** * Get the currently running brew services. - * - * @param bool $asUser - * @return \Illuminate\Support\Collection */ - public function getRunningServices($asUser = false) + public function getRunningServices(bool $asUser = false): Collection { $command = 'brew services list | grep started | awk \'{ print $1; }\''; $onError = function ($exitCode, $errorOutput) { @@ -462,10 +405,8 @@ public function getRunningServices($asUser = false) /** * Tell Homebrew to forcefully remove all PHP versions that Valet supports. - * - * @return string */ - public function uninstallAllPhpVersions() + public function uninstallAllPhpVersions(): string { $this->supportedPhpVersions()->each(function ($formula) { $this->uninstallFormula($formula); @@ -476,11 +417,8 @@ public function uninstallAllPhpVersions() /** * Uninstall a Homebrew app by formula name. - * - * @param string $formula - * @return void */ - public function uninstallFormula($formula) + public function uninstallFormula(string $formula): void { $this->cli->runAsUser(static::BREW_DISABLE_AUTO_CLEANUP.' brew uninstall --force '.$formula); $this->cli->run('rm -rf '.BREW_PREFIX.'/Cellar/'.$formula); @@ -488,10 +426,8 @@ public function uninstallFormula($formula) /** * Run Homebrew's cleanup commands. - * - * @return string */ - public function cleanupBrew() + public function cleanupBrew(): string { return $this->cli->runAsUser( 'brew cleanup && brew services cleanup', @@ -503,11 +439,8 @@ function ($exitCode, $errorOutput) { /** * Parse homebrew PHP Path. - * - * @param string $resolvedPath - * @return array */ - public function parsePhpPath($resolvedPath) + public function parsePhpPath(string $resolvedPath): array { /** * Typical homebrew path resolutions are like: @@ -523,12 +456,8 @@ public function parsePhpPath($resolvedPath) /** * Check if two PHP versions are equal. - * - * @param string $versionA - * @param string $versionB - * @return bool */ - public function arePhpVersionsEqual($versionA, $versionB) + public function arePhpVersionsEqual(string $versionA, string $versionB): bool { $versionANormalized = preg_replace('/[^\d]/', '', $versionA); $versionBNormalized = preg_replace('/[^\d]/', '', $versionB); diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index 1696633e5..186d093bf 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -7,70 +7,49 @@ class CommandLine { /** - * Simple global function to run commands. - * - * @param string $command - * @return void + * Simple global function to run commands quietly. */ - public function quietly($command) + public function quietly(string $command): void { $this->runCommand($command.' > /dev/null 2>&1'); } /** * Simple global function to run commands. - * - * @param string $command - * @return void */ - public function quietlyAsUser($command) + public function quietlyAsUser(string $command): void { $this->quietly('sudo -u "'.user().'" '.$command.' > /dev/null 2>&1'); } /** * Pass the command to the command line and display the output. - * - * @param string $command - * @return void */ - public function passthru($command) + public function passthru(string $command): void { passthru($command); } /** * Run the given command as the non-root user. - * - * @param string $command - * @param callable $onError - * @return string */ - public function run($command, callable $onError = null) + public function run(string $command, callable $onError = null): string { return $this->runCommand($command, $onError); } /** * Run the given command. - * - * @param string $command - * @param callable $onError - * @return string */ - public function runAsUser($command, callable $onError = null) + public function runAsUser(string $command, callable $onError = null): string { return $this->runCommand('sudo -u "'.user().'" '.$command, $onError); } /** * Run the given command. - * - * @param string $command - * @param callable $onError - * @return string */ - public function runCommand($command, callable $onError = null) + public function runCommand(string $command, callable $onError = null): string { $onError = $onError ?: function () { }; diff --git a/cli/Valet/Composer.php b/cli/Valet/Composer.php new file mode 100644 index 000000000..60068d52c --- /dev/null +++ b/cli/Valet/Composer.php @@ -0,0 +1,58 @@ +cli->runAsUser("composer global show --format json -- $namespacedPackage"); + + if (str_contains($result, 'InvalidArgumentException') && str_contains($result, 'not found')) { + return false; + } + + if (starts_with($result, 'Changed current')) { + $result = strstr($result, '{'); + } + + $details = json_decode($result, true); + + return ! empty($details); + } + + public function installOrFail(string $namespacedPackage): void + { + info('['.$namespacedPackage.'] is not installed, installing it now via Composer... 🎼'); + + $this->cli->runAsUser(('composer global require '.$namespacedPackage), function ($exitCode, $errorOutput) use ($namespacedPackage) { + output($errorOutput); + + throw new DomainException('Composer was unable to install ['.$namespacedPackage.'].'); + }); + } + + public function installedVersion(string $namespacedPackage): ?string + { + $result = $this->cli->runAsUser("composer global show --format json -- $namespacedPackage"); + + if (str_contains($result, 'InvalidArgumentException') && str_contains($result, 'not found')) { + return null; + } + + if (starts_with($result, 'Changed current')) { + $result = strstr($result, '{'); + } + + $details = json_decode($result, true); + $versions = $details['versions']; + + return reset($versions); + } +} diff --git a/cli/Valet/Configuration.php b/cli/Valet/Configuration.php index 829e20833..6ff70ee48 100644 --- a/cli/Valet/Configuration.php +++ b/cli/Valet/Configuration.php @@ -4,27 +4,18 @@ class Configuration { - public $files; - - /** - * Create a new Valet configuration class instance. - */ - public function __construct(Filesystem $files) + public function __construct(public Filesystem $files) { - $this->files = $files; } /** * Install the Valet configuration file. - * - * @return void */ - public function install() + public function install(): void { $this->createConfigurationDirectory(); $this->createDriversDirectory(); $this->createSitesDirectory(); - $this->createExtensionsDirectory(); $this->createLogDirectory(); $this->createCertificatesDirectory(); $this->writeBaseConfiguration(); @@ -34,39 +25,25 @@ public function install() /** * Forcefully delete the Valet home configuration directory and contents. - * - * @return void */ - public function uninstall() + public function uninstall(): void { $this->files->unlink(VALET_HOME_PATH); } /** * Create the Valet configuration directory. - * - * @return void */ - public function createConfigurationDirectory() + public function createConfigurationDirectory(): void { $this->files->ensureDirExists(preg_replace('~/valet$~', '', VALET_HOME_PATH), user()); - - $oldPath = posix_getpwuid(fileowner(__FILE__))['dir'].'/.valet'; - - if ($this->files->isDir($oldPath)) { - rename($oldPath, VALET_HOME_PATH); - $this->prependPath(VALET_HOME_PATH.'/Sites'); - } - $this->files->ensureDirExists(VALET_HOME_PATH, user()); } /** * Create the Valet drivers directory. - * - * @return void */ - public function createDriversDirectory() + public function createDriversDirectory(): void { if ($this->files->isDir($driversDirectory = VALET_HOME_PATH.'/Drivers')) { return; @@ -82,30 +59,16 @@ public function createDriversDirectory() /** * Create the Valet sites directory. - * - * @return void */ - public function createSitesDirectory() + public function createSitesDirectory(): void { $this->files->ensureDirExists(VALET_HOME_PATH.'/Sites', user()); } - /** - * Create the directory for the Valet extensions. - * - * @return void - */ - public function createExtensionsDirectory() - { - $this->files->ensureDirExists(VALET_HOME_PATH.'/Extensions', user()); - } - /** * Create the directory for Nginx logs. - * - * @return void */ - public function createLogDirectory() + public function createLogDirectory(): void { $this->files->ensureDirExists(VALET_HOME_PATH.'/Log', user()); @@ -114,10 +77,8 @@ public function createLogDirectory() /** * Create the directory for SSL certificates. - * - * @return void */ - public function createCertificatesDirectory() + public function createCertificatesDirectory(): void { $this->files->ensureDirExists(VALET_HOME_PATH.'/Certificates', user()); } @@ -125,34 +86,17 @@ public function createCertificatesDirectory() /** * Write the base, initial configuration for Valet. */ - public function writeBaseConfiguration() + public function writeBaseConfiguration(): void { if (! $this->files->exists($this->path())) { $this->write(['tld' => 'test', 'loopback' => VALET_LOOPBACK, 'paths' => []]); } - - /** - * Migrate old configurations from 'domain' to 'tld'. - */ - $config = $this->read(); - - if (! isset($config['tld'])) { - $this->updateKey('tld', ! empty($config['domain']) ? $config['domain'] : 'test'); - } - - if (! isset($config['loopback'])) { - $this->updateKey('loopback', VALET_LOOPBACK); - } } /** * Add the given path to the configuration. - * - * @param string $path - * @param bool $prepend - * @return void */ - public function addPath($path, $prepend = false) + public function addPath(string $path, bool $prepend = false): void { $this->write(tap($this->read(), function (&$config) use ($path, $prepend) { $method = $prepend ? 'prepend' : 'push'; @@ -163,25 +107,20 @@ public function addPath($path, $prepend = false) /** * Prepend the given path to the configuration. - * - * @param string $path - * @return void */ - public function prependPath($path) + public function prependPath(string $path): void { $this->addPath($path, true); } /** * Remove the given path from the configuration. - * - * @param string $path - * @return void */ - public function removePath($path) + public function removePath(string $path): void { if ($path == VALET_HOME_PATH.'/Sites') { - info("Cannot remove this directory because this is where Valet stores its site definitions.\nRun [valet paths] for a list of parked paths."); + info('Cannot remove this directory because this is where Valet stores its site definitions.'); + info('Run [valet paths] for a list of parked paths.'); exit(); } @@ -194,10 +133,8 @@ public function removePath($path) /** * Prune all non-existent paths from the configuration. - * - * @return void */ - public function prune() + public function prune(): void { if (! $this->files->exists($this->path())) { return; @@ -212,22 +149,16 @@ public function prune() /** * Read the configuration file as JSON. - * - * @return array */ - public function read() + public function read(): array { - return json_decode($this->files->get($this->path()), true); + return json_decode($this->files->get($this->path()), true, 512, JSON_THROW_ON_ERROR); } /** * Update a specific key in the configuration file. - * - * @param string $key - * @param mixed $value - * @return array */ - public function updateKey($key, $value) + public function updateKey(string $key, mixed $value): array { return tap($this->read(), function (&$config) use ($key, $value) { $config[$key] = $value; @@ -238,11 +169,8 @@ public function updateKey($key, $value) /** * Write the given configuration to disk. - * - * @param array $config - * @return void */ - public function write($config) + public function write(array $config): void { $this->files->putAsUser($this->path(), json_encode( $config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES @@ -251,10 +179,8 @@ public function write($config) /** * Get the configuration file path. - * - * @return string */ - public function path() + public function path(): string { return VALET_HOME_PATH.'/config.json'; } diff --git a/cli/Valet/Diagnose.php b/cli/Valet/Diagnose.php index a73bc9b9d..4dd9cf6b8 100644 --- a/cli/Valet/Diagnose.php +++ b/cli/Valet/Diagnose.php @@ -2,6 +2,7 @@ namespace Valet; +use Illuminate\Support\Collection; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\ConsoleOutput; @@ -27,7 +28,7 @@ class Diagnose 'nginx -v', 'curl --version', 'php --ri curl', - '~/.composer/vendor/laravel/valet/bin/ngrok version', + BREW_PREFIX.'/bin/ngrok version', 'ls -al ~/.ngrok2', 'brew info nginx', 'brew info php', @@ -50,29 +51,18 @@ class Diagnose 'sh -c \'for file in ~/.config/valet/nginx/*; do echo "------\n~/.config/valet/nginx/$(basename $file)\n---\n"; cat $file | grep -n "# valet loopback"; echo "\n------\n"; done\'', ]; - public $cli; - - public $files; - public $print; public $progressBar; - /** - * Create a new Diagnose instance. - * - * @return void - */ - public function __construct(CommandLine $cli, Filesystem $files) + public function __construct(public CommandLine $cli, public Filesystem $files) { - $this->cli = $cli; - $this->files = $files; } /** * Run diagnostics. */ - public function run($print, $plainText) + public function run(bool $print, bool $plainText): void { $this->print = $print; @@ -103,7 +93,7 @@ public function run($print, $plainText) $this->afterRun(); } - public function beforeRun() + public function beforeRun(): void { if ($this->print) { return; @@ -114,7 +104,7 @@ public function beforeRun() $this->progressBar->start(); } - public function afterRun() + public function afterRun(): void { if ($this->progressBar) { $this->progressBar->finish(); @@ -123,21 +113,21 @@ public function afterRun() output(''); } - public function runCommand($command) + public function runCommand(string $command): string { return strpos($command, 'sudo ') === 0 ? $this->cli->run($command) : $this->cli->runAsUser($command); } - public function beforeCommand($command) + public function beforeCommand(string $command): void { if ($this->print) { info(PHP_EOL."$ $command"); } } - public function afterCommand($command, $output) + public function afterCommand(string $command, string $output): void { if ($this->print) { output(trim($output)); @@ -146,12 +136,12 @@ public function afterCommand($command, $output) } } - public function ignoreOutput($command) + public function ignoreOutput(string $command): bool { return strpos($command, '> /dev/null 2>&1') !== false; } - public function format($results, $plainText) + public function format(Collection $results, bool $plainText): string { return $results->map(function ($result) use ($plainText) { $command = $result['command']; diff --git a/cli/Valet/DnsMasq.php b/cli/Valet/DnsMasq.php index efa5645fd..280682439 100644 --- a/cli/Valet/DnsMasq.php +++ b/cli/Valet/DnsMasq.php @@ -4,37 +4,20 @@ class DnsMasq { - public $brew; - - public $cli; - - public $files; - - public $configuration; - public $dnsmasqMasterConfigFile = BREW_PREFIX.'/etc/dnsmasq.conf'; public $dnsmasqSystemConfDir = BREW_PREFIX.'/etc/dnsmasq.d'; public $resolverPath = '/etc/resolver'; - /** - * Create a new DnsMasq instance. - */ - public function __construct(Brew $brew, CommandLine $cli, Filesystem $files, Configuration $configuration) + public function __construct(public Brew $brew, public CommandLine $cli, public Filesystem $files, public Configuration $configuration) { - $this->cli = $cli; - $this->brew = $brew; - $this->files = $files; - $this->configuration = $configuration; } /** * Install and configure DnsMasq. - * - * @return void */ - public function install($tld = 'test') + public function install(string $tld = 'test'): void { $this->brew->ensureInstalled('dnsmasq'); @@ -54,10 +37,8 @@ public function install($tld = 'test') /** * Forcefully uninstall dnsmasq. - * - * @return void */ - public function uninstall() + public function uninstall(): void { $this->brew->stopService('dnsmasq'); $this->brew->uninstallFormula('dnsmasq'); @@ -68,20 +49,16 @@ public function uninstall() /** * Tell Homebrew to restart dnsmasq. - * - * @return void */ - public function restart() + public function restart(): void { $this->brew->restartService('dnsmasq'); } /** * Ensure the DnsMasq configuration primary config is set to read custom configs. - * - * @return void */ - public function ensureUsingDnsmasqDForConfigs() + public function ensureUsingDnsmasqDForConfigs(): void { info('Updating Dnsmasq configuration...'); @@ -115,11 +92,8 @@ public function ensureUsingDnsmasqDForConfigs() /** * Create the TLD-specific dnsmasq config file. - * - * @param string $tld - * @return void */ - public function createDnsmasqTldConfigFile($tld) + public function createDnsmasqTldConfigFile(string $tld): void { $tldConfigFile = $this->dnsmasqUserConfigDir().'tld-'.$tld.'.conf'; $loopback = $this->configuration->read()['loopback']; @@ -129,11 +103,8 @@ public function createDnsmasqTldConfigFile($tld) /** * Create the resolver file to point the configured TLD to configured loopback address. - * - * @param string $tld - * @return void */ - public function createTldResolver($tld) + public function createTldResolver(string $tld): void { $this->files->ensureDirExists($this->resolverPath); $loopback = $this->configuration->read()['loopback']; @@ -143,12 +114,8 @@ public function createTldResolver($tld) /** * Update the TLD/domain resolved by DnsMasq. - * - * @param string $oldTld - * @param string $newTld - * @return void */ - public function updateTld($oldTld, $newTld) + public function updateTld(string $oldTld, string $newTld): void { $this->files->unlink($this->resolverPath.'/'.$oldTld); $this->files->unlink($this->dnsmasqUserConfigDir().'tld-'.$oldTld.'.conf'); @@ -158,10 +125,8 @@ public function updateTld($oldTld, $newTld) /** * Refresh the DnsMasq configuration. - * - * @return void */ - public function refreshConfiguration() + public function refreshConfiguration(): void { $tld = $this->configuration->read()['tld']; @@ -170,10 +135,8 @@ public function refreshConfiguration() /** * Get the custom configuration path. - * - * @return string */ - public function dnsmasqUserConfigDir() + public function dnsmasqUserConfigDir(): string { return $_SERVER['HOME'].'/.config/valet/dnsmasq.d/'; } diff --git a/cli/Valet/Drivers/BasicValetDriver.php b/cli/Valet/Drivers/BasicValetDriver.php index 5b1ad3791..f18f33b21 100644 --- a/cli/Valet/Drivers/BasicValetDriver.php +++ b/cli/Valet/Drivers/BasicValetDriver.php @@ -6,32 +6,28 @@ class BasicValetDriver extends ValetDriver { /** * Determine if the driver serves the request. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return bool */ - public function serves($sitePath, $siteName, $uri) + public function serves(string $sitePath, string $siteName, string $uri): bool { return true; } + /** + * Take any steps necessary before loading the front controller for this driver. + */ + public function beforeLoading(string $sitePath, string $siteName, string $uri): void + { + $_SERVER['PHP_SELF'] = $uri; + $_SERVER['SERVER_ADDR'] = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; + $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + } + /** * Determine if the incoming request is for a static file. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string|false */ - public function isStaticFile($sitePath, $siteName, $uri) + public function isStaticFile(string $sitePath, string $siteName, string $uri)/*: string|false */ { - if (file_exists($staticFilePath = $sitePath.'/public'.rtrim($uri, '/').'/index.html')) { - return $staticFilePath; - } elseif (file_exists($staticFilePath = $sitePath.'/public'.rtrim($uri, '/').'/index.php')) { - return $staticFilePath; - } elseif (file_exists($staticFilePath = $sitePath.'/public'.$uri)) { + if (file_exists($staticFilePath = $sitePath.rtrim($uri, '/').'/index.html')) { return $staticFilePath; } elseif ($this->isActualFile($staticFilePath = $sitePath.$uri)) { return $staticFilePath; @@ -42,25 +38,19 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { - $_SERVER['PHP_SELF'] = $uri; - $_SERVER['SERVER_ADDR'] = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; - $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + $uri = rtrim($uri, '/'); - $dynamicCandidates = [ - $this->asActualFile($sitePath, $uri), - $this->asPhpIndexFileInDirectory($sitePath, $uri), - $this->asHtmlIndexFileInDirectory($sitePath, $uri), + $candidates = [ + $sitePath.$uri, + $sitePath.$uri.'/index.php', + $sitePath.'/index.php', + $sitePath.'/index.html', ]; - foreach ($dynamicCandidates as $candidate) { + foreach ($candidates as $candidate) { if ($this->isActualFile($candidate)) { $_SERVER['SCRIPT_FILENAME'] = $candidate; $_SERVER['SCRIPT_NAME'] = str_replace($sitePath, '', $candidate); @@ -70,89 +60,6 @@ public function frontControllerPath($sitePath, $siteName, $uri) } } - $fixedCandidatesAndDocroots = [ - $this->asRootPhpIndexFile($sitePath) => $sitePath, - $this->asPublicPhpIndexFile($sitePath) => $sitePath.'/public', - $this->asPublicHtmlIndexFile($sitePath) => $sitePath.'/public', - ]; - - foreach ($fixedCandidatesAndDocroots as $candidate => $docroot) { - if ($this->isActualFile($candidate)) { - $_SERVER['SCRIPT_FILENAME'] = $candidate; - $_SERVER['SCRIPT_NAME'] = '/index.php'; - $_SERVER['DOCUMENT_ROOT'] = $docroot; - - return $candidate; - } - } - } - - /** - * Concatenate the site path and URI as a single file name. - * - * @param string $sitePath - * @param string $uri - * @return string - */ - protected function asActualFile($sitePath, $uri) - { - return $sitePath.$uri; - } - - /** - * Format the site path and URI with a trailing "index.php". - * - * @param string $sitePath - * @param string $uri - * @return string - */ - protected function asPhpIndexFileInDirectory($sitePath, $uri) - { - return $sitePath.rtrim($uri, '/').'/index.php'; - } - - /** - * Format the site path and URI with a trailing "index.html". - * - * @param string $sitePath - * @param string $uri - * @return string - */ - protected function asHtmlIndexFileInDirectory($sitePath, $uri) - { - return $sitePath.rtrim($uri, '/').'/index.html'; - } - - /** - * Format the incoming site path as root "index.php" file path. - * - * @param string $sitePath - * @return string - */ - protected function asRootPhpIndexFile($sitePath) - { - return $sitePath.'/index.php'; - } - - /** - * Format the incoming site path as a "public/index.php" file path. - * - * @param string $sitePath - * @return string - */ - protected function asPublicPhpIndexFile($sitePath) - { - return $sitePath.'/public/index.php'; - } - - /** - * Format the incoming site path as a "public/index.php" file path. - * - * @param string $sitePath - * @return string - */ - protected function asPublicHtmlIndexFile($sitePath) - { - return $sitePath.'/public/index.html'; + return null; } } diff --git a/cli/Valet/Drivers/BasicWithPublicValetDriver.php b/cli/Valet/Drivers/BasicWithPublicValetDriver.php new file mode 100644 index 000000000..3de22ddde --- /dev/null +++ b/cli/Valet/Drivers/BasicWithPublicValetDriver.php @@ -0,0 +1,62 @@ +isActualFile($publicPath)) { + return $publicPath; + } elseif (file_exists($publicPath.'/index.html')) { + return $publicPath.'/index.html'; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + */ + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string + { + $_SERVER['PHP_SELF'] = $uri; + $_SERVER['SERVER_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + + $docRoot = $sitePath.'/public'; + $uri = rtrim($uri, '/'); + + $candidates = [ + $docRoot.$uri, + $docRoot.$uri.'/index.php', + $docRoot.'/index.php', + $docRoot.'/index.html', + ]; + + foreach ($candidates as $candidate) { + if ($this->isActualFile($candidate)) { + $_SERVER['SCRIPT_FILENAME'] = $candidate; + $_SERVER['SCRIPT_NAME'] = str_replace($sitePath.'/public', '', $candidate); + $_SERVER['DOCUMENT_ROOT'] = $sitePath.'/public'; + + return $candidate; + } + } + + return null; + } +} diff --git a/cli/Valet/Drivers/JoomlaValetDriver.php b/cli/Valet/Drivers/JoomlaValetDriver.php deleted file mode 100644 index d03455d6f..000000000 --- a/cli/Valet/Drivers/JoomlaValetDriver.php +++ /dev/null @@ -1,34 +0,0 @@ -isModernSculpinProject($sitePath) || - $this->isLegacySculpinProject($sitePath); - } - - private function isModernSculpinProject($sitePath) - { - return is_dir($sitePath.'/source') && - is_dir($sitePath.'/output_dev') && - $this->composerRequiresSculpin($sitePath); - } - - private function isLegacySculpinProject($sitePath) - { - return is_dir($sitePath.'/.sculpin'); - } - - private function composerRequiresSculpin($sitePath) - { - if (! file_exists($sitePath.'/composer.json')) { - return false; - } - - $composer_json_source = file_get_contents($sitePath.'/composer.json'); - $composer_json = json_decode($composer_json_source, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - return false; - } - - return isset($composer_json['require']['sculpin/sculpin']); - } - - /** - * Mutate the incoming URI. - * - * @param string $uri - * @return string - */ - public function mutateUri($uri) - { - return rtrim('/output_dev'.$uri, '/'); - } -} diff --git a/cli/Valet/Drivers/BedrockValetDriver.php b/cli/Valet/Drivers/Specific/BedrockValetDriver.php similarity index 65% rename from cli/Valet/Drivers/BedrockValetDriver.php rename to cli/Valet/Drivers/Specific/BedrockValetDriver.php index 3fdf07645..558d1dfb2 100644 --- a/cli/Valet/Drivers/BedrockValetDriver.php +++ b/cli/Valet/Drivers/Specific/BedrockValetDriver.php @@ -1,18 +1,15 @@ isActualFile($staticFilePath = $sitePath.'/webroot/'.$uri)) { return $staticFilePath; @@ -36,13 +28,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { $_SERVER['DOCUMENT_ROOT'] = $sitePath.'/webroot'; $_SERVER['SCRIPT_FILENAME'] = $sitePath.'/webroot/index.php'; diff --git a/cli/Valet/Drivers/Concrete5ValetDriver.php b/cli/Valet/Drivers/Specific/Concrete5ValetDriver.php similarity index 66% rename from cli/Valet/Drivers/Concrete5ValetDriver.php rename to cli/Valet/Drivers/Specific/Concrete5ValetDriver.php index c51c6493b..3dc518ad9 100644 --- a/cli/Valet/Drivers/Concrete5ValetDriver.php +++ b/cli/Valet/Drivers/Specific/Concrete5ValetDriver.php @@ -1,31 +1,23 @@ isActualFile($staticFilePath = $sitePath.'/web'.$uri)) { return $staticFilePath; @@ -36,13 +28,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { if ($uri === '/install.php') { return $sitePath.'/web/install.php'; diff --git a/cli/Valet/Drivers/CraftValetDriver.php b/cli/Valet/Drivers/Specific/CraftValetDriver.php similarity index 86% rename from cli/Valet/Drivers/CraftValetDriver.php rename to cli/Valet/Drivers/Specific/CraftValetDriver.php index 5153a2641..0940de709 100644 --- a/cli/Valet/Drivers/CraftValetDriver.php +++ b/cli/Valet/Drivers/Specific/CraftValetDriver.php @@ -1,29 +1,23 @@ frontControllerDirectory($sitePath); @@ -57,13 +47,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { $frontControllerDirectory = $this->frontControllerDirectory($sitePath); diff --git a/cli/Valet/Drivers/DrupalValetDriver.php b/cli/Valet/Drivers/Specific/DrupalValetDriver.php similarity index 77% rename from cli/Valet/Drivers/DrupalValetDriver.php rename to cli/Valet/Drivers/Specific/DrupalValetDriver.php index e1a162934..1adcf1f67 100644 --- a/cli/Valet/Drivers/DrupalValetDriver.php +++ b/cli/Valet/Drivers/Specific/DrupalValetDriver.php @@ -1,18 +1,15 @@ addSubdirectory($sitePath); @@ -24,17 +21,14 @@ public function serves($sitePath, $siteName, $uri) file_exists($sitePath.'/core/lib/Drupal.php')) { return true; } + + return false; } /** * Determine if the incoming request is for a static file. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string|false */ - public function isStaticFile($sitePath, $siteName, $uri) + public function isStaticFile(string $sitePath, string $siteName, string $uri)/*: string|false */ { $sitePath = $this->addSubdirectory($sitePath); @@ -49,13 +43,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { $sitePath = $this->addSubdirectory($sitePath); @@ -84,7 +73,7 @@ public function frontControllerPath($sitePath, $siteName, $uri) /** * Add any matching subdirectory to the site path. */ - public function addSubdirectory($sitePath) + public function addSubdirectory($sitePath): string { $paths = array_map(function ($subDir) use ($sitePath) { return "$sitePath/$subDir"; @@ -105,10 +94,8 @@ public function addSubdirectory($sitePath) /** * Return an array of possible subdirectories. - * - * @return array */ - private function possibleSubdirectories() + private function possibleSubdirectories(): array { return ['docroot', 'public', 'web']; } diff --git a/cli/Valet/Drivers/JigsawValetDriver.php b/cli/Valet/Drivers/Specific/JigsawValetDriver.php similarity index 50% rename from cli/Valet/Drivers/JigsawValetDriver.php rename to cli/Valet/Drivers/Specific/JigsawValetDriver.php index 641d50cfe..61f5340de 100644 --- a/cli/Valet/Drivers/JigsawValetDriver.php +++ b/cli/Valet/Drivers/Specific/JigsawValetDriver.php @@ -1,30 +1,24 @@ isActualFile($staticFilePath = $sitePath.$uri)) { return $staticFilePath; @@ -38,13 +30,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { $scriptName = '/index.php'; diff --git a/cli/Valet/Drivers/Magento2ValetDriver.php b/cli/Valet/Drivers/Specific/Magento2ValetDriver.php similarity index 78% rename from cli/Valet/Drivers/Magento2ValetDriver.php rename to cli/Valet/Drivers/Specific/Magento2ValetDriver.php index d4e43e14d..1651926d1 100644 --- a/cli/Valet/Drivers/Magento2ValetDriver.php +++ b/cli/Valet/Drivers/Specific/Magento2ValetDriver.php @@ -1,38 +1,32 @@ checkMageMode($sitePath); @@ -77,34 +71,34 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Rewrite URLs that look like "versions12345/" to remove * the versions12345/ part. - * - * @param string $route */ - private function handleForVersions($route) + private function handleForVersions($route): string { return preg_replace('/version\d*\//', '', $route); } /** * Determine the current MAGE_MODE. - * - * @param string $sitePath */ - private function checkMageMode($sitePath) + private function checkMageMode($sitePath): void { if (null !== $this->mageMode) { // We have already figure out mode, no need to check it again return; } + if (! file_exists($sitePath.'/index.php')) { $this->mageMode = 'production'; // Can't use developer mode without index.php in project root return; } + $mageConfig = []; + if (file_exists($sitePath.'/app/etc/env.php')) { $mageConfig = require $sitePath.'/app/etc/env.php'; } + if (array_key_exists('MAGE_MODE', $mageConfig)) { $this->mageMode = $mageConfig['MAGE_MODE']; } @@ -113,18 +107,14 @@ private function checkMageMode($sitePath) /** * Checks to see if route is referencing any directory inside pub. This is a dynamic check so that if any new * directories are added to pub this driver will not need to be updated. - * - * @param string $sitePath - * @param string $route - * @param string $pub - * @return bool */ - private function isPubDirectory($sitePath, $route, $pub = '') + private function isPubDirectory($sitePath, $route, $pub = ''): bool { $sitePath .= '/pub/'; $dirs = glob($sitePath.'*', GLOB_ONLYDIR); $dirs = str_replace($sitePath, '', $dirs); + foreach ($dirs as $dir) { if (strpos($route, $pub.$dir.'/') === 0) { return true; @@ -136,13 +126,8 @@ private function isPubDirectory($sitePath, $route, $pub = '') /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { $this->checkMageMode($sitePath); @@ -151,6 +136,7 @@ public function frontControllerPath($sitePath, $siteName, $uri) return $sitePath.'/index.php'; } + $_SERVER['DOCUMENT_ROOT'] = $sitePath.'/pub'; return $sitePath.'/pub/index.php'; diff --git a/cli/Valet/Drivers/Specific/NeosValetDriver.php b/cli/Valet/Drivers/Specific/NeosValetDriver.php new file mode 100644 index 000000000..92c25caf2 --- /dev/null +++ b/cli/Valet/Drivers/Specific/NeosValetDriver.php @@ -0,0 +1,47 @@ +isActualFile($staticFilePath = $sitePath.'/Web'.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + */ + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string + { + return $sitePath.'/Web/index.php'; + } +} diff --git a/cli/Valet/Drivers/Specific/SculpinValetDriver.php b/cli/Valet/Drivers/Specific/SculpinValetDriver.php new file mode 100644 index 000000000..a9c66f2e5 --- /dev/null +++ b/cli/Valet/Drivers/Specific/SculpinValetDriver.php @@ -0,0 +1,37 @@ +isModernSculpinProject($sitePath) || + $this->isLegacySculpinProject($sitePath); + } + + private function isModernSculpinProject(string $sitePath): bool + { + return is_dir($sitePath.'/source') && + is_dir($sitePath.'/output_dev') && + $this->composerRequires($sitePath, 'sculpin/sculpin'); + } + + private function isLegacySculpinProject(string $sitePath): bool + { + return is_dir($sitePath.'/.sculpin'); + } + + /** + * Mutate the incoming URI. + */ + public function mutateUri(string $uri): string + { + return rtrim('/output_dev'.$uri, '/'); + } +} diff --git a/cli/Valet/Drivers/StatamicV1ValetDriver.php b/cli/Valet/Drivers/Specific/StatamicV1ValetDriver.php similarity index 67% rename from cli/Valet/Drivers/StatamicV1ValetDriver.php rename to cli/Valet/Drivers/Specific/StatamicV1ValetDriver.php index 1589e304f..2718194a6 100644 --- a/cli/Valet/Drivers/StatamicV1ValetDriver.php +++ b/cli/Valet/Drivers/Specific/StatamicV1ValetDriver.php @@ -1,31 +1,23 @@ isActualFile($staticPath = $this->getStaticPath($sitePath))) { return $staticPath; @@ -91,17 +78,14 @@ public function frontControllerPath($sitePath, $siteName, $uri) /** * Get the locale from this URI. - * - * @param string $uri - * @return string|null */ - public function getUriLocale($uri) + public function getUriLocale(string $uri): ?string { $parts = explode('/', $uri); $locale = $parts[1]; if (count($parts) < 2 || ! in_array($locale, $this->getLocales())) { - return; + return null; } return $locale; @@ -109,10 +93,8 @@ public function getUriLocale($uri) /** * Get the list of possible locales used in the first segment of a URI. - * - * @return array */ - public function getLocales() + public function getLocales(): array { return [ 'af', 'ax', 'al', 'dz', 'as', 'ad', 'ao', 'ai', 'aq', 'ag', 'ar', 'am', 'aw', 'au', 'at', 'az', 'bs', 'bh', @@ -134,11 +116,8 @@ public function getLocales() /** * Get the path to a statically cached page. - * - * @param string $sitePath - * @return string */ - protected function getStaticPath($sitePath) + protected function getStaticPath(string $sitePath): string { $parts = parse_url($_SERVER['REQUEST_URI']); $query = isset($parts['query']) ? $parts['query'] : ''; diff --git a/cli/Valet/Drivers/SymfonyValetDriver.php b/cli/Valet/Drivers/Specific/SymfonyValetDriver.php similarity index 69% rename from cli/Valet/Drivers/SymfonyValetDriver.php rename to cli/Valet/Drivers/Specific/SymfonyValetDriver.php index 9ed20d1b3..8860c25d6 100644 --- a/cli/Valet/Drivers/SymfonyValetDriver.php +++ b/cli/Valet/Drivers/Specific/SymfonyValetDriver.php @@ -1,18 +1,15 @@ isActualFile($staticFilePath = $sitePath.'/web/'.$uri)) { return $staticFilePath; @@ -40,13 +32,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Get the fully resolved path to the application's front controller. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { $frontControllerPath = null; diff --git a/cli/Valet/Drivers/Typo3ValetDriver.php b/cli/Valet/Drivers/Specific/Typo3ValetDriver.php similarity index 82% rename from cli/Valet/Drivers/Typo3ValetDriver.php rename to cli/Valet/Drivers/Specific/Typo3ValetDriver.php index 695343998..3548c4907 100644 --- a/cli/Valet/Drivers/Typo3ValetDriver.php +++ b/cli/Valet/Drivers/Specific/Typo3ValetDriver.php @@ -1,6 +1,8 @@ handleRedirectBackendShorthandUris($uri); + + $_SERVER['SERVER_NAME'] = $siteName.'.dev'; + $_SERVER['DOCUMENT_URI'] = $uri; + $_SERVER['SCRIPT_NAME'] = $uri; + $_SERVER['PHP_SELF'] = $uri; + } + /** * Determine if the driver serves the request. For TYPO3, this is the * case, if a folder called "typo3" is present in the document root. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return bool */ - public function serves($sitePath, $siteName, $uri) + public function serves(string $sitePath, string $siteName, string $uri): bool { $typo3Dir = $sitePath.$this->documentRoot.'/typo3'; @@ -59,13 +70,8 @@ public function serves($sitePath, $siteName, $uri) * Determine if the incoming request is for a static file. That is, it is * no PHP script file and the URI points to a valid file (no folder) on * the disk. Access to those static files will be authorized. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string|false */ - public function isStaticFile($sitePath, $siteName, $uri) + public function isStaticFile(string $sitePath, string $siteName, string $uri)/*: string|false */ { // May the file contains a cache busting version string like filename.12345678.css // If that is the case, the file cannot be found on disk, so remove the version @@ -87,11 +93,8 @@ public function isStaticFile($sitePath, $siteName, $uri) /** * Determines if the given URI is blacklisted so that access is prevented. - * - * @param string $uri - * @return bool */ - private function isAccessAuthorized($uri) + private function isAccessAuthorized(string $uri): bool { foreach ($this->forbiddenUriPatterns as $forbiddenUriPattern) { if (preg_match("@$forbiddenUriPattern@", $uri)) { @@ -106,17 +109,9 @@ private function isAccessAuthorized($uri) * Get the fully resolved path to the application's front controller. * This can be the currently requested PHP script, a folder that * contains an index.php or the global index.php otherwise. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return string */ - public function frontControllerPath($sitePath, $siteName, $uri) + public function frontControllerPath(string $sitePath, string $siteName, string $uri): ?string { - // without modifying the URI, redirect if necessary - $this->handleRedirectBackendShorthandUris($uri); - // from now on, remove trailing / for convenience for all the following join operations $uri = rtrim($uri, '/'); @@ -125,7 +120,7 @@ public function frontControllerPath($sitePath, $siteName, $uri) if (is_dir($absoluteFilePath)) { if (file_exists($absoluteFilePath.'/index.php')) { // this folder can be served by index.php - return $this->serveScript($sitePath, $siteName, $uri.'/index.php'); + return $this->serveScript($sitePath, $uri.'/index.php'); } if (file_exists($absoluteFilePath.'/index.html')) { @@ -134,22 +129,20 @@ public function frontControllerPath($sitePath, $siteName, $uri) } } elseif (pathinfo($absoluteFilePath, PATHINFO_EXTENSION) === 'php') { // this file can be served directly - return $this->serveScript($sitePath, $siteName, $uri); + return $this->serveScript($sitePath, $uri); } } // the global index.php will handle all other cases - return $this->serveScript($sitePath, $siteName, '/index.php'); + return $this->serveScript($sitePath, '/index.php'); } /** * Direct access to installtool via domain.dev/typo3/install/ will be redirected to * sysext install script. domain.dev/typo3 will be redirected to /typo3/, because * the generated JavaScript URIs on the login screen would be broken on /typo3. - * - * @param string $uri */ - private function handleRedirectBackendShorthandUris($uri) + private function handleRedirectBackendShorthandUris(string $uri): void { if (rtrim($uri, '/') === '/typo3/install') { header('Location: /typo3/sysext/install/Start/Install.php'); @@ -165,24 +158,14 @@ private function handleRedirectBackendShorthandUris($uri) /** * Configures the $_SERVER globals for serving the script at * the specified URI and returns it absolute file path. - * - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @param string $script - * @return string */ - private function serveScript($sitePath, $siteName, $uri) + private function serveScript(string $sitePath, string $uri): string { $docroot = $sitePath.$this->documentRoot; $abspath = $docroot.$uri; - $_SERVER['SERVER_NAME'] = $siteName.'.dev'; $_SERVER['DOCUMENT_ROOT'] = $docroot; - $_SERVER['DOCUMENT_URI'] = $uri; $_SERVER['SCRIPT_FILENAME'] = $abspath; - $_SERVER['SCRIPT_NAME'] = $uri; - $_SERVER['PHP_SELF'] = $uri; return $abspath; } diff --git a/cli/Valet/Drivers/WordPressValetDriver.php b/cli/Valet/Drivers/Specific/WordPressValetDriver.php similarity index 59% rename from cli/Valet/Drivers/WordPressValetDriver.php rename to cli/Valet/Drivers/Specific/WordPressValetDriver.php index 6d37ea716..932ea05da 100644 --- a/cli/Valet/Drivers/WordPressValetDriver.php +++ b/cli/Valet/Drivers/Specific/WordPressValetDriver.php @@ -1,31 +1,23 @@ serves($sitePath, $siteName, $driver->mutateUri($uri))) { return $driver; @@ -97,14 +59,11 @@ public static function assign($sitePath, $siteName, $uri) /** * Get the custom driver class from the site path, if one exists. - * - * @param string $sitePath - * @return string */ - public static function customSiteDriver($sitePath) + public static function customSiteDriver(string $sitePath): ?string { if (! file_exists($sitePath.'/LocalValetDriver.php')) { - return; + return null; } require_once $sitePath.'/LocalValetDriver.php'; @@ -114,11 +73,8 @@ public static function customSiteDriver($sitePath) /** * Get all of the driver classes in a given path. - * - * @param string $path - * @return array */ - public static function driversIn($path) + public static function driversIn(string $path): array { if (! is_dir($path)) { return []; @@ -139,27 +95,46 @@ public static function driversIn($path) return $drivers; } + /** + * Get all of the specific drivers shipped with Valet. + */ + public static function specificDrivers(): array + { + return array_map(function ($item) { + return 'Specific\\'.$item; + }, static::driversIn(__DIR__.'/Specific')); + } + + /** + * Get all of the custom drivers defined by the user locally. + */ + public static function customDrivers(): array + { + return array_map(function ($item) { + return 'Custom\\'.$item; + }, static::driversIn(VALET_HOME_PATH.'/Drivers')); + } + + /** + * Take any steps necessary before loading the front controller for this driver. + */ + public function beforeLoading(string $sitePath, string $siteName, string $uri): void + { + // Do nothing + } + /** * Mutate the incoming URI. - * - * @param string $uri - * @return string */ - public function mutateUri($uri) + public function mutateUri(string $uri): string { return $uri; } /** * Serve the static file at the given path. - * - * @param string $staticFilePath - * @param string $sitePath - * @param string $siteName - * @param string $uri - * @return void */ - public function serveStaticFile($staticFilePath, $sitePath, $siteName, $uri) + public function serveStaticFile(string $staticFilePath, string $sitePath, string $siteName, string $uri): void { /** * Back story... @@ -184,11 +159,8 @@ public function serveStaticFile($staticFilePath, $sitePath, $siteName, $uri) /** * Determine if the path is a file and not a directory. - * - * @param string $path - * @return bool */ - protected function isActualFile($path) + protected function isActualFile(string $path): bool { return ! is_dir($path) && file_exists($path); } @@ -196,12 +168,8 @@ protected function isActualFile($path) /** * Load server environment variables if available. * Processes any '*' entries first, and then adds site-specific entries. - * - * @param string $sitePath - * @param string $siteName - * @return void */ - public function loadServerEnvironmentVariables($sitePath, $siteName) + public function loadServerEnvironmentVariables(string $sitePath, string $siteName): void { $varFilePath = $sitePath.'/.valet-env.php'; if (! file_exists($varFilePath)) { @@ -228,4 +196,20 @@ public function loadServerEnvironmentVariables($sitePath, $siteName) putenv($key.'='.$value); } } + + public function composerRequires(string $sitePath, string $namespacedPackage): bool + { + if (! file_exists($sitePath.'/composer.json')) { + return false; + } + + $composer_json_source = file_get_contents($sitePath.'/composer.json'); + $composer_json = json_decode($composer_json_source, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return false; + } + + return isset($composer_json['require'][$namespacedPackage]); + } } diff --git a/cli/Valet/Expose.php b/cli/Valet/Expose.php new file mode 100644 index 000000000..abb1428c0 --- /dev/null +++ b/cli/Valet/Expose.php @@ -0,0 +1,78 @@ +get($endpoint)->getBody()); + + if (isset($body->tunnels) && count($body->tunnels) > 0) { + if ($tunnelUrl = $this->findHttpTunnelUrl($body->tunnels, $domain)) { + return $tunnelUrl; + } + } + }, 250); + + if (! empty($response)) { + return $response; + } + + return warning("The project $domain cannot be found as an Expose share.\nEither it is not currently shared, or you may be on a free plan."); + } catch (ConnectException $e) { + return warning('There is no Expose instance running.'); + } + } + + /** + * Find the HTTP tunnel URL from the list of tunnels. + */ + public function findHttpTunnelUrl(array $tunnels, string $domain): ?string + { + foreach ($tunnels as $tunnel) { + if (strpos($tunnel, strtolower($domain))) { + return $tunnel; + } + } + + return null; + } + + /** + * Return whether Expose is installed. + */ + public function installed(): bool + { + return $this->composer->installed('beyondcode/expose'); + } + + /** + * Return which version of Expose is installed. + */ + public function installedVersion(): ?string + { + return $this->composer->installedVersion('beyondcode/expose'); + } + + /** + * Make sure Expose is installed. + */ + public function ensureInstalled(): void + { + if (! $this->installed()) { + $this->composer->installOrFail('beyondcode/expose'); + } + } +} diff --git a/cli/Valet/Filesystem.php b/cli/Valet/Filesystem.php index fcfc9a095..6e924fa94 100644 --- a/cli/Valet/Filesystem.php +++ b/cli/Valet/Filesystem.php @@ -10,24 +10,16 @@ class Filesystem { /** * Determine if the given path is a directory. - * - * @param string $path - * @return bool */ - public function isDir($path) + public function isDir(string $path): bool { return is_dir($path); } /** * Create a directory. - * - * @param string $path - * @param string|null $owner - * @param int $mode - * @return void */ - public function mkdir($path, $owner = null, $mode = 0755) + public function mkdir(string $path, ?string $owner = null, int $mode = 0755): void { mkdir($path, $mode, true); @@ -38,13 +30,8 @@ public function mkdir($path, $owner = null, $mode = 0755) /** * Ensure that the given directory exists. - * - * @param string $path - * @param string|null $owner - * @param int $mode - * @return void */ - public function ensureDirExists($path, $owner = null, $mode = 0755) + public function ensureDirExists(string $path, ?string $owner = null, int $mode = 0755): void { if (! $this->isDir($path)) { $this->mkdir($path, $owner, $mode); @@ -53,24 +40,16 @@ public function ensureDirExists($path, $owner = null, $mode = 0755) /** * Create a directory as the non-root user. - * - * @param string $path - * @param int $mode - * @return void */ - public function mkdirAsUser($path, $mode = 0755) + public function mkdirAsUser(string $path, int $mode = 0755): void { $this->mkdir($path, user(), $mode); } /** * Touch the given path. - * - * @param string $path - * @param string|null $owner - * @return string */ - public function touch($path, $owner = null) + public function touch(string $path, ?string $owner = null): string { touch($path); @@ -83,46 +62,32 @@ public function touch($path, $owner = null) /** * Touch the given path as the non-root user. - * - * @param string $path - * @return void */ - public function touchAsUser($path) + public function touchAsUser(string $path): string { return $this->touch($path, user()); } /** * Determine if the given file exists. - * - * @param string $path - * @return bool */ - public function exists($path) + public function exists(string $path): bool { return file_exists($path); } /** * Read the contents of the given file. - * - * @param string $path - * @return string */ - public function get($path) + public function get(string $path): string { return file_get_contents($path); } /** * Write to the given file. - * - * @param string $path - * @param string $contents - * @param string|null $owner - * @return void */ - public function put($path, $contents, $owner = null) + public function put(string $path, string $contents, ?string $owner = null): void { file_put_contents($path, $contents); @@ -133,25 +98,16 @@ public function put($path, $contents, $owner = null) /** * Write to the given file as the non-root user. - * - * @param string $path - * @param string $contents - * @return void */ - public function putAsUser($path, $contents) + public function putAsUser(string $path, ?string $contents): void { $this->put($path, $contents, user()); } /** * Append the contents to the given file. - * - * @param string $path - * @param string $contents - * @param string|null $owner - * @return void */ - public function append($path, $contents, $owner = null) + public function append(string $path, string $contents, ?string $owner = null): void { file_put_contents($path, $contents, FILE_APPEND); @@ -162,36 +118,24 @@ public function append($path, $contents, $owner = null) /** * Append the contents to the given file as the non-root user. - * - * @param string $path - * @param string $contents - * @return void */ - public function appendAsUser($path, $contents) + public function appendAsUser(string $path, string $contents): void { $this->append($path, $contents, user()); } /** * Copy the given file to a new location. - * - * @param string $from - * @param string $to - * @return void */ - public function copy($from, $to) + public function copy(string $from, string $to): void { copy($from, $to); } /** * Copy the given file to a new location for the non-root user. - * - * @param string $from - * @param string $to - * @return void */ - public function copyAsUser($from, $to) + public function copyAsUser(string $from, string $to): void { copy($from, $to); @@ -200,12 +144,8 @@ public function copyAsUser($from, $to) /** * Create a symlink to the given target. - * - * @param string $target - * @param string $link - * @return void */ - public function symlink($target, $link) + public function symlink(string $target, string $link): void { if ($this->exists($link)) { $this->unlink($link); @@ -218,12 +158,8 @@ public function symlink($target, $link) * Create a symlink to the given target for the non-root user. * * This uses the command line as PHP can't change symlink permissions. - * - * @param string $target - * @param string $link - * @return void */ - public function symlinkAsUser($target, $link) + public function symlinkAsUser(string $target, string $link): void { if ($this->exists($link)) { $this->unlink($link); @@ -234,11 +170,8 @@ public function symlinkAsUser($target, $link) /** * Delete the file at the given path. - * - * @param string $path - * @return void */ - public function unlink($path) + public function unlink(string $path): void { if (file_exists($path) || is_link($path)) { @unlink($path); @@ -247,17 +180,18 @@ public function unlink($path) /** * Recursively delete a directory and its contents. - * - * @param string $path - * @return void */ - public function rmDirAndContents($path) + public function rmDirAndContents(string $path): void { $dir = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); $files = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); foreach ($files as $file) { - $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); + if ($file->isLink()) { + unlink($file->getPathname()); + } else { + $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); + } } rmdir($path); @@ -265,66 +199,48 @@ public function rmDirAndContents($path) /** * Change the owner of the given path. - * - * @param string $path - * @param string $user */ - public function chown($path, $user) + public function chown(string $path, string $user): void { chown($path, $user); } /** * Change the group of the given path. - * - * @param string $path - * @param string $group */ - public function chgrp($path, $group) + public function chgrp(string $path, string $group): void { chgrp($path, $group); } /** * Resolve the given path. - * - * @param string $path - * @return string */ - public function realpath($path) + public function realpath(string $path): string { return realpath($path); } /** * Determine if the given path is a symbolic link. - * - * @param string $path - * @return bool */ - public function isLink($path) + public function isLink(string $path): bool { return is_link($path); } /** * Resolve the given symbolic link. - * - * @param string $path - * @return string */ - public function readLink($path) + public function readLink(string $path): string { return readlink($path); } /** * Remove all of the broken symbolic links at the given path. - * - * @param string $path - * @return void */ - public function removeBrokenLinksAt($path) + public function removeBrokenLinksAt(string $path): void { collect($this->scandir($path)) ->filter(function ($file) use ($path) { @@ -337,22 +253,16 @@ public function removeBrokenLinksAt($path) /** * Determine if the given path is a broken symbolic link. - * - * @param string $path - * @return bool */ - public function isBrokenLink($path) + public function isBrokenLink(string $path): bool { return is_link($path) && ! file_exists($path); } /** * Scan the given directory path. - * - * @param string $path - * @return array */ - public function scandir($path) + public function scandir(string $path): array { return collect(scandir($path)) ->reject(function ($file) { @@ -362,11 +272,8 @@ public function scandir($path) /** * Get custom stub file if exists. - * - * @param string $filename - * @return string */ - public function getStub($filename) + public function getStub(string $filename): string { $default = __DIR__.'/../stubs/'.$filename; diff --git a/cli/Valet/Nginx.php b/cli/Valet/Nginx.php index 55fd09e85..33f7b89de 100644 --- a/cli/Valet/Nginx.php +++ b/cli/Valet/Nginx.php @@ -3,42 +3,21 @@ namespace Valet; use DomainException; +use Illuminate\Support\Collection; class Nginx { - public $brew; - - public $cli; - - public $files; - - public $configuration; - - public $site; - const NGINX_CONF = BREW_PREFIX.'/etc/nginx/nginx.conf'; - /** - * Create a new Nginx instance. - * - * @return void - */ - public function __construct(Brew $brew, CommandLine $cli, Filesystem $files, - Configuration $configuration, Site $site) + public function __construct(public Brew $brew, public CommandLine $cli, public Filesystem $files, + public Configuration $configuration, public Site $site) { - $this->cli = $cli; - $this->brew = $brew; - $this->site = $site; - $this->files = $files; - $this->configuration = $configuration; } /** * Install the configuration files for Nginx. - * - * @return void */ - public function install() + public function install(): void { if (! $this->brew->hasInstalledNginx()) { $this->brew->installOrFail('nginx', []); @@ -51,10 +30,8 @@ public function install() /** * Install the Nginx configuration file. - * - * @return void */ - public function installConfiguration() + public function installConfiguration(): void { info('Installing nginx configuration...'); @@ -68,10 +45,8 @@ public function installConfiguration() /** * Install the Valet Nginx server configuration file. - * - * @return void */ - public function installServer() + public function installServer(): void { $this->files->ensureDirExists(BREW_PREFIX.'/etc/nginx/valet'); @@ -94,10 +69,8 @@ public function installServer() * Install the Nginx configuration directory to the ~/.config/valet directory. * * This directory contains all site-specific Nginx servers. - * - * @return void */ - public function installNginxDirectory() + public function installNginxDirectory(): void { info('Installing nginx directory...'); @@ -105,7 +78,7 @@ public function installNginxDirectory() $this->files->mkdirAsUser($nginxDirectory); } - $this->files->putAsUser($nginxDirectory.'/.keep', "\n"); + $this->files->putAsUser($nginxDirectory.'/.keep', PHP_EOL); $this->rewriteSecureNginxFiles(); } @@ -113,7 +86,7 @@ public function installNginxDirectory() /** * Check nginx.conf for errors. */ - private function lint() + private function lint(): void { $this->cli->run( 'sudo nginx -c '.static::NGINX_CONF.' -t', @@ -125,10 +98,8 @@ function ($exitCode, $outputMessage) { /** * Generate fresh Nginx servers for existing secure sites. - * - * @return void */ - public function rewriteSecureNginxFiles() + public function rewriteSecureNginxFiles(): void { $tld = $this->configuration->read()['tld']; $loopback = $this->configuration->read()['loopback']; @@ -144,10 +115,8 @@ public function rewriteSecureNginxFiles() /** * Restart the Nginx service. - * - * @return void */ - public function restart() + public function restart(): void { $this->lint(); @@ -156,20 +125,16 @@ public function restart() /** * Stop the Nginx service. - * - * @return void */ - public function stop() + public function stop(): void { $this->brew->stopService(['nginx']); } /** * Forcefully uninstall Nginx. - * - * @return void */ - public function uninstall() + public function uninstall(): void { $this->brew->stopService(['nginx', 'nginx-full']); $this->brew->uninstallFormula('nginx nginx-full'); @@ -178,10 +143,8 @@ public function uninstall() /** * Return a list of all sites with explicit Nginx configurations. - * - * @return \Illuminate\Support\Collection */ - public function configuredSites() + public function configuredSites(): Collection { return collect($this->files->scandir(VALET_HOME_PATH.'/Nginx')) ->reject(function ($file) { diff --git a/cli/Valet/Ngrok.php b/cli/Valet/Ngrok.php index 349f19b2e..c7c0d11d4 100644 --- a/cli/Valet/Ngrok.php +++ b/cli/Valet/Ngrok.php @@ -8,24 +8,19 @@ class Ngrok { - public $cli; - public $tunnelsEndpoints = [ 'http://127.0.0.1:4040/api/tunnels', 'http://127.0.0.1:4041/api/tunnels', ]; - public function __construct(CommandLine $cli) + public function __construct(public CommandLine $cli, public Brew $brew) { - $this->cli = $cli; } /** * Get the current tunnel URL from the Ngrok API. - * - * @return string */ - public function currentTunnelUrl($domain = null) + public function currentTunnelUrl(?string $domain = null): string { // wait a second for ngrok to start before attempting to find available tunnels sleep(1); @@ -52,16 +47,13 @@ public function currentTunnelUrl($domain = null) } } - throw new DomainException('Tunnel not established.'); + throw new DomainException('There is no Ngrok tunnel established for '.$domain.'.'); } /** * Find the HTTP tunnel URL from the list of tunnels. - * - * @param array $tunnels - * @return string|null */ - public function findHttpTunnelUrl($tunnels, $domain) + public function findHttpTunnelUrl(array $tunnels, string $domain): ?string { // If there are active tunnels on the Ngrok instance we will spin through them and // find the one responding on HTTP. Each tunnel has an HTTP and a HTTPS address @@ -71,16 +63,31 @@ public function findHttpTunnelUrl($tunnels, $domain) return $tunnel->public_url; } } + + return null; } /** * Set the Ngrok auth token. - * - * @param string $token - * @return string */ - public function setToken($token) + public function setToken($token): string + { + return $this->cli->runAsUser(BREW_PREFIX.'/bin/ngrok authtoken '.$token); + } + + /** + * Return whether ngrok is installed. + */ + public function installed(): bool + { + return $this->brew->installed('ngrok'); + } + + /** + * Make sure ngrok is installed. + */ + public function ensureInstalled(): void { - return $this->cli->runAsUser('./bin/ngrok authtoken '.$token); + $this->brew->ensureInstalled('ngrok'); } } diff --git a/cli/Valet/PhpFpm.php b/cli/Valet/PhpFpm.php index 89f98f4b4..e56cea13f 100644 --- a/cli/Valet/PhpFpm.php +++ b/cli/Valet/PhpFpm.php @@ -3,47 +3,23 @@ namespace Valet; use DomainException; +use Illuminate\Support\Collection; class PhpFpm { - public $brew; - - public $cli; - - public $files; - - public $config; - - public $site; - - public $nginx; - public $taps = [ 'homebrew/homebrew-core', 'shivammathur/php', ]; - /** - * Create a new PHP FPM class instance. - * - * @return void - */ - public function __construct(Brew $brew, CommandLine $cli, Filesystem $files, Configuration $config, Site $site, Nginx $nginx) + public function __construct(public Brew $brew, public CommandLine $cli, public Filesystem $files, public Configuration $config, public Site $site, public Nginx $nginx) { - $this->cli = $cli; - $this->brew = $brew; - $this->files = $files; - $this->config = $config; - $this->site = $site; - $this->nginx = $nginx; } /** * Install and configure PhpFpm. - * - * @return void */ - public function install() + public function install(): void { info('Installing and configuring phpfpm...'); @@ -67,10 +43,8 @@ public function install() /** * Forcefully uninstall all of Valet's supported PHP versions and configurations. - * - * @return void */ - public function uninstall() + public function uninstall(): void { $this->brew->uninstallAllPhpVersions(); rename(BREW_PREFIX.'/etc/php', BREW_PREFIX.'/etc/php-valet-bak'.time()); @@ -81,11 +55,8 @@ public function uninstall() * 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 createConfigurationFiles($phpVersion) + public function createConfigurationFiles(string $phpVersion): void { info("Updating PHP configuration for {$phpVersion}..."); @@ -130,21 +101,16 @@ public function createConfigurationFiles($phpVersion) /** * Restart the PHP FPM process (if one specified) or processes (if none specified). - * - * @param string|null $phpVersion - * @return void */ - public function restart($phpVersion = null) + public function restart(?string $phpVersion = null): void { $this->brew->restartService($phpVersion ?: $this->utilizedPhpVersions()); } /** * Stop the PHP FPM process. - * - * @return void */ - public function stop() + public function stop(): void { call_user_func_array( [$this->brew, 'stopService'], @@ -154,11 +120,8 @@ 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($phpVersion = null) + public function fpmConfigPath(?string $phpVersion = null): string { if (! $phpVersion) { $phpVersion = $this->brew->linkedPhp(); @@ -173,7 +136,7 @@ public function fpmConfigPath($phpVersion = null) /** * Stop only the running php services. */ - public function stopRunning() + public function stopRunning(): void { $this->brew->stopService( $this->brew->getAllRunningServices() @@ -186,11 +149,8 @@ public function stopRunning() /** * Stop a given PHP version, if that specific version isn't being used globally or by any sites. - * - * @param string|null $phpVersion - * @return void */ - public function stopIfUnused($phpVersion = null) + public function stopIfUnused(?string $phpVersion = null): void { if (! $phpVersion) { return; @@ -205,12 +165,8 @@ public function stopIfUnused($phpVersion = null) /** * Isolate a given directory to use a specific version of PHP. - * - * @param string $directory - * @param string $version - * @return void */ - public function isolateDirectory($directory, $version) + public function isolateDirectory(string $directory, string $version): void { $site = $this->site->getSiteUrl($directory); @@ -232,11 +188,8 @@ public function isolateDirectory($directory, $version) /** * Remove PHP version isolation for a given directory. - * - * @param string $directory - * @return void */ - public function unIsolateDirectory($directory) + public function unIsolateDirectory(string $directory): void { $site = $this->site->getSiteUrl($directory); @@ -251,10 +204,8 @@ public function unIsolateDirectory($directory) /** * List all directories with PHP isolation configured. - * - * @return \Illuminate\Support\Collection */ - public function isolatedDirectories() + public function isolatedDirectories(): Collection { return $this->nginx->configuredSites()->filter(function ($item) { return strpos($this->files->get(VALET_HOME_PATH.'/Nginx/'.$item), ISOLATED_PHP_VERSION) !== false; @@ -265,12 +216,8 @@ public function isolatedDirectories() /** * Use a specific version of PHP globally. - * - * @param string $version - * @param bool $force - * @return string|void */ - public function useVersion($version, $force = false) + public function useVersion(string $version, bool $force = false): ?string { $version = $this->validateRequestedVersion($version); @@ -311,11 +258,8 @@ public function useVersion($version, $force = false) /** * Symlink (Capistrano-style) a given Valet.sock file to be the primary valet.sock. - * - * @param string $phpVersion - * @return void */ - public function symlinkPrimaryValetSock($phpVersion) + public function symlinkPrimaryValetSock(string $phpVersion): void { $this->files->symlinkAsUser(VALET_HOME_PATH.'/'.$this->fpmSockName($phpVersion), VALET_HOME_PATH.'/valet.sock'); } @@ -323,18 +267,15 @@ public function symlinkPrimaryValetSock($phpVersion) /** * If passed php7.4, or php74, 7.4, or 74 formats, normalize to php@7.4 format. */ - public function normalizePhpVersion($version) + public function normalizePhpVersion(?string $version): string { return preg_replace('/(?:php@?)?([0-9+])(?:.)?([0-9+])/i', 'php@$1.$2', (string) $version); } /** * Validate the requested version to be sure we can support it. - * - * @param string $version - * @return string */ - public function validateRequestedVersion($version) + public function validateRequestedVersion(string $version): string { if (is_null($version)) { throw new DomainException("Please specify a PHP version (try something like 'php@8.1')"); @@ -361,11 +302,8 @@ public function validateRequestedVersion($version) /** * Get FPM sock file name for a given PHP version. - * - * @param string|null $phpVersion - * @return string */ - public static function fpmSockName($phpVersion = null) + public static function fpmSockName(?string $phpVersion = null): string { $versionInteger = preg_replace('~[^\d]~', '', $phpVersion); @@ -375,10 +313,8 @@ public static function fpmSockName($phpVersion = null) /** * 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() + public function utilizedPhpVersions(): array { $fpmSockFiles = $this->brew->supportedPhpVersions()->map(function ($version) { return self::fpmSockName($this->normalizePhpVersion($version)); diff --git a/cli/Valet/Server.php b/cli/Valet/Server.php new file mode 100644 index 000000000..85c37ed9a --- /dev/null +++ b/cli/Valet/Server.php @@ -0,0 +1,211 @@ +config = $config; + } + + /** + * Extract $uri from $SERVER['REQUEST_URI'] variable. + */ + public static function uriFromRequestUri(string $requestUri): string + { + return rawurldecode( + explode('?', $requestUri)[0] + ); + } + + /** + * Extract the domain from the site name. + */ + public static function domainFromSiteName(string $siteName): string + { + return array_slice(explode('.', $siteName), -1)[0]; + } + + /** + * Show the Valet 404 "Not Found" page. + */ + public static function show404() + { + http_response_code(404); + require __DIR__.'/../../cli/templates/404.html'; + exit; + } + + /** + * Show directory listing or 404 if directory doesn't exist. + */ + public static function showDirectoryListing(string $valetSitePath, string $uri) + { + $is_root = ($uri == '/'); + $directory = ($is_root) ? $valetSitePath : $valetSitePath.$uri; + + if (! file_exists($directory)) { + static::show404(); + } + + // Sort directories at the top + $paths = glob("$directory/*"); + usort($paths, function ($a, $b) { + return (is_dir($a) == is_dir($b)) ? strnatcasecmp($a, $b) : (is_dir($a) ? -1 : 1); + }); + + // Output the HTML for the directory listing + echo "

Index of $uri

"; + echo '
'; + echo implode('
'.PHP_EOL, array_map(function ($path) use ($uri, $is_root) { + $file = basename($path); + + return ($is_root) ? "/$file" : "$uri/$file/"; + }, $paths)); + + exit; + } + + /** + * Return whether a given host (from $_SERVER['HTTP_HOST']) is an IP address. + */ + public static function hostIsIpAddress(string $host): bool + { + return preg_match('/^([0-9]+\.){3}[0-9]+$/', $host); + } + + /** + * Return the root level Valet site if given the request URI ($_SERVER['REQUEST_URI']) + * of an address using IP address local access. + * + * E.g. URL is 192.168.1.100/onramp.tes/auth/login, passes $uri as onramp.test/auth/login and + * $tld as 'test', and this method returns onramp.test + * + * For use when accessing Valet sites across a local network. + */ + public static function valetSiteFromIpAddressUri(string $uri, string $tld): ?string + { + if (preg_match('/^[-.0-9a-zA-Z]+\.'.$tld.'/', $uri, $matches)) { + return $matches[0]; + } + + return null; + } + + /** + * Extract site name from HTTP host, stripping www. and supporting wildcard DNS. + */ + public function siteNameFromHttpHost(string $httpHost): string + { + $siteName = basename( + // Filter host to support wildcard dns feature + $this->allowWildcardDnsDomains($httpHost), + '.'.$this->config['tld'] + ); + + if (strpos($siteName, 'www.') === 0) { + $siteName = substr($siteName, 4); + } + + return $siteName; + } + + /** + * You may use wildcard DNS provider nip.io as a tool for testing your site via an IP address. + * First, determine the IP address of your local computer (like 192.168.0.10). + * Then, visit http://project.your-ip.nip.io - e.g.: http://laravel.192.168.0.10.nip.io. + */ + public function allowWildcardDnsDomains(string $domain): string + { + $services = [ + '.*.*.*.*.nip.io', + '-*-*-*-*.nip.io', + ]; + + if (isset($this->config['tunnel_services'])) { + $services = array_merge($services, (array) $this->config['tunnel_services']); + } + + $patterns = []; + foreach ($services as $service) { + $pattern = preg_quote($service, '#'); + $pattern = str_replace('\*', '.*', $pattern); + $patterns[] = '(.*)'.$pattern; + } + + $pattern = implode('|', $patterns); + + if (preg_match('#(?:'.$pattern.')\z#u', $domain, $matches)) { + $domain = array_pop($matches); + } + + if (strpos($domain, ':') !== false) { + $domain = explode(':', $domain)[0]; + } + + return $domain; + } + + /** + * Determine the fully qualified path to the site. + * Inspects registered path directories, case-sensitive. + */ + public function sitePath(string $siteName): ?string + { + $valetSitePath = null; + $domain = static::domainFromSiteName($siteName); + + foreach ($this->config['paths'] as $path) { + $handle = opendir($path); + + if ($handle === false) { + continue; + } + + $dirs = []; + + while (false !== ($file = readdir($handle))) { + if (is_dir($path.'/'.$file) && ! in_array($file, ['.', '..'])) { + $dirs[] = $file; + } + } + + closedir($handle); + + // Note: strtolower used below because Nginx only tells us lowercase names + foreach ($dirs as $dir) { + if (strtolower($dir) === $siteName) { + // early return when exact match for linked subdomain + return $path.'/'.$dir; + } + + if (strtolower($dir) === $domain) { + // no early return here because the foreach may still have some subdomains to process with higher priority + $valetSitePath = $path.'/'.$dir; + } + } + + if ($valetSitePath) { + return $valetSitePath; + } + } + + return null; + } + + /** + * Return the default site path for uncaught URLs, if it's set. + **/ + public function defaultSitePath(): ?string + { + if (isset($this->config['default']) && is_string($this->config['default']) && is_dir($this->config['default'])) { + return $this->config['default']; + } + + return null; + } +} diff --git a/cli/Valet/Site.php b/cli/Valet/Site.php index dca754074..1136c9278 100644 --- a/cli/Valet/Site.php +++ b/cli/Valet/Site.php @@ -3,36 +3,19 @@ namespace Valet; use DomainException; +use Illuminate\Support\Collection; use PhpFpm; class Site { - public $brew; - - public $config; - - public $cli; - - public $files; - - /** - * Create a new Site instance. - */ - public function __construct(Brew $brew, Configuration $config, CommandLine $cli, Filesystem $files) + public function __construct(public Brew $brew, public Configuration $config, public CommandLine $cli, public Filesystem $files) { - $this->brew = $brew; - $this->cli = $cli; - $this->files = $files; - $this->config = $config; } /** * Get the name of the site. - * - * @param string|null $name - * @return string */ - private function getRealSiteName($name) + private function getSiteLinkName(?string $name): string { if (! is_null($name)) { return $name; @@ -42,15 +25,13 @@ private function getRealSiteName($name) return $link; } - return basename(getcwd()); + throw new DomainException(basename(getcwd()).' is not linked.'); } /** * Get link name based on the current directory. - * - * @return null|string */ - private function getLinkNameByCurrentDir() + private function getLinkNameByCurrentDir(): ?string { $count = count($links = $this->links()->where('path', getcwd())); @@ -61,18 +42,17 @@ private function getLinkNameByCurrentDir() if ($count > 1) { throw new DomainException("There are {$count} links related to the current directory, please specify the name: valet unlink ."); } + + return null; } /** * Get the real hostname for the given path, checking links. - * - * @param string $path - * @return string|null */ - public function host($path) + public function host(string $path): ?string { foreach ($this->files->scandir($this->sitesPath()) as $link) { - if ($resolved = realpath($this->sitesPath($link)) === $path) { + if (realpath($this->sitesPath($link)) === $path) { return $link; } } @@ -82,12 +62,8 @@ public function host($path) /** * Link the current working directory with the given name. - * - * @param string $target - * @param string $link - * @return string */ - public function link($target, $link) + public function link(string $target, string $link): string { $this->files->ensureDirExists( $linkPath = $this->sitesPath(), user() @@ -102,10 +78,8 @@ public function link($target, $link) /** * Pretty print out all links in Valet. - * - * @return \Illuminate\Support\Collection */ - public function links() + public function links(): Collection { $certsPath = $this->certificatesPath(); @@ -118,10 +92,8 @@ public function links() /** * Pretty print out all parked links in Valet. - * - * @return \Illuminate\Support\Collection */ - public function parked() + public function parked(): Collection { $certs = $this->getCertificates(); @@ -147,10 +119,8 @@ public function parked() /** * Get all sites which are proxies (not Links, and contain proxy_pass directive). - * - * @return \Illuminate\Support\Collection */ - public function proxies() + public function proxies(): Collection { $dir = $this->nginxPath(); $tld = $this->config->read()['tld']; @@ -194,11 +164,8 @@ public function proxies() /** * Get the site URL from a directory if it's a valid Valet site. - * - * @param string $directory - * @return string */ - public function getSiteUrl($directory) + public function getSiteUrl(string $directory): string { $tld = $this->config->read()['tld']; @@ -220,12 +187,8 @@ public function getSiteUrl($directory) /** * Identify whether a site is for a proxy by reading the host name from its config file. - * - * @param string $site Site name without TLD - * @param string $configContents Config file contents - * @return string|null */ - public function getProxyHostForSite($site, $configContents = null) + public function getProxyHostForSite(string $site, string $configContents = null): ?string { $siteConf = $configContents ?: $this->getSiteConfigFileContents($site); @@ -241,7 +204,10 @@ public function getProxyHostForSite($site, $configContents = null) return $host; } - public function getSiteConfigFileContents($site, $suffix = null) + /** + * Get the contents of the configuration for the given site. + */ + public function getSiteConfigFileContents(string $site, ?string $suffix = null): ?string { $config = $this->config->read(); $suffix = $suffix ?: '.'.$config['tld']; @@ -252,11 +218,8 @@ public function getSiteConfigFileContents($site, $suffix = null) /** * Get all certificates from config folder. - * - * @param string $path - * @return \Illuminate\Support\Collection */ - public function getCertificates($path = null) + public function getCertificates(?string $path = null): Collection { $path = $path ?: $this->certificatesPath(); @@ -280,25 +243,11 @@ public function getCertificates($path = null) })->flip(); } - /** - * @deprecated Use getSites instead which works for both normal and symlinked paths. - * - * @param string $path - * @param \Illuminate\Support\Collection $certs - * @return \Illuminate\Support\Collection - */ - public function getLinks($path, $certs) - { - return $this->getSites($path, $certs); - } - /** * Get list of sites and return them formatted * Will work for symlink and normal site paths. - * - * @return \Illuminate\Support\Collection */ - public function getSites($path, $certs) + public function getSites(string $path, Collection $certs): Collection { $config = $this->config->read(); @@ -333,13 +282,10 @@ public function getSites($path, $certs) /** * Unlink the given symbolic link. - * - * @param string $name - * @return void */ - public function unlink($name) + public function unlink(?string $name = null): string { - $name = $this->getRealSiteName($name); + $name = $this->getSiteLinkName($name); if ($this->files->exists($path = $this->sitesPath($name))) { $this->files->unlink($path); @@ -350,11 +296,13 @@ public function unlink($name) /** * Remove all broken symbolic links. - * - * @return void */ - public function pruneLinks() + public function pruneLinks(): void { + if (! $this->files->isDir(VALET_HOME_PATH)) { + return; + } + $this->files->ensureDirExists($this->sitesPath(), user()); $this->files->removeBrokenLinksAt($this->sitesPath()); @@ -362,11 +310,8 @@ public function pruneLinks() /** * Get the PHP version for the given site. - * - * @param string $url Site URL including the TLD - * @return string */ - public function getPhpVersion($url) + public function getPhpVersion(string $url): string { $defaultPhpVersion = $this->brew->linkedPhp(); $phpVersion = PhpFpm::normalizePhpVersion($this->customPhpVersion($url)); @@ -383,10 +328,8 @@ public function getPhpVersion($url) * There are only two supported values: tld and loopback * And those must be submitted in pairs else unexpected results may occur. * eg: both $old and $new should contain the same indexes. - * - * @return void */ - public function resecureForNewConfiguration(array $old, array $new) + public function resecureForNewConfiguration(array $old, array $new): void { if (! $this->files->exists($this->certificatesPath())) { return; @@ -427,13 +370,8 @@ public function resecureForNewConfiguration(array $old, array $new) /** * Parse Nginx site config file contents to swap old domain to new. - * - * @param string $siteConf Nginx site config content - * @param string $old Old domain - * @param string $new New domain - * @return string */ - public function replaceOldDomainWithNew($siteConf, $old, $new) + public function replaceOldDomainWithNew(string $siteConf, string $old, string $new): string { $lookups = []; $lookups[] = '~server_name .*;~'; @@ -454,13 +392,8 @@ public function replaceOldDomainWithNew($siteConf, $old, $new) /** * Parse Nginx site config file contents to swap old loopback address to new. - * - * @param string $siteConf Nginx site config content - * @param string $old Old loopback address - * @param string $new New loopback address - * @return string */ - public function replaceOldLoopbackWithNew($siteConf, $old, $new) + public function replaceOldLoopbackWithNew(string $siteConf, string $old, string $new): string { $shouldComment = $new === VALET_LOOPBACK; @@ -491,10 +424,8 @@ public function replaceOldLoopbackWithNew($siteConf, $old, $new) /** * Get all of the URLs that are currently secured. - * - * @return array */ - public function secured() + public function secured(): array { return collect($this->files->scandir($this->certificatesPath())) ->filter(function ($file) { @@ -504,20 +435,24 @@ public function secured() })->unique()->values()->all(); } + public function isSecured(string $site): bool + { + $tld = $this->config->read()['tld']; + + return in_array($site.'.'.$tld, $this->secured()); + } + /** * Secure the given host with TLS. * - * @param string $url - * @param string $siteConf pregenerated Nginx config file contents + * @param string|null $siteConf pregenerated Nginx config file contents * @param int $certificateExpireInDays The number of days the self signed certificate is valid. * Certificates SHOULD NOT have a validity period greater than 397 days. * @param int $caExpireInYears The number of years the self signed certificate authority is valid. * * @see https://github.com/cabforum/servercert/blob/main/docs/BR.md - * - * @return void */ - public function secure($url, $siteConf = null, $certificateExpireInDays = 396, $caExpireInYears = 20) + public function secure(string $url, ?string $siteConf = null, int $certificateExpireInDays = 396, int $caExpireInYears = 20): void { // Extract in order to later preserve custom PHP version config when securing $phpVersion = $this->customPhpVersion($url); @@ -549,9 +484,8 @@ public function secure($url, $siteConf = null, $certificateExpireInDays = 396, $ * If CA and root certificates are nonexistent, create them and trust the root cert. * * @param int $caExpireInDays The number of days the self signed certificate authority is valid. - * @return void */ - public function createCa($caExpireInDays) + public function createCa(int $caExpireInDays): void { $caPemPath = $this->caPath('LaravelValetCASelfSigned.pem'); $caKeyPath = $this->caPath('LaravelValetCASelfSigned.key'); @@ -584,10 +518,8 @@ public function createCa($caExpireInDays) /** * If CA and root certificates exist, remove them. - * - * @return void */ - public function removeCa() + public function removeCa(): void { foreach (['pem', 'key', 'srl'] as $ending) { $path = $this->caPath('LaravelValetCASelfSigned.'.$ending); @@ -608,11 +540,9 @@ public function removeCa() /** * Create and trust a certificate for the given URL. * - * @param string $url * @param int $caExpireInDays The number of days the self signed certificate is valid. - * @return void */ - public function createCertificate($url, $caExpireInDays) + public function createCertificate(string $url, int $caExpireInDays): void { $caPemPath = $this->caPath('LaravelValetCASelfSigned.pem'); $caKeyPath = $this->caPath('LaravelValetCASelfSigned.key'); @@ -649,22 +579,16 @@ public function createCertificate($url, $caExpireInDays) /** * Create the private key for the TLS certificate. - * - * @param string $keyPath - * @return void */ - public function createPrivateKey($keyPath) + public function createPrivateKey(string $keyPath): void { $this->cli->runAsUser(sprintf('openssl genrsa -out "%s" 2048', $keyPath)); } /** * Create the signing request for the TLS certificate. - * - * @param string $keyPath - * @return void */ - public function createSigningRequest($url, $keyPath, $csrPath, $confPath) + public function createSigningRequest(string $url, string $keyPath, string $csrPath, string $confPath): void { $this->cli->runAsUser(sprintf( 'openssl req -new -key "%s" -out "%s" -subj "/C=/ST=/O=/localityName=/commonName=%s/organizationalUnitName=/emailAddress=%s%s/" -config "%s"', @@ -673,12 +597,9 @@ public function createSigningRequest($url, $keyPath, $csrPath, $confPath) } /** - * Trust the given root certificate file in the Mac Keychain. - * - * @param string $pemPath - * @return void + * Trust the given root certificate file in the macOS Keychain. */ - public function trustCa($caPemPath) + public function trustCa(string $caPemPath): void { $this->cli->run(sprintf( 'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "%s"', $caPemPath @@ -687,11 +608,8 @@ public function trustCa($caPemPath) /** * Trust the given certificate file in the Mac Keychain. - * - * @param string $crtPath - * @return void */ - public function trustCertificate($crtPath) + public function trustCertificate(string $crtPath): void { $this->cli->run(sprintf( 'sudo security add-trusted-cert -d -r trustAsRoot -k /Library/Keychains/System.keychain "%s"', $crtPath @@ -700,11 +618,8 @@ public function trustCertificate($crtPath) /** * Build the SSL config for the given URL. - * - * @param string $url - * @return string */ - public function buildCertificateConf($path, $url) + public function buildCertificateConf(string $path, string $url): void { $config = str_replace('VALET_DOMAIN', $url, $this->files->getStub('openssl.conf')); $this->files->putAsUser($path, $config); @@ -712,12 +627,8 @@ public function buildCertificateConf($path, $url) /** * Build the TLS secured Nginx server for the given URL. - * - * @param string $url - * @param string $siteConf (optional) Nginx site config file content - * @return string */ - public function buildSecureNginxServer($url, $siteConf = null) + public function buildSecureNginxServer(string $url, ?string $siteConf = null): string { if ($siteConf === null) { $siteConf = $this->replaceOldLoopbackWithNew( @@ -744,12 +655,8 @@ 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) + public function isolate(string $valetSite, string $phpVersion): void { if ($this->files->exists($this->nginxPath($valetSite))) { // Modify the existing config if it exists (likely because it's secured) @@ -768,11 +675,8 @@ public function isolate($valetSite, $phpVersion) /** * Remove PHP Version isolation from a specific site. - * - * @param string $valetSite - * @return void */ - public function removeIsolation($valetSite) + public function removeIsolation(string $valetSite): void { // 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 @@ -787,11 +691,8 @@ public function removeIsolation($valetSite) /** * Unsecure the given URL so that it will use HTTP again. - * - * @param string $url - * @return void */ - public function unsecure($url) + public function unsecure(string $url): void { // Extract in order to later preserve custom PHP version config when unsecuring. Example output: "74" $phpVersion = $this->customPhpVersion($url); @@ -818,7 +719,10 @@ public function unsecure($url) } } - public function unsecureAll() + /** + * Un-secure all sites. + */ + public function unsecureAll(): void { $tld = $this->config->read()['tld']; @@ -828,7 +732,9 @@ public function unsecureAll() ->where('secured', ' X'); if ($secured->count() === 0) { - return info('No sites to unsecure. You may list all servable sites or links by running valet parked or valet links.'); + info('No sites to unsecure. You may list all servable sites or links by running valet parked or valet links.'); + + return; } info('Attempting to unsecure the following sites:'); @@ -854,10 +760,8 @@ public function unsecureAll() * * @param string $url The domain name to serve * @param string $host The URL to proxy to, eg: http://127.0.0.1:8080 - * @param bool $secure - * @return string */ - public function proxyCreate($url, $host, $secure = false) + public function proxyCreate(string $url, string $host, bool $secure = false): void { if (! preg_match('~^https?://.*$~', $host)) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid URL', $host)); @@ -893,11 +797,8 @@ public function proxyCreate($url, $host, $secure = false) /** * Unsecure the given URL so that it will use HTTP again. - * - * @param string $url - * @return void */ - public function proxyDelete($url) + public function proxyDelete(string $url): void { $tld = $this->config->read()['tld']; if (! ends_with($url, '.'.$tld)) { @@ -912,12 +813,8 @@ public function proxyDelete($url) /** * Create the given nginx host. - * - * @param string $url - * @param string $siteConf pregenerated Nginx config file contents - * @return void */ - public function put($url, $siteConf) + public function put(string $url, string $siteConf): void { $this->unsecure($url); @@ -930,12 +827,8 @@ public function put($url, $siteConf) /** * Remove old loopback interface alias and add a new one if necessary. - * - * @param string $oldLoopback - * @param string $loopback - * @return void */ - public function aliasLoopback($oldLoopback, $loopback) + public function aliasLoopback(string $oldLoopback, string $loopback): void { if ($oldLoopback !== VALET_LOOPBACK) { $this->removeLoopbackAlias($oldLoopback); @@ -950,11 +843,8 @@ public function aliasLoopback($oldLoopback, $loopback) /** * Remove loopback interface alias. - * - * @param string $loopback - * @return void */ - public function removeLoopbackAlias($loopback) + public function removeLoopbackAlias(string $loopback): void { $this->cli->run(sprintf( 'sudo ifconfig lo0 -alias %s', $loopback @@ -965,11 +855,8 @@ public function removeLoopbackAlias($loopback) /** * Add loopback interface alias. - * - * @param string $loopback - * @return void */ - public function addLoopbackAlias($loopback) + public function addLoopbackAlias(string $loopback): void { $this->cli->run(sprintf( 'sudo ifconfig lo0 alias %s', $loopback @@ -980,11 +867,8 @@ public function addLoopbackAlias($loopback) /** * Remove old LaunchDaemon and create a new one if necessary. - * - * @param string $loopback - * @return void */ - public function updateLoopbackPlist($loopback) + public function updateLoopbackPlist(string $loopback): void { $this->removeLoopbackPlist(); @@ -1004,10 +888,8 @@ public function updateLoopbackPlist($loopback) /** * Remove loopback interface alias launch daemon plist file. - * - * @return void */ - public function removeLoopbackPlist() + public function removeLoopbackPlist(): void { if ($this->files->exists($this->plistPath())) { $this->files->unlink($this->plistPath()); @@ -1018,10 +900,8 @@ public function removeLoopbackPlist() /** * Remove loopback interface alias and launch daemon plist file for uninstall purpose. - * - * @return void */ - public function uninstallLoopback() + public function uninstallLoopback(): void { if (($loopback = $this->valetLoopback()) !== VALET_LOOPBACK) { $this->removeLoopbackAlias($loopback); @@ -1030,22 +910,26 @@ public function uninstallLoopback() $this->removeLoopbackPlist(); } - public function valetHomePath() + /** + * Return Valet home path constant. + */ + public function valetHomePath(): string { return VALET_HOME_PATH; } - public function valetLoopback() + /** + * Return Valet loopback configuration. + */ + public function valetLoopback(): string { return $this->config->read()['loopback']; } /** * Get the path to loopback LaunchDaemon. - * - * @return string */ - public function plistPath() + public function plistPath(): string { return '/Library/LaunchDaemons/com.laravel.valet.loopback.plist'; } @@ -1053,37 +937,31 @@ public function plistPath() /** * Get the path to Nginx site configuration files. */ - public function nginxPath($additionalPath = null) + public function nginxPath(?string $additionalPath = null): string { return $this->valetHomePath().'/Nginx'.($additionalPath ? '/'.$additionalPath : ''); } /** * Get the path to the linked Valet sites. - * - * @return string */ - public function sitesPath($link = null) + public function sitesPath(?string $link = null): string { return $this->valetHomePath().'/Sites'.($link ? '/'.$link : ''); } /** * Get the path to the Valet CA certificates. - * - * @return string */ - public function caPath($caFile = null) + public function caPath(?string $caFile = null): string { return $this->valetHomePath().'/CA'.($caFile ? '/'.$caFile : ''); } /** * Get the path to the Valet TLS certificates. - * - * @return string */ - public function certificatesPath($url = null, $extension = null) + public function certificatesPath(?string $url = null, ?string $extension = null): string { $url = $url ? '/'.$url : ''; $extension = $extension ? '.'.$extension : ''; @@ -1093,10 +971,8 @@ public function certificatesPath($url = null, $extension = null) /** * Make the domain name based on parked domains or the internal TLD. - * - * @return string */ - public function domain($domain) + public function domain(?string $domain): string { // if ($this->parked()->pluck('site')->contains($domain)) { // return $domain; @@ -1106,16 +982,19 @@ public function domain($domain) // return $parked['site']; // } + // Don't add .TLD if user already passed the string in with the TLD on the end + if ($domain && str_contains($domain, '.'.$this->config->read()['tld'])) { + return $domain; + } + + // Return either the passed domain, or the current folder name, with .TLD appended return ($domain ?: $this->host(getcwd())).'.'.$this->config->read()['tld']; } /** * Replace Loopback configuration line in Valet site configuration file contents. - * - * @param string $siteConf - * @return string */ - public function replaceLoopback($siteConf) + public function replaceLoopback(string $siteConf): string { $loopback = $this->config->read()['loopback']; @@ -1134,11 +1013,8 @@ public function replaceLoopback($siteConf) /** * Extract PHP version of exising nginx conifg. - * - * @param string $url - * @return string|void */ - public function customPhpVersion($url) + public function customPhpVersion(string $url): ?string { if ($this->files->exists($this->nginxPath($url))) { $siteConf = $this->files->get($this->nginxPath($url)); @@ -1149,16 +1025,14 @@ public function customPhpVersion($url) return preg_replace("/[^\d]*/", '', $firstLine); // Example output: "74" or "81" } } + + return null; } /** * Replace .sock file in an Nginx site configuration file contents. - * - * @param string $siteConf - * @param string $phpVersion - * @return string */ - public function replaceSockFile($siteConf, $phpVersion) + public function replaceSockFile(string $siteConf, string $phpVersion): string { $sockFile = PhpFpm::fpmSockName($phpVersion); @@ -1169,28 +1043,50 @@ public function replaceSockFile($siteConf, $phpVersion) } /** - * Get PHP version from .valetphprc for a site. - * - * @param string $site - * @param string $cwd In contexts in which a current working directory has been passed, - * the cwd, which is prioritized over looking for the site's information - * from the config. - * @return string|null + * Get configuration items defined in .valetrc for a site. */ - public function phpRcVersion($site, $cwd = null) + public function valetRc(string $siteName, ?string $cwd = null): array { if ($cwd) { - $path = $cwd.'/.valetphprc'; - } elseif ($site = $this->parked()->merge($this->links())->where('site', $site)->first()) { - $path = data_get($site, 'path').'/.valetphprc'; + $path = $cwd.'/.valetrc'; + } elseif ($site = $this->parked()->merge($this->links())->where('site', $siteName)->first()) { + $path = data_get($site, 'path').'/.valetrc'; + } else { + return []; } - if (! isset($path)) { + if ($this->files->exists($path)) { + return collect(explode(PHP_EOL, trim($this->files->get($path))))->filter(function ($line) { + return str_contains($line, '='); + })->mapWithKeys(function ($item, $index) { + [$key, $value] = explode('=', $item); + + return [strtolower($key) => $value]; + })->all(); + } + + return []; + } + + /** + * Get PHP version from .valetrc or .valetphprc for a site. + */ + public function phpRcVersion(string $siteName, ?string $cwd = null): ?string + { + if ($cwd) { + $oldPath = $cwd.'/.valetphprc'; + } elseif ($site = $this->parked()->merge($this->links())->where('site', $siteName)->first()) { + $oldPath = data_get($site, 'path').'/.valetphprc'; + } else { return null; } - if ($this->files->exists($path)) { - return PhpFpm::normalizePhpVersion(trim($this->files->get($path))); + if ($this->files->exists($oldPath)) { + return PhpFpm::normalizePhpVersion(trim($this->files->get($oldPath))); } + + $valetRc = $this->valetRc($siteName, $cwd); + + return PhpFpm::normalizePhpVersion(data_get($valetRc, 'php')); } } diff --git a/cli/Valet/Status.php b/cli/Valet/Status.php new file mode 100644 index 000000000..3f757a51d --- /dev/null +++ b/cli/Valet/Status.php @@ -0,0 +1,210 @@ +checks())->map(function (array $check) use (&$isValid) { + if (! $thisIsValid = $check['check']()) { + $this->debugInstructions[] = $check['debug']; + $isValid = false; + } + + return ['description' => $check['description'], 'success' => $thisIsValid ? 'Yes' : 'No']; + }); + + return [ + 'success' => $isValid, + 'output' => $output->all(), + 'debug' => $this->debugInstructions(), + ]; + } + + /** + * Define a list of checks to test the health of the Valet ecosystem of tools and configs. + */ + public function checks(): array + { + $linkedPhp = $this->brew->getLinkedPhpFormula(); + + return [ + [ + 'description' => 'Is Valet fully installed?', + 'check' => function () { + return $this->valetInstalled(); + }, + 'debug' => 'Run `composer require laravel/valet` and `valet install`.', + ], + [ + 'description' => 'Is Valet config valid?', + 'check' => function () { + try { + $config = $this->config->read(); + + foreach (['tld', 'loopback', 'paths'] as $key) { + if (! array_key_exists($key, $config)) { + $this->debugInstructions[] = 'Your Valet config is missing the "'.$key.'" key. Re-add this manually, or delete your config file and re-install.'; + + return false; + } + } + + return true; + } catch (\JsonException $e) { + return false; + } + }, + 'debug' => 'Run `valet install` to update your configuration.', + ], + [ + 'description' => 'Is Homebrew installed?', + 'check' => function () { + return $this->cli->run('which brew') !== ''; + }, + 'debug' => 'Visit https://brew.sh/ for instructions on installing Homebrew.', + ], + [ + 'description' => 'Is DnsMasq installed?', + 'check' => function () { + return $this->brew->installed('dnsmasq'); + }, + 'debug' => 'Run `valet install`.', + ], + [ + 'description' => 'Is Dnsmasq running?', + 'check' => function () { + return $this->isBrewServiceRunning('dnsmasq'); + }, + 'debug' => 'Run `valet restart`.', + ], + [ + 'description' => 'Is Dnsmasq running as root?', + 'check' => function () { + return $this->isBrewServiceRunningAsRoot('dnsmasq'); + }, + 'debug' => 'Uninstall Dnsmasq with Brew and run `valet install`.', + ], + [ + 'description' => 'Is Nginx installed?', + 'check' => function () { + return $this->brew->installed('nginx') || $this->brew->installed('nginx-full'); + }, + 'debug' => 'Run `valet install`.', + ], + [ + 'description' => 'Is Nginx running?', + 'check' => function () { + return $this->isBrewServiceRunning('nginx'); + }, + 'debug' => 'Run `valet restart`.', + ], + [ + 'description' => 'Is Nginx running as root?', + 'check' => function () { + return $this->isBrewServiceRunningAsRoot('nginx'); + }, + 'debug' => 'Uninstall nginx with Brew and run `valet install`.', + ], + [ + 'description' => 'Is PHP installed?', + 'check' => function () { + return $this->brew->hasInstalledPhp(); + }, + 'debug' => 'Run `valet install`.', + ], + [ + 'description' => 'Is linked PHP ('.$linkedPhp.') running?', + 'check' => function () use ($linkedPhp) { + return $this->isBrewServiceRunning($linkedPhp); + }, + 'debug' => 'Run `valet restart`.', + ], + [ + 'description' => 'Is linked PHP ('.$linkedPhp.') running as root?', + 'check' => function () use ($linkedPhp) { + return $this->isBrewServiceRunningAsRoot($linkedPhp); + }, + 'debug' => 'Uninstall PHP with Brew and run `valet use php@8.2`', + ], + [ + 'description' => 'Is valet.sock present?', + 'check' => function () { + return $this->files->exists(VALET_HOME_PATH.'/valet.sock'); + }, + 'debug' => 'Run `valet install`.', + ], + ]; + } + + public function isBrewServiceRunning(string $name, bool $exactMatch = true): bool + { + return $this->isBrewServiceRunningAsUser($name, $exactMatch) + || $this->isBrewServiceRunningAsRoot($name, $exactMatch); + } + + public function isBrewServiceRunningAsRoot(string $name, bool $exactMatch = true): bool + { + if (! $this->brewServicesRootOutput) { + $this->brewServicesRootOutput = json_decode($this->cli->run('brew services info --all --json'), false); + } + + return $this->isBrewServiceRunningGivenServiceList($this->brewServicesRootOutput, $name, $exactMatch); + } + + public function isBrewServiceRunningAsUser(string $name, bool $exactMatch = true): bool + { + if (! $this->brewServicesUserOutput) { + $this->brewServicesUserOutput = json_decode($this->cli->runAsUser('brew services info --all --json'), false); + } + + return $this->isBrewServiceRunningGivenServiceList($this->brewServicesUserOutput, $name, $exactMatch); + } + + protected function isBrewServiceRunningGivenServiceList(array $serviceList, string $name, bool $exactMatch = true): bool + { + foreach ($serviceList as $service) { + if ($service->running === true) { + if ($exactMatch && $service->name == $name) { + return true; + } elseif (! $exactMatch && str_contains($service->name, $name)) { + return true; + } + } + } + + return false; + } + + public function valetInstalled(): bool + { + return is_dir(VALET_HOME_PATH) + && file_exists($this->config->path()) + && is_dir(VALET_HOME_PATH.'/Drivers') + && is_dir(VALET_HOME_PATH.'/Sites') + && is_dir(VALET_HOME_PATH.'/Log') + && is_dir(VALET_HOME_PATH.'/Certificates'); + } + + public function debugInstructions(): string + { + return collect($this->debugInstructions)->unique()->join(PHP_EOL); + } +} diff --git a/cli/Valet/Upgrader.php b/cli/Valet/Upgrader.php new file mode 100644 index 000000000..970573531 --- /dev/null +++ b/cli/Valet/Upgrader.php @@ -0,0 +1,98 @@ +pruneMissingDirectories(); + $this->pruneSymbolicLinks(); + $this->fixOldSampleValetDriver(); + $this->errorIfOldCustomDrivers(); + } + + /** + * Prune all non-existent paths from the configuration. + */ + public function pruneMissingDirectories(): void + { + try { + Configuration::prune(); + } catch (\JsonException $e) { + warning('Invalid confiuration file at '.Configuration::path().'.'); + exit; + } + } + + /** + * Remove all broken symbolic links in the Valet config Sites diretory. + */ + public function pruneSymbolicLinks(): void + { + Site::pruneLinks(); + } + + /** + * If the user has the old `SampleValetDriver` without the Valet namespace, + * replace it with the new `SampleValetDriver` that uses the namespace. + */ + public function fixOldSampleValetDriver(): void + { + $samplePath = VALET_HOME_PATH.'/Drivers/SampleValetDriver.php'; + + if ($this->files->exists($samplePath)) { + $contents = $this->files->get($samplePath); + + if (! str_contains($contents, 'namespace')) { + if ($contents !== $this->files->get(__DIR__.'/../stubs/Valet3SampleValetDriver.php')) { + warning('Existing SampleValetDriver.php has been customized.'); + warning('Backing up at '.$samplePath.'.bak'); + + $this->files->putAsUser( + VALET_HOME_PATH.'/Drivers/SampleValetDriver.php.bak', + $contents + ); + } + + $this->files->putAsUser( + VALET_HOME_PATH.'/Drivers/SampleValetDriver.php', + $this->files->getStub('SampleValetDriver.php') + ); + } + } + } + + /** + * Throw an exception if the user has old (non-namespaced) custom drivers. + */ + public function errorIfOldCustomDrivers(): void + { + $driversPath = VALET_HOME_PATH.'/Drivers'; + + if (! $this->files->isDir($driversPath)) { + return; + } + + foreach ($this->files->scanDir($driversPath) as $driver) { + if (! ends_with($driver, 'ValetDriver.php')) { + continue; + } + + if (! str_contains($this->files->get($driversPath.'/'.$driver), 'namespace')) { + warning('Please make sure all custom drivers have been upgraded for Valet 4.'); + exit; + } + } + } +} diff --git a/cli/Valet/Valet.php b/cli/Valet/Valet.php index 344108355..35910ccb9 100644 --- a/cli/Valet/Valet.php +++ b/cli/Valet/Valet.php @@ -6,27 +6,16 @@ class Valet { - public $cli; - - public $files; - public $valetBin = BREW_PREFIX.'/bin/valet'; - /** - * Create a new Valet instance. - */ - public function __construct(CommandLine $cli, Filesystem $files) + public function __construct(public CommandLine $cli, public Filesystem $files) { - $this->cli = $cli; - $this->files = $files; } /** * Symlink the Valet Bash script into the user's local bin. - * - * @return void */ - public function symlinkToUsersBin() + public function symlinkToUsersBin(): void { $this->unlinkFromUsersBin(); @@ -35,44 +24,18 @@ public function symlinkToUsersBin() /** * Remove the symlink from the user's local bin. - * - * @return void */ - public function unlinkFromUsersBin() + public function unlinkFromUsersBin(): void { $this->cli->quietlyAsUser('rm '.$this->valetBin); } - /** - * Get the paths to all of the Valet extensions. - * - * @return array - */ - public function extensions() - { - if (! $this->files->isDir(VALET_HOME_PATH.'/Extensions')) { - return []; - } - - return collect($this->files->scandir(VALET_HOME_PATH.'/Extensions')) - ->reject(function ($file) { - return is_dir($file); - }) - ->map(function ($file) { - return VALET_HOME_PATH.'/Extensions/'.$file; - }) - ->values()->all(); - } - /** * Determine if this is the latest version of Valet. * - * @param string $currentVersion - * @return bool - * * @throws \GuzzleHttp\Exception\GuzzleException */ - public function onLatestVersion($currentVersion) + public function onLatestVersion(string $currentVersion): bool { $url = 'https://api.github.com/repos/laravel/valet/releases/latest'; $response = json_decode((new Client())->get($url)->getBody()); @@ -82,10 +45,8 @@ public function onLatestVersion($currentVersion) /** * Create the "sudoers.d" entry for running Valet. - * - * @return void */ - public function createSudoersEntry() + public function createSudoersEntry(): void { $this->files->ensureDirExists('/etc/sudoers.d'); @@ -95,10 +56,8 @@ public function createSudoersEntry() /** * Remove the "sudoers.d" entry for running Valet. - * - * @return void */ - public function removeSudoersEntry() + public function removeSudoersEntry(): void { $this->cli->quietly('rm /etc/sudoers.d/valet'); } @@ -106,7 +65,7 @@ public function removeSudoersEntry() /** * Run composer global diagnose. */ - public function composerGlobalDiagnose() + public function composerGlobalDiagnose(): void { $this->cli->runAsUser('composer global diagnose'); } @@ -114,8 +73,69 @@ public function composerGlobalDiagnose() /** * Run composer global update. */ - public function composerGlobalUpdate() + public function composerGlobalUpdate(): void { $this->cli->runAsUser('composer global update'); } + + public function forceUninstallText(): string + { + return 'NOTE: +Valet has attempted to uninstall itself, but there are some steps you need to do manually: + +1. Run php -v, and also which php, to see what PHP version you are now really using. +2. Run composer global update to update your globally-installed Composer packages to work with your default PHP. + NOTE: Composer may have other dependencies for other global apps you have installed, and those may not be compatible with your default PHP. +3. Finish removing any Composer fragments of Valet: + Run composer global remove laravel/valet + and then rm '.BREW_PREFIX.'/bin/valet to remove the Valet bin link if it still exists. + +Optional: +- brew list --formula will show any other Homebrew services installed, in case you want to make changes to those as well. +- brew doctor can indicate if there might be any broken things left behind. +- brew cleanup can purge old cached Homebrew downloads. + +If you had customized your Mac DNS settings in System Preferences->Network, you will need to remove 127.0.0.1 from that list. + +You may also want to open Keychain Access and search for valet to remove any leftover trust certificates.'; + } + + public function uninstallText(): string + { + return ' +You did not pass the --force parameter, so this will only return instructions on how to uninstall, not ACTUALLY uninstall anything. +A --force removal WILL delete your custom configuration information, so be sure to make backups first. + +IF YOU WANT TO UNINSTALL VALET MANUALLY, DO THE FOLLOWING... + +1. Valet Keychain Certificates +Before removing Valet configuration files, we recommend that you run valet unsecure --all to clean up the certificates that Valet inserted into your Keychain. +Alternatively you can do a search for @laravel.valet in Keychain Access and delete those certificates there manually. + +2. Valet Configuration Files +You may remove your user-specific Valet config files by running: rm -rf ~/.config/valet + +3. Remove Valet package +You can run composer global remove laravel/valet to uninstall the Valet package. + +4. Homebrew Services +You may remove the core services (php, nginx, dnsmasq) by running: brew uninstall --force php nginx dnsmasq +You can then remove selected leftover configurations for these services manually in both '.BREW_PREFIX.'/etc/ and '.BREW_PREFIX.'/logs/. +(If you have other PHP versions installed, run brew list --formula | grep php to see which versions you should also uninstall manually.) + +BEWARE: Uninstalling PHP via Homebrew will leave your Mac with its original PHP version, which may not be compatible with other Composer dependencies you have installed. As a result, you may get unexpected errors. + +If you have customized your Mac DNS settings in System Preferences->Network, you may need to add or remove 127.0.0.1 from the top of that list. + +5. GENERAL TROUBLESHOOTING +If your reasons for considering an uninstall are more for troubleshooting purposes, consider running brew doctor and/or brew cleanup to see if any problems exist there. +Also consider running sudo nginx -t to test your nginx configs in case there are failures/errors there preventing nginx from running. +Most of the nginx configs used by Valet are in your ~/.config/valet/Nginx directory. + +You might also want to investigate your global Composer configs. Helpful commands include: +composer global update to apply updates to packages +composer global outdated to identify outdated packages +composer global diagnose to run diagnostics + '; + } } diff --git a/cli/app.php b/cli/app.php index 91480edd8..5fd8e6d94 100644 --- a/cli/app.php +++ b/cli/app.php @@ -2,6 +2,7 @@ use Illuminate\Container\Container; use Silly\Application; +use Silly\Command\Command; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Input\InputInterface; @@ -15,6 +16,8 @@ use function Valet\warning; use function Valet\writer; +$version = '4.0.0'; + /** * Load correct autoloader depending on install location. */ @@ -26,24 +29,16 @@ require_once getenv('HOME').'/.composer/vendor/autoload.php'; } -/** - * Relocate config dir to ~/.config/valet/ if found in old location. - */ -if (is_dir(VALET_LEGACY_HOME_PATH) && ! is_dir(VALET_HOME_PATH)) { - Configuration::createConfigurationDirectory(); -} - /** * Create the application. */ Container::setInstance(new Container); -$version = '3.3.3'; +$version = '4.0.0'; $app = new Application('Laravel Valet', $version); -$dispatcher = new EventDispatcher(); -$app->setDispatcher($dispatcher); +$app->setDispatcher($dispatcher = new EventDispatcher()); $dispatcher->addListener( ConsoleEvents::COMMAND, @@ -51,14 +46,7 @@ function (ConsoleCommandEvent $event) { writer($event->getOutput()); }); -/** - * Prune missing directories and symbolic links on every command. - */ -if (is_dir(VALET_HOME_PATH)) { - Configuration::prune(); - - Site::pruneLinks(); -} +Upgrader::onEveryRun(); /** * Install Valet and any required services. @@ -67,21 +55,52 @@ function (ConsoleCommandEvent $event) { Nginx::stop(); Configuration::install(); + output(); Nginx::install(); + output(); PhpFpm::install(); + output(); DnsMasq::install(Configuration::read()['tld']); + output(); Nginx::restart(); + output(); Valet::symlinkToUsersBin(); output(PHP_EOL.'Valet installed successfully!'); })->descriptions('Install the Valet services'); +/** + * Output the status of Valet and its installed services and config. + */ +$app->command('status', function (OutputInterface $output) { + info('Checking status...'); + + $status = Status::check(); + + if ($status['success']) { + info("\nValet status: Healthy\n"); + } else { + warning("\nValet status: Error\n"); + } + + table(['Check', 'Success?'], $status['output']); + + if ($status['success']) { + return Command::SUCCESS; + } + + info(PHP_EOL.'Debug suggestions:'); + info($status['debug']); + + return Command::FAILURE; +})->descriptions('Output the status of Valet and its installed services and config.'); + /** * Most commands are available only if valet is installed. */ if (is_dir(VALET_HOME_PATH)) { /** - * Upgrade helper: ensure the tld config exists or the loopback config exists. + * Upgrade helper: ensure the tld config exists and the loopback config exists. */ if (empty(Configuration::read()['tld']) || empty(Configuration::read()['loopback'])) { Configuration::writeBaseConfiguration(); @@ -142,7 +161,7 @@ function (ConsoleCommandEvent $event) { Nginx::installServer(); Nginx::restart(); - info('Your valet loopback address has been updated to ['.$loopback.']'); + info('Your Valet loopback address has been updated to ['.$loopback.']'); })->descriptions('Get or set the loopback address used for Valet sites'); /** @@ -175,7 +194,7 @@ function (ConsoleCommandEvent $event) { /** * Register a symbolic link with Valet. */ - $app->command('link [name] [--secure] [--isolate]', function (OutputInterface $output, $name, $secure, $isolate) { + $app->command('link [name] [--secure] [--isolate]', function ($name, $secure, $isolate) { $linkPath = Site::link(getcwd(), $name = $name ?: basename(getcwd())); info('A ['.$name.'] symbolic link has been created in ['.$linkPath.'].'); @@ -193,7 +212,7 @@ function (ConsoleCommandEvent $event) { } })->descriptions('Link the current working directory to Valet', [ '--secure' => 'Link the site with a trusted TLS certificate.', - '--isolate' => 'Isolate the site to the PHP version specified in the current working directory\'s .valetphprc file.', + '--isolate' => 'Isolate the site to the PHP version specified in the current working directory\'s .valetrc file.', ]); /** @@ -209,7 +228,16 @@ function (ConsoleCommandEvent $event) { * Unlink a link from the Valet links directory. */ $app->command('unlink [name]', function (OutputInterface $output, $name) { - info('The ['.Site::unlink($name).'] symbolic link has been removed.'); + $name = Site::unlink($name); + info('The ['.$name.'] symbolic link has been removed.'); + + if (Site::isSecured($name)) { + info('Unsecuring '.$name.'...'); + + Site::unsecure(Site::domain($name)); + + Nginx::restart(); + } })->descriptions('Remove the specified Valet link'); /** @@ -234,6 +262,10 @@ function (ConsoleCommandEvent $event) { if ($all) { Site::unsecureAll(); + Nginx::restart(); + + info('All Valet sites will now serve traffic over HTTP.'); + return; } @@ -285,7 +317,7 @@ function (ConsoleCommandEvent $event) { })->descriptions('Display all of the proxy sites'); /** - * Determine which Valet driver the current directory is using. + * Display which Valet driver the current directory is using. */ $app->command('which', function (OutputInterface $output) { $driver = ValetDriver::assign(getcwd(), basename(getcwd()), '/'); @@ -295,7 +327,7 @@ function (ConsoleCommandEvent $event) { } else { warning('Valet could not determine which driver to use for this site.'); } - })->descriptions('Determine which Valet driver serves the current working directory'); + })->descriptions('Display which Valet driver serves the current working directory'); /** * Display all of the registered paths. @@ -328,9 +360,78 @@ function (ConsoleCommandEvent $event) { /** * Echo the currently tunneled URL. */ - $app->command('fetch-share-url [domain]', function (OutputInterface $output, $domain = null) { - output(Ngrok::currentTunnelUrl(Site::domain($domain))); - })->descriptions('Get the URL to the current Ngrok tunnel'); + $app->command('fetch-share-url [domain]', function ($domain = null) { + $tool = Configuration::read()['share-tool'] ?? null; + + switch ($tool) { + case 'expose': + if ($url = Expose::currentTunnelUrl($domain ?: Site::host(getcwd()))) { + output($url); + } + break; + case 'ngrok': + try { + output(Ngrok::currentTunnelUrl(Site::domain($domain))); + } catch (\Throwable $e) { + warning($e->getMessage()); + } + break; + default: + info('Please set your share tool with `valet share-tool expose` or `valet share-tool ngrok`.'); + + return Command::FAILURE; + } + })->descriptions('Get the URL to the current share tunnel (for Expose or ngrok)'); + + /** + * Echo or set the name of the currently-selected share tool (either "ngrok" or "expose"). + */ + $app->command('share-tool [tool]', function (InputInterface $input, OutputInterface $output, $tool = null) { + if ($tool === null) { + return output(Configuration::read()['share-tool'] ?? '(not set)'); + } + + if ($tool !== 'expose' && $tool !== 'ngrok') { + warning($tool.' is not a valid share tool. Please use `ngrok` or `expose`.'); + + return Command::FAILURE; + } + + Configuration::updateKey('share-tool', $tool); + info('Share tool set to '.$tool.'.'); + + if ($tool === 'expose') { + if (Expose::installed()) { + // @todo: Check it's the right version (has /api/tunnels/) + // E.g. if (Expose::installedVersion) + // if (version_compare(Expose::installedVersion(), $minimumExposeVersion) < 0) { + // prompt them to upgrade + return; + } + + $helper = $this->getHelperSet()->get('question'); + $question = new ConfirmationQuestion('Would you like to install Expose now? ', false); + + if (false === $helper->ask($input, $output, $question)) { + return; + } + + Expose::ensureInstalled(); + + return; + } + + if (! Ngrok::installed()) { + $helper = $this->getHelperSet()->get('question'); + $question = new ConfirmationQuestion('Would you like to install ngrok now? ', false); + + if (false === $helper->ask($input, $output, $question)) { + return; + } + + Ngrok::ensureInstalled(); + } + })->descriptions('Get the name of the current share tool (Expose or ngrok).'); /** * Set the ngrok auth token. @@ -426,9 +527,11 @@ function (ConsoleCommandEvent $event) { warning('YOU ARE ABOUT TO UNINSTALL Nginx, PHP, Dnsmasq and all Valet configs and logs.'); $helper = $this->getHelperSet()->get('question'); $question = new ConfirmationQuestion('Are you sure you want to proceed? ', false); + if (false === $helper->ask($input, $output, $question)) { return warning('Uninstall aborted.'); } + info('Removing certificates for all Secured sites...'); Site::unsecureAll(); info('Removing certificate authority...'); @@ -449,66 +552,12 @@ function (ConsoleCommandEvent $event) { Brew::removeSudoersEntry(); Valet::removeSudoersEntry(); - return output('NOTE: -Valet has attempted to uninstall itself, but there are some steps you need to do manually: -Run php -v to see what PHP version you are now really using. -Run composer global update to update your globally-installed Composer packages to work with your default PHP. -NOTE: Composer may have other dependencies for other global apps you have installed, and those may not be compatible with your default PHP. -Thus, you may need to delete things from your ~/.composer/composer.json file before running composer global update successfully. -Then to finish removing any Composer fragments of Valet: -Run composer global remove laravel/valet -and then rm '.BREW_PREFIX.'/bin/valet to remove the Valet bin link if it still exists. -Optional: -- brew list --formula will show any other Homebrew services installed, in case you want to make changes to those as well. -- brew doctor can indicate if there might be any broken things left behind. -- brew cleanup can purge old cached Homebrew downloads. -If you had customized your Mac DNS settings in System Preferences->Network, you will need to remove 127.0.0.1 from that list. -Additionally you might also want to open Keychain Access and search for valet to remove any leftover trust certificates. -'); - } - - output('WAIT! Before you uninstall things, consider cleaning things up in the following order. (Or skip to the bottom for troubleshooting suggestions.): -You did not pass the --force parameter so we are NOT ACTUALLY uninstalling anything. -A --force removal WILL delete your custom configuration information, so you will want to make backups first. - -IF YOU WANT TO UNINSTALL VALET MANUALLY, DO THE FOLLOWING... - -1. Valet Keychain Certificates -Before removing Valet configuration files, we recommend that you run valet unsecure --all to clean up the certificates that Valet inserted into your Keychain. -Alternatively you can do a search for @laravel.valet in Keychain Access and delete those certificates there manually. -You may also run valet parked to see a list of all sites Valet could serve. - -2. Valet Configuration Files -You may remove your user-specific Valet config files by running: rm -rf ~/.config/valet - -3. Remove Valet package -You can run composer global remove laravel/valet to uninstall the Valet package. - -4. Homebrew Services -You may remove the core services (php, nginx, dnsmasq) by running: brew uninstall --force php nginx dnsmasq -You can then remove selected leftover configurations for these services manually in both '.BREW_PREFIX.'/etc/ and '.BREW_PREFIX.'/logs/. -(If you have other PHP versions installed, run brew list --formula | grep php to see which versions you should also uninstall manually.) - -BEWARE: Uninstalling PHP via Homebrew will leave your Mac with its original PHP version, which may not be compatible with other Composer dependencies you have installed. Thus you may get unexpected errors. - -Some additional services which you may have installed (but which Valet does not directly configure or manage) include: mariadb mysql mailhog. -If you wish to also remove them, you may manually run brew uninstall SERVICENAME and clean up their configurations in '.BREW_PREFIX.'/etc if necessary. - -You can discover more Homebrew services by running: brew services list and brew list --formula - -If you have customized your Mac DNS settings in System Preferences->Network, you may need to add or remove 127.0.0.1 from the top of that list. - -5. GENERAL TROUBLESHOOTING -If your reasons for considering an uninstall are more for troubleshooting purposes, consider running brew doctor and/or brew cleanup to see if any problems exist there. -Also consider running sudo nginx -t to test your nginx configs in case there are failures/errors there preventing nginx from running. -Most of the nginx configs used by Valet are in your ~/.config/valet/Nginx directory. - -You might also want to investigate your global Composer configs. Helpful commands include: -composer global update to apply updates to packages -composer global outdated to identify outdated packages -composer global diagnose to run diagnostics -'); - // Stopping PHP so the ~/.config/valet/valet.sock file is released so the directory can be deleted if desired + return output(Valet::forceUninstallText()); + } + + output(Valet::uninstallText()); + + // Stop PHP so the ~/.config/valet/valet.sock file is released so the directory can be deleted if desired PhpFpm::stopRunning(); Nginx::stop(); })->descriptions('Uninstall the Valet services', ['--force' => 'Do a forceful uninstall of Valet and related Homebrew pkgs']); @@ -553,6 +602,7 @@ function (ConsoleCommandEvent $event) { $linkedVersion = Brew::linkedPhp(); if ($phpVersion = Site::phpRcVersion($site, getcwd())) { + info("Found '{$site}/.valetrc' or '{$site}/.valetphprc' specifying version: {$phpVersion}"); info("Found '{$site}/.valetphprc' specifying version: {$phpVersion}"); } else { $domain = $site.'.'.data_get(Configuration::read(), 'tld'); @@ -572,7 +622,7 @@ function (ConsoleCommandEvent $event) { PhpFpm::useVersion($phpVersion, $force); })->descriptions('Change the version of PHP used by Valet', [ - 'phpVersion' => 'The PHP version you want to use, e.g php@8.1', + 'phpVersion' => 'The PHP version you want to use; e.g. php@8.2', ]); /** @@ -584,12 +634,13 @@ function (ConsoleCommandEvent $event) { } if (is_null($phpVersion)) { - if ($phpVersion = Site::phpRcVersion($site)) { - info("Found '{$site}/.valetphprc' specifying version: {$phpVersion}"); + if ($phpVersion = Site::phpRcVersion($site, getcwd())) { + info("Found '{$site}/.valetrc' or '{$site}/.valetphprc' specifying version: {$phpVersion}"); } else { - info("\nPlease provide a version number. E.g.:"); + info(PHP_EOL.'Please provide a version number. E.g.:'); info('valet isolate php@8.2'); - exit; + + return; } } @@ -701,7 +752,7 @@ function (ConsoleCommandEvent $event) { 'to your "'.Configuration::path().'" file.', ])); - exit; + return; } if (! isset($logs[$key])) { @@ -761,11 +812,4 @@ function (ConsoleCommandEvent $event) { ]); } -/** - * Load all of the Valet extensions. - */ -foreach (Valet::extensions() as $extension) { - include $extension; -} - return $app; diff --git a/cli/includes/compatibility.php b/cli/includes/compatibility.php index 86e57fe74..605acc21d 100644 --- a/cli/includes/compatibility.php +++ b/cli/includes/compatibility.php @@ -1,7 +1,7 @@ make(static::containerKey()); @@ -38,6 +32,9 @@ class Nginx extends Facade class CommandLine extends Facade { } +class Composer extends Facade +{ +} class Configuration extends Facade { } @@ -47,6 +44,9 @@ class Diagnose extends Facade class DnsMasq extends Facade { } +class Expose extends Facade +{ +} class Filesystem extends Facade { } @@ -59,6 +59,12 @@ class PhpFpm extends Facade class Site extends Facade { } +class Status extends Facade +{ +} +class Upgrader extends Facade +{ +} class Valet extends Facade { } diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php index af20c8d4c..f9d0ce89e 100644 --- a/cli/includes/helpers.php +++ b/cli/includes/helpers.php @@ -6,6 +6,7 @@ use Illuminate\Container\Container; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; /** * Define constants. @@ -23,7 +24,6 @@ define('VALET_LOOPBACK', '127.0.0.1'); define('VALET_SERVER_PATH', realpath(__DIR__.'/../../server.php')); -define('VALET_LEGACY_HOME_PATH', $_SERVER['HOME'].'/.valet'); define('BREW_PREFIX', (new CommandLine())->runAsUser('printf $(brew --prefix)')); @@ -31,11 +31,8 @@ /** * Set or get a global console writer. - * - * @param null|OutputInterface $writer - * @return OutputInterface|\NullWriter|null */ -function writer($writer = null) +function writer(?OutputInterface $writer = null): OutputInterface|\NullWriter|null { $container = Container::getInstance(); @@ -54,32 +51,24 @@ function writer($writer = null) /** * Output the given text to the console. - * - * @param string $output - * @return void */ -function info($output) +function info($output): void { output(''.$output.''); } /** * Output the given text to the console. - * - * @param string $output - * @return void */ -function warning($output) +function warning(string $output): void { output(''.$output.''); } /** * Output a table to the console. - * - * @return void */ -function table(array $headers = [], array $rows = []) +function table(array $headers = [], array $rows = []): void { $table = new Table(writer()); @@ -90,57 +79,40 @@ function table(array $headers = [], array $rows = []) /** * Return whether the app is in the testing environment. - * - * @return bool */ -function testing() +function testing(): bool { return strpos($_SERVER['SCRIPT_NAME'], 'phpunit') !== false; } /** * Output the given text to the console. - * - * @param string $output - * @return void */ -function output($output) +function output(?string $output = ''): void { writer()->writeln($output); } /** * Resolve the given class from the container. - * - * @param string $class - * @return mixed */ -function resolve($class) +function resolve(string $class): mixed { return Container::getInstance()->make($class); } /** * Swap the given class implementation in the container. - * - * @param string $class - * @param mixed $instance - * @return void */ -function swap($class, $instance) +function swap(string $class, mixed $instance): void { Container::getInstance()->instance($class, $instance); } /** * Retry the given function N times. - * - * @param int $retries - * @param callable $retries - * @param int $sleep - * @return mixed */ -function retry($retries, $fn, $sleep = 0) +function retry(int $retries, callable $fn, int $sleep = 0): mixed { beginning: try { @@ -162,10 +134,8 @@ function retry($retries, $fn, $sleep = 0) /** * Verify that the script is currently running as "sudo". - * - * @return void */ -function should_be_sudo() +function should_be_sudo(): void { if (! isset($_SERVER['SUDO_USER'])) { throw new Exception('This command must be run with sudo.'); @@ -174,11 +144,8 @@ function should_be_sudo() /** * Tap the given value. - * - * @param mixed $value - * @return mixed */ -function tap($value, callable $callback) +function tap(mixed $value, callable $callback): mixed { $callback($value); @@ -187,12 +154,8 @@ function tap($value, callable $callback) /** * Determine if a given string ends with a given substring. - * - * @param string $haystack - * @param string|array $needles - * @return bool */ -function ends_with($haystack, $needles) +function ends_with(string $haystack, array|string $needles): bool { foreach ((array) $needles as $needle) { if (substr($haystack, -strlen($needle)) === (string) $needle) { @@ -205,12 +168,8 @@ function ends_with($haystack, $needles) /** * Determine if a given string starts with a given substring. - * - * @param string $haystack - * @param string|string[] $needles - * @return bool */ -function starts_with($haystack, $needles) +function starts_with(string $haystack, array|string $needles): bool { foreach ((array) $needles as $needle) { if ((string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0) { @@ -224,7 +183,7 @@ function starts_with($haystack, $needles) /** * Get the user. */ -function user() +function user(): string { if (! isset($_SERVER['SUDO_USER'])) { return $_SERVER['USER']; diff --git a/cli/includes/legacy/BasicValetDriver.php b/cli/includes/legacy/BasicValetDriver.php deleted file mode 100644 index e00557e0a..000000000 --- a/cli/includes/legacy/BasicValetDriver.php +++ /dev/null @@ -1,5 +0,0 @@ -isActualFile($staticFilePath = $sitePath.'/Web'.$uri)) { + if (file_exists($staticFilePath = $sitePath.'/public/'.$uri)) { return $staticFilePath; } @@ -44,11 +46,6 @@ public function isStaticFile($sitePath, $siteName, $uri) */ public function frontControllerPath($sitePath, $siteName, $uri) { - putenv('FLOW_CONTEXT=Development'); - putenv('FLOW_REWRITEURLS=1'); - $_SERVER['SCRIPT_FILENAME'] = $sitePath.'/Web/index.php'; - $_SERVER['SCRIPT_NAME'] = '/index.php'; - - return $sitePath.'/Web/index.php'; + return $sitePath.'/public/index.php'; } } diff --git a/composer.json b/composer.json index c4285eadc..4a48a7d23 100644 --- a/composer.json +++ b/composer.json @@ -35,18 +35,19 @@ ] }, "require": { - "php": "^7.0|^8.0", + "php": "^7.1|^8.0", + "illuminate/collections": "^8.0|^9.0|^10.0", "illuminate/container": "~5.1|^6.0|^7.0|^8.0|^9.0|^10.0", "mnapoli/silly": "^1.0", "symfony/console": "^3.0|^4.0|^5.0|^6.0", "symfony/process": "^3.0|^4.0|^5.0|^6.0", - "tightenco/collect": "^5.3|^6.0|^7.0|^8.0", "guzzlehttp/guzzle": "^6.0|^7.4", "symfony/event-dispatcher": "^3.0|^4.0|^5.0|^6.0" }, "require-dev": { "mockery/mockery": "^1.2.3", - "yoast/phpunit-polyfills": "^0.2.0" + "yoast/phpunit-polyfills": "^0.2.0", + "laravel/pint": "^1.4" }, "bin": [ "valet" diff --git a/find-usable-php.php b/find-usable-php.php new file mode 100644 index 000000000..9ff3869d9 --- /dev/null +++ b/find-usable-php.php @@ -0,0 +1,91 @@ += 0) { + echo exec('which php'); + + return; +} + +// If not, let's find it whether we have a version of PHP installed that's 8+; +// all users that run through this code path will see Valet run more slowly +$phps = explode(PHP_EOL, trim(shell_exec('brew list --formula | grep php'))); + +// Normalize version numbers +$phps = array_reduce($phps, function ($carry, $php) { + $carry[$php] = presumePhpVersionFromBrewFormulaName($php); + + return $carry; +}, []); + +// Filter out older versions of PHP +$modernPhps = array_filter($phps, function ($php) use ($minimumPhpVersion) { + return version_compare($php, $minimumPhpVersion) >= 0; +}); + +// If we don't have any modern versions of PHP, throw an error +if (empty($modernPhps)) { + throw new Exception('Sorry, but you do not have a version of PHP installed that is compatible with Valet (8+).'); +} + +// Sort newest version to oldest +sort($modernPhps); +$modernPhps = array_reverse($modernPhps); + +// Grab the highest, set as $foundVersion, and output its path +$foundVersion = reset($modernPhps); +echo getPhpExecutablePath(array_search($foundVersion, $phps)); + +/** + * Function definitions. + */ + +/** + * Extract PHP executable path from PHP Version. + * Copied from Brew.php and modified. + * + * @param string|null $phpFormulaName For example, "php@8.1" + * @return string + */ +function getPhpExecutablePath(string $phpFormulaName = null) +{ + $brewPrefix = exec('printf $(brew --prefix)'); + + // Check the default `/opt/homebrew/opt/php@8.1/bin/php` location first + if (file_exists($brewPrefix."/opt/{$phpFormulaName}/bin/php")) { + return $brewPrefix."/opt/{$phpFormulaName}/bin/php"; + } + + // Check the `/opt/homebrew/opt/php71/bin/php` location for older installations + $oldPhpFormulaName = str_replace(['@', '.'], '', $phpFormulaName); // php@7.1 to php71 + if (file_exists($brewPrefix."/opt/{$oldPhpFormulaName}/bin/php")) { + return $brewPrefix."/opt/{$oldPhpFormulaName}/bin/php"; + } + + throw new Exception('Cannot find an executable path for provided PHP version: '.$phpFormulaName); +} + +function presumePhpVersionFromBrewFormulaName(string $formulaName) +{ + if ($formulaName === 'php') { + // Figure out its link + $details = json_decode(shell_exec("brew info $formulaName --json")); + + if (! empty($details[0]->aliases[0])) { + $formulaName = $details[0]->aliases[0]; + } else { + return null; + } + } + + if (strpos($formulaName, 'php@') === false) { + return null; + } + + return substr($formulaName, strpos($formulaName, '@') + 1); +} diff --git a/server.php b/server.php index 1d340930e..f010e822a 100644 --- a/server.php +++ b/server.php @@ -1,8 +1,10 @@ Index of $uri"; - echo '
'; - echo implode("
\n", array_map(function ($path) use ($uri, $is_root) { - $file = basename($path); - - return ($is_root) ? "/$file" : "$uri/$file/"; - }, $paths)); - - exit; -} - -/** - * You may use wildcard DNS provider nip.io as a tool for testing your site via an IP address. - * It's simple to use: First determine the IP address of your local computer (like 192.168.0.10). - * Then simply use http://project.your-ip.nip.io - ie: http://laravel.192.168.0.10.nip.io. - */ -function valet_support_wildcard_dns($domain, $config) -{ - $services = [ - '.*.*.*.*.nip.io', - '-*-*-*-*.nip.io', - ]; - - if (isset($config['tunnel_services'])) { - $services = array_merge($services, (array) $config['tunnel_services']); - } - - $patterns = []; - foreach ($services as $service) { - $pattern = preg_quote($service, '#'); - $pattern = str_replace('\*', '.*', $pattern); - $patterns[] = '(.*)'.$pattern; - } - - $pattern = implode('|', $patterns); - - if (preg_match('#(?:'.$pattern.')\z#u', $domain, $matches)) { - $domain = array_pop($matches); - } - - if (strpos($domain, ':') !== false) { - $domain = explode(':', $domain)[0]; - } - - return $domain; -} - -/** - * @param array $config Valet configuration array - * @return string|null If set, default site path for uncaught urls - * */ -function valet_default_site_path($config) -{ - if (isset($config['default']) && is_string($config['default']) && is_dir($config['default'])) { - return $config['default']; - } - - return null; -} - /** * Load the Valet configuration. */ @@ -111,82 +24,26 @@ function valet_default_site_path($config) * valid hostname, extract and use it as the effective HTTP_HOST in place * of the IP. It enables the use of Valet in a local network. */ -if (preg_match('/^([0-9]+\.){3}[0-9]+$/', $_SERVER['HTTP_HOST'])) { - $uri = ltrim($_SERVER['REQUEST_URI'], '/'); +if (Server::hostIsIpAddress($_SERVER['HTTP_HOST'])) { + $uriForIpAddressExtraction = ltrim($_SERVER['REQUEST_URI'], '/'); - if (preg_match('/^[-.0-9a-zA-Z]+\.'.$valetConfig['tld'].'/', $uri, $matches)) { - $host = $matches[0]; + if ($host = Server::valetSiteFromIpAddressUri($uriForIpAddressExtraction, $valetConfig['tld'])) { $_SERVER['HTTP_HOST'] = $host; - $_SERVER['REQUEST_URI'] = str_replace($host, '', $uri); + $_SERVER['REQUEST_URI'] = str_replace($host, '', $uriForIpAddressExtraction); } } -/** - * Parse the URI and site / host for the incoming request. - */ -$uri = rawurldecode( - explode('?', $_SERVER['REQUEST_URI'])[0] -); - -$siteName = basename( - // Filter host to support wildcard dns feature - valet_support_wildcard_dns($_SERVER['HTTP_HOST'], $valetConfig), - '.'.$valetConfig['tld'] -); - -if (strpos($siteName, 'www.') === 0) { - $siteName = substr($siteName, 4); -} +$server = new Server($valetConfig); /** - * Determine the fully qualified path to the site. - * Inspects registered path directories, case-sensitive. + * Parse the URI and site / host for the incoming request. */ -function get_valet_site_path($valetConfig, $siteName, $domain) -{ - $valetSitePath = null; - - foreach ($valetConfig['paths'] as $path) { - $handle = opendir($path); - - if ($handle === false) { - continue; - } - - $dirs = []; - - while (false !== ($file = readdir($handle))) { - if (is_dir($path.'/'.$file) && ! in_array($file, ['.', '..'])) { - $dirs[] = $file; - } - } - - closedir($handle); - - // Note: strtolower used below because Nginx only tells us lowercase names - foreach ($dirs as $dir) { - if (strtolower($dir) === $siteName) { - // early return when exact match for linked subdomain - return $path.'/'.$dir; - } - - if (strtolower($dir) === $domain) { - // no early return here because the foreach may still have some subdomains to process with higher priority - $valetSitePath = $path.'/'.$dir; - } - } - - if ($valetSitePath) { - return $valetSitePath; - } - } -} - -$domain = array_slice(explode('.', $siteName), -1)[0]; -$valetSitePath = get_valet_site_path($valetConfig, $siteName, $domain); +$uri = Server::uriFromRequestUri($_SERVER['REQUEST_URI']); +$siteName = $server->siteNameFromHttpHost($_SERVER['HTTP_HOST']); +$valetSitePath = $server->sitePath($siteName); -if (is_null($valetSitePath) && is_null($valetSitePath = valet_default_site_path($valetConfig))) { - show_valet_404(); +if (is_null($valetSitePath) && is_null($valetSitePath = $server->defaultSitePath())) { + Server::show404(); } $valetSitePath = realpath($valetSitePath); @@ -194,12 +51,10 @@ function get_valet_site_path($valetConfig, $siteName, $domain) /** * Find the appropriate Valet driver for the request. */ -$valetDriver = null; - $valetDriver = ValetDriver::assign($valetSitePath, $siteName, $uri); if (! $valetDriver) { - show_valet_404(); + Server::show404(); } /** @@ -230,6 +85,11 @@ function get_valet_site_path($valetConfig, $siteName, $domain) return $valetDriver->serveStaticFile($staticFilePath, $valetSitePath, $siteName, $uri); } +/** + * Allow for drivers to take pre-loading actions (e.g. setting server variables). + */ +$valetDriver->beforeLoading($valetSitePath, $siteName, $uri); + /** * Attempt to dispatch to a front controller. */ @@ -239,10 +99,10 @@ function get_valet_site_path($valetConfig, $siteName, $domain) if (! $frontControllerPath) { if (isset($valetConfig['directory-listing']) && $valetConfig['directory-listing'] == 'on') { - show_directory_listing($valetSitePath, $uri); + Server::showDirectoryListing($valetSitePath, $uri); } - show_valet_404(); + Server::show404(); } chdir(dirname($frontControllerPath)); diff --git a/tests/BaseApplicationTestCase.php b/tests/BaseApplicationTestCase.php index 827475d16..66c0ec156 100644 --- a/tests/BaseApplicationTestCase.php +++ b/tests/BaseApplicationTestCase.php @@ -1,8 +1,10 @@ setNullWriter(); } - public function prepTestConfig() + public function tearDown(): void + { + Mockery::close(); + } + + /** + * Prepare a test to run using the full application. + */ + public function prepTestConfig(): void { require_once __DIR__.'/../cli/includes/helpers.php'; + Container::setInstance(new Container); // Reset app container from previous tests if (Filesystem::isDir(VALET_HOME_PATH)) { Filesystem::rmDirAndContents(VALET_HOME_PATH); } Configuration::createConfigurationDirectory(); + Configuration::createDriversDirectory(); + Configuration::createLogDirectory(); + Configuration::createCertificatesDirectory(); Configuration::writeBaseConfiguration(); + + // Keep this file empty, as it's tailed in a test + Filesystem::touch(VALET_HOME_PATH.'/Log/nginx-error.log'); } - public function appAndTester() + /** + * Return an array with two items: the application instance and the ApplicationTester. + */ + public function appAndTester(): array { $app = require __DIR__.'/../cli/app.php'; $app->setAutoExit(false); diff --git a/tests/BrewTest.php b/tests/BrewTest.php index 24e66d0ea..a54ab588e 100644 --- a/tests/BrewTest.php +++ b/tests/BrewTest.php @@ -34,29 +34,38 @@ public function test_brew_can_be_resolved_from_container() public function test_installed_returns_true_when_given_formula_is_installed() { $cli = Mockery::mock(CommandLine::class); - $cli->shouldReceive('runAsUser')->once()->with('brew info php@7.4 --json') - ->andReturn('[{"name":"php@7.4","full_name":"php@7.4","aliases":[],"versioned_formulae":[],"versions":{"stable":"7.4.5"},"installed":[{"version":"7.4.5"}]}]'); + $cli->shouldReceive('runAsUser')->once()->with('brew info php@8.2 --json=v2') + ->andReturn('{"formulae":[{"name":"php@8.2","full_name":"php@8.2","aliases":[],"versioned_formulae":[],"versions":{"stable":"8.2.5"},"installed":[{"version":"8.2.5"}]}]}'); swap(CommandLine::class, $cli); - $this->assertTrue(resolve(Brew::class)->installed('php@7.4')); + $this->assertTrue(resolve(Brew::class)->installed('php@8.2')); $cli = Mockery::mock(CommandLine::class); - $cli->shouldReceive('runAsUser')->once()->with('brew info php --json') - ->andReturn('[{"name":"php","full_name":"php","aliases":["php@8.0"],"versioned_formulae":[],"versions":{"stable":"8.0.0"},"installed":[{"version":"8.0.0"}]}]'); + $cli->shouldReceive('runAsUser')->once()->with('brew info php --json=v2') + ->andReturn('{"formulae":[{"name":"php","full_name":"php","aliases":["php@8.0"],"versioned_formulae":[],"versions":{"stable":"8.0.0"},"installed":[{"version":"8.0.0"}]}]}'); swap(CommandLine::class, $cli); $this->assertTrue(resolve(Brew::class)->installed('php')); } + public function test_installed_returns_true_when_given_cask_formula_is_installed() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('brew info ngrok --json=v2') + ->andReturn('{"casks":[{"name":"ngrok","full_name":"ngrok","aliases":[],"versioned_formulae":[],"versions":{"stable":"8.2.5"},"installed":[{"version":"8.2.5"}]}]}'); + swap(CommandLine::class, $cli); + $this->assertTrue(resolve(Brew::class)->installed('ngrok')); + } + public function test_installed_returns_false_when_given_formula_is_not_installed() { $cli = Mockery::mock(CommandLine::class); - $cli->shouldReceive('runAsUser')->once()->with('brew info php@7.4 --json')->andReturn(''); + $cli->shouldReceive('runAsUser')->once()->with('brew info php@8.2 --json=v2')->andReturn(''); swap(CommandLine::class, $cli); - $this->assertFalse(resolve(Brew::class)->installed('php@7.4')); + $this->assertFalse(resolve(Brew::class)->installed('php@8.2')); $cli = Mockery::mock(CommandLine::class); - $cli->shouldReceive('runAsUser')->once()->with('brew info php@7.4 --json')->andReturn('Error: No formula found'); + $cli->shouldReceive('runAsUser')->once()->with('brew info php@8.2 --json=v2')->andReturn('Error: No formula found'); swap(CommandLine::class, $cli); - $this->assertFalse(resolve(Brew::class)->installed('php@7.4')); + $this->assertFalse(resolve(Brew::class)->installed('php@8.2')); } public function test_has_installed_php_indicates_if_php_is_installed_via_brew() @@ -78,27 +87,23 @@ public function test_has_installed_php_indicates_if_php_is_installed_via_brew() $this->assertTrue($brew->hasInstalledPhp()); $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]); - $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@7.4'])); - $this->assertTrue($brew->hasInstalledPhp()); - - $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]); - $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@7.3'])); + $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@8.2'])); $this->assertTrue($brew->hasInstalledPhp()); $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]); - $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php73'])); + $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@8.2'])); $this->assertTrue($brew->hasInstalledPhp()); $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]); - $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@7.2', 'php72'])); + $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@8.2', 'php82'])); $this->assertTrue($brew->hasInstalledPhp()); $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]); - $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php71', 'php@7.1'])); + $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php81', 'php@8.1'])); $this->assertTrue($brew->hasInstalledPhp()); $brew = Mockery::mock(Brew::class.'[installedPhpFormulae]', [new CommandLine, new Filesystem]); - $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@7.0'])); + $brew->shouldReceive('installedPhpFormulae')->andReturn(collect(['php@8.0'])); $this->assertTrue($brew->hasInstalledPhp()); } @@ -106,17 +111,17 @@ public function test_tap_taps_the_given_homebrew_repository() { $cli = Mockery::mock(CommandLine::class); $prefix = Brew::BREW_DISABLE_AUTO_CLEANUP; + $cli->shouldReceive('passthru')->once()->with($prefix.' sudo -u "'.user().'" brew tap php@8.2'); + $cli->shouldReceive('passthru')->once()->with($prefix.' sudo -u "'.user().'" brew tap php@8.1'); $cli->shouldReceive('passthru')->once()->with($prefix.' sudo -u "'.user().'" brew tap php@8.0'); - $cli->shouldReceive('passthru')->once()->with($prefix.' sudo -u "'.user().'" brew tap php@7.1'); - $cli->shouldReceive('passthru')->once()->with($prefix.' sudo -u "'.user().'" brew tap php@7.0'); swap(CommandLine::class, $cli); - resolve(Brew::class)->tap('php@8.0', 'php@7.1', 'php@7.0'); + resolve(Brew::class)->tap('php@8.2', 'php@8.1', 'php@8.0'); } public function test_restart_restarts_the_service_using_homebrew_services() { $cli = Mockery::mock(CommandLine::class); - $cli->shouldReceive('runAsUser')->once()->with('brew info dnsmasq --json')->andReturn('[{"name":"dnsmasq","full_name":"dnsmasq","aliases":[],"versioned_formulae":[],"versions":{"stable":"1"},"installed":[{"version":"1"}]}]'); + $cli->shouldReceive('runAsUser')->once()->with('brew info dnsmasq --json=v2')->andReturn('{"formulae":[{"name":"dnsmasq","full_name":"dnsmasq","aliases":[],"versioned_formulae":[],"versions":{"stable":"1"},"installed":[{"version":"1"}]}]}'); $cli->shouldReceive('quietly')->once()->with('brew services stop dnsmasq'); $cli->shouldReceive('quietly')->once()->with('sudo brew services stop dnsmasq'); $cli->shouldReceive('quietly')->once()->with('sudo brew services start dnsmasq'); @@ -127,7 +132,7 @@ public function test_restart_restarts_the_service_using_homebrew_services() public function test_stop_stops_the_service_using_homebrew_services() { $cli = Mockery::mock(CommandLine::class); - $cli->shouldReceive('runAsUser')->once()->with('brew info dnsmasq --json')->andReturn('[{"name":"dnsmasq","full_name":"dnsmasq","aliases":[],"versioned_formulae":[],"versions":{"stable":"1"},"installed":[{"version":"1"}]}]'); + $cli->shouldReceive('runAsUser')->once()->with('brew info dnsmasq --json=v2')->andReturn('{"formulae":[{"name":"dnsmasq","full_name":"dnsmasq","aliases":[],"versioned_formulae":[],"versions":{"stable":"1"},"installed":[{"version":"1"}]}]}'); $cli->shouldReceive('quietly')->once()->with('brew services stop dnsmasq'); $cli->shouldReceive('quietly')->once()->with('sudo brew services stop dnsmasq'); $cli->shouldReceive('quietly')->once()->with('sudo chown -R '.user().":admin '".BREW_PREFIX."/Cellar/dnsmasq'"); @@ -147,24 +152,24 @@ 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/php/7.4.0/test'); - $this->assertSame('php@7.4', $getBrewMock($files)->linkedPhp()); + $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php/8.0.0/test'); + $this->assertSame('php@8.0', $getBrewMock($files)->linkedPhp()); $files = Mockery::mock(Filesystem::class); - $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php/7.3.0/test'); - $this->assertSame('php@7.3', $getBrewMock($files)->linkedPhp()); + $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php/8.1.0/test'); + $this->assertSame('php@8.1', $getBrewMock($files)->linkedPhp()); $files = Mockery::mock(Filesystem::class); - $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php@7.2/7.2.13/test'); - $this->assertSame('php@7.2', $getBrewMock($files)->linkedPhp()); + $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php@8.2/8.2.13/test'); + $this->assertSame('php@8.2', $getBrewMock($files)->linkedPhp()); $files = Mockery::mock(Filesystem::class); - $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php/7.2.9_2/test'); - $this->assertSame('php@7.2', $getBrewMock($files)->linkedPhp()); + $files->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php/8.2.9_2/test'); + $this->assertSame('php@8.2', $getBrewMock($files)->linkedPhp()); $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->shouldReceive('readLink')->once()->with(BREW_PREFIX.'/bin/php')->andReturn('/test/path/php81/8.1.9_2/test'); + $this->assertSame('php@8.1', $getBrewMock($files)->linkedPhp()); } public function test_linked_php_throws_exception_if_no_php_link() @@ -394,8 +399,8 @@ public function test_get_linked_php_formula_will_return_linked_php_directory($pa public function test_restart_linked_php_will_pass_through_linked_php_formula_to_restart_service() { $brewMock = Mockery::mock(Brew::class)->makePartial(); - $brewMock->shouldReceive('getLinkedPhpFormula')->once()->andReturn('php@7.2-test'); - $brewMock->shouldReceive('restartService')->once()->with('php@7.2-test'); + $brewMock->shouldReceive('getLinkedPhpFormula')->once()->andReturn('php@8.2-test'); + $brewMock->shouldReceive('restartService')->once()->with('php@8.2-test'); $brewMock->restartLinkedPhp(); } @@ -407,9 +412,9 @@ public function test_it_can_get_php_binary_path_from_php_version() $files = Mockery::mock(Filesystem::class), ])->makePartial(); - $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@7.4/bin/php')->andReturn(true); - $files->shouldNotReceive('exists')->with(BREW_PREFIX.'/opt/php@74/bin/php'); - $this->assertEquals(BREW_PREFIX.'/opt/php@7.4/bin/php', $brewMock->getPhpExecutablePath('php@7.4')); + $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@8.2/bin/php')->andReturn(true); + $files->shouldNotReceive('exists')->with(BREW_PREFIX.'/opt/php@82/bin/php'); + $this->assertEquals(BREW_PREFIX.'/opt/php@8.2/bin/php', $brewMock->getPhpExecutablePath('php@8.2')); // Check the `/opt/homebrew/opt/php71/bin/php` location for older installations $brewMock = Mockery::mock(Brew::class, [ @@ -417,9 +422,9 @@ public function test_it_can_get_php_binary_path_from_php_version() $files = Mockery::mock(Filesystem::class), ])->makePartial(); - $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@7.4/bin/php')->andReturn(false); - $files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php74/bin/php')->andReturn(true); - $this->assertEquals(BREW_PREFIX.'/opt/php74/bin/php', $brewMock->getPhpExecutablePath('php@7.4')); + $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@8.2/bin/php')->andReturn(false); + $files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php82/bin/php')->andReturn(true); + $this->assertEquals(BREW_PREFIX.'/opt/php82/bin/php', $brewMock->getPhpExecutablePath('php@8.2')); // When the default PHP is the version we are looking for $brewMock = Mockery::mock(Brew::class, [ @@ -427,11 +432,11 @@ public function test_it_can_get_php_binary_path_from_php_version() $files = Mockery::mock(Filesystem::class), ])->makePartial(); - $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@7.4/bin/php')->andReturn(false); - $files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php74/bin/php')->andReturn(false); + $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@8.2/bin/php')->andReturn(false); + $files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php82/bin/php')->andReturn(false); $files->shouldReceive('isLink')->with(BREW_PREFIX.'/opt/php')->andReturn(true); - $files->shouldReceive('readLink')->with(BREW_PREFIX.'/opt/php')->andReturn('../Cellar/php@7.4/7.4.13/bin/php'); - $this->assertEquals(BREW_PREFIX.'/opt/php/bin/php', $brewMock->getPhpExecutablePath('php@7.4')); + $files->shouldReceive('readLink')->with(BREW_PREFIX.'/opt/php')->andReturn('../Cellar/php@8.2/8.2.13/bin/php'); + $this->assertEquals(BREW_PREFIX.'/opt/php/bin/php', $brewMock->getPhpExecutablePath('php@8.2')); // When the default PHP is not the version we are looking for $brewMock = Mockery::mock(Brew::class, [ @@ -439,11 +444,11 @@ public function test_it_can_get_php_binary_path_from_php_version() $files = Mockery::mock(Filesystem::class), ])->makePartial(); - $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@7.4/bin/php')->andReturn(false); - $files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php74/bin/php')->andReturn(false); + $files->shouldReceive('exists')->once()->with(BREW_PREFIX.'/opt/php@8.2/bin/php')->andReturn(false); + $files->shouldReceive('exists')->with(BREW_PREFIX.'/opt/php82/bin/php')->andReturn(false); $files->shouldReceive('isLink')->with(BREW_PREFIX.'/opt/php')->andReturn(true); $files->shouldReceive('readLink')->with(BREW_PREFIX.'/opt/php')->andReturn('../Cellar/php@8.1/8.1.13/bin/php'); - $this->assertEquals(BREW_PREFIX.'/bin/php', $brewMock->getPhpExecutablePath('php@7.4')); // Could not find a version, so retuned the default binary + $this->assertEquals(BREW_PREFIX.'/bin/php', $brewMock->getPhpExecutablePath('php@8.2')); // Could not find a version, so retuned the default binary // When no PHP Version is provided $brewMock = Mockery::mock(Brew::class, [ @@ -456,76 +461,74 @@ public function test_it_can_get_php_binary_path_from_php_version() public function test_it_can_compare_two_php_versions() { - $this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php71', 'php@7.1')); - $this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php71', 'php@71')); - $this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php71', '71')); + $this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php81', 'php@8.1')); + $this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php81', 'php@81')); + $this->assertTrue(resolve(Brew::class)->arePhpVersionsEqual('php81', '81')); - $this->assertFalse(resolve(Brew::class)->arePhpVersionsEqual('php71', 'php@70')); - $this->assertFalse(resolve(Brew::class)->arePhpVersionsEqual('php71', '72')); + $this->assertFalse(resolve(Brew::class)->arePhpVersionsEqual('php81', 'php@80')); + $this->assertFalse(resolve(Brew::class)->arePhpVersionsEqual('php81', '82')); } /** * Provider of php links and their expected split matches. - * - * @return array */ - public function supportedPhpLinkPathProvider() + public function supportedPhpLinkPathProvider(): array { return [ [ - '/test/path/php/7.4.0/test', // linked path + '/test/path/php/8.2.0/test', // linked path [ // matches - 'path/php/7.4.0/test', + 'path/php/8.2.0/test', 'php', '', - '7.4', + '8.2', '.0', ], 'php', // expected link formula ], [ - '/test/path/php@7.4/7.4.13/test', + '/test/path/php@8.2/8.2.13/test', [ - 'path/php@7.4/7.4.13/test', + 'path/php@8.2/8.2.13/test', 'php', - '@7.4', - '7.4', + '@8.2', + '8.2', '.13', ], - 'php@7.4', + 'php@8.2', ], [ - '/test/path/php/7.4.9_2/test', + '/test/path/php/8.2.9_2/test', [ - 'path/php/7.4.9_2/test', + 'path/php/8.2.9_2/test', 'php', '', - '7.4', + '8.2', '.9_2', ], 'php', ], [ - '/test/path/php74/7.4.9_2/test', + '/test/path/php82/8.2.9_2/test', [ - 'path/php74/7.4.9_2/test', + 'path/php82/8.2.9_2/test', 'php', - '74', - '7.4', + '82', + '8.2', '.9_2', ], - 'php74', + 'php82', ], [ - '/test/path/php71/test', + '/test/path/php81/test', [ - 'path/php71/test', + 'path/php81/test', 'php', - '71', + '81', '', '', ], - 'php71', + 'php81', ], ]; } diff --git a/tests/CliTest.php b/tests/CliTest.php index f3bf71407..9bc40a06b 100644 --- a/tests/CliTest.php +++ b/tests/CliTest.php @@ -1,10 +1,111 @@ = 8.0 */ class CliTest extends BaseApplicationTestCase { + public function test_tld_command_reads_tld() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'tld']); + + $tester->assertCommandIsSuccessful(); + + $this->assertEquals('test', trim($tester->getDisplay())); + } + + public function test_tld_command_sets_tld() + { + [$app, $tester] = $this->appAndTester(); + + $tester->setInputs(['Y']); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('updateTld')->with('old', 'buzz')->once(); + + $config = Mockery::mock(RealConfiguration::class); + $config->shouldReceive('read')->andReturn(['tld' => 'old'])->once(); + $config->shouldReceive('updateKey')->with('tld', 'buzz')->once(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('resecureForNewConfiguration')->with(['tld' => 'old'], ['tld' => 'buzz'])->once(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('restart')->once(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(DnsMasq::class, $dnsmasq); + swap(RealConfiguration::class, $config); + swap(RealSite::class, $site); + swap(PhpFpm::class, $phpfpm); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'tld', 'tld' => 'buzz']); + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('Your Valet TLD has been updated to [buzz]', $tester->getDisplay()); + } + + public function test_loopback_command_reads_loopback() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'loopback']); + $tester->assertCommandIsSuccessful(); + + $this->assertEquals('127.0.0.1', trim($tester->getDisplay())); + } + + public function test_loopback_command_sets_loopback() + { + [$app, $tester] = $this->appAndTester(); + + $config = Mockery::mock(RealConfiguration::class); + $config->shouldReceive('read')->andReturn(['loopback' => '127.9.9.9'])->once(); + $config->shouldReceive('updateKey')->with('loopback', '127.0.0.1')->once(); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('refreshConfiguration')->once(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('aliasLoopback')->with('127.9.9.9', '127.0.0.1')->once(); + $site->shouldReceive('resecureForNewConfiguration')->with(['loopback' => '127.9.9.9'], ['loopback' => '127.0.0.1'])->once(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('restart')->once(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('installServer')->once(); + $nginx->shouldReceive('restart')->once(); + + swap(RealConfiguration::class, $config); + swap(DnsMasq::class, $dnsmasq); + swap(RealSite::class, $site); + swap(PhpFpm::class, $phpfpm); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'loopback', 'loopback' => '127.0.0.1']); + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('Your Valet loopback address has been updated to [127.0.0.1]', $tester->getDisplay()); + } + public function test_park_command() { [$app, $tester] = $this->appAndTester(); @@ -23,4 +124,1091 @@ public function test_park_command() $this->assertEquals(1, count($paths)); $this->assertEquals('./tests/output', reset($paths)); } + + public function test_status_command_succeeding() + { + [$app, $tester] = $this->appAndTester(); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('getLinkedPhpFormula')->andReturn('php@8.2'); + $brew->shouldReceive('hasInstalledPhp')->andReturn(true); + $brew->shouldReceive('installed')->twice()->andReturn(true); + + $cli = Mockery::mock(CommandLine::class); + + $cli->shouldReceive('run')->once()->andReturn(true); + $cli->shouldReceive('runAsUser')->once()->with('brew services info --all --json')->andReturn('[{"name":"nginx","running":true}]'); + $cli->shouldReceive('run')->once()->with('brew services info --all --json')->andReturn('[{"name":"nginx","running":true},{"name":"dnsmasq","running":true},{"name":"php@8.2","running":true}]'); + + $files = Mockery::mock(Filesystem::class.'[exists]'); + $files->shouldReceive('exists')->once()->andReturn(true); + + swap(Brew::class, $brew); + swap(CommandLine::class, $cli); + swap(Filesystem::class, $files); + + $tester->run(['command' => 'status']); + + // $tester->assertCommandIsSuccessful(); + $this->assertStringNotContainsString('No', $tester->getDisplay()); + } + + public function test_status_command_failing() + { + [$app, $tester] = $this->appAndTester(); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('getLinkedPhpFormula')->andReturn('php@8.2'); + $brew->shouldReceive('hasInstalledPhp')->andReturn(true); + $brew->shouldReceive('installed')->twice()->andReturn(true); + + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('run')->once()->andReturn(true); + $cli->shouldReceive('runAsUser')->once()->with('brew services info --all --json')->andReturn('[{"name":"nginx","running":true}]'); + $cli->shouldReceive('run')->once()->with('brew services info --all --json')->andReturn('[{"name":"nginx","running":true}]'); + + $files = Mockery::mock(Filesystem::class.'[exists]'); + $files->shouldReceive('exists')->once()->andReturn(false); + + swap(Brew::class, $brew); + swap(CommandLine::class, $cli); + swap(Filesystem::class, $files); + + $tester->run(['command' => 'status']); + + $this->assertNotEquals(0, $tester->getStatusCode()); + $this->assertStringContainsString('No', $tester->getDisplay()); + } + + public function test_parked_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'parked']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringNotContainsString('test', $tester->getDisplay()); + + Configuration::addPath(__DIR__.'/fixtures/Parked/Sites'); + + $tester->run(['command' => 'parked']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('my-best-site', $tester->getDisplay()); + } + + public function test_forget_command() + { + [$app, $tester] = $this->appAndTester(); + + Configuration::addPath(__DIR__.'/fixtures/Parked/Sites'); + + $tester->run(['command' => 'forget', 'path' => __DIR__.'/fixtures/Parked/Sites']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringNotContainsString('my-best-site', $tester->getDisplay()); + } + + public function test_link_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'link', 'name' => 'tighten']); + $tester->assertCommandIsSuccessful(); + + $this->assertEquals(1, Site::links()->count()); + $this->assertEquals(1, Site::links()->filter(function ($site) { + return $site['site'] === 'tighten'; + })->count()); + } + + public function test_link_command_defaults_to_cwd() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('link')->with(getcwd(), basename(getcwd()))->once(); + swap(RealSite::class, $site); + + $tester->run(['command' => 'link']); + $tester->assertCommandIsSuccessful(); + } + + public function test_link_command_with_secure_flag_secures() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('link')->once(); + $site->shouldReceive('domain')->andReturn('mysite.test'); + $site->shouldReceive('secure')->once(); + swap(RealSite::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'link', '--secure' => true]); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('site has been secured', $tester->getDisplay()); + } + + public function test_links_command() + { + [$app, $tester] = $this->appAndTester(); + + Site::link(__DIR__.'/fixtures/Parked/Sites/my-best-site', 'tighten'); + $tester->run(['command' => 'links']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('tighten', $tester->getDisplay()); + } + + public function test_unlink_command() + { + [$app, $tester] = $this->appAndTester(); + + Site::link(getcwd(), basename(getcwd())); + + $tester->run(['command' => 'unlink']); + $tester->assertCommandIsSuccessful(); + + $this->assertEquals(0, Site::links()->count()); + } + + public function test_unlink_command_with_parameter() + { + [$app, $tester] = $this->appAndTester(); + + Site::link(__DIR__.'/fixtures/Parked/Sites/my-best-site', 'tighten'); + + $tester->run(['command' => 'unlink', 'name' => 'tighten']); + $tester->assertCommandIsSuccessful(); + + $this->assertEquals(0, Site::links()->count()); + } + + public function test_unlink_command_unsecures_as_well() + { + [$app, $tester] = $this->appAndTester(); + + Site::link(__DIR__.'/fixtures/Parked/Sites/my-best-site', 'tighten'); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('domain')->with('tighten')->once()->andReturn('tighten.test'); + $site->shouldReceive('unlink')->with('tighten')->once()->andReturn('tighten'); + $site->shouldReceive('unsecure')->with('tighten.test')->once(); + $site->shouldReceive('isSecured')->with('tighten')->once()->andReturn(true); + swap(RealSite::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'unlink', 'name' => 'tighten']); + $tester->assertCommandIsSuccessful(); + } + + public function test_secure_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('domain')->with('tighten')->andReturn('tighten.test'); + $site->shouldReceive('unsecure')->with('tighten.test')->once(); + swap(RealSite::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'unsecure', 'domain' => 'tighten']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('will now serve traffic over HTTP.', $tester->getDisplay()); + } + + public function test_unsecure_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('domain')->andReturn('tighten.test'); + $site->shouldReceive('secure')->with('tighten.test', null, 12345)->once(); + swap(RealSite::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'secure', 'domain' => 'tighten', '--expireIn' => '12345']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('site has been secured', $tester->getDisplay()); + } + + public function test_unsecure_all_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('unSecureAll')->once(); + swap(RealSite::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'unsecure', '--all' => true]); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('All Valet sites will now serve traffic over HTTP.', $tester->getDisplay()); + } + + public function test_secured_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('secured')->andReturn(['tighten.test']); + swap(RealSite::class, $site); + + $tester->run(['command' => 'secured']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('tighten.test', $tester->getDisplay()); + } + + public function test_proxy_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('proxyCreate')->with('elasticsearch', 'http://127.0.0.1:9200', false)->once(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(Nginx::class, $nginx); + swap(RealSite::class, $site); + + $tester->run(['command' => 'proxy', 'domain' => 'elasticsearch', 'host' => 'http://127.0.0.1:9200']); + $tester->assertCommandIsSuccessful(); + } + + public function test_unproxy_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('proxyDelete')->with('elasticsearch')->once(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(Nginx::class, $nginx); + swap(RealSite::class, $site); + + $tester->run(['command' => 'unproxy', 'domain' => 'elasticsearch']); + $tester->assertCommandIsSuccessful(); + } + + public function test_proxies_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('proxies')->andReturn(collect([ + ['site' => 'elasticsearch', 'secured' => 'X', 'url' => 'https://elasticsearch.test/', 'host' => 'http://127.0.0.1:9200'], + ])); + + swap(RealSite::class, $site); + + $tester->run(['command' => 'proxies']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('elasticsearch', $tester->getDisplay()); + } + + public function test_which_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'which']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('served by [', $tester->getDisplay()); + } + + public function test_paths_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'paths']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('No paths have been registered.', $tester->getDisplay()); + + Configuration::addPath(__DIR__); + + $tester->run(['command' => 'paths']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString(__DIR__, $tester->getDisplay()); + } + + public function test_open_command() + { + [$app, $tester] = $this->appAndTester(); + + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->with("open 'http://tighten.test'")->once(); + swap(CommandLine::class, $cli); + + $tester->run(['command' => 'open', 'domain' => 'tighten']); + $tester->assertCommandIsSuccessful(); + } + + public function test_get_share_tool_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'share-tool']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('(not set)', $tester->getDisplay()); + + Configuration::updateKey('share-tool', 'expose'); + + $tester->run(['command' => 'share-tool']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('expose', $tester->getDisplay()); + } + + public function test_set_share_tool_command() + { + [$app, $tester] = $this->appAndTester(); + + $expose = Mockery::mock(Expose::class); + $expose->shouldReceive('installed')->andReturn(true); + + swap(Expose::class, $expose); + + $tester->run(['command' => 'share-tool', 'tool' => 'expose']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('expose', Configuration::read()['share-tool']); + + $ngrok = Mockery::mock(Ngrok::class); + $ngrok->shouldReceive('installed')->andReturn(true); + + swap(Ngrok::class, $ngrok); + + $tester->run(['command' => 'share-tool', 'tool' => 'ngrok']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('ngrok', Configuration::read()['share-tool']); + + $tester->run(['command' => 'share-tool', 'tool' => 'asdfoijasdofijqoiwejrqwer']); + $this->assertEquals(1, $tester->getStatusCode()); // Assert comand failure + + $this->assertStringContainsString('ngrok', Configuration::read()['share-tool']); + } + + public function test_share_tool_prompts_expose_install() + { + [$app, $tester] = $this->appAndTester(); + + $tester->setInputs(['Y']); + + $expose = Mockery::mock(Expose::class); + $expose->shouldReceive('installed')->once()->andReturn(false); + $expose->shouldReceive('ensureInstalled')->once(); + + swap(Expose::class, $expose); + + $tester->run(['command' => 'share-tool', 'tool' => 'expose']); + $tester->assertCommandIsSuccessful(); + } + + public function test_share_tool_prompts_ngrok_install() + { + [$app, $tester] = $this->appAndTester(); + + $tester->setInputs(['Y']); + + $ngrok = Mockery::mock(Ngrok::class); + $ngrok->shouldReceive('installed')->once()->andReturn(false); + $ngrok->shouldReceive('ensureInstalled')->once(); + + swap(Ngrok::class, $ngrok); + + $tester->run(['command' => 'share-tool', 'tool' => 'ngrok']); + $tester->assertCommandIsSuccessful(); + } + + public function test_set_ngrok_token_command() + { + [$app, $tester] = $this->appAndTester(); + + $ngrok = Mockery::mock(Ngrok::class); + $ngrok->shouldReceive('setToken')->with('your-token-here')->once()->andReturn('yay'); + swap(Ngrok::class, $ngrok); + + $tester->run(['command' => 'set-ngrok-token', 'token' => 'your-token-here']); + $tester->assertCommandIsSuccessful(); + } + + public function test_fetch_share_url_command_with_expose() + { + [$app, $tester] = $this->appAndTester(); + + Configuration::updateKey('share-tool', 'expose'); + + $expose = Mockery::mock(Expose::class); + $expose->shouldReceive('currentTunnelUrl')->once()->andReturn('command-output'); + swap(Expose::class, $expose); + + $tester->run(['command' => 'fetch-share-url']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('command-output', $tester->getDisplay()); + } + + public function test_fetch_share_url_command_with_ngrok() + { + [$app, $tester] = $this->appAndTester(); + + Configuration::updateKey('share-tool', 'ngrok'); + + $ngrok = Mockery::mock(Ngrok::class); + $ngrok->shouldReceive('currentTunnelUrl')->once()->andReturn('command-output'); + swap(Ngrok::class, $ngrok); + + $tester->run(['command' => 'fetch-share-url']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('command-output', $tester->getDisplay()); + } + + public function test_fetch_share_url_command_with_unset_share_tool() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'fetch-share-url']); + $this->assertEquals(1, $tester->getStatusCode()); + + $this->assertStringContainsString('Please set your share tool', $tester->getDisplay()); + } + + public function test_restart_command() + { + [$app, $tester] = $this->appAndTester(); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('restart')->once(); + + swap(DnsMasq::class, $dnsmasq); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('restart')->once(); + + swap(PhpFpm::class, $phpfpm); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'restart']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Valet services have been restarted.', $tester->getDisplay()); + } + + public function test_restart_command_restarts_dnsmasq() + { + [$app, $tester] = $this->appAndTester(); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('restart')->once(); + + swap(DnsMasq::class, $dnsmasq); + + $tester->run(['command' => 'restart', 'service' => 'dnsmasq']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('dnsmasq has been restarted.', $tester->getDisplay()); + } + + public function test_restart_command_restarts_nginx() + { + [$app, $tester] = $this->appAndTester(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'restart', 'service' => 'nginx']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Nginx has been restarted.', $tester->getDisplay()); + } + + public function test_restart_command_restarts_php() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('restart')->once(); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'restart', 'service' => 'php']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('PHP has been restarted.', $tester->getDisplay()); + } + + public function test_start_command() + { + [$app, $tester] = $this->appAndTester(); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('restart')->once(); + + swap(DnsMasq::class, $dnsmasq); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('restart')->once(); + + swap(PhpFpm::class, $phpfpm); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'start']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Valet services have been started.', $tester->getDisplay()); + } + + public function test_start_command_starts_dnsmasq() + { + [$app, $tester] = $this->appAndTester(); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('restart')->once(); + + swap(DnsMasq::class, $dnsmasq); + + $tester->run(['command' => 'start', 'service' => 'dnsmasq']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('dnsmasq has been started.', $tester->getDisplay()); + } + + public function test_start_command_starts_nginx() + { + [$app, $tester] = $this->appAndTester(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'start', 'service' => 'nginx']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Nginx has been started.', $tester->getDisplay()); + } + + public function test_start_command_starts_php() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('restart')->once(); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'start', 'service' => 'php']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('PHP has been started.', $tester->getDisplay()); + } + + public function test_stop_command() + { + [$app, $tester] = $this->appAndTester(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('stop'); + + swap(Nginx::class, $nginx); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('stopRunning'); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'stop']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Valet services have been stopped.', $tester->getDisplay()); + } + + public function test_stop_command_stops_nginx() + { + [$app, $tester] = $this->appAndTester(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('stop'); + + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'stop', 'service' => 'nginx']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Nginx has been stopped', $tester->getDisplay()); + } + + public function test_stop_command_stops_php() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('stopRunning'); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'stop', 'service' => 'php']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('PHP has been stopped', $tester->getDisplay()); + } + + public function test_stop_command_handles_bad_services() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'stop', 'service' => 'not-real-services']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Invalid valet service', $tester->getDisplay()); + } + + public function test_force_uninstall_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->setInputs(['Y']); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('unsecureAll')->once(); + $site->shouldReceive('removeCa')->once(); + $site->shouldReceive('uninstallLoopback')->once(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('uninstall')->once(); + + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('uninstall')->once(); + + $config = Mockery::mock(RealConfiguration::class); + $config->shouldReceive('uninstall')->once(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('uninstall')->once(); + + $valet = Mockery::mock(Valet::class); + $valet->shouldReceive('unlinkFromUsersBin')->once(); + $valet->shouldReceive('removeSudoersEntry')->once(); + $valet->shouldReceive('forceUninstallText')->once()->andReturn('uninstall-text'); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('removeSudoersEntry')->once(); + + swap(RealSite::class, $site); + swap(Nginx::class, $nginx); + swap(DnsMasq::class, $dnsmasq); + swap(RealConfiguration::class, $config); + swap(PhpFpm::class, $phpfpm); + swap(Valet::class, $valet); + swap(Brew::class, $brew); + + $tester->run(['command' => 'uninstall', '--force' => true]); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('YOU ARE ABOUT TO UNINSTALL', $tester->getDisplay()); + } + + public function test_non_force_uninstall_command() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('stopRunning'); + + swap(PhpFpm::class, $phpfpm); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('stop'); + + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'uninstall']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('IF YOU WANT TO UNINSTALL VALET MANUALLY', $tester->getDisplay()); + } + + public function test_on_latest_version_command_succeeding() + { + [$app, $tester] = $this->appAndTester(); + + $valet = Mockery::mock(Valet::class); + $valet->shouldReceive('onLatestVersion')->once()->andReturn(true); + + swap(Valet::class, $valet); + + $tester->run(['command' => 'on-latest-version']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Yes', $tester->getDisplay()); + } + + public function test_on_latest_version_command_failing() + { + [$app, $tester] = $this->appAndTester(); + + $valet = Mockery::mock(Valet::class); + $valet->shouldReceive('onLatestVersion')->once()->andReturn(false); + + swap(Valet::class, $valet); + + $tester->run(['command' => 'on-latest-version']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('not the latest', $tester->getDisplay()); + } + + public function test_trust_command_on() + { + [$app, $tester] = $this->appAndTester(); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('createSudoersEntry')->once(); + + swap(Brew::class, $brew); + + $valet = Mockery::mock(Valet::class); + $valet->shouldReceive('createSudoersEntry')->once(); + + swap(Valet::class, $valet); + + $tester->run(['command' => 'trust']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('have been added', $tester->getDisplay()); + } + + public function test_trust_command_off() + { + [$app, $tester] = $this->appAndTester(); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('removeSudoersEntry')->once(); + + swap(Brew::class, $brew); + + $valet = Mockery::mock(Valet::class); + $valet->shouldReceive('removeSudoersEntry')->once(); + + swap(Valet::class, $valet); + + $tester->run(['command' => 'trust', '--off' => true]); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('have been removed', $tester->getDisplay()); + } + + public function test_use_command() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('useVersion')->with('8.2', false); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'use', 'phpVersion' => '8.2']); + $tester->assertCommandIsSuccessful(); + } + + public function test_use_command_without_specified_version_reads_phprc() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('useVersion')->with('php@8.1', false); + + swap(PhpFpm::class, $phpfpm); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('linkedPhp')->andReturn('php@8.2'); + + swap(Brew::class, $brew); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('phpRcVersion')->andReturn('php@8.1'); + + swap(RealSite::class, $site); + + $tester->run(['command' => 'use']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString(".valetphprc' specifying version: php@8.1", $tester->getDisplay()); + } + + public function test_use_command_without_specified_version_reads_isolation_list() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('useVersion')->with('php@8.0', false); + $phpfpm->shouldReceive('normalizePhpVersion')->with('php@8.0')->andReturn('php@8.0'); + + swap(PhpFpm::class, $phpfpm); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('linkedPhp')->andReturn('php@8.2'); + + swap(Brew::class, $brew); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('phpRcVersion')->andReturn(null); + $site->shouldReceive('customPhpVersion')->andReturn('php@8.0'); + + swap(RealSite::class, $site); + + $tester->run(['command' => 'use']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('isolated site', $tester->getDisplay()); + } + + public function test_use_command_with_no_specifications() + { + [$app, $tester] = $this->appAndTester(); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('linkedPhp')->andReturn('php@8.2'); + + swap(Brew::class, $brew); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('phpRcVersion')->andReturn(null); + $site->shouldReceive('customPhpVersion')->andReturn(null); + + swap(RealSite::class, $site); + + $tester->run(['command' => 'use']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Valet is using php@8.2', $tester->getDisplay()); + } + + public function test_isolate_command() + { + [$app, $tester] = $this->appAndTester(); + + // The site this command should assume we're in if we don't pass in --site + $getcwd = 'valet'; + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('isolateDirectory')->with($getcwd, '8.1'); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'isolate', 'phpVersion' => '8.1']); + $tester->assertCommandIsSuccessful(); + } + + public function test_isolate_command_with_phprc() + { + [$app, $tester] = $this->appAndTester(); + + // The site this command should assume we're in if we don't pass in --site + $getcwd = 'valet'; + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('isolateDirectory')->with($getcwd, '8.2'); + + swap(PhpFpm::class, $phpfpm); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('phpRcVersion')->once()->andReturn('8.2'); + + swap(RealSite::class, $site); + + $tester->run(['command' => 'isolate']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('specifying version', $tester->getDisplay()); + } + + public function test_unisolate_command() + { + [$app, $tester] = $this->appAndTester(); + + // The site this command should assume we're in if we don't pass in --site + $getcwd = 'valet'; + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('unisolateDirectory')->with($getcwd)->once(); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'unisolate']); + $tester->assertCommandIsSuccessful(); + } + + public function test_unisolate_command_with_custom_site() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('unisolateDirectory')->with('my-best-site'); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'unisolate', '--site' => 'my-best-site']); + $tester->assertCommandIsSuccessful(); + } + + public function test_isolated_command() + { + [$app, $tester] = $this->appAndTester(); + + $phpfpm = Mockery::mock(PhpFpm::class); + $phpfpm->shouldReceive('isolatedDirectories')->andReturn(collect([['best-directory', '8.1']])); + + swap(PhpFpm::class, $phpfpm); + + $tester->run(['command' => 'isolated']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('best-directory', $tester->getDisplay()); + } + + public function test_which_php_command_reads_nginx() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('host')->once()->andReturn('whatever'); + $site->shouldReceive('customPhpVersion')->once()->andReturn('8.2'); + + swap(RealSite::class, $site); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('getPhpExecutablePath')->with('8.2')->once()->andReturn('testOutput'); + + swap(Brew::class, $brew); + + $tester->run(['command' => 'which-php']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('testOutput', $tester->getDisplay()); + } + + public function test_which_php_command_reads_phprc() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('host')->once()->andReturn('whatever'); + $site->shouldReceive('customPhpVersion')->once()->andReturn(null); + $site->shouldReceive('phpRcVersion')->once()->andReturn('8.1'); + + swap(RealSite::class, $site); + + $brew = Mockery::mock(Brew::class); + $brew->shouldReceive('getPhpExecutablePath')->with('8.1')->once()->andReturn('testOutput'); + + swap(Brew::class, $brew); + + $tester->run(['command' => 'which-php']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('testOutput', $tester->getDisplay()); + } + + public function test_log_command() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'log']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('In order to tail a log', $tester->getDisplay()); + } + + public function test_log_command_with_key() + { + [$app, $tester] = $this->appAndTester(); + + $tester->run(['command' => 'log', 'key' => 'nginx']); + $tester->assertCommandIsSuccessful(); + + // Can't test this because log's output is run via the tail command + // $this->assertStringContainsString('nginx-error-log-contents', $tester->getDisplay()); + + // So instead, we'll test *against* the negative states + $this->assertStringNotContainsString('does not (yet) exit', $tester->getDisplay()); + $this->assertStringNotContainsString('No logs found', $tester->getDisplay()); + $this->assertStringNotContainsString('In order to tail a log', $tester->getDisplay()); + } + + public function test_directory_listing_command_reads() + { + [$app, $tester] = $this->appAndTester(); + Configuration::updateKey('directory-listing', 'off'); + + $tester->run(['command' => 'directory-listing']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Directory listing is off', $tester->getDisplay()); + } + + public function test_directory_listing_command_sets() + { + [$app, $tester] = $this->appAndTester(); + Configuration::updateKey('directory-listing', 'off'); + + $tester->run(['command' => 'directory-listing', 'status' => 'on']); + $tester->assertCommandIsSuccessful(); + + $this->assertStringContainsString('Directory listing setting is now: on', $tester->getDisplay()); + } + + public function test_diagnose_command() + { + [$app, $tester] = $this->appAndTester(); + + $diagnose = Mockery::mock(Diagnose::class); + $diagnose->shouldReceive('run')->with(false, false); + + swap(Diagnose::class, $diagnose); + + $tester->run(['command' => 'diagnose']); + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('Diagnostics output', $tester->getDisplay()); + } } diff --git a/tests/ComposerTest.php b/tests/ComposerTest.php new file mode 100644 index 000000000..ad3642891 --- /dev/null +++ b/tests/ComposerTest.php @@ -0,0 +1,80 @@ +setNullWriter(); + } + + public function tear_down() + { + Mockery::close(); + } + + public function test_composer_can_be_resolved_from_container() + { + $this->assertInstanceOf(Composer::class, resolve(Composer::class)); + } + + public function test_installed_returns_true_when_given_package_is_installed() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('composer global show --format json -- beyondcode/expose') + ->andReturn('{"name":"beyondcode/expose"}'); + swap(CommandLine::class, $cli); + + $this->assertTrue(resolve(Composer::class)->installed('beyondcode/expose')); + } + + public function test_installed_returns_false_when_given_formula_is_not_installed() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('composer global show --format json -- beyondcode/expose') + ->andReturn("Changed current directory to /Users/mattstauffer/.composer\n\n[InvalidArgumentException]\nPackage beyondcode/expose not found"); + swap(CommandLine::class, $cli); + + $this->assertFalse(resolve(Composer::class)->installed('beyondcode/expose')); + } + + public function test_install_or_fail_will_install_composer_package() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('composer global require beyondcode/expose', Mockery::type('Closure')); + swap(CommandLine::class, $cli); + + resolve(Composer::class)->installOrFail('beyondcode/expose'); + } + + public function test_installed_version_returns_null_when_given_package_is_not_installed() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('composer global show --format json -- beyondcode/expose') + ->andReturn("Changed current directory to /Users/mattstauffer/.composer\n\n[InvalidArgumentException]\nPackage beyondcode/expose not found"); + swap(CommandLine::class, $cli); + + $this->assertNull(resolve(Composer::class)->installedVersion('beyondcode/expose')); + } + + public function test_installed_version_returns_version_when_package_is_installed() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('composer global show --format json -- beyondcode/expose') + ->andReturn('{"versions":["1.4.2"]}'); + swap(CommandLine::class, $cli); + + $this->assertEquals('1.4.2', resolve(Composer::class)->installedVersion('beyondcode/expose')); + } +} diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index bde7e62fc..6af29ab51 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -31,7 +31,6 @@ public function test_configuration_directory_is_created_if_it_doesnt_exist() $files = Mockery::mock(Filesystem::class.'[ensureDirExists,isDir]'); $files->shouldReceive('ensureDirExists')->once()->with(preg_replace('~/valet$~', '', VALET_HOME_PATH), user()); $files->shouldReceive('ensureDirExists')->once()->with(VALET_HOME_PATH, user()); - $files->shouldReceive('isDir')->once(); swap(Filesystem::class, $files); resolve(Configuration::class)->createConfigurationDirectory(); } diff --git a/tests/DnsMasqTest.php b/tests/DnsMasqTest.php index 1c45b68c6..5f9b58133 100644 --- a/tests/DnsMasqTest.php +++ b/tests/DnsMasqTest.php @@ -70,7 +70,7 @@ public function test_update_tld_removes_old_resolver_and_reinstalls() class StubForCreatingCustomDnsMasqConfigFiles extends DnsMasq { - public function dnsmasqUserConfigDir() + public function dnsmasqUserConfigDir(): string { return __DIR__.'/output/'; } diff --git a/tests/Drivers/BaseDriverTestCase.php b/tests/Drivers/BaseDriverTestCase.php index d48359fc0..8bbc2c6f3 100644 --- a/tests/Drivers/BaseDriverTestCase.php +++ b/tests/Drivers/BaseDriverTestCase.php @@ -2,6 +2,11 @@ class BaseDriverTestCase extends Yoast\PHPUnitPolyfills\TestCases\TestCase { + public function set_up(): void + { + $_SERVER['HTTP_HOST'] = 'this is set in Valet requests but not phpunit'; + } + public function projects(): array { return Filesystem::scanDir(__DIR__.'/projects'); diff --git a/tests/Drivers/BasicValetDriverTest.php b/tests/Drivers/BasicValetDriverTest.php index a33368427..49a4a96c4 100644 --- a/tests/Drivers/BasicValetDriverTest.php +++ b/tests/Drivers/BasicValetDriverTest.php @@ -12,4 +12,59 @@ public function test_it_serves_anything() $this->assertTrue($driver->serves($projectDir, 'my-site', '/')); } } + + public function test_it_serves_php_files_from_root() + { + $projectPath = $this->projectDir('basic-no-public'); + $driver = new BasicValetDriver(); + + $this->assertEquals( + $projectPath.'/file-in-root.php', + $driver->frontControllerPath($projectPath, 'my-site', '/file-in-root.php') + ); + } + + public function test_it_serves_directory_with_index_php() + { + $projectPath = $this->projectDir('basic-no-public'); + $driver = new BasicValetDriver(); + + $this->assertEquals( + $projectPath.'/about/index.php', + $driver->frontControllerPath($projectPath, 'my-site', '/about') + ); + } + + public function test_it_routes_to_index_if_404() + { + $projectPath = $this->projectDir('basic-no-public'); + $driver = new BasicValetDriver(); + + $this->assertEquals( + $projectPath.'/index.php', + $driver->frontControllerPath($projectPath, 'my-site', '/not-a-real-url') + ); + } + + public function test_it_serves_directory_with_index_html() + { + $projectPath = $this->projectDir('basic-no-public'); + $driver = new BasicValetDriver(); + + $this->assertEquals( + $projectPath.'/team/index.html', + $driver->isStaticFile($projectPath, 'my-site', '/team') + ); + } + + public function test_it_serves_static_files() + { + $projectPath = $this->projectDir('basic-no-public'); + $driver = new BasicValetDriver(); + + $this->assertEquals( + $projectPath.'/assets/document.txt', + $driver->isStaticFile($projectPath, 'my-site', '/assets/document.txt') + ); + } } diff --git a/tests/Drivers/BasicWithPublicValetDriverTest.php b/tests/Drivers/BasicWithPublicValetDriverTest.php new file mode 100644 index 000000000..51a7ed2a5 --- /dev/null +++ b/tests/Drivers/BasicWithPublicValetDriverTest.php @@ -0,0 +1,86 @@ +assertTrue($driver->serves($this->projectDir('public-with-index-non-laravel'), 'my-site', '/')); + } + + public function test_it_doesnt_serve_from_not_public() + { + $driver = new BasicWithPublicValetDriver(); + + $this->assertFalse($driver->serves($this->projectDir('basic-no-public'), 'my-site', '/')); + } + + public function test_it_serves_php_files_from_public() + { + $projectPath = $this->projectDir('public-with-index-non-laravel'); + $driver = new BasicWithPublicValetDriver(); + + $this->assertEquals( + $projectPath.'/public/file-in-public.php', + $driver->frontControllerPath($projectPath, 'my-site', '/file-in-public.php') + ); + } + + public function test_it_doesnt_serve_php_files_from_root() + { + $projectPath = $this->projectDir('public-with-index-non-laravel'); + $driver = new BasicWithPublicValetDriver(); + + $this->assertEquals( + $projectPath.'/public/index.php', + $driver->frontControllerPath($projectPath, 'my-site', '/file-in-root.php') + ); + } + + public function test_it_serves_directory_with_index_php() + { + $projectPath = $this->projectDir('public-with-index-non-laravel'); + $driver = new BasicWithPublicValetDriver(); + + $this->assertEquals( + $projectPath.'/public/about/index.php', + $driver->frontControllerPath($projectPath, 'my-site', '/about') + ); + } + + public function test_it_route_to_public_index_if_404() + { + $projectPath = $this->projectDir('public-with-index-non-laravel'); + $driver = new BasicWithPublicValetDriver(); + + $this->assertEquals( + $projectPath.'/public/index.php', + $driver->frontControllerPath($projectPath, 'my-site', '/not-a-real-url') + ); + } + + public function test_it_serves_directory_with_index_html() + { + $projectPath = $this->projectDir('public-with-index-non-laravel'); + $driver = new BasicWithPublicValetDriver(); + + $this->assertEquals( + $projectPath.'/public/team/index.html', + $driver->isStaticFile($projectPath, 'my-site', '/team') + ); + } + + public function test_it_serves_static_files() + { + $projectPath = $this->projectDir('public-with-index-non-laravel'); + $driver = new BasicWithPublicValetDriver(); + + $this->assertEquals( + $projectPath.'/public/assets/document.txt', + $driver->isStaticFile($projectPath, 'my-site', '/assets/document.txt') + ); + } +} diff --git a/tests/Drivers/BedrockValetDriverTest.php b/tests/Drivers/BedrockValetDriverTest.php index 39613acaf..2423dd63e 100644 --- a/tests/Drivers/BedrockValetDriverTest.php +++ b/tests/Drivers/BedrockValetDriverTest.php @@ -1,6 +1,6 @@ projectDir('bedrock'); $this->assertEquals($projectPath.'/web/index.php', $driver->frontControllerPath($projectPath, 'my-site', '/')); } diff --git a/tests/Drivers/CakeValetDriverTest.php b/tests/Drivers/CakeValetDriverTest.php index 318af4606..24d615973 100644 --- a/tests/Drivers/CakeValetDriverTest.php +++ b/tests/Drivers/CakeValetDriverTest.php @@ -1,6 +1,6 @@ projectDir('craft'); $this->assertEquals($projectPath.'/public/index.php', $driver->frontControllerPath($projectPath, 'my-site', '/')); } diff --git a/tests/Drivers/DrupalValetDriverTest.php b/tests/Drivers/DrupalValetDriverTest.php index 6253b27ea..a7633355d 100644 --- a/tests/Drivers/DrupalValetDriverTest.php +++ b/tests/Drivers/DrupalValetDriverTest.php @@ -1,6 +1,6 @@ projectDir('drupal'); $this->assertEquals($projectPath.'/public/index.php', $driver->frontControllerPath($projectPath, 'my-site', '/')); } diff --git a/tests/Drivers/JigsawValetDriverTest.php b/tests/Drivers/JigsawValetDriverTest.php index b048e19a4..aa01327b3 100644 --- a/tests/Drivers/JigsawValetDriverTest.php +++ b/tests/Drivers/JigsawValetDriverTest.php @@ -1,6 +1,6 @@ projectDir('joomla'); $this->assertEquals($projectPath.'/index.php', $driver->frontControllerPath($projectPath, 'my-site', '/')); } diff --git a/tests/Drivers/KatanaValetDriverTest.php b/tests/Drivers/KatanaValetDriverTest.php index a243b702e..738f44aef 100644 --- a/tests/Drivers/KatanaValetDriverTest.php +++ b/tests/Drivers/KatanaValetDriverTest.php @@ -1,6 +1,6 @@ projectDir('kirby'); $this->assertEquals($projectPath.'/index.php', $driver->frontControllerPath($projectPath, 'my-site', '/')); } diff --git a/tests/Drivers/Magento2ValetDriverTest.php b/tests/Drivers/Magento2ValetDriverTest.php index 9e675d2d9..a0b8a4b0b 100644 --- a/tests/Drivers/Magento2ValetDriverTest.php +++ b/tests/Drivers/Magento2ValetDriverTest.php @@ -1,6 +1,6 @@ assertEquals(BedrockValetDriver::class, get_class($assignedDriver)); } + + public function test_it_prioritizes_non_basic_matches() + { + $assignedDriver = ValetDriver::assign($this->projectDir('laravel'), 'my-site', '/'); + + $this->assertNotEquals('Valet\Drivers\BasicWithPublicValetDriver', get_class($assignedDriver)); + $this->assertNotEquals('Valet\Drivers\BasicValetDriver', get_class($assignedDriver)); + } + + public function test_it_checks_composer_dependencies() + { + $driver = new BasicValetDriver; + $this->assertTrue($driver->composerRequires(__DIR__.'/../files/sites/has-composer', 'tightenco/collect')); + $this->assertFalse($driver->composerRequires(__DIR__.'/../files/sites/has-composer', 'tightenco/ziggy')); + } } diff --git a/tests/Drivers/WordPressValetDriverTest.php b/tests/Drivers/WordPressValetDriverTest.php index 5f0d493e4..317831b6f 100644 --- a/tests/Drivers/WordPressValetDriverTest.php +++ b/tests/Drivers/WordPressValetDriverTest.php @@ -1,6 +1,6 @@ projectDir('wordpress'); $this->assertEquals($projectPath.'/index.php', $driver->frontControllerPath($projectPath, 'my-site', '/')); } diff --git a/tests/Drivers/projects/basic-no-public/about/index.php b/tests/Drivers/projects/basic-no-public/about/index.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/basic-no-public/assets/document.txt b/tests/Drivers/projects/basic-no-public/assets/document.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/basic-no-public/file-in-root.php b/tests/Drivers/projects/basic-no-public/file-in-root.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/basic-no-public/index.php b/tests/Drivers/projects/basic-no-public/index.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/basic-no-public/team/index.html b/tests/Drivers/projects/basic-no-public/team/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/public-with-index-non-laravel/file-in-root.php b/tests/Drivers/projects/public-with-index-non-laravel/file-in-root.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/public-with-index-non-laravel/public/about/index.php b/tests/Drivers/projects/public-with-index-non-laravel/public/about/index.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/public-with-index-non-laravel/public/assets/document.txt b/tests/Drivers/projects/public-with-index-non-laravel/public/assets/document.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/public-with-index-non-laravel/public/file-in-public.php b/tests/Drivers/projects/public-with-index-non-laravel/public/file-in-public.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Drivers/projects/public-with-index-non-laravel/public/team/index.html b/tests/Drivers/projects/public-with-index-non-laravel/public/team/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/tests/NgrokTest.php b/tests/NgrokTest.php index e7f8374df..23e61b726 100644 --- a/tests/NgrokTest.php +++ b/tests/NgrokTest.php @@ -1,8 +1,8 @@ assertEquals('http://right-one.ngrok.io/', $ngrok->findHttpTunnelUrl($tunnels, 'mysite')); } @@ -64,7 +64,7 @@ public function test_it_checks_against_lowercased_domain() ], ]; - $ngrok = new Ngrok(new CommandLine); + $ngrok = resolve(Ngrok::class); $this->assertEquals('http://right-one.ngrok.io/', $ngrok->findHttpTunnelUrl($tunnels, 'MySite')); } } diff --git a/tests/PhpFpmTest.php b/tests/PhpFpmTest.php index 7b5cc4de1..f8723d0c0 100644 --- a/tests/PhpFpmTest.php +++ b/tests/PhpFpmTest.php @@ -42,9 +42,9 @@ public function test_fpm_is_configured_with_the_correct_user_group_and_port() 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.'/valet72.sock', $contents); + $this->assertStringContainsString(sprintf(PHP_EOL.'user = %s', user()), $contents); + $this->assertStringContainsString(PHP_EOL.'group = staff', $contents); + $this->assertStringContainsString(PHP_EOL.'listen = '.VALET_HOME_PATH.'/valet72.sock', $contents); // It should disable old or default FPM Pool configuration $this->assertFileDoesNotExist(__DIR__.'/output/www.conf'); @@ -448,7 +448,7 @@ public function test_isolate_will_throw_if_site_is_not_parked_or_linked() class StubForUpdatingFpmConfigFiles extends PhpFpm { - public function fpmConfigPath($phpVersion = null) + public function fpmConfigPath(?string $phpVersion = null): string { return __DIR__.'/output/fpm.conf'; } diff --git a/tests/ServerTest.php b/tests/ServerTest.php new file mode 100644 index 000000000..8c4b16d4d --- /dev/null +++ b/tests/ServerTest.php @@ -0,0 +1,105 @@ +setNullWriter(); + } + + public function tear_down() + { + Mockery::close(); + } + + public function test_it_extracts_uri_from_server_request_uri() + { + $this->assertEquals('/about/index.php', Server::uriFromRequestUri('/about/index.php?abc=def&qrs=tuv')); + $this->assertEquals('/', Server::uriFromRequestUri('/?abc=def&qrs=tuv')); + } + + public function test_it_extracts_domain_from_site_name() + { + $this->assertEquals('tighten', Server::domainFromSiteName('subdomain.tighten')); + } + + public function test_it_gets_site_name_from_http_host() + { + $server = new Server(['tld' => 'test']); + + $httpHost = 'tighten.test'; + $this->assertEquals('tighten', $server->siteNameFromHttpHost($httpHost)); + } + + public function test_it_gets_site_name_from_http_host_using_wildcard() + { + $server = new Server(['tld' => 'test']); + + $httpHost = 'tighten.192.168.0.10.nip.io'; + $this->assertEquals('tighten', $server->siteNameFromHttpHost($httpHost)); + $httpHost = 'tighten-192-168-0-10.nip.io'; + $this->assertEquals('tighten', $server->siteNameFromHttpHost($httpHost)); + } + + public function test_it_strips_www_dot_from_http_host() + { + $server = new Server(['tld' => 'test']); + + $httpHost = 'www.tighten.test'; + $this->assertEquals('tighten', $server->siteNameFromHttpHost($httpHost)); + } + + public function test_it_gets_site_path_from_site_name() + { + $server = new Server(['paths' => [__DIR__.'/files/sites']]); + + $realPath = __DIR__.'/files/sites/tighten'; + $this->assertEquals($realPath, $server->sitePath('tighten')); + $realPath = __DIR__.'/files/sites/tighten'; + $this->assertEquals($realPath, $server->sitePath('subdomain.tighten')); + } + + public function test_it_returns_null_if_site_does_not_match() + { + $server = new Server(['paths' => []]); + + $this->assertNull($server->sitePath('tighten')); + } + + public function test_it_gets_default_site_path() + { + $server = new Server(['default' => __DIR__.'/files/sites/tighten']); + + $this->assertEquals(__DIR__.'/files/sites/tighten', $server->defaultSitePath()); + } + + public function test_it_returns_null_default_site_path_if_not_set() + { + $server = new Server([]); + + $this->assertNull($server->defaultSitePath()); + } + + public function test_it_tests_whether_host_is_ip_address() + { + $this->assertTrue(Server::hostIsIpAddress('192.168.1.1')); + $this->assertFalse(Server::hostIsIpAddress('google.com')); + $this->assertFalse(Server::hostIsIpAddress('19.google.com')); + $this->assertFalse(Server::hostIsIpAddress('19.19.19.19.google.com')); + } + + public function test_it_extracts_host_from_ip_address_uri() + { + $this->assertEquals('onramp.test', Server::valetSiteFromIpAddressUri('onramp.test/auth/login', 'test')); + $this->assertNull(Server::valetSiteFromIpAddressUri('onramp.dev/auth/login', 'test')); + } +} diff --git a/tests/SiteTest.php b/tests/SiteTest.php index c933e3dad..ffa80c78c 100644 --- a/tests/SiteTest.php +++ b/tests/SiteTest.php @@ -900,72 +900,76 @@ public function test_it_returns_secured_sites() $this->assertSame(['helloworld.tld'], $sites); } - public function test_it_can_read_php_rc_version() + public function test_it_returns_true_if_a_site_is_secured() { - $config = Mockery::mock(Configuration::class); $files = Mockery::mock(Filesystem::class); + $files->shouldReceive('scandir') + ->once() + ->andReturn(['helloworld.tld.crt', '.DS_Store']); - swap(Configuration::class, $config); - swap(Filesystem::class, $files); + $config = Mockery::mock(Configuration::class); + $config->shouldReceive('read') + ->once() + ->andReturn(['tld' => 'tld']); - $siteMock = Mockery::mock(Site::class, [ - resolve(Brew::class), - resolve(Configuration::class), - resolve(CommandLine::class), - resolve(Filesystem::class), - ])->makePartial(); + swap(Filesystem::class, $files); + swap(Configuration::class, $config); - swap(Site::class, $siteMock); + $site = resolve(Site::class); - $config->shouldReceive('read') - ->andReturn(['tld' => 'test', 'loopback' => VALET_LOOPBACK, 'paths' => []]); + $this->assertTrue($site->isSecured('helloworld')); + } - $siteMock->shouldReceive('parked') - ->andReturn(collect([ - 'site1' => [ - 'site' => 'site1', - 'secured' => '', - 'url' => 'http://site1.test', - 'path' => '/Users/name/code/site1', - ], - ])); + public function test_it_can_read_valet_rc_files() + { + resolve(Configuration::class)->addPath(__DIR__.'/fixtures/Parked/Sites'); + $site = resolve(Site::class); - $siteMock->shouldReceive('links')->andReturn(collect([ - 'site2' => [ - 'site' => 'site2', - 'secured' => 'X', - 'url' => 'http://site2.test', - 'path' => '/Users/name/some-other-directory/site2', - ], - ])); + $this->assertEquals([ + 'item' => 'value', + 'php' => 'php@8.0', + 'other_item' => 'othervalue', + ], $site->valetRc('site-w-valetrc-1')); - $files->shouldReceive('exists')->with('/Users/name/code/site1/.valetphprc')->andReturn(true); - $files->shouldReceive('get')->with('/Users/name/code/site1/.valetphprc')->andReturn('php@8.1'); + $this->assertEquals([ + 'php' => 'php@8.1', + ], $site->valetRc('site-w-valetrc-2')); - $files->shouldReceive('exists')->with('/Users/name/some-other-directory/site2/.valetphprc')->andReturn(true); - $files->shouldReceive('get')->with('/Users/name/some-other-directory/site2/.valetphprc')->andReturn('php@8.0'); + $this->assertEquals([ + 'item' => 'value', + 'php' => 'php@8.2', + ], $site->valetRc('site-w-valetrc-3')); + } - $files->shouldReceive('exists')->with('/tmp/cwd-site/.valetphprc')->andReturn(true); - $files->shouldReceive('get')->with('/tmp/cwd-site/.valetphprc')->andReturn('php@8.2'); + public function test_it_can_read_php_rc_version() + { + resolve(Configuration::class)->addPath(__DIR__.'/fixtures/Parked/Sites'); + $site = resolve(Site::class); - $this->assertEquals('php@8.1', $siteMock->phpRcVersion('site1')); - $this->assertEquals('php@8.0', $siteMock->phpRcVersion('site2')); - $this->assertEquals('php@8.2', $siteMock->phpRcVersion('blabla', '/tmp/cwd-site')); - $this->assertEquals(null, $siteMock->phpRcVersion('site3')); // Site doesn't exist + $this->assertEquals('php@8.1', $site->phpRcVersion('site-w-valetphprc-1')); + $this->assertEquals('php@8.0', $site->phpRcVersion('site-w-valetphprc-2')); + $this->assertEquals(null, $site->phpRcVersion('my-best-site')); + $this->assertEquals(null, $site->phpRcVersion('non-existent-site')); + $this->assertEquals('php@8.0', $site->phpRcVersion('site-w-valetrc-1')); + $this->assertEquals('php@8.1', $site->phpRcVersion('site-w-valetrc-2')); + $this->assertEquals('php@8.2', $site->phpRcVersion('site-w-valetrc-3')); + $this->assertEquals('php@8.2', $site->phpRcVersion('blabla', __DIR__.'/fixtures/Parked/Sites/site-w-valetrc-3')); } } class CommandLineFake extends CommandLine { - public function runCommand($command, callable $onError = null) + public function runCommand(string $command, callable $onError = null): string { // noop // - // This let's us pretend like every command executes correctly + // This lets us pretend like every command executes correctly // so we can (elsewhere) ensure the things we meant to do // (like "create a certificate") look like they // happened without actually running any // commands for real. + + return 'hooray!'; } } @@ -975,7 +979,7 @@ class FixturesSiteFake extends Site private $crtCounter = 0; - public function valetHomePath() + public function valetHomePath(): string { if (! isset($this->valetHomePath)) { throw new \RuntimeException(static::class.' needs to be configured using useFixtures or useOutput'); @@ -1002,7 +1006,7 @@ public function useOutput() $this->valetHomePath = __DIR__.'/output'; } - public function createCa($caExpireInDays) + public function createCa(int $caExpireInDays): void { // noop // @@ -1011,7 +1015,7 @@ public function createCa($caExpireInDays) // CA for our faked Site. } - public function createCertificate($urlWithTld, $caExpireInDays) + public function createCertificate(string $urlWithTld, int $caExpireInDays): void { // We're not actually going to generate a real certificate // here. We are going to do something basic to include @@ -1078,7 +1082,7 @@ public function assertCertificateExistsWithCounterValue($urlWithTld, $counter) class StubForRemovingLinks extends Site { - public function sitesPath($additionalPath = null) + public function sitesPath(?string $additionalPath = null): string { return __DIR__.'/output'.($additionalPath ? '/'.$additionalPath : ''); } diff --git a/tests/StatusTest.php b/tests/StatusTest.php new file mode 100644 index 000000000..d0bfb1d16 --- /dev/null +++ b/tests/StatusTest.php @@ -0,0 +1,59 @@ +setNullWriter(); + } + + public function tear_down() + { + Mockery::close(); + } + + public function test_status_can_be_resolved_from_container() + { + $this->assertInstanceOf(Status::class, resolve(Status::class)); + } + + public function test_it_checks_if_brew_services_are_running() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('brew services info --all --json')->andReturn('[{"name":"nginx","running":true}]'); + $cli->shouldReceive('run')->once()->with('brew services info --all --json')->andReturn('[{"name":"php","running":true}]'); + + swap(CommandLine::class, $cli); + + $status = resolve(Status::class); + + $this->assertTrue($status->isBrewServiceRunning('nginx')); + $this->assertTrue($status->isBrewServiceRunning('php')); + } + + public function test_it_checks_imprecisely_if_brew_services_are_running() + { + $cli = Mockery::mock(CommandLine::class); + $cli->shouldReceive('runAsUser')->once()->with('brew services info --all --json')->andReturn('[{"name":"nginx","running":true}]'); + $cli->shouldReceive('run')->once()->with('brew services info --all --json')->andReturn('[{"name":"php@8.1","running":true}]'); + + swap(CommandLine::class, $cli); + + $status = resolve(Status::class); + + $this->assertTrue($status->isBrewServiceRunning('nginx')); + $this->assertTrue($status->isBrewServiceRunning('php', exactMatch: false)); + } +} diff --git a/tests/files/sites/has-composer/composer.json b/tests/files/sites/has-composer/composer.json new file mode 100644 index 000000000..8c13f001c --- /dev/null +++ b/tests/files/sites/has-composer/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "tightenco/collect": "1.0.0" + } +} diff --git a/tests/files/sites/tighten/.gitkeep b/tests/files/sites/tighten/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/Parked/Sites/my-best-site/.gitkeep b/tests/fixtures/Parked/Sites/my-best-site/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/Parked/Sites/site-w-valetphprc-1/.valetphprc b/tests/fixtures/Parked/Sites/site-w-valetphprc-1/.valetphprc new file mode 100644 index 000000000..a37cb985d --- /dev/null +++ b/tests/fixtures/Parked/Sites/site-w-valetphprc-1/.valetphprc @@ -0,0 +1 @@ +php@8.1 diff --git a/tests/fixtures/Parked/Sites/site-w-valetphprc-2/.valetphprc b/tests/fixtures/Parked/Sites/site-w-valetphprc-2/.valetphprc new file mode 100644 index 000000000..232a0c0a4 --- /dev/null +++ b/tests/fixtures/Parked/Sites/site-w-valetphprc-2/.valetphprc @@ -0,0 +1 @@ +php@8.0 diff --git a/tests/fixtures/Parked/Sites/site-w-valetrc-1/.valetrc b/tests/fixtures/Parked/Sites/site-w-valetrc-1/.valetrc new file mode 100644 index 000000000..4f0d634f0 --- /dev/null +++ b/tests/fixtures/Parked/Sites/site-w-valetrc-1/.valetrc @@ -0,0 +1,4 @@ +ITEM=value +PHP=php@8.0 +# comment line +OTHER_ITEM=othervalue diff --git a/tests/fixtures/Parked/Sites/site-w-valetrc-2/.valetrc b/tests/fixtures/Parked/Sites/site-w-valetrc-2/.valetrc new file mode 100644 index 000000000..55d111222 --- /dev/null +++ b/tests/fixtures/Parked/Sites/site-w-valetrc-2/.valetrc @@ -0,0 +1 @@ +PHP=php@8.1 diff --git a/tests/fixtures/Parked/Sites/site-w-valetrc-3/.valetrc b/tests/fixtures/Parked/Sites/site-w-valetrc-3/.valetrc new file mode 100644 index 000000000..7b6c229ad --- /dev/null +++ b/tests/fixtures/Parked/Sites/site-w-valetrc-3/.valetrc @@ -0,0 +1,2 @@ +ITEM=value +PHP=php@8.2 diff --git a/upgrade.md b/upgrade.md new file mode 100644 index 000000000..3f59eae6c --- /dev/null +++ b/upgrade.md @@ -0,0 +1,9 @@ +# Upgrading to v4 + +- You must run `valet` once for the upgrader to run +- Only works on PHP 8.0+ +- Update custom drivers and SampleValetDriver: + - Match the new type hints of the base ValetDriver + - Extend the new namespaced drivers instead of the old globally-namespaced drivers + - Add namespace +- Probably a lot more, @todo forgot to flesh this out as i went diff --git a/valet b/valet index ec78ab259..4cc0f703d 100755 --- a/valet +++ b/valet @@ -20,35 +20,93 @@ then DIR=$(php -r "echo realpath('$DIR/../laravel/valet');") fi +# Get a command-line executable we can use for php that's 8+; if this +# is the inside loop (Valet runs itself 2x in some settings), skip +# checking and pulling again by reading the exported env var +if [[ $PHP_EXECUTABLE = "" ]] +then + PHP=$(php $DIR/find-usable-php.php) + + # Validate output before running it on the CLI + if [[ ! -f $PHP ]]; then + echo "Error finding executable PHP. Quitting for safety." + echo "Provided output from find-usable-php.php:" + echo $PHP + exit + fi + + export PHP_EXECUTABLE=$PHP +else + PHP=$PHP_EXECUTABLE +fi + # If the command is the "share" command we will need to resolve out any # symbolic links for the site. Before starting Ngrok, we will fire a # process to retrieve the live Ngrok tunnel URL in the background. if [[ "$1" = "share" ]] then - # Check for parameters to pass through to ngrok (these will start with '-' or '--') - PARAMS=(${@:2}) - for PARAM in ${PARAMS[@]} - do - if [[ ${PARAM:0:1} != '-' ]]; then - PARAMS=("${PARAMS[@]/$PARAM}") #Quotes when working with strings - fi - done + SHARETOOL="$($PHP "$DIR/cli/valet.php" share-tool)" - PARAMS=${PARAMS[@]} + if [[ $SHARETOOL = "ngrok" ]] + then + # ngrok + # Check for parameters to pass through to ngrok (these will start with '-' or '--') + PARAMS=(${@:2}) + for PARAM in ${PARAMS[@]} + do + if [[ ${PARAM:0:1} != '-' ]]; then + PARAMS=("${PARAMS[@]/$PARAM}") # Quotes when working with strings + fi + done + + PARAMS=${PARAMS[@]} + + HOST="${PWD##*/}" + + # Find the first linked site for the current dir, if one exists + for linkname in ~/.config/valet/Sites/*; do + if [[ "$(readlink $linkname)" = "$PWD" ]] + then + HOST="${linkname##*/}" + break + fi + done - HOST="${PWD##*/}" + TLD=$($PHP "$DIR/cli/valet.php" tld) - # Check for custom domain passed through to the share command ($2 w/o '-' prefix) - if [[ ${2:0:1} != '-' ]]; then - # If not blank and is a link, or is the cwd, use it - if [[ ! -z $2 && (-L ~/.config/valet/Sites/$2 || $2 == $HOST) ]]; then - HOST=$2 - CLIHOST=$2 + # Decide the correct PORT: uses 60 for secure, else 80 + if grep --quiet --no-messages 443 ~/.config/valet/Nginx/$HOST* + then + PORT=60 + else + PORT=80 fi - fi - # If no custom domain passed, then check if there's a linked site for cwd - if [[ -z $CLIHOST ]]; then + # Lowercase the host to match how the rest of our domains are looked up + HOST=$(echo "$HOST" | tr '[:upper:]' '[:lower:]') + + BREW_PREFIX=$(brew --prefix) + sudo -u "$USER" "$BREW_PREFIX/bin/ngrok" http "$HOST.$TLD:$PORT" --host-header=rewrite $PARAMS + + exit + + elif [[ $SHARETOOL = "expose" ]] + then + + # expose + # Check for parameters to pass through to Expose (these will start with '-' or '--') + PARAMS=(${@:2}) + for PARAM in ${PARAMS[@]} + do + if [[ ${PARAM:0:1} != '-' ]]; then + PARAMS=("${PARAMS[@]/$PARAM}") #Quotes when working with strings + fi + done + + PARAMS=${PARAMS[@]} + + HOST="${PWD##*/}" + # Find the first linked site for the current dir, if one exists for linkname in ~/.config/valet/Sites/*; do if [[ "$(readlink $linkname)" = "$PWD" ]] @@ -57,27 +115,30 @@ then break fi done - fi - TLD=$(php "$DIR/cli/valet.php" tld) + TLD=$($PHP "$DIR/cli/valet.php" tld) - # Decide the correct PORT: uses 60 for secure, else 80 - if grep --quiet --no-messages 443 ~/.config/valet/Nginx/$HOST* - then - PORT=60 - else - PORT=80 - fi + # Decide the correct PORT: uses 443 for secure, else 80 + if grep --quiet --no-messages 443 ~/.config/valet/Nginx/$HOST* + then + PORT=443 + else + PORT=80 + fi - # Lowercase the host to match how the rest of our domains are looked up - HOST=$(echo "$HOST" | tr '[:upper:]' '[:lower:]') + # Lowercase the host to match how the rest of our domains are looked up + HOST=$(echo "$HOST" | tr '[:upper:]' '[:lower:]') - # Fetch Ngrok URL In Background... - bash "$DIR/cli/scripts/fetch-share-url.sh" "$HOST" & + sudo -u "$USER" expose share "$HOST.$TLD:$PORT" $PARAMS - sudo -u "$USER" "$DIR/bin/ngrok" http "$HOST.$TLD:$PORT" --host-header=rewrite $PARAMS + exit - exit + else + echo '' + echo "Please use 'valet share-tool ngrok' or 'valet share-tool expose'" + echo "to set your preferred share tool." + exit + fi # Proxy PHP commands to the "php" executable on the isolated site elif [[ "$1" = "php" ]] @@ -113,5 +174,5 @@ else exit fi - php "$DIR/cli/valet.php" "$@" + $PHP "$DIR/cli/valet.php" "$@" fi