diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12b4bc8..6a55133 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,17 +1,16 @@ -name: CI-Ubuntu +name: CI on: [pull_request] jobs: - build: - name: Build & Test on Ubuntu 22.04 + test: + name: Build & Test runs-on: ubuntu-22.04 - if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' strategy: matrix: php: - - 8.0 - - 8.1 + - 8.2 + - 8.3 steps: - uses: actions/checkout@v2 - name: Set up PHP @@ -25,3 +24,23 @@ jobs: composer install --no-interaction --prefer-dist - name: Test run: vendor/bin/phpunit + cs_fixer: + name: CS Fixer + runs-on: ubuntu-22.04 + strategy: + matrix: + php: + - 8.3 + steps: + - uses: actions/checkout@v2 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y network-manager libnss3-tools jq xsel + composer install --no-interaction --prefer-dist + - name: CS Fixer + run: vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php diff --git a/.gitignore b/.gitignore index e60bf7f..ef1889c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ vendor/ +bin/ +tests/config composer.lock error.log *.sublime-project *.sublime-workspace -psysh -Gemfile -Gemfile.lock -_site/ .idea -/.php-cs-fixer.cache +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..3df7873 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,56 @@ +in([ + __DIR__.'/cli/', + __DIR__.'/tests/' + ]); + +$config = new PhpCsFixer\Config(); +return $config + ->setRiskyAllowed(true) + ->setUsingCache(false) + ->setRules([ + '@PSR12' => true, + 'list_syntax' => ['syntax' => 'short'], + 'multiline_comment_opening_closing' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => true, + 'no_spaces_after_function_name' => true, + 'no_superfluous_elseif' => true, + 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['class', 'function', 'const']], + 'php_unit_construct' => true, + 'php_unit_expectation' => true, + 'php_unit_mock' => ['target' => 'newest'], + 'php_unit_mock_short_will_return' => true, + 'php_unit_no_expectation_annotation' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_line_span' => ['method' => 'multi', 'property' => 'multi'], + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'protected_to_private' => true, + 'return_assignment' => true, + 'short_scalar_cast' => true, + 'single_trait_insert_per_statement' => true, + 'standardize_not_equals' => true, + 'trim_array_spaces' => true, + 'visibility_required' => ['elements' => ['const']], + 'whitespace_after_comma_in_array' => true, + ]) + ->setFinder($finder); diff --git a/README.md b/README.md index 307636c..8d0c794 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@

-StyleCI Scrutinizer Total Downloads Latest Stable Version diff --git a/TODO b/TODO index 420f6e1..8bc1a3f 100644 --- a/TODO +++ b/TODO @@ -1,13 +1,27 @@ -1. Finish refactoring classes in Valet directory -- Done -2. Fix facade.php -- Done -3. Move config directory from ~/.valet to ~/.config/valet - 3.1 Copy directory from ~/.valet to ~/.config - 3.2 Replace /home/uttam/.valet path with /home/uttam/.config/valet for Nginx directory files (Logs & Certificate, .sock files) - 3.3 Replace .sock file paths in valet.conf file. - 3.4 Regenerate symbolic links in Sites directory -4. Add relevant test cases -5. Enable Phpstan & CodeSniffer -- Done -6. Enable unit coverage report -7. Add Infection -8. Ngrok remove binary file and install it via `valet install` command -9. self-signed certificate verification issues in Firefox & PHP's curl requests +Remain Items: +- Enable unit coverage report +- Add Infection +- Valet php extension install separately so that if not installed we can install it via valet install command. (Optional) +- Improve valet uninstall experience + - User should be able to select which services they want to uninstall +- mbstring extension was not installed in system, but composer install command fail to stop process as it was mentioned in required json + + +Done Items: +- Implement termwind -- Done +- Fix extra messages (Fixing, already enabled, restarting) -- Done + - Remove was already enabled message -- Done +- Enable Phpstan & CodeSniffer -- Done +- Finish refactoring classes in Valet directory -- Done +- Fix facade.php -- Done +- Move config directory from ~/.valet to ~/.config/valet -- Done +- Update valet use command to only work with current supported PHP versions +- Update valet isolate command to work with every php version from 7.0 to latest +- Optimise php bash file to handle blank output of which-php command +- self-signed certificate verification issues in Firefox & PHP's curl requests -- Done +- Ngrok remove binary file and install it via `valet install` command +- Optimise PHP Bash file to not relay on which-php command + - It should read fallback version from ~/.config/valet/config.json file + - It should read isolated version for directory from ~/.config/valet/config.json file +- Add relevant test cases + - diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index bfcb843..56b0edb 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -48,21 +48,13 @@ public function runAsUser(string $command, callable $onError = null): string /** * Run the given command. - * TODO: Refactor new Process instance, we might not need if statement there. */ private function runCommand(string $command, callable $onError = null): string { - $onError = $onError ?: function () {}; + $onError = $onError ?: function () { + }; - // Symfony's 4.x Process component has deprecated passing a command string - // to the constructor, but older versions (which Valet's Composer - // constraints allow) don't have the fromShellCommandLine method. - // For more information, see: https://github.com/laravel/valet/pull/761 - if (method_exists(Process::class, 'fromShellCommandline')) { - $process = Process::fromShellCommandline($command); - } else { - $process = new Process($command); - } + $process = Process::fromShellCommandline($command); $processOutput = ''; $process->setTimeout(null)->run(function ($type, $line) use (&$processOutput) { diff --git a/cli/Valet/Configuration.php b/cli/Valet/Configuration.php index d0b0a67..be32570 100644 --- a/cli/Valet/Configuration.php +++ b/cli/Valet/Configuration.php @@ -6,7 +6,7 @@ class Configuration { - public $files; + public Filesystem $files; /** * Create a new Valet configuration class instance. @@ -43,80 +43,6 @@ public function uninstall(): void } } - /** - * Create the Valet configuration directory. - */ - public function createConfigurationDirectory(): void - { - $this->files->ensureDirExists(VALET_HOME_PATH, user()); - } - - /** - * Create the Valet drivers directory. - */ - public function createDriversDirectory(): void - { - if ($this->files->isDir($driversDirectory = VALET_HOME_PATH.'/Drivers')) { - return; - } - - $this->files->mkdirAsUser($driversDirectory); - - $this->files->putAsUser( - $driversDirectory.'/SampleValetDriver.php', - $this->files->get(__DIR__.'/../stubs/SampleValetDriver.php') - ); - } - - /** - * Create the Valet sites directory. - */ - public function createSitesDirectory(): void - { - $this->files->ensureDirExists(VALET_HOME_PATH.'/Sites', user()); - } - - /** - * Create the directory for the Valet extensions. - */ - public function createExtensionsDirectory(): void - { - $this->files->ensureDirExists(VALET_HOME_PATH.'/Extensions', user()); - } - - /** - * Create the directory for Nginx logs. - */ - public function createLogDirectory(): void - { - $this->files->ensureDirExists(VALET_HOME_PATH.'/Log', user()); - - $this->files->touch(VALET_HOME_PATH.'/Log/nginx-error.log'); - } - - /** - * Create the directory for SSL certificates. - */ - public function createCertificatesDirectory(): void - { - $this->files->ensureDirExists(VALET_HOME_PATH.'/Certificates', user()); - } - - /** - * Write the base, initial configuration for Valet. - */ - public function writeBaseConfiguration(): void - { - if (!$this->files->exists($this->path())) { - $this->write([ - 'domain' => 'test', - 'paths' => [], - 'port' => '80', - 'installed_php_version' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, - ]); - } - } - /** * Add the given path to the configuration. */ @@ -129,14 +55,6 @@ public function addPath(string $path, bool $prepend = false): void })); } - /** - * Prepend the given path to the configuration. - */ - public function prependPath(string $path): void - { - $this->addPath($path, true); - } - /** * Add the given path to the configuration. */ @@ -165,14 +83,6 @@ public function prune(): void })); } - /** - * Read the configuration file as JSON. - */ - public function read(): array - { - return json_decode($this->files->get($this->path()), true); - } - /** * Get a configuration value. * @param mixed $default @@ -195,13 +105,22 @@ public function set(string $key, $value) return $this->updateKey($key, $value); } + public function parseDomain(string $siteName): string + { + $domain = $this->get('domain'); + if (str_ends_with($siteName, ".$domain") !== true) { + return \sprintf('%s.%s', $siteName, $domain); + } + return $siteName; + } + /** * Update a specific key in the configuration file. * * @param mixed $value * @return array */ - public function updateKey(string $key, $value) + private function updateKey(string $key, $value) { return tap($this->read(), function (&$config) use ($key, $value) { $config[$key] = $value; @@ -212,7 +131,7 @@ public function updateKey(string $key, $value) /** * Write the given configuration to disk. */ - public function write(array $config): void + private function write(array $config): void { $this->files->putAsUser( $this->path(), @@ -223,10 +142,91 @@ public function write(array $config): void ); } + /** + * Read the configuration file as JSON. + */ + private function read(): array + { + return json_decode($this->files->get($this->path()), true); + } + + /** + * Create the Valet configuration directory. + */ + private function createConfigurationDirectory(): void + { + $this->files->ensureDirExists(VALET_HOME_PATH, user()); + } + + /** + * Create the Valet drivers directory. + */ + private function createDriversDirectory(): void + { + if ($this->files->isDir($driversDirectory = VALET_HOME_PATH.'/Drivers')) { + return; + } + + $this->files->mkdirAsUser($driversDirectory); + + $this->files->putAsUser( + $driversDirectory.'/SampleValetDriver.php', + $this->files->get(VALET_ROOT_PATH.'/cli/stubs/SampleValetDriver.php') + ); + } + + /** + * Create the Valet sites directory. + */ + private function createSitesDirectory(): void + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Sites', user()); + } + + /** + * Create the directory for the Valet extensions. + */ + private function createExtensionsDirectory(): void + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Extensions', user()); + } + + /** + * Create the directory for Nginx logs. + */ + private function createLogDirectory(): void + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Log', user()); + + $this->files->touch(VALET_HOME_PATH.'/Log/nginx-error.log'); + } + + /** + * Create the directory for SSL certificates. + */ + private function createCertificatesDirectory(): void + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Certificates', user()); + } + + /** + * Write the base, initial configuration for Valet. + */ + private function writeBaseConfiguration(): void + { + if (!$this->files->exists($this->path())) { + $this->write([ + 'domain' => 'test', + 'paths' => [], + 'port' => '80', + ]); + } + } + /** * Get the configuration file path. */ - public function path(): string + private function path(): string { return VALET_HOME_PATH.'/config.json'; } diff --git a/cli/Valet/Contracts/PackageManager.php b/cli/Valet/Contracts/PackageManager.php index 3d2d10e..4223937 100644 --- a/cli/Valet/Contracts/PackageManager.php +++ b/cli/Valet/Contracts/PackageManager.php @@ -36,7 +36,6 @@ public function getPhpFpmName(string $version): string; /** * Get Php extension pattern from distro - * TODO: This function is refactored, please update the usage. */ public function getPhpExtensionPrefix(string $version): string; @@ -44,4 +43,9 @@ public function getPhpExtensionPrefix(string $version): string; * Restart network manager in distro */ public function restartNetworkManager(): void; + + /** + * Get package name by service + */ + public function packageName(string $name): string; } diff --git a/cli/Valet/Contracts/ServiceManager.php b/cli/Valet/Contracts/ServiceManager.php index 84b7751..2b88da6 100644 --- a/cli/Valet/Contracts/ServiceManager.php +++ b/cli/Valet/Contracts/ServiceManager.php @@ -7,23 +7,23 @@ interface ServiceManager /** * Start the given services. * - * @param array|string $services + * @param array|string|string[]|null $services */ - public function start($services): void; + public function start(array|string|null $services): void; /** * Stop the given services. * - * @param array|string $services + * @param array|string|string[]|null $services */ - public function stop($services): void; + public function stop(array|string|null $services): void; /** * Restart the given services. * - * @param array|string $services + * @param array|string|string[]|null $services */ - public function restart($services): void; + public function restart(array|string|null $services): void; /** * Enable the given services. @@ -54,4 +54,9 @@ public function printStatus(string $service): void; * If the service manager is systemd. */ public function isSystemd(): bool; + + /** + * Remove Valet DNS services. + */ + public function removeValetDns(): void; } diff --git a/cli/Valet/DevTools.php b/cli/Valet/DevTools.php index 361b597..38c5d99 100644 --- a/cli/Valet/DevTools.php +++ b/cli/Valet/DevTools.php @@ -2,6 +2,7 @@ namespace Valet; +use ConsoleComponents\Writer; use DomainException; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; @@ -11,38 +12,27 @@ class DevTools /** * Sublime binary selector.\ */ - const VS_CODE = 'code'; + public const VS_CODE = 'code'; + /** * Sublime binary selector. */ - const SUBLIME = 'subl'; + public const SUBLIME = 'subl'; /** * PHPStorm binary selector. */ - const PHP_STORM = 'phpstorm.sh'; + public const PHP_STORM = 'phpstorm.sh'; /** * Atom binary selector. */ - const ATOM = 'atom'; + public const ATOM = 'atom'; - /** - * @var PackageManager - */ - public $pm; - /** - * @var ServiceManager - */ - public $sm; - /** - * @var CommandLine - */ - public $cli; - /** - * @var Filesystem - */ - public $files; + public PackageManager $pm; + public ServiceManager $sm; + public CommandLine $cli; + public Filesystem $files; /** * Create a new DevTools instance. @@ -56,23 +46,41 @@ public function __construct(PackageManager $pm, ServiceManager $sm, CommandLine } /** - * @return false|string + * @param string[] $ignoredServices */ - public function getBin(string $service) + public function getBin(string $service, array $ignoredServices = []): false|string { - if (!($bin = $this->getService($service))) { - $bin = $this->getService($service, true); + $bin = $this->getService($service); + + $bin = trim($bin, "\n"); + if (count($ignoredServices) && in_array($bin, $ignoredServices)) { + $bin = null; + } + + if (!$bin) { + $bin = $this->getServiceByLocate("bin/$service"); + } + + if (!$bin) { + return false; } + + $bin = trim($bin, "\n"); + /** @var string[] $bins */ $bins = preg_split('/\n/', $bin); $servicePath = null; foreach ($bins as $bin) { - if (endsWith($bin, "bin/${service}")) { + if ((count($ignoredServices) && !in_array($bin, $ignoredServices)) + || !count($ignoredServices) + ) { $servicePath = $bin; break; } } - if ($servicePath) { - return trim(preg_replace('/\s\s+/', ' ', $servicePath)); + if ($servicePath !== null) { + /** @var string $servicePath */ + $servicePath = preg_replace('/\s\s+/', ' ', $servicePath); + return trim($servicePath); } return false; @@ -80,10 +88,10 @@ public function getBin(string $service) public function run(string $folder, string $service): void { - if ($this->ensureInstalled($service)) { - $this->runService($service, $folder); + if ($bin = $this->ensureInstalled($service)) { + $this->runService($bin, $folder); } else { - warning("$service not available"); + Writer::warn("$service not available"); } } @@ -98,13 +106,11 @@ private function ensureInstalled(string $service) /** * @return false|string */ - private function getService(string $service, bool $locate = false) + private function getService(string $service) { try { - $locator = $locate ? 'locate' : 'which'; - return $this->cli->run( - "$locator $service", + "which $service", function () { throw new DomainException('Service not available'); } @@ -114,14 +120,25 @@ function () { } } - private function runService(string $service, ?string $folder = null): void + /** + * @return false|string + */ + private function getServiceByLocate(string $service) { - $bin = $this->getBin($service); - try { - $this->cli->quietly("$bin $folder"); + return $this->cli->run( + "locate --regex $service$", + function () { + throw new DomainException('Service not available'); + } + ); } catch (DomainException $e) { - warning("Error while opening [$folder] with $service"); + return false; } } + + private function runService(string $bin, ?string $folder = null): void + { + $this->cli->quietly("$bin $folder"); + } } diff --git a/cli/Valet/DnsMasq.php b/cli/Valet/DnsMasq.php index d6dbad4..79f6831 100644 --- a/cli/Valet/DnsMasq.php +++ b/cli/Valet/DnsMasq.php @@ -2,56 +2,24 @@ namespace Valet; +use ConsoleComponents\Writer; use Exception; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; class DnsMasq { - /** - * @var PackageManager - */ - public $pm; - /** - * @var ServiceManager - */ - public $sm; - /** - * @var CommandLine - */ - public $cli; - /** - * @var Filesystem - */ - public $files; - /** - * @var string - */ - public $rclocal = '/etc/rc.local'; - /** - * @var string - */ - public $resolvconf = '/etc/resolv.conf'; - /** - * @var string - */ - public $dnsmasqconf = '/etc/dnsmasq.conf'; - /** - * @var string - */ - public $dnsmasqOpts = '/etc/dnsmasq.d/options'; - /** - * @var string - */ - public $resolvedConfigPath = '/etc/systemd/resolved.conf'; - /** - * @var string - */ - public $configPath = '/etc/dnsmasq.d/valet'; - /** - * @var string - */ - public $nmConfigPath = '/etc/NetworkManager/conf.d/valet.conf'; + public PackageManager $pm; + public ServiceManager $sm; + public CommandLine $cli; + public Filesystem $files; + public string $rclocal = '/etc/rc.local'; + public string $resolvconf = '/etc/resolv.conf'; + public string $dnsmasqconf = '/etc/dnsmasq.conf'; + public string $dnsmasqOpts = '/etc/dnsmasq.d/options'; + public string $resolvedConfigPath = '/etc/systemd/resolved.conf'; + public string $configPath = '/etc/dnsmasq.d/valet'; + public string $nmConfigPath = '/etc/NetworkManager/conf.d/valet.conf'; /** * Create a new DnsMasq instance. @@ -69,7 +37,7 @@ public function __construct(PackageManager $pm, ServiceManager $sm, Filesystem $ * * @throws Exception */ - public function install(string $domain = 'test'): void + public function install(string $domain): void { $this->dnsmasqSetup(); $this->stopResolved(); @@ -115,7 +83,7 @@ public function uninstall(): void $this->files->unlink($this->nmConfigPath); $this->files->restore($this->resolvedConfigPath); - $this->lockResolvConf(false); + $this->lockResolvConf(); $this->files->restore($this->rclocal); $this->cli->passthru('rm -f /etc/resolv.conf'); @@ -129,21 +97,19 @@ public function uninstall(): void $this->pm->restartNetworkManager(); $this->sm->restart('dnsmasq'); - info('Valet DNS changes have been rolled back'); + Writer::info('Valet DNS changes have been rolled back'); } /** * Install and configure DnsMasq. */ - private function lockResolvConf(bool $lock = true): void + private function lockResolvConf(): void { - $arg = $lock ? '+i' : '-i'; - if (!$this->files->isLink($this->resolvconf)) { $this->cli->run( - "chattr {$arg} {$this->resolvconf}", + "chattr -i $this->resolvconf", function ($code, $msg) { - warning($msg); + Writer::warn($msg); } ); } @@ -202,14 +168,23 @@ private function dnsmasqSetup(): void $this->files->uncommentLine('IGNORE_RESOLVCONF', '/etc/default/dnsmasq'); - $this->lockResolvConf(false); + $this->lockResolvConf(); $this->mergeDns(); $this->files->unlink('/etc/dnsmasq.d/network-manager'); $this->files->backup($this->dnsmasqconf); - $this->files->putAsUser($this->dnsmasqconf, $this->files->get(__DIR__.'/../stubs/dnsmasq.conf')); - $this->files->putAsUser($this->dnsmasqOpts, $this->files->get(__DIR__.'/../stubs/dnsmasq_options')); - $this->files->putAsUser($this->nmConfigPath, $this->files->get(__DIR__.'/../stubs/networkmanager.conf')); + $this->files->putAsUser( + $this->dnsmasqconf, + $this->files->get(VALET_ROOT_PATH.'/cli/stubs/dnsmasq.conf') + ); + $this->files->putAsUser( + $this->dnsmasqOpts, + $this->files->get(VALET_ROOT_PATH.'/cli/stubs/dnsmasq_options') + ); + $this->files->putAsUser( + $this->nmConfigPath, + $this->files->get(VALET_ROOT_PATH.'/cli/stubs/networkmanager.conf') + ); } } diff --git a/cli/Valet/Drivers/BasicValetDriver.php b/cli/Valet/Drivers/BasicValetDriver.php index 37fa6ac..5741d2f 100644 --- a/cli/Valet/Drivers/BasicValetDriver.php +++ b/cli/Valet/Drivers/BasicValetDriver.php @@ -30,7 +30,8 @@ public function isStaticFile(string $sitePath, string $siteName, string $uri) { if (file_exists($sitePath.rtrim($uri, '/').'/index.html')) { return $sitePath.rtrim($uri, '/').'/index.html'; - } elseif ($this->isActualFile($sitePath.$uri)) { + } + if ($this->isActualFile($sitePath.$uri)) { return $sitePath.$uri; } diff --git a/cli/Valet/Drivers/BasicWithPublicValetDriver.php b/cli/Valet/Drivers/BasicWithPublicValetDriver.php index 062066b..f98bd5d 100644 --- a/cli/Valet/Drivers/BasicWithPublicValetDriver.php +++ b/cli/Valet/Drivers/BasicWithPublicValetDriver.php @@ -22,7 +22,8 @@ public function isStaticFile(string $sitePath, string $siteName, string $uri) if ($this->isActualFile($publicPath)) { return $publicPath; - } elseif (file_exists($publicPath.'/index.html')) { + } + if (file_exists($publicPath.'/index.html')) { return $publicPath.'/index.html'; } diff --git a/cli/Valet/Drivers/Specific/BedrockValetDriver.php b/cli/Valet/Drivers/Specific/BedrockValetDriver.php index 1c4579d..0c30964 100644 --- a/cli/Valet/Drivers/Specific/BedrockValetDriver.php +++ b/cli/Valet/Drivers/Specific/BedrockValetDriver.php @@ -46,6 +46,7 @@ public function frontControllerPath(string $sitePath, string $siteName, string $ /** * Redirect to uri with trailing slash. + * @param mixed $uri */ private function forceTrailingSlash($uri) { diff --git a/cli/Valet/Drivers/Specific/CraftValetDriver.php b/cli/Valet/Drivers/Specific/CraftValetDriver.php index 75ef3a0..57a68a5 100644 --- a/cli/Valet/Drivers/Specific/CraftValetDriver.php +++ b/cli/Valet/Drivers/Specific/CraftValetDriver.php @@ -16,6 +16,7 @@ public function serves(string $sitePath, string $siteName, string $uri): bool /** * Determine the name of the directory where the front controller lives. + * @param mixed $sitePath */ public function frontControllerDirectory($sitePath): string { diff --git a/cli/Valet/Drivers/Specific/DrupalValetDriver.php b/cli/Valet/Drivers/Specific/DrupalValetDriver.php index fe7151a..72a3d27 100644 --- a/cli/Valet/Drivers/Specific/DrupalValetDriver.php +++ b/cli/Valet/Drivers/Specific/DrupalValetDriver.php @@ -72,6 +72,7 @@ public function frontControllerPath(string $sitePath, string $siteName, string $ /** * Add any matching subdirectory to the site path. + * @param mixed $sitePath */ public function addSubdirectory($sitePath): string { diff --git a/cli/Valet/Drivers/Specific/KirbyValetDriver.php b/cli/Valet/Drivers/Specific/KirbyValetDriver.php index 55918a9..ccce5e8 100644 --- a/cli/Valet/Drivers/Specific/KirbyValetDriver.php +++ b/cli/Valet/Drivers/Specific/KirbyValetDriver.php @@ -21,7 +21,8 @@ public function isStaticFile(string $sitePath, string $siteName, string $uri) { if ($this->isActualFile($staticFilePath = $sitePath.$uri)) { return $staticFilePath; - } elseif ($this->isActualFile($staticFilePath = $sitePath.'/public'.$uri)) { + } + if ($this->isActualFile($staticFilePath = $sitePath.'/public'.$uri)) { return $staticFilePath; } diff --git a/cli/Valet/Drivers/Specific/StatamicV2ValetDriver.php b/cli/Valet/Drivers/Specific/StatamicV2ValetDriver.php index cbe00e1..ab3d1e5 100644 --- a/cli/Valet/Drivers/Specific/StatamicV2ValetDriver.php +++ b/cli/Valet/Drivers/Specific/StatamicV2ValetDriver.php @@ -21,11 +21,14 @@ public function isStaticFile(string $sitePath, string $siteName, string $uri) { if (strpos($uri, '/site') === 0 && strpos($uri, '/site/themes') !== 0) { return false; - } elseif (strpos($uri, '/local') === 0 || strpos($uri, '/statamic') === 0) { + } + if (strpos($uri, '/local') === 0 || strpos($uri, '/statamic') === 0) { return false; - } elseif ($this->isActualFile($staticFilePath = $sitePath.$uri)) { + } + if ($this->isActualFile($staticFilePath = $sitePath.$uri)) { return $staticFilePath; - } elseif ($this->isActualFile($staticFilePath = $sitePath.'/public'.$uri)) { + } + if ($this->isActualFile($staticFilePath = $sitePath.'/public'.$uri)) { return $staticFilePath; } diff --git a/cli/Valet/Drivers/Specific/SymfonyValetDriver.php b/cli/Valet/Drivers/Specific/SymfonyValetDriver.php index 4be7c66..31c0e15 100644 --- a/cli/Valet/Drivers/Specific/SymfonyValetDriver.php +++ b/cli/Valet/Drivers/Specific/SymfonyValetDriver.php @@ -23,7 +23,8 @@ public function isStaticFile(string $sitePath, string $siteName, string $uri) { if ($this->isActualFile($staticFilePath = $sitePath.'/web/'.$uri)) { return $staticFilePath; - } elseif ($this->isActualFile($staticFilePath = $sitePath.'/public/'.$uri)) { + } + if ($this->isActualFile($staticFilePath = $sitePath.'/public/'.$uri)) { return $staticFilePath; } diff --git a/cli/Valet/Drivers/Specific/WordPressValetDriver.php b/cli/Valet/Drivers/Specific/WordPressValetDriver.php index 932ea05..5bb2adc 100644 --- a/cli/Valet/Drivers/Specific/WordPressValetDriver.php +++ b/cli/Valet/Drivers/Specific/WordPressValetDriver.php @@ -28,6 +28,7 @@ public function frontControllerPath(string $sitePath, string $siteName, string $ /** * Redirect to uri with trailing slash. + * @param mixed $uri */ private function forceTrailingSlash($uri): ?string { diff --git a/cli/Valet/Drivers/ValetDriver.php b/cli/Valet/Drivers/ValetDriver.php index b46ff85..4217cf5 100755 --- a/cli/Valet/Drivers/ValetDriver.php +++ b/cli/Valet/Drivers/ValetDriver.php @@ -52,10 +52,10 @@ public static function assign(string $sitePath, string $siteName, string $uri): foreach ($drivers as $driver) { if ($driver === 'LocalValetDriver') { - $driver = new $driver; + $driver = new $driver(); } else { $className = "Valet\Drivers\\{$driver}"; - $driver = new $className; + $driver = new $className(); } if ($driver->serves($sitePath, $siteName, $driver->mutateUri($uri))) { @@ -72,7 +72,7 @@ public static function assign(string $sitePath, string $siteName, string $uri): public static function customSiteDriver(string $sitePath): ?string { if (!file_exists($sitePath.'/LocalValetDriver.php')) { - return null; // TODO: Validate this. + return null; } require_once $sitePath.'/LocalValetDriver.php'; diff --git a/cli/Valet/Facades/Configuration.php b/cli/Valet/Facades/Configuration.php index d7966e7..d2ae3b5 100644 --- a/cli/Valet/Facades/Configuration.php +++ b/cli/Valet/Facades/Configuration.php @@ -7,23 +7,12 @@ * * @method static void install() * @method static void uninstall() - * @method static void createConfigurationDirectory() - * @method static void createDriversDirectory() - * @method static void createSitesDirectory() - * @method static void createExtensionsDirectory() - * @method static void createLogDirectory() - * @method static void createCertificatesDirectory() - * @method static void writeBaseConfiguration() - * @method static void addPath(string $path, bool $prepend = false) - * @method static void prependPath(string $path) - * @method static void removePath(string $path) * @method static void prune() - * @method static array read() * @method static mixed get(string $key, mixed $default = null) * @method static mixed set(string $key, mixed $default = null) - * @method static array updateKey(string $key, mixed $value) - * @method static void write(array $config) - * @method static string path() + * @method static void addPath(string $path, bool $prepend = false) + * @method static void removePath(string $path) + * @method static string parseDomain(string $site) */ class Configuration extends Facade { diff --git a/cli/Valet/Facades/DevTools.php b/cli/Valet/Facades/DevTools.php index 446859a..4968b65 100644 --- a/cli/Valet/Facades/DevTools.php +++ b/cli/Valet/Facades/DevTools.php @@ -5,7 +5,7 @@ /** * Class DevTools. * - * @method static false|string getBin(string $service) + * @method static false|string getBin(string $service, array $ignoredServices = []) * @method static void run(string $folder,string $service) */ class DevTools extends Facade diff --git a/cli/Valet/Facades/Filesystem.php b/cli/Valet/Facades/Filesystem.php new file mode 100644 index 0000000..1e6d26f --- /dev/null +++ b/cli/Valet/Facades/Filesystem.php @@ -0,0 +1,12 @@ + secured() + * @method static void regenerateSecuredSitesConfig() + * @method static void reSecureForNewDomain(string $oldDomain, string $domain) + */ +class SiteSecure extends Facade +{ +} diff --git a/cli/Valet/Facades/Valet.php b/cli/Valet/Facades/Valet.php index 4cfefa3..b2bd6b9 100644 --- a/cli/Valet/Facades/Valet.php +++ b/cli/Valet/Facades/Valet.php @@ -6,11 +6,13 @@ * Class Valet. * * @method static void symlinkToUsersBin() + * @method static void symlinkPhpToUsersBin() * @method static void uninstall() * @method static array extensions() * @method static bool onLatestVersion(string $currentVersion) * @method static string getLatestVersion() * @method static void environmentSetup() + * @method static void migrateConfig() */ class Valet extends Facade { diff --git a/cli/Valet/Filesystem.php b/cli/Valet/Filesystem.php index fa16716..f014ee5 100644 --- a/cli/Valet/Filesystem.php +++ b/cli/Valet/Filesystem.php @@ -3,10 +3,11 @@ namespace Valet; use ArrayObject; -use Valet\Facades\CommandLine; +use ConsoleComponents\Writer; use Exception; use FilesystemIterator; use Traversable; +use Valet\Facades\CommandLine; /** * Class Filesystem. @@ -19,8 +20,6 @@ class Filesystem * @param string $files * * @throws Exception - * - * @return void */ public function remove($files): void { @@ -98,7 +97,7 @@ public function touch(string $path, ?string $owner = null): string { touch($path); - if ($owner === null) { + if ($owner !== null) { $this->chown($path, $owner); } @@ -145,11 +144,6 @@ public function put(string $path, string $contents, ?string $owner = null): stri /** * Write to the given file as the non-root user. - * - * @param string $path - * @param string $contents - * - * @return string */ public function putAsUser(string $path, string $contents): string { @@ -176,6 +170,38 @@ public function appendAsUser(string $path, string $contents): void $this->append($path, $contents, user()); } + /** + * Copy the given directory to a new location. + */ + public function copyDirectory(string $from, string $to): void + { + if ($this->isDir($to)) { + Writer::warn('Destination directory already exists'); + return; + } + + $this->mkdir($to); + $sourceContents = $this->scandir($from); + + foreach ($sourceContents as $sourceContent) { + if ($sourceContent == '.' || $sourceContent == '..') { + continue; + } + + $sourcePath = $from . '/' . $sourceContent; + $destinationPath = $to . '/' . $sourceContent; + + if (!$this->isLink($sourcePath) && $this->isDir($sourcePath)) { + $this->copyDirectory($sourcePath, $destinationPath); + } elseif ($this->isLink($sourcePath)) { + $sourcePath = $this->readLink($sourcePath); + $this->symlink($sourcePath, $destinationPath); + } else { + $this->copy($sourcePath, $destinationPath); + } + } + } + /** * Copy the given file to a new location. */ @@ -312,10 +338,6 @@ public function isLink(string $path): bool /** * Resolve the given symbolic link. - * - * @param string $path - * - * @return string */ public function readLink(string $path): string { @@ -330,10 +352,6 @@ public function readLink(string $path): string /** * Remove all the broken symbolic links at the given path. - * - * @param string $path - * - * @return void */ public function removeBrokenLinksAt(string $path): void { @@ -361,16 +379,16 @@ public function scandir(string $path): array { return collect(scandir($path)) ->reject(function ($file) { - return in_array($file, ['.', '..']); + return in_array($file, ['.', '..', '.keep']); })->values()->all(); } /** * @param array|string $files * - * @return ArrayObject + * @return ArrayObject|Traversable */ - private function toIterator($files): ArrayObject + private function toIterator($files) { if (!$files instanceof Traversable) { $files = new ArrayObject(is_array($files) ? $files : [$files]); diff --git a/cli/Valet/Mailpit.php b/cli/Valet/Mailpit.php index b2eb7ed..5d69d96 100644 --- a/cli/Valet/Mailpit.php +++ b/cli/Valet/Mailpit.php @@ -5,9 +5,9 @@ use DomainException; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use Valet\Facades\CommandLine; use Valet\Facades\Configuration; -use Valet\Facades\Site; +use Valet\Facades\SiteProxy as SiteProxyFacade; +use Valet\Facades\SiteSecure as SiteSecureFacade; class Mailpit { @@ -30,16 +30,11 @@ class Mailpit /** * @var string */ - const SERVICE_NAME = 'mailpit'; + public const SERVICE_NAME = 'mailpit'; /** * Create a new Mailpit instance. * - * @param PackageManager $pm - * @param ServiceManager $sm - * @param CommandLine $cli - * @param Filesystem $files - * * @return void */ public function __construct(PackageManager $pm, ServiceManager $sm, CommandLine $cli, Filesystem $files) @@ -61,22 +56,23 @@ public function install(): void $this->sm->start(self::SERVICE_NAME); - if (!$this->sm->disabled('Mailpit')) { - $this->sm->disable('Mailpit'); - if ($this->files->exists('/opt/valet-linux/Mailpit')) { - $this->files->remove('/opt/valet-linux/Mailpit'); - } - $domain = Configuration::get('domain'); - if ($this->files->exists(VALET_HOME_PATH."/Nginx/Mailpit.$domain")) { - Site::proxyDelete("Mailpit.$domain"); + try { + if (!$this->sm->disabled('mailhog')) { + $this->sm->disable('mailhog'); + if ($this->files->exists('/opt/valet-linux/mailhog')) { + $this->files->remove('/opt/valet-linux/mailhog'); + } + $domain = Configuration::get('domain'); + if ($this->files->exists(VALET_HOME_PATH . "/Nginx/mailhog.$domain")) { + SiteSecureFacade::unsecure("mailhog.$domain"); + } } + } catch (\DomainException $e) { } } /** * Start the Mailpit service. - * - * @return void */ public function start(): void { @@ -85,8 +81,6 @@ public function start(): void /** * Restart the Mailpit service. - * - * @return void */ public function restart(): void { @@ -95,8 +89,6 @@ public function restart(): void /** * Stop the Mailpit service. - * - * @return void */ public function stop(): void { @@ -105,8 +97,6 @@ public function stop(): void /** * Mailpit service status. - * - * @return void */ public function status(): void { @@ -115,8 +105,6 @@ public function status(): void /** * Prepare Mailpit for uninstall. - * - * @return void */ public function uninstall(): void { @@ -140,15 +128,13 @@ private function ensureInstalled(): void */ private function createService(): void { - info('Installing Mailpit service...'); - $servicePath = '/etc/init.d/mailpit'; - $serviceFile = VALET_ROOT_PATH.'/cli/stubs/init/mailpit.sh'; + $serviceFile = VALET_ROOT_PATH . '/cli/stubs/init/mailpit.sh'; $hasSystemd = $this->sm->isSystemd(); if ($hasSystemd) { $servicePath = '/etc/systemd/system/mailpit.service'; - $serviceFile = VALET_ROOT_PATH.'/cli/stubs/init/mailpit'; + $serviceFile = VALET_ROOT_PATH . '/cli/stubs/init/mailpit'; } $this->files->put( @@ -172,12 +158,9 @@ private function updateDomain(): void { $domain = Configuration::get('domain'); - Site::proxyCreate("mails.$domain", 'http://localhost:8025', true); + SiteProxyFacade::proxyCreate("mails.$domain", 'http://localhost:8025', true); } - /** - * @return bool - */ private function isAvailable(): bool { try { diff --git a/cli/Valet/Mysql.php b/cli/Valet/Mysql.php index 1f04a48..b9947bf 100644 --- a/cli/Valet/Mysql.php +++ b/cli/Valet/Mysql.php @@ -2,13 +2,8 @@ namespace Valet; +use ConsoleComponents\Writer; use PDO; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Console\Question\ConfirmationQuestion; -use Symfony\Component\Console\Question\Question; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; use Valet\Facades\PhpFpm as PhpFpmFacade; @@ -17,52 +12,21 @@ class Mysql { + private const DATABASE_USER = 'valet'; + public CommandLine $cli; + public Filesystem $files; + public PackageManager $pm; + public ServiceManager $sm; + public Configuration $configuration; /** - * @var string + * @var array|string[] */ - const MYSQL_USER = 'valet'; - /** - * @var CommandLine - */ - public $cli; - /** - * @var Filesystem - */ - public $files; - /** - * @var PackageManager - */ - public $pm; - /** - * @var ServiceManager - */ - public $sm; - /** - * @var Configuration - */ - public $configuration; - /** - * @var string[] - */ - public $systemDatabases = ['sys', 'performance_schema', 'information_schema', 'mysql']; - /** - * @var PDO - */ - private $pdoConnection = false; - /** - * @var string - */ - protected $currentPackage = ''; + public array $systemDatabases = ['sys', 'performance_schema', 'information_schema', 'mysql']; + private ?PDO $pdoConnection = null; + private ?string $currentPackage = null; /** * Create a new instance. - * - * @param PackageManager $pm - * @param ServiceManager $sm - * @param CommandLine $cli - * @param Filesystem $files - * @param Configuration $configuration - * @param Site $site */ public function __construct( PackageManager $pm, @@ -76,69 +40,52 @@ public function __construct( $this->sm = $sm; $this->files = $files; $this->configuration = $configuration; - if ($this->pm->installed($this->pm->mysqlPackageName)) { - $this->currentPackage = $this->pm->mysqlPackageName; - } - if ($this->pm->installed($this->pm->mariaDBPackageName)) { - $this->currentPackage = $this->pm->mariaDBPackageName; + + if ($this->pm->installed($packageName = $this->pm->packageName('mysql'))) { + $this->currentPackage = $packageName; + } elseif ($this->pm->installed($packageName = $this->pm->packageName('mariadb'))) { + $this->currentPackage = $packageName; } } /** * Install the service. */ - public function install($useMariaDB = false) + public function install(bool $useMariaDB = false): void { - if ($this->pm instanceof Pacman || $this->pm instanceof Dnf) { - $useMariaDB = true; - } - $package = $useMariaDB ? $this->pm->mariaDBPackageName : $this->pm->mysqlPackageName; - $this->currentPackage = $package; - $service = $this->serviceName(); - if (!$this->pm instanceof Pacman && !extension_loaded('mysql')) { - $phpVersion = PhpFpmFacade::getCurrentVersion(); - $this->pm->ensureInstalled("php{$phpVersion}-mysql"); - } - - if ($package === $this->pm->mariaDBPackageName - && $this->pm->installed($this->pm->mysqlPackageName) - ) { - warning('MySQL is already installed, please remove --mariadb flag and try again!'); - return; - } - - if ($package === $this->pm->mysqlPackageName - && $this->pm->installed($this->pm->mariaDBPackageName) - ) { - warning('MariaDB is already installed, please add --mariadb flag and try again!'); - return; + if ($this->currentPackage === null) { + if ($this->pm instanceof Pacman || $this->pm instanceof Dnf) { + $useMariaDB = true; + } + $package = $useMariaDB ? $this->pm->packageName('mariadb') : $this->pm->packageName('mysql'); + $this->currentPackage = $package; + if (!$this->pm instanceof Pacman && !extension_loaded('mysql')) { + $phpVersion = PhpFpmFacade::getCurrentVersion(); + $this->pm->ensureInstalled("php{$phpVersion}-mysql"); + } } - if ($this->pm->installed($package)) { - $config = $this->configuration->read(); - if (!isset($config['mysql'])) { - $config['mysql'] = []; - } - if (!isset($config['mysql']['password'])) { - info('Looks like MySQL/MariaDB already installed to your system'); + if ($this->pm->installed($this->currentPackage)) { + /** @var array $config */ + $config = $this->configuration->get('mysql', []); + if (!isset($config['password'])) { + Writer::info('Looks like MySQL/MariaDB already installed to your system'); $this->configure(); } } else { - $this->pm->installOrFail($package); - $this->sm->enable($service); - $this->stop(); + $this->pm->installOrFail($this->currentPackage); + $this->sm->enable($this->serviceName()); if ($this->pm instanceof Pacman) { - // Configure data directory. + $this->stop(); $this->configureDataDirectory(); + $this->restart(); + } + + /** @var ?string $password */ + $password = Writer::ask(\sprintf('Please enter new password for [%s] database user', self::DATABASE_USER)); + if ($password === null) { + $password = ''; } - $this->restart(); - $input = new ArgvInput(); - $output = new ConsoleOutput(); - $question = new Question('Please enter new password for `'.self::MYSQL_USER.'` database user: '); - $helper = new HelperSet([new QuestionHelper()]); - $question->setHidden(true); - $helper = $helper->get('question'); - $password = $helper->ask($input, $output, $question); $this->createValetUser($password); } } @@ -168,94 +115,81 @@ public function uninstall(): void } /** - * Print table of exists databases. + * Configure Database user for Valet. */ - public function listDatabases(): void + public function configure(bool $force = false): void { - table(['Database'], $this->getDatabases()); - } + /** @var array $config */ + $config = $this->configuration->get('mysql', []); - /** - * Import Mysql database from file. - */ - public function importDatabase(string $file, string $database, bool $isDatabaseExists): void - { - $database = $this->getDatabaseName($database); + if (!$force && isset($config['password'])) { + Writer::info('Valet database user is already configured. Use --force to reconfigure database user.'); + return; + } - if (!$isDatabaseExists) { - $this->createDatabase($database); + $defaultUser = null; + if (!empty($config['user'])) { + $defaultUser = $config['user']; } - $gzip = ''; - $sqlFile = ''; - if (\stristr($file, '.gz')) { - $file = escapeshellarg($file); - $gzip = "zcat {$file} | "; - } else { - $file = escapeshellarg($file); - $sqlFile = " < {$file}"; + /** @var string $user */ + $user = Writer::ask('Please enter MySQL/MariaDB user:', $defaultUser); + + /** @var string $password */ + $password = Writer::ask('Please enter MySQL/MariaDB password:'); + + $connection = $this->validateCredentials($user, $password); + if (!$connection) { + $confirm = Writer::confirm('Would you like to try again?', true); + if (!$confirm) { + Writer::warn('Valet database user is not configured'); + return; + } + $this->configure($force); + return; } - $database = escapeshellarg($database); - $credentials = $this->getCredentials(); - $this->cli->run("{$gzip}mysql -u {$credentials['user']} -p{$credentials['password']} {$database} {$sqlFile}"); + $config['user'] = $user; + $config['password'] = $password; + $this->configuration->set('mysql', $config); + Writer::info('Database user configured successfully'); } /** - * Drop Mysql database. + * Create Mysql database. */ - public function dropDatabase(string $name): bool + public function createDatabase(string $name): bool { - $name = $this->getDatabaseName($name); - - if (!$this->isDatabaseExists($name)) { - warning("Database [$name] does not exists!"); - + if ($this->isDatabaseExists($name)) { + Writer::warn("Database [$name] is already exists!"); return false; } - $dbDropped = $this->query('DROP DATABASE `'.$name.'`') ? true : false; - - if (!$dbDropped) { - warning('Error dropping database'); - + $isCreated = (bool)$this->query('CREATE DATABASE IF NOT EXISTS `'.$name.'`'); + if (!$isCreated) { + Writer::warn('Error creating database'); return false; } - info("Database [{$name}] dropped successfully"); - return true; } /** - * Create Mysql database. + * Drop Mysql database. */ - public function createDatabase(string $name): void + public function dropDatabase(string $name): bool { - if ($this->isDatabaseExists($name)) { - warning("Database [$name] is already exists!"); - - return; + if (!$this->isDatabaseExists($name)) { + Writer::warn("Database [$name] does not exists!"); + return false; } - try { - $name = $this->getDatabaseName($name); - if ($this->query('CREATE DATABASE IF NOT EXISTS `'.$name.'`')) { - info("Database [{$name}] created successfully"); - } - } catch (\Exception $exception) { - warning('Error while creating database!'); - } - } + $dbDropped = (bool)$this->query('DROP DATABASE `'.$name.'`'); - /** - * Check if database already exists. - */ - public function isDatabaseExists(string $name): bool - { - $name = $this->getDatabaseName($name); - $query = $this->query("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '{$name}'"); - $query->execute(); + if (!$dbDropped) { + Writer::warn('Error dropping database'); + return false; + } - return (bool) $query->rowCount(); + return true; } /** @@ -263,8 +197,6 @@ public function isDatabaseExists(string $name): bool */ public function exportDatabase(string $database, bool $exportSql = false): array { - $database = $this->getDatabaseName($database); - $filename = $database.'-'.\date('Y-m-d-H-i-s', \time()); if ($exportSql) { @@ -289,33 +221,51 @@ public function exportDatabase(string $database, bool $exportSql = false): array } /** - * Get database name via name or current dir. - */ - private function getDatabaseName(string $database = ''): string - { - return $database ?: $this->getDirName(); - } - - /** - * Get current dir name. + * Import Mysql database from file. */ - private function getDirName(): string + public function importDatabase(string $file, string $database): void { - $gitDir = $this->cli->runAsUser('git rev-parse --show-toplevel 2>/dev/null'); - - if ($gitDir) { - return \trim(\basename($gitDir)); + $isExistsDatabase = false; + // check if database already exists. + if ($this->isDatabaseExists($database)) { + $confirm = Writer::confirm('Database already exists, are you sure you want to continue?'); + if (!$confirm) { + Writer::warn('Aborted'); + return; + } + $isExistsDatabase = true; } - return \trim(\basename(\getcwd())); + if (!$isExistsDatabase) { + $this->createDatabase($database); + } + $gzip = ''; + $sqlFile = ''; + if (\stristr($file, '.gz')) { + $file = escapeshellarg($file); + $gzip = "zcat {$file} | "; + } else { + $file = escapeshellarg($file); + $sqlFile = " < {$file}"; + } + $database = escapeshellarg($database); + $credentials = $this->getCredentials(); + $this->cli->run( + \sprintf( + '%smysql -u %s -p%s %s %s', + $gzip, + $credentials['user'], + $credentials['password'], + $database, + $sqlFile + ) + ); } /** - * Get exists databases. - * - * @return array + * Get a list of databases. */ - private function getDatabases(): array + public function getDatabases(): array { $result = $this->query('SHOW DATABASES'); @@ -331,6 +281,17 @@ private function getDatabases(): array })->toArray(); } + /** + * Check if database already exists. + */ + private function isDatabaseExists(string $name): bool + { + $query = $this->query("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); + $query->execute(); + + return (bool) $query->rowCount(); + } + /** * Run Mysql query. * @@ -343,7 +304,7 @@ private function query(string $query) try { return $link->query($query); } catch (\PDOException $e) { - warning($e->getMessage()); + Writer::warn($e->getMessage()); } } @@ -363,7 +324,7 @@ private function validateCredentials(string $username, string $password): bool return true; } catch (\PDOException $e) { - warning('Connection failed due to `'.$e->getMessage().'`'); + Writer::error('Invalid database credentials'); return false; } @@ -391,7 +352,7 @@ private function getConnection(): PDO return $this->pdoConnection; } catch (\PDOException $e) { - warning('Failed to connect MySQL due to :`'.$e->getMessage().'`'); + Writer::warn('Failed to connect MySQL due to :`'.$e->getMessage().'`'); exit; } } @@ -412,65 +373,12 @@ private function configureDataDirectory(): void $this->cli->run( 'sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql', function ($statusCode, $output) { - output(\sprintf('%s: %s', $statusCode, $output)); + Writer::error(\sprintf('%s: %s', $statusCode, $output)); } ); $this->restart(); } - /** - * Configure Database user for Valet. - */ - private function configure(bool $force = false): void - { - $config = $this->configuration->read(); - if (!isset($config['mysql'])) { - $config['mysql'] = []; - } - - if (!$force && isset($config['mysql']['password'])) { - info('Valet database user is already configured. Use --force to reconfigure database user.'); - - return; - } - $input = new ArgvInput(); - $output = new ConsoleOutput(); - if (empty($config['mysql']['user'])) { - $question = new Question('Please enter MySQL/MariaDB user: '); - } else { - $question = new Question( - 'Please enter MySQL/MariaDB user [current: '.$config['mysql']['user'].']: ', - $config['mysql']['user'] - ); - } - $helper = new HelperSet([new QuestionHelper()]); - $helper = $helper->get('question'); - $user = $helper->ask($input, $output, $question); - $question = new Question('Please enter MySQL/MariaDB password: '); - $helper = new HelperSet([new QuestionHelper()]); - $question->setHidden(true); - $helper = $helper->get('question'); - $password = $helper->ask($input, $output, $question); - - $connection = $this->validateCredentials($user, $password); - if (!$connection) { - $question = new ConfirmationQuestion('Would you like to try again? [Y/n] ', true); - if (!$helper->ask($input, $output, $question)) { - warning('Valet database user is not configured!'); - - return; - } else { - $this->configure($force); - - return; - } - } - $config['mysql']['user'] = $user; - $config['mysql']['password'] = $password; - $this->configuration->write($config); - info('Database user configured successfully!'); - } - private function serviceName(): string { if ($this->isMariaDB()) { @@ -482,7 +390,7 @@ private function serviceName(): string private function isMariaDB(): bool { - return $this->currentPackage === $this->pm->mariaDBPackageName; + return $this->currentPackage === $this->pm->packageName('mariadb'); } /** @@ -491,45 +399,46 @@ private function isMariaDB(): bool private function createValetUser(string $password): void { $success = true; - $query = "sudo mysql -e \"CREATE USER '".self::MYSQL_USER."'@'localhost' IDENTIFIED WITH mysql_native_password BY '".$password."';GRANT ALL PRIVILEGES ON *.* TO '".self::MYSQL_USER."'@'localhost' WITH GRANT OPTION;FLUSH PRIVILEGES;\""; + $query = "sudo mysql -e \"CREATE USER '".self::DATABASE_USER."'@'localhost' IDENTIFIED WITH mysql_native_password BY '".$password."';GRANT ALL PRIVILEGES ON *.* TO '".self::DATABASE_USER."'@'localhost' WITH GRANT OPTION;FLUSH PRIVILEGES;\""; if ($this->isMariaDB()) { - $query = "sudo mysql -e \"CREATE USER '".self::MYSQL_USER."'@'localhost' IDENTIFIED BY '".$password."';GRANT ALL PRIVILEGES ON *.* TO '".self::MYSQL_USER."'@'localhost' WITH GRANT OPTION;FLUSH PRIVILEGES;\""; + $query = "sudo mysql -e \"CREATE USER '".self::DATABASE_USER."'@'localhost' IDENTIFIED BY '".$password."';GRANT ALL PRIVILEGES ON *.* TO '".self::DATABASE_USER."'@'localhost' WITH GRANT OPTION;FLUSH PRIVILEGES;\""; } $this->cli->run( $query, function ($statusCode, $error) use (&$success) { - warning('Setting password for valet user failed due to `['.$statusCode.'] '.$error.'`'); + Writer::warn('Setting password for valet user failed due to `['.$statusCode.'] '.$error.'`'); $success = false; } ); if ($success !== false) { - $config = $this->configuration->read(); - if (!isset($config['mysql'])) { - $config['mysql'] = []; - } - $config['mysql']['user'] = self::MYSQL_USER; - $config['mysql']['password'] = $password; - $this->configuration->write($config); + /** @var array $config */ + $config = $this->configuration->get('mysql', []); + + $config['user'] = self::DATABASE_USER; + $config['password'] = $password; + $this->configuration->set('mysql', $config); } } /** * Returns the stored password from the config. If not configured returns the default root password. + * @return array{user: string, password: string} */ private function getCredentials(): array { - $config = $this->configuration->read(); - if (!isset($config['mysql']['password']) && !is_null($config['mysql']['password'])) { - warning('Valet database user is not configured!'); + /** @var array $config */ + $config = $this->configuration->get('mysql', []); + if (!isset($config['password']) && $config['password'] !== null) { + Writer::warn('Valet database user is not configured!'); exit; } // For previously installed user. - if (empty($config['mysql']['user'])) { - $config['mysql']['user'] = 'root'; + if (empty($config['user'])) { + $config['user'] = 'root'; } - return ['user' => $config['mysql']['user'], 'password' => $config['mysql']['password']]; + return ['user' => $config['user'], 'password' => $config['password']]; } } diff --git a/cli/Valet/Nginx.php b/cli/Valet/Nginx.php index 0ce1f94..39dc117 100644 --- a/cli/Valet/Nginx.php +++ b/cli/Valet/Nginx.php @@ -2,7 +2,7 @@ namespace Valet; -use Tightenco\Collect\Support\Collection; +use Illuminate\Support\Collection; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; use Valet\Facades\PhpFpm as PhpFpmFacade; @@ -30,40 +30,33 @@ class Nginx */ public $configuration; /** - * @var Site + * @var SiteSecure */ - public $site; + public $siteSecure; /** * @var string */ - const NGINX_CONF = '/etc/nginx/nginx.conf'; - const SITES_AVAILABLE_CONF = '/etc/nginx/sites-available/valet.conf'; - const SITES_ENABLED_CONF = '/etc/nginx/sites-enabled/valet.conf'; + public const NGINX_CONF = '/etc/nginx/nginx.conf'; + public const SITES_AVAILABLE_CONF = '/etc/nginx/sites-available/valet.conf'; + public const SITES_ENABLED_CONF = '/etc/nginx/sites-enabled/valet.conf'; /** * Create a new Nginx instance. * - * @param PackageManager $pm - * @param ServiceManager $sm - * @param CommandLine $cli - * @param Filesystem $files - * @param Configuration $configuration - * @param Site $site - * * @return void */ public function __construct( PackageManager $pm, ServiceManager $sm, - CommandLine $cli, - Filesystem $files, - Configuration $configuration, - Site $site + CommandLine $cli, + Filesystem $files, + Configuration $configuration, + SiteSecure $siteSecure ) { $this->cli = $cli; $this->pm = $pm; $this->sm = $sm; - $this->site = $site; + $this->siteSecure = $siteSecure; $this->files = $files; $this->configuration = $configuration; } @@ -94,7 +87,7 @@ public function updatePort(string $newPort): void 'VALET_HOME_PATH' => VALET_HOME_PATH, 'VALET_SERVER_PATH' => VALET_SERVER_PATH, 'VALET_PORT' => $newPort, - ], $this->files->get(__DIR__.'/../stubs/valet.conf')); + ], $this->files->get(VALET_ROOT_PATH . '/cli/stubs/valet.conf')); $this->files->putAsUser(self::SITES_AVAILABLE_CONF, $valetConfig); } @@ -143,9 +136,9 @@ public function uninstall(): void */ public function configuredSites(): Collection { - return collect($this->files->scandir(VALET_HOME_PATH.'/Nginx')) + return collect($this->files->scandir(VALET_HOME_PATH . '/Nginx')) ->reject(function ($file) { - return starts_with($file, '.'); + return str_starts_with($file, '.'); }); } @@ -158,11 +151,11 @@ public function installServer($phpVersion = null): void { $valetConf = strArrayReplace([ 'VALET_HOME_PATH' => VALET_HOME_PATH, - 'VALET_FPM_SOCKET_FILE' => VALET_HOME_PATH.'/'.PhpFpmFacade::socketFileName($phpVersion), + 'VALET_FPM_SOCKET_FILE' => VALET_HOME_PATH . '/' . PhpFpmFacade::socketFileName($phpVersion), 'VALET_SERVER_PATH' => VALET_SERVER_PATH, 'VALET_STATIC_PREFIX' => VALET_STATIC_PREFIX, - 'VALET_PORT' => $this->configuration->read()['port'], - ], $this->files->get(__DIR__.'/../stubs/valet.conf')); + 'VALET_PORT' => $this->configuration->get('port'), + ], $this->files->get(VALET_ROOT_PATH . '/cli/stubs/valet.conf')); $this->files->putAsUser(self::SITES_AVAILABLE_CONF, $valetConf); if ($this->files->exists('/etc/nginx/sites-enabled/default')) { @@ -174,7 +167,7 @@ public function installServer($phpVersion = null): void $this->files->putAsUser( '/etc/nginx/fastcgi_params', - $this->files->get(__DIR__.'/../stubs/fastcgi_params') + $this->files->get(VALET_ROOT_PATH . '/cli/stubs/fastcgi_params') ); } @@ -183,9 +176,9 @@ public function installServer($phpVersion = null): void */ private function rewriteSecureNginxFiles(): void { - $domain = $this->configuration->read()['domain']; + $domain = $this->configuration->get('domain'); - $this->site->resecureForNewDomain($domain, $domain); + $this->siteSecure->reSecureForNewDomain($domain, $domain); } /** @@ -207,7 +200,7 @@ private function handleApacheService(): void */ private function installConfiguration(): void { - $contents = $this->files->get(__DIR__.'/../stubs/nginx.conf'); + $contents = $this->files->get(VALET_ROOT_PATH . '/cli/stubs/nginx.conf'); $nginxConfig = self::NGINX_CONF; $pidPath = 'pid /run/nginx.pid'; @@ -237,11 +230,11 @@ private function installConfiguration(): void */ private function installNginxDirectory(): void { - if (!$this->files->isDir($nginxDirectory = VALET_HOME_PATH.'/Nginx')) { + if (!$this->files->isDir($nginxDirectory = VALET_HOME_PATH . '/Nginx')) { $this->files->mkdirAsUser($nginxDirectory); } - $this->files->putAsUser($nginxDirectory.'/.keep', "\n"); + $this->files->putAsUser($nginxDirectory . '/.keep', "\n"); $this->rewriteSecureNginxFiles(); } diff --git a/cli/Valet/Ngrok.php b/cli/Valet/Ngrok.php index 9394cb1..45d5f3a 100644 --- a/cli/Valet/Ngrok.php +++ b/cli/Valet/Ngrok.php @@ -2,9 +2,10 @@ namespace Valet; +use ConsoleComponents\Writer; use DomainException; use Exception; -use Httpful\Request; +use Valet\Facades\Request as RequestFacade; class Ngrok { @@ -12,38 +13,65 @@ class Ngrok * @var string */ private const TUNNEL_ENDPOINT = 'http://127.0.0.1:4040/api/tunnels'; - /** - * @var CommandLine - */ - public $cli; + private const BINARY_DOWNLOAD_LINK = 'https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz'; + + public CommandLine $cli; + + public Filesystem $files; /** * Create a new Ngrok instance. - * - * @param CommandLine $cli */ - public function __construct(CommandLine $cli) + public function __construct(CommandLine $cli, Filesystem $filesystem) { $this->cli = $cli; + $this->files = $filesystem; + } + + /** + * Install Ngrok binary + * @throws Exception + */ + public function install(): void + { + if ($this->files->exists(\sprintf('%s/bin/ngrok', VALET_ROOT_PATH))) { + return; + } + + Writer::twoColumnDetail('Ngrok', 'Installing'); + $this->files->ensureDirExists(\sprintf('%s/bin', VALET_ROOT_PATH), user()); + + $response = RequestFacade::get(self::BINARY_DOWNLOAD_LINK)->send(); + if ($response->hasErrors()) { + Writer::twoColumnDetail('Ngrok', 'Failed'); + return; + } + $zipFile = \sprintf('%s/bin/%s', VALET_ROOT_PATH, basename(self::BINARY_DOWNLOAD_LINK)); + + $this->files->putAsUser($zipFile, $response->raw_body); + + $phar = new \PharData($zipFile); + $phar->extractTo(VALET_ROOT_PATH.'/bin/'); + + $this->files->remove($zipFile); } /** * Get the current tunnel URL from the Ngrok API. * @throws Exception */ - public function currentTunnelUrl(): string + public function currentTunnelUrl(): ?string { return retry(20, function () { - $body = Request::get(self::TUNNEL_ENDPOINT)->send()->body; + $body = RequestFacade::get(self::TUNNEL_ENDPOINT)->send()->body; // 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 // but for local testing purposes we just desire the plain HTTP URL endpoint. if (isset($body->tunnels) && count($body->tunnels) > 0) { return $this->findHttpTunnelUrl($body->tunnels); - } else { - throw new DomainException('Tunnel not established.'); } + throw new DomainException('Tunnel not established.'); }, 250); } @@ -52,8 +80,9 @@ public function currentTunnelUrl(): string */ public function setAuthToken(string $authToken): void { - $this->cli->run(__DIR__.'/../../bin/ngrok config add-authtoken '.$authToken); - info('Ngrok authentication token set.'); + $this->cli->run( + \sprintf('%s/bin/ngrok config add-authtoken %s', VALET_ROOT_PATH, $authToken) + ); } /** diff --git a/cli/Valet/PackageManagers/Apt.php b/cli/Valet/PackageManagers/Apt.php index 21f80f8..9efe7af 100644 --- a/cli/Valet/PackageManagers/Apt.php +++ b/cli/Valet/PackageManagers/Apt.php @@ -2,11 +2,11 @@ namespace Valet\PackageManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use function Valet\output; class Apt implements PackageManager { @@ -18,23 +18,17 @@ class Apt implements PackageManager * @var ServiceManager */ public $serviceManager; - /** - * @var string - */ - public $redisPackageName = 'redis-server'; - /** - * @var string - */ - public $mysqlPackageName = 'mysql-server'; - /** - * @var string - */ - public $mariaDBPackageName = 'mariadb-server'; + + private const PACKAGES = [ + 'redis' => 'redis-server', + 'mysql' => 'mysql-server', + 'mariadb' => 'mariadb-server', + ]; /** * @var array */ - const PHP_FPM_PATTERN_BY_VERSION = []; + public const PHP_FPM_PATTERN_BY_VERSION = []; /** * Create a new Apt instance. @@ -78,10 +72,10 @@ public function ensureInstalled(string $package): void */ public function installOrFail(string $package): void { - output('['.$package.'] is not installed, installing it now via Apt'); + Writer::twoColumnDetail($package, 'Installing'); $this->cli->run(trim('apt-get install -y '.$package), function ($exitCode, $errorOutput) use ($package) { - output(\sprintf('%s: %s', $exitCode, $errorOutput)); + Writer::error(\sprintf('%s: %s', $exitCode, $errorOutput)); throw new DomainException('Apt was unable to install ['.$package.'].'); }); @@ -138,4 +132,15 @@ public function restartNetworkManager(): void { $this->serviceManager->restart(['NetworkManager']); } + + /** + * Get package name by service. + */ + public function packageName(string $name): string + { + if (isset(self::PACKAGES[$name])) { + return self::PACKAGES[$name]; + } + throw new \InvalidArgumentException(\sprintf('Package not found by %s', $name)); + } } diff --git a/cli/Valet/PackageManagers/Dnf.php b/cli/Valet/PackageManagers/Dnf.php index 905f149..a6b70fc 100644 --- a/cli/Valet/PackageManagers/Dnf.php +++ b/cli/Valet/PackageManagers/Dnf.php @@ -2,11 +2,11 @@ namespace Valet\PackageManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use function Valet\output; class Dnf implements PackageManager { @@ -18,23 +18,16 @@ class Dnf implements PackageManager * @var ServiceManager */ public $serviceManager; - /** - * @var string - */ - public $redisPackageName = 'redis'; - /** - * @var string - */ - public $mysqlPackageName = 'mysql-server'; - /** - * @var string - */ - public $mariaDBPackageName = 'mariadb-server'; - /** * @var array */ - const PHP_FPM_PATTERN_BY_VERSION = []; + public const PHP_FPM_PATTERN_BY_VERSION = []; + + private const PACKAGES = [ + 'redis' => 'redis', + 'mysql' => 'mysql-server', + 'mariadb' => 'mariadb-server', + ]; /** * Create a new Apt instance. @@ -72,10 +65,10 @@ public function ensureInstalled(string $package): void */ public function installOrFail(string $package): void { - output('['.$package.'] is not installed, installing it now via Dnf'); + Writer::twoColumnDetail($package, 'Installing'); $this->cli->run(trim('dnf install -y '.$package), function ($exitCode, $errorOutput) use ($package) { - output(\sprintf('%s: %s', $exitCode, $errorOutput)); + Writer::error(\sprintf('%s: %s', $exitCode, $errorOutput)); throw new DomainException('Dnf was unable to install ['.$package.'].'); }); @@ -132,4 +125,15 @@ public function restartNetworkManager(): void { $this->serviceManager->restart('NetworkManager'); } + + /** + * Get package name by service. + */ + public function packageName(string $name): string + { + if (isset(self::PACKAGES[$name])) { + return self::PACKAGES[$name]; + } + throw new \InvalidArgumentException(\sprintf('Package not found by %s', $name)); + } } diff --git a/cli/Valet/PackageManagers/Eopkg.php b/cli/Valet/PackageManagers/Eopkg.php index f0eea65..0cee021 100644 --- a/cli/Valet/PackageManagers/Eopkg.php +++ b/cli/Valet/PackageManagers/Eopkg.php @@ -2,11 +2,11 @@ namespace Valet\PackageManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use function Valet\output; class Eopkg implements PackageManager { @@ -18,23 +18,16 @@ class Eopkg implements PackageManager * @var ServiceManager */ public $serviceManager; - /** - * @var string - */ - public $redisPackageName = 'redis-server'; - /** - * @var string - */ - public $mysqlPackageName = 'mysql-server'; - /** - * @var string - */ - public $mariaDBPackageName = 'mariadb-server'; - /** * @var array */ - const PHP_FPM_PATTERN_BY_VERSION = []; + public const PHP_FPM_PATTERN_BY_VERSION = []; + + private const PACKAGES = [ + 'redis' => 'redis-server', + 'mysql' => 'mysql-server', + 'mariadb' => 'mariadb-server', + ]; /** * Create a new Eopkg instance. @@ -78,10 +71,10 @@ public function ensureInstalled(string $package): void */ public function installOrFail(string $package): void { - output('['.$package.'] is not installed, installing it now via Eopkg'); + Writer::twoColumnDetail($package, 'Installing'); $this->cli->run(trim('eopkg install -y '.$package), function ($exitCode, $errorOutput) use ($package) { - output(\sprintf('%s: %s', $exitCode, $errorOutput)); + Writer::error(\sprintf('%s: %s', $exitCode, $errorOutput)); throw new DomainException('Eopkg was unable to install ['.$package.'].'); }); @@ -138,4 +131,15 @@ public function restartNetworkManager(): void { $this->serviceManager->restart('NetworkManager'); } + + /** + * Get package name by service. + */ + public function packageName(string $name): string + { + if (isset(self::PACKAGES[$name])) { + return self::PACKAGES[$name]; + } + throw new \InvalidArgumentException(\sprintf('Package not found by %s', $name)); + } } diff --git a/cli/Valet/PackageManagers/PackageKit.php b/cli/Valet/PackageManagers/PackageKit.php index 987dbd4..d1a897c 100644 --- a/cli/Valet/PackageManagers/PackageKit.php +++ b/cli/Valet/PackageManagers/PackageKit.php @@ -2,11 +2,11 @@ namespace Valet\PackageManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use function Valet\output; class PackageKit implements PackageManager { @@ -18,23 +18,16 @@ class PackageKit implements PackageManager * @var ServiceManager */ public $serviceManager; - /** - * @var string - */ - public $redisPackageName = 'redis-server'; - /** - * @var string - */ - public $mysqlPackageName = 'mysql-server'; - /** - * @var string - */ - public $mariaDBPackageName = 'mariadb-server'; - /** * @var array */ - const PHP_FPM_PATTERN_BY_VERSION = []; + public const PHP_FPM_PATTERN_BY_VERSION = []; + + private const PACKAGES = [ + 'redis' => 'redis-server', + 'mysql' => 'mysql-server', + 'mariadb' => 'mariadb-server', + ]; /** * Create a new Apt instance. @@ -78,10 +71,10 @@ public function ensureInstalled(string $package): void */ public function installOrFail(string $package): void { - output('['.$package.'] is not installed, installing it now via PackageKit'); + Writer::twoColumnDetail($package, 'Installing'); $this->cli->run(trim('pkcon install -y '.$package), function ($exitCode, $errorOutput) use ($package) { - output(\sprintf('%s: %s', $exitCode, $errorOutput)); + Writer::error(\sprintf('%s: %s', $exitCode, $errorOutput)); throw new DomainException('PackageKit was unable to install ['.$package.'].'); }); @@ -145,4 +138,15 @@ public function restartNetworkManager(): void $this->serviceManager->restart('systemd-resolved'); } } + + /** + * Get package name by service. + */ + public function packageName(string $name): string + { + if (isset(self::PACKAGES[$name])) { + return self::PACKAGES[$name]; + } + throw new \InvalidArgumentException(\sprintf('Package not found by %s', $name)); + } } diff --git a/cli/Valet/PackageManagers/Pacman.php b/cli/Valet/PackageManagers/Pacman.php index 3d6fd54..5c520b8 100644 --- a/cli/Valet/PackageManagers/Pacman.php +++ b/cli/Valet/PackageManagers/Pacman.php @@ -2,11 +2,11 @@ namespace Valet\PackageManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use function Valet\output; class Pacman implements PackageManager { @@ -18,23 +18,16 @@ class Pacman implements PackageManager * @var ServiceManager */ public $serviceManager; - /** - * @var string - */ - public $redisPackageName = 'redis'; - /** - * @var string - */ - public $mysqlPackageName = 'mysql'; - /** - * @var string - */ - public $mariaDBPackageName = 'mariadb'; - /** * @var array */ - const PHP_FPM_PATTERN_BY_VERSION = []; + public const PHP_FPM_PATTERN_BY_VERSION = []; + + private const PACKAGES = [ + 'redis' => 'redis', + 'mysql' => 'mysql', + 'mariadb' => 'mariadb', + ]; /** * Create a new Apt instance. @@ -78,12 +71,12 @@ public function ensureInstalled(string $package): void */ public function installOrFail(string $package): void { - output('['.$package.'] is not installed, installing it now via Pacman'); + Writer::twoColumnDetail($package, 'Installing'); $this->cli->run( trim('pacman --noconfirm --needed -S '.$package), function ($exitCode, $errorOutput) use ($package) { - output(\sprintf('%s: %s', $exitCode, $errorOutput)); + Writer::error(\sprintf('%s: %s', $exitCode, $errorOutput)); throw new DomainException('Pacman was unable to install ['.$package.'].'); } @@ -143,4 +136,15 @@ public function restartNetworkManager(): void { $this->serviceManager->restart('NetworkManager'); } + + /** + * Get package name by service. + */ + public function packageName(string $name): string + { + if (isset(self::PACKAGES[$name])) { + return self::PACKAGES[$name]; + } + throw new \InvalidArgumentException(\sprintf('Package not found by %s', $name)); + } } diff --git a/cli/Valet/PackageManagers/Yum.php b/cli/Valet/PackageManagers/Yum.php index 554aa4d..896f043 100644 --- a/cli/Valet/PackageManagers/Yum.php +++ b/cli/Valet/PackageManagers/Yum.php @@ -2,11 +2,11 @@ namespace Valet\PackageManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; -use function Valet\output; class Yum implements PackageManager { @@ -22,19 +22,17 @@ class Yum implements PackageManager * @var string */ public $redisPackageName = 'redis'; - /** - * @var string - */ - public $mysqlPackageName = 'mysql-server'; - /** - * @var string - */ - public $mariaDBPackageName = 'mariadb-server'; /** * @var array */ - const PHP_FPM_PATTERN_BY_VERSION = []; + public const PHP_FPM_PATTERN_BY_VERSION = []; + + private const PACKAGES = [ + 'redis' => 'redis', + 'mysql' => 'mysql-server', + 'mariadb' => 'mariadb-server', + ]; /** * Create a new Apt instance. @@ -72,10 +70,10 @@ public function ensureInstalled(string $package): void */ public function installOrFail(string $package): void { - output('['.$package.'] is not installed, installing it now via Yum'); + Writer::twoColumnDetail($package, 'Installing'); $this->cli->run(trim('yum install -y '.$package), function ($exitCode, $errorOutput) use ($package) { - output(\sprintf('%s: %s', $exitCode, $errorOutput)); + Writer::error(\sprintf('%s: %s', $exitCode, $errorOutput)); throw new DomainException('Yum was unable to install ['.$package.'].'); }); @@ -132,4 +130,15 @@ public function restartNetworkManager(): void { $this->serviceManager->restart('NetworkManager'); } + + /** + * Get package name by service. + */ + public function packageName(string $name): string + { + if (isset(self::PACKAGES[$name])) { + return self::PACKAGES[$name]; + } + throw new \InvalidArgumentException(\sprintf('Package not found by %s', $name)); + } } diff --git a/cli/Valet/PhpFpm.php b/cli/Valet/PhpFpm.php index fb0c3e4..b34f75b 100644 --- a/cli/Valet/PhpFpm.php +++ b/cli/Valet/PhpFpm.php @@ -2,44 +2,39 @@ namespace Valet; -use Exception; -use Tightenco\Collect\Support\Collection; +use ConsoleComponents\Writer; +use Illuminate\Support\Collection; use Valet\Contracts\PackageManager; use Valet\Contracts\ServiceManager; use Valet\Exceptions\VersionException; use Valet\Facades\DevTools as DevToolsFacade; -use Valet\Facades\Nginx as NginxFacade; class PhpFpm { - protected $config; - protected $pm; - protected $sm; - protected $cli; - protected $files; - protected $site; - protected $nginx; - - const SUPPORTED_PHP_VERSIONS = [ - '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', + protected Configuration $config; + protected PackageManager $pm; + protected ServiceManager $sm; + protected CommandLine $cli; + protected Filesystem $files; + protected Site $site; + protected Nginx $nginx; + + public const SUPPORTED_PHP_VERSIONS = [ + '8.2', '8.3', ]; - const COMMON_EXTENSIONS = [ + public const ISOLATION_SUPPORTED_PHP_VERSIONS = [ + '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', ...self::SUPPORTED_PHP_VERSIONS + ]; + + public const COMMON_EXTENSIONS = [ 'cli', 'mysql', 'gd', 'zip', 'xml', 'curl', 'mbstring', 'pgsql', 'intl', 'posix', ]; - const FPM_CONFIG_FILE_NAME = 'valet.conf'; + public const FPM_CONFIG_FILE_NAME = 'valet.conf'; /** * Create a new PHP FPM class instance. - * - * @param Configuration $config - * @param PackageManager $pm - * @param ServiceManager $sm - * @param CommandLine $cli - * @param Filesystem $files - * @param Site $site - * @param Nginx $nginx */ public function __construct( Configuration $config, @@ -51,9 +46,9 @@ public function __construct( Nginx $nginx ) { $this->config = $config; - $this->cli = $cli; $this->pm = $pm; $this->sm = $sm; + $this->cli = $cli; $this->files = $files; $this->site = $site; $this->nginx = $nginx; @@ -61,13 +56,14 @@ public function __construct( /** * Install and configure PHP FPM. - * @throws VersionException */ public function install(?string $version = null, bool $installExt = true): void { $version = $version ?: $this->getCurrentVersion(); $version = $this->normalizePhpVersion($version); - $this->validateVersion($version); + if ($version === '') { + return; + } $packageName = $this->pm->getPhpFpmName($version); if (!$this->pm->installed($packageName)) { @@ -88,39 +84,39 @@ public function install(?string $version = null, bool $installExt = true): void /** * Uninstall PHP FPM valet config. */ - public function uninstall(): void + public function uninstall(?string $version = null): void { - if ($this->files->exists($this->fpmConfigPath().'/'.self::FPM_CONFIG_FILE_NAME)) { - $this->files->unlink($this->fpmConfigPath().'/'.self::FPM_CONFIG_FILE_NAME); - $this->stop(); + $version = $version ?: $this->getCurrentVersion(); + $version = $this->normalizePhpVersion($version); + if ($version === '') { + return; + } + + $fpmConfPath = $this->fpmConfigPath($version) . '/' . self::FPM_CONFIG_FILE_NAME; + if ($this->files->exists($fpmConfPath)) { + $this->files->unlink($fpmConfPath); + $this->stop($version); } } /** * Change the php-fpm version. - * @throws Exception */ public function switchVersion( - string $version = null, + string $version, bool $updateCli = false, - bool $ignoreExt = false, - bool $ignoreUpdate = false + bool $ignoreExt = false ): void { - $exception = null; - $currentVersion = $this->getCurrentVersion(); // Validate if in use $version = $this->normalizePhpVersion($version); - try { - $this->install($version, !$ignoreExt); - } catch (Exception $e) { - $version = $currentVersion; - $exception = $e; - } + Writer::info('Changing php version...'); + + $this->install($version, !$ignoreExt); - if ($this->sm->disabled($this->serviceName())) { - $this->sm->enable($this->serviceName()); + if ($this->sm->disabled($this->serviceName($version))) { + $this->sm->enable($this->serviceName($version)); } $this->config->set('php_version', $version); @@ -128,24 +124,16 @@ public function switchVersion( $this->stopIfUnused($currentVersion); $this->updateNginxConfigFiles($version); - NginxFacade::restart(); + $this->nginx->restart(); $this->status($version); if ($updateCli) { $this->cli->run("update-alternatives --set php /usr/bin/php$version"); - if (!$ignoreUpdate) { - $this->handlePackageUpdate($version); - } - } - - if ($exception) { - warning('Changing version failed'); - - throw $exception; } } /** * Restart the PHP FPM process. + * @param null|mixed $version */ public function restart($version = null): void { @@ -154,6 +142,7 @@ public function restart($version = null): void /** * Stop the PHP FPM process. + * @param null|mixed $version */ public function stop($version = null): void { @@ -162,71 +151,13 @@ public function stop($version = null): void /** * PHP-FPM service status. + * @param null|mixed $version */ public function status($version = null): void { $this->sm->printStatus($this->serviceName($version)); } - /** - * Isolate a given directory to use a specific version of PHP. - * @throws VersionException - */ - public function isolateDirectory(string $directory, string $version, bool $secure = false): void - { - $site = $this->site->getSiteUrl($directory); - - $version = $this->normalizePhpVersion($version); - $this->validateVersion($version); - - $fpmName = $this->pm->getPhpFpmName($version); - if (!$this->pm->installed($fpmName)) { - $this->install($version); - } - - $oldCustomPhpVersion = $this->site->customPhpVersion($site); // Example output: "74" - - $this->site->isolate($site, $version, $secure); - - if ($oldCustomPhpVersion) { - $this->stopIfUnused($oldCustomPhpVersion); - } - $this->restart($version); - NginxFacade::restart(); - - info(sprintf('The site [%s] is now using %s.', $site, $version)); - } - - /** - * Remove PHP version isolation for a given directory. - */ - public function unIsolateDirectory(string $directory): void - { - $site = $this->site->getSiteUrl($directory); - - $oldCustomPhpVersion = $this->site->customPhpVersion($site); // Example output: "74" - - $this->site->removeIsolation($site); - if ($oldCustomPhpVersion) { - $this->stopIfUnused($oldCustomPhpVersion); - } - NginxFacade::restart(); - - info(sprintf('The site [%s] is now using the default PHP version.', $site)); - } - - /** - * List isolated directories with version. - */ - public function isolatedDirectories(): Collection - { - return NginxFacade::configuredSites()->filter(function ($item) { - return strpos($this->files->get(VALET_HOME_PATH.'/Nginx/'.$item), ISOLATED_PHP_VERSION) !== false; - })->map(function ($item) { - return ['url' => $item, 'version' => $this->normalizePhpVersion($this->site->customPhpVersion($item))]; - }); - } - /** * Get FPM socket file name for a given PHP version. */ @@ -243,9 +174,19 @@ public function socketFileName(string $version = null): string /** * Normalize inputs (php-x.x, php@x.x, phpx.x, phpxx) to version (x.x). */ - public function normalizePhpVersion($version): string + public function normalizePhpVersion(string $version): string { - return substr(preg_replace('/(?:php@?)?([0-9+])(?:.)?([0-9+])/i', '$1.$2', (string) $version), 0, 3); + preg_match( + '/^(?:php[@-]?)?(?[\d]{1}).?(?[\d]{1})$/i', + $version, + $matches + ); + + if (!isset($matches['MAJOR_VERSION'], $matches['MINOR_VERSION'])) { + return ''; + } + + return \sprintf('%s.%s', $matches['MAJOR_VERSION'], $matches['MINOR_VERSION']); } /** @@ -263,23 +204,47 @@ public function getCurrentVersion(): string public function getPhpExecutablePath(string $version = null) { if (!$version) { - return DevToolsFacade::getBin('php'); + return DevToolsFacade::getBin('php', ['/usr/local/bin/php']); } $version = $this->normalizePhpVersion($version); - return DevToolsFacade::getBin('php'.$version); + return DevToolsFacade::getBin('php' . $version, ['/usr/local/bin/php']); } public function fpmSocketFile(string $version): string { - return VALET_HOME_PATH.'/'.$this->socketFileName($version); + return VALET_HOME_PATH . '/' . $this->socketFileName($version); + } + + public function updateHomePath(string $oldHomePath, string $newHomePath): void + { + foreach (self::ISOLATION_SUPPORTED_PHP_VERSIONS as $version) { + $confPath = $this->fpmConfigPath($version) . '/' . self::FPM_CONFIG_FILE_NAME; + if ($this->files->exists($confPath)) { + $valetConf = $this->files->get($confPath); + $valetConf = str_replace($oldHomePath, $newHomePath, $valetConf); + $this->files->put($confPath, $valetConf); + } + } + } + + /** + * Validate PHP version. + */ + public function validateVersion(string $version): bool + { + if (!in_array($version, self::SUPPORTED_PHP_VERSIONS)) { + return false; + } + + return true; } /** * Stop a given PHP version, if that specific version isn't being used globally or by any sites. */ - private function stopIfUnused(string $version): void + public function stopIfUnused(string $version): void { $version = $this->normalizePhpVersion($version); @@ -303,12 +268,12 @@ private function serviceName(string $version = null): string private function updateNginxConfigFiles(string $version): void { //Action 1: Update all separate secured versions - NginxFacade::configuredSites()->map(function ($file) use ($version) { - $content = $this->files->get(VALET_HOME_PATH.'/Nginx/'.$file); + $this->nginx->configuredSites()->map(function ($file) use ($version) { + $content = $this->files->get(VALET_HOME_PATH . '/Nginx/' . $file); if (!$content) { return; } - if (strpos($content, '# '.ISOLATED_PHP_VERSION) !== false) { + if (strpos($content, '# ' . ISOLATED_PHP_VERSION) !== false) { return; } preg_match_all('/unix:(.*?.sock)/m', $content, $matchCount); @@ -317,22 +282,22 @@ private function updateNginxConfigFiles(string $version): void } $content = preg_replace( '/unix:(.*?.sock)/m', - 'unix:'.VALET_HOME_PATH.'/'.$this->socketFileName($version), + 'unix:' . VALET_HOME_PATH . '/' . $this->socketFileName($version), $content ); - $this->files->put(VALET_HOME_PATH.'/Nginx/'.$file, $content); + $this->files->put(VALET_HOME_PATH . '/Nginx/' . $file, $content); }); //Action 2: Update NGINX valet.conf for php socket version. - NginxFacade::installServer($version); + $this->nginx->installServer($version); } private function installExtensions(string $version): void { $extArray = []; - $extensionPrefix = $this->getExtensionPrefix($version); + $extensionPrefix = $this->pm->getPhpExtensionPrefix($version); foreach (self::COMMON_EXTENSIONS as $ext) { - $extArray[] = "{$extensionPrefix}-{$ext}"; + $extArray[] = "{$extensionPrefix}{$ext}"; } $this->pm->ensureInstalled(implode(' ', $extArray)); } @@ -342,43 +307,46 @@ private function installExtensions(string $version): void */ private function installConfiguration(string $version): void { - $contents = $this->files->get(__DIR__.'/../../stubs/fpm.conf'); + $contents = $this->files->get(VALET_ROOT_PATH . '/cli/stubs/fpm.conf'); $contents = strArrayReplace([ 'VALET_USER' => user(), 'VALET_GROUP' => group(), 'VALET_FPM_SOCKET_FILE' => $this->fpmSocketFile($version), ], $contents); - $this->files->putAsUser($this->fpmConfigPath($version).'/'.self::FPM_CONFIG_FILE_NAME, $contents); + $this->files->putAsUser($this->fpmConfigPath($version) . '/' . self::FPM_CONFIG_FILE_NAME, $contents); } /** * Get a list including the global PHP version and all PHP versions currently serving "isolated sites" (sites with * custom Nginx configs pointing them to a specific PHP version). + * @return array */ private function utilizedPhpVersions(): array { - $fpmSockFiles = collect(self::SUPPORTED_PHP_VERSIONS)->map(function ($version) { + /** @var array $fpmSockFiles */ + $fpmSockFiles = collect(self::ISOLATION_SUPPORTED_PHP_VERSIONS)->map(function ($version) { return $this->socketFileName($this->normalizePhpVersion($version)); })->unique(); - $versions = NginxFacade::configuredSites()->map(function ($file) use ($fpmSockFiles) { - $content = $this->files->get(VALET_HOME_PATH.'/Nginx/'.$file); + $versions = $this->nginx->configuredSites()->map(function ($file) use ($fpmSockFiles) { + $content = $this->files->get(VALET_HOME_PATH . '/Nginx/' . $file); // Get the normalized PHP version for this config file, if it's defined foreach ($fpmSockFiles as $sock) { if (strpos($content, $sock) !== false) { - // Extract the PHP version number from a custom .sock path and normalize it to, e.g., "php@7.4" + // Extract the PHP version number from a custom .sock path and normalize it, e.g. "7.4" return $this->normalizePhpVersion(str_replace(['valet', '.sock'], '', $sock)); } } })->filter()->unique()->values()->toArray(); // Adding Default version in utilized versions list. - if (!in_array($this->getCurrentVersion(), $versions)) { - $versions[] = $this->getCurrentVersion(); + if (!in_array($currentVersion = $this->getCurrentVersion(), $versions)) { + $versions[] = $currentVersion; } + /** @var array $versions */ return $versions; } @@ -390,11 +358,12 @@ private function fpmConfigPath(string $version = null): string $version = $version ?: $this->getCurrentVersion(); $versionWithoutDot = preg_replace('~[^\d]~', '', $version); + /** @var string $confDir */ return collect([ - '/etc/php/'.$version.'/fpm/pool.d', // Ubuntu - '/etc/php'.$version.'/fpm/pool.d', // Ubuntu - '/etc/php'.$version.'/php-fpm.d', // Manjaro - '/etc/php'.$versionWithoutDot.'/php-fpm.d', // ArchLinux + '/etc/php/' . $version . '/fpm/pool.d', // Ubuntu + '/etc/php' . $version . '/fpm/pool.d', // Ubuntu + '/etc/php' . $version . '/php-fpm.d', // Manjaro + '/etc/php' . $versionWithoutDot . '/php-fpm.d', // ArchLinux '/etc/php7/fpm/php-fpm.d', // openSUSE PHP7 '/etc/php8/fpm/php-fpm.d', // openSUSE PHP8 '/etc/php8/fpm/php-fpm.d', // openSUSE PHP8 @@ -402,21 +371,24 @@ private function fpmConfigPath(string $version = null): string '/etc/php-fpm.d', // Fedora '/etc/php/php-fpm.d', // Arch ])->first(function ($path) { - return is_dir($path); + return $this->files->isDir($path); }, function () { throw new \DomainException('Unable to determine PHP-FPM configuration folder.'); }); } /** - * Validate PHP version. - * @throws VersionException + * Validate PHP version for isolation process. */ - private function validateVersion(string $version): void + private function validateIsolationVersion(string $version): void { - if (!in_array($version, self::SUPPORTED_PHP_VERSIONS)) { - throw new VersionException( - "Invalid version [$version] used. Supported versions are :".implode(self::SUPPORTED_PHP_VERSIONS) + if (!in_array($version, self::ISOLATION_SUPPORTED_PHP_VERSIONS)) { + throw new \DomainException( + \sprintf( + "Invalid version [%s] used. Supported versions are: %s", + $version, + implode(', ', self::ISOLATION_SUPPORTED_PHP_VERSIONS) + ) ); } } @@ -426,27 +398,6 @@ private function validateVersion(string $version): void */ private function getDefaultVersion(): string { - return $this->normalizePhpVersion(PHP_VERSION); - } - - private function getExtensionPrefix($version = null): string - { - $version = $version ?: $this->getCurrentVersion(); - return $this->pm->getPhpExtensionPrefix($version); - } - - private function handlePackageUpdate($version): void - { - $installedPhpVersion = $this->config->get('installed_php_version'); - if ($installedPhpVersion && $installedPhpVersion >= $version) { - if (is_dir(__DIR__.'/../../../vendor')) { - // Local vendor - $this->cli->runAsUser('composer update'); - } else { - // Global vendor - $this->cli->runAsUser('composer global require genesisweb/valet-linux-plus:'.VALET_VERSION.' -W'); - } - $this->config->set('installed_php_version', $version); - } + return $this->normalizePhpVersion(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION); } } diff --git a/cli/Valet/Request.php b/cli/Valet/Request.php new file mode 100644 index 0000000..701d020 --- /dev/null +++ b/cli/Valet/Request.php @@ -0,0 +1,11 @@ +allowWildcardDnsDomains($httpHost), - '.'.$this->config['tld'] + '.'.$this->config['domain'] ); if (strpos($siteName, 'www.') === 0) { @@ -204,11 +204,10 @@ public function sitePath(string $siteName): ?string /** * Return the default site path for uncaught URLs, if it's set. - **/ + */ public function defaultSitePath(): ?string { - if ( - isset($this->config['default']) + if (isset($this->config['default']) && is_string($this->config['default']) && is_dir($this->config['default']) ) { diff --git a/cli/Valet/ServiceManagers/LinuxService.php b/cli/Valet/ServiceManagers/LinuxService.php index 45a0fc5..92e76b3 100644 --- a/cli/Valet/ServiceManagers/LinuxService.php +++ b/cli/Valet/ServiceManagers/LinuxService.php @@ -2,12 +2,11 @@ namespace Valet\ServiceManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\ServiceManager; use Valet\Filesystem; -use function Valet\info; -use function Valet\warning; class LinuxService implements ServiceManager { @@ -31,42 +30,45 @@ public function __construct(CommandLine $cli, Filesystem $files) /** * Start the given services. - * @param array|string $services Service name + * @param string|string[]|null $services Service name */ - public function start($services): void + public function start(array|string|null $services): void { + /** @var string[] $services */ $services = is_array($services) ? $services : func_get_args(); foreach ($services as $service) { - info("Starting $service..."); + Writer::twoColumnDetail(ucfirst($service), 'Starting'); $this->cli->quietly('sudo service '.$this->getRealService($service).' start'); } } /** * Stop the given services. - * @param array|string $services Service name + * @param string|string[]|null $services Service name */ - public function stop($services): void + public function stop(array|string|null $services): void { + /** @var string[] $services */ $services = is_array($services) ? $services : func_get_args(); foreach ($services as $service) { - info("Stopping $service..."); + Writer::twoColumnDetail(ucfirst($service), 'Stopping'); $this->cli->quietly('sudo service '.$this->getRealService($service).' stop'); } } /** * Restart the given services. - * @param array|string $services Service name + * @param string|string[]|null $services Service name */ - public function restart($services): void + public function restart(array|string|null $services): void { + /** @var string[] $services */ $services = is_array($services) ? $services : func_get_args(); foreach ($services as $service) { - info("Restarting $service..."); + Writer::twoColumnDetail(ucfirst($service), 'Restarting'); $this->cli->quietly('sudo service '.$this->getRealService($service).' restart'); } } @@ -76,7 +78,14 @@ public function restart($services): void */ public function printStatus(string $service): void { - info($this->cli->run('service '.$this->getRealService($service))); + $status = $this->cli->run('service '.$this->getRealService($service). ' status'); + $running = strpos(trim($status), 'running'); + + if ($running) { + Writer::info(ucfirst($service).' is running...'); + } else { + Writer::warn(ucfirst($service).' is stopped...'); + } } /** @@ -85,8 +94,8 @@ public function printStatus(string $service): void public function disabled(string $service): bool { $service = $this->getRealService($service); - - return strpos(trim($this->cli->run("systemctl is-enabled {$service}")), 'enabled') === false; + // TODO: Do not use systemctl and stop using linux service class if systemd is available on all minimum versions + return !str_contains(trim($this->cli->run("systemctl is-enabled {$service}")), 'enabled'); } /** @@ -98,26 +107,24 @@ public function disable(string $service): void $service = $this->getRealService($service); $this->cli->quietly("sudo chmod -x /etc/init.d/{$service}"); $this->cli->quietly("sudo update-rc.d $service defaults"); + + Writer::twoColumnDetail(ucfirst($service), 'Disabled'); } catch (DomainException $e) { - warning(ucfirst($service).' not available.'); + Writer::warn(ucfirst($service).' not available.'); } } /** * Enable services. - * - * @param mixed $services Service or services to enable - * - * @return void */ public function enable(string $service): void { try { $service = $this->getRealService($service); $this->cli->quietly("sudo update-rc.d $service defaults"); - info(ucfirst($service).' has been enabled'); + Writer::twoColumnDetail(ucfirst($service), 'Enabled'); } catch (DomainException $e) { - warning(ucfirst($service).' not available.'); + Writer::warn(ucfirst($service).' unavailable.'); } } @@ -148,7 +155,7 @@ public function removeValetDns(): void $servicePath = '/etc/init.d/valet-dns'; if ($this->files->exists($servicePath)) { - info('Removing Valet DNS service...'); + Writer::info('Removing Valet DNS service...'); $this->disable('valet-dns'); $this->stop('valet-dns'); $this->files->remove($servicePath); diff --git a/cli/Valet/ServiceManagers/Systemd.php b/cli/Valet/ServiceManagers/Systemd.php index 5de5d41..8554a99 100644 --- a/cli/Valet/ServiceManagers/Systemd.php +++ b/cli/Valet/ServiceManagers/Systemd.php @@ -2,12 +2,11 @@ namespace Valet\ServiceManagers; +use ConsoleComponents\Writer; use DomainException; use Valet\CommandLine; use Valet\Contracts\ServiceManager; use Valet\Filesystem; -use function Valet\info; -use function Valet\warning; class Systemd implements ServiceManager { @@ -31,42 +30,45 @@ public function __construct(CommandLine $cli, Filesystem $files) /** * Start the given services. - * @param array|string $services Service name + * @param string|string[]|null $services Service name */ - public function start($services): void + public function start(array|string|null $services): void { + /** @var string[] $services */ $services = is_array($services) ? $services : func_get_args(); foreach ($services as $service) { - info("Starting $service..."); + Writer::twoColumnDetail(ucfirst($service), 'Starting'); $this->cli->quietly('sudo systemctl start '.$this->getRealService($service)); } } /** * Stop the given services. - * @param array|string $services Service name + * @param string|string[]|null $services Service name */ - public function stop($services): void + public function stop(array|string|null $services): void { + /** @var string[] $services */ $services = is_array($services) ? $services : func_get_args(); foreach ($services as $service) { - info("Stopping $service..."); + Writer::twoColumnDetail(ucfirst($service), 'Stopping'); $this->cli->quietly('sudo systemctl stop '.$this->getRealService($service)); } } /** * Restart the given services. - * @param array|string $services Service name + * @param string|string[]|null $services Service name */ - public function restart($services): void + public function restart(array|string|null $services): void { + /** @var string[] $services */ $services = is_array($services) ? $services : func_get_args(); foreach ($services as $service) { - info("Restarting $service..."); + Writer::twoColumnDetail(ucfirst($service), 'Restarting'); $this->cli->quietly('sudo systemctl restart '.$this->getRealService($service)); } } @@ -80,9 +82,9 @@ public function printStatus(string $service): void $running = strpos(trim($status), 'running'); if ($running) { - info(ucfirst($service).' is running...'); + Writer::info(ucfirst($service).' is running...'); } else { - warning(ucfirst($service).' is stopped...'); + Writer::warn(ucfirst($service).' is stopped...'); } } @@ -93,7 +95,7 @@ public function disabled(string $service): bool { $service = $this->getRealService($service); - return strpos(trim($this->cli->run("systemctl is-enabled {$service}")), 'enabled') === false; + return !str_contains(trim($this->cli->run(\sprintf('systemctl is-enabled %s', $service))), 'enabled'); } /** @@ -106,12 +108,11 @@ public function enable(string $service): void if ($this->disabled($service)) { $this->cli->quietly('sudo systemctl enable '.$service); - info(ucfirst($service).' has been enabled'); } - info(ucfirst($service).' was already enabled'); + Writer::twoColumnDetail(ucfirst($service), 'Enabled'); } catch (DomainException $e) { - warning(ucfirst($service).' unavailable.'); + Writer::warn(ucfirst($service).' unavailable.'); } } @@ -125,12 +126,11 @@ public function disable(string $service): void if (!$this->disabled($service)) { $this->cli->quietly('sudo systemctl disable '.$service); - info(ucfirst($service).' has been disabled'); } - info(ucfirst($service).' was already disabled'); + Writer::twoColumnDetail(ucfirst($service), 'Disabled'); } catch (DomainException $e) { - warning(ucfirst($service).' unavailable.'); + Writer::warn(ucfirst($service).' unavailable.'); } } @@ -154,13 +154,13 @@ function () { } /** - * Install Valet DNS services. + * Remove Valet DNS services. */ public function removeValetDns(): void { $servicePath = '/etc/systemd/system/valet-dns.service'; if ($this->files->exists($servicePath)) { - info('Removing Valet DNS service...'); + Writer::info('Removing Valet DNS service...'); $this->disable('valet-dns'); $this->stop('valet-dns'); $this->files->remove($servicePath); @@ -174,13 +174,13 @@ public function isSystemd(): bool /** * Determine real service name. - * TODO: Validate this function + * @throws DomainException */ private function getRealService(string $service): string { return collect($service)->first( function ($service) { - return strpos($this->cli->run("systemctl status {$service} | grep Loaded"), 'Loaded: loaded'); + return strpos($this->cli->run("systemctl status $service | grep Loaded"), 'Loaded: loaded'); }, function () { throw new DomainException('Unable to determine service name.'); diff --git a/cli/Valet/Site.php b/cli/Valet/Site.php index 2b370ea..268c0cc 100644 --- a/cli/Valet/Site.php +++ b/cli/Valet/Site.php @@ -2,202 +2,26 @@ namespace Valet; -use Tightenco\Collect\Support\Collection; +use Illuminate\Support\Collection; use Valet\Facades\PhpFpm as PhpFpmFacade; +use Valet\Traits\Paths; class Site { - /** - * @var Configuration - */ - public $config; - /** - * @var CommandLine - */ - public $cli; - /** - * @var Filesystem - */ - public $files; + use Paths; + + public Configuration $config; + public CommandLine $cli; + public Filesystem $files; /** * Create a new Site instance. - * - * @param Configuration $config - * @param CommandLine $cli - * @param Filesystem $files */ public function __construct(Configuration $config, CommandLine $cli, Filesystem $files) { + $this->config = $config; $this->cli = $cli; $this->files = $files; - $this->config = $config; - } - - /** - * Get the real hostname for the given path, checking links. - */ - public function host(string $path): string - { - foreach ($this->files->scandir($this->sitesPath()) as $link) { - if (realpath($this->sitesPath().'/'.$link) === $path) { - return $link; - } - } - - return basename($path); - } - - /** - * Link the current working directory with the given name. - */ - public function link(string $target, string $link): string - { - $this->files->ensureDirExists( - $linkPath = $this->sitesPath(), - user() - ); - - $this->config->prependPath($linkPath); - - $this->files->symlinkAsUser($target, $linkPath.'/'.$link); - - return $linkPath.'/'.$link; - } - - /** - * Pretty print out all links in Valet. - */ - public function links(): Collection - { - $certsPath = $this->certificatesPath(); - - $this->files->ensureDirExists($certsPath, user()); - - $certs = $this->getCertificates($certsPath); - - return $this->getLinks($this->sitesPath(), $certs); - } - - /** - * Get all sites which are proxies (not Links, and contain proxy_pass directive). - */ - public function proxies(): Collection - { - $dir = $this->nginxPath(); - $domain = $this->config->read()['domain']; - $links = $this->links(); - $certs = $this->getCertificates($this->certificatesPath()); - if (!$this->files->exists($dir)) { - return collect(); - } - - return collect($this->files->scandir($dir)) - ->filter(function ($site) use ($domain) { - // keep sites that match our TLD - - return endsWith($site, '.'.$domain); - })->map(function ($site) use ($domain) { - // remove the TLD suffix for consistency - return str_replace('.'.$domain, '', $site); - })->reject(function ($site) use ($links) { - return $links->has($site); - })->mapWithKeys(function ($site) { - $host = $this->getProxyHostForSite($site) ?: '(other)'; - - return [$site => $host]; - })->reject(function ($host) { - // If proxy host is null, it may be just a normal SSL stub, or something else; - // either way we exclude it from the list - return $host === '(other)'; - })->map(function ($host, $site) use ($certs, $domain) { - $secured = $certs->has($site); - $url = ($secured ? 'https' : 'http').'://'.$site.'.'.$domain; - - return [ - 'url' => $url, - 'secured' => $secured ? ' X' : '', - 'path' => $host, - ]; - }); - } - - /** - * Unsecure the given URL so that it will use HTTP again. - */ - public function proxyDelete(string $url): void - { - $tld = $this->config->read()['domain']; - if (!endsWith($url, '.'.$tld)) { - $url .= '.'.$tld; - } - - $this->unsecure($url); - $this->files->unlink($this->nginxPath($url)); - - info('Valet will no longer proxy [https://'.$url.'].'); - } - - /** - * Build the Nginx proxy config for the specified domain. - * @throws \InvalidArgumentException - */ - 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)); - } - - $domain = $this->config->read()['domain']; - - if (!endsWith($url, '.'.$domain)) { - $url .= '.'.$domain; - } - - $siteConf = $this->files->get( - $secure ? __DIR__.'/../stubs/secure.proxy.valet.conf' : __DIR__.'/../stubs/proxy.valet.conf' - ); - - // General Variables - $siteConf = strArrayReplace( - [ - 'VALET_HOME_PATH' => $this->valetHomePath(), - 'VALET_SERVER_PATH' => VALET_SERVER_PATH, - 'VALET_STATIC_PREFIX' => VALET_STATIC_PREFIX, - 'VALET_SITE' => $url, - 'VALET_HTTP_PORT' => $this->config->get('port', 80), - 'VALET_HTTPS_PORT' => $this->config->get('https_port', 443), - ], - $siteConf - ); - - // Proxy specific variables - $siteConf = strArrayReplace( - [ - 'VALET_PROXY_HOST' => $host, - ], - $siteConf - ); - - if ($secure) { - $this->secure($url, $siteConf); - } else { - $this->put($url, $siteConf); - } - - $protocol = $secure ? 'https' : 'http'; - - info('Valet will now proxy ['.$protocol.'://'.$url.'] traffic to ['.$host.'].'); - } - - /** - * Unlink the given symbolic link. - */ - public function unlink(string $name): void - { - if ($this->files->exists($path = $this->sitesPath().'/'.$name)) { - $this->files->unlink($path); - } } /** @@ -210,178 +34,26 @@ public function pruneLinks(): void $this->files->removeBrokenLinksAt($this->sitesPath()); } - /** - * Re-secure all currently secured sites with a fresh domain. - * - * @param string $oldDomain - * @param string $domain - * - * @return void - */ - public function resecureForNewDomain(string $oldDomain, string $domain): void - { - if (!$this->files->exists($this->certificatesPath())) { - return; - } - - $secured = $this->secured(); - - foreach ($secured as $oldUrl) { - $newUrl = str_replace('.'.$oldDomain, '.'.$domain, $oldUrl); - $nginxConf = $this->getNginxConf($oldUrl); - if ($nginxConf) { - $nginxConf = str_replace($oldUrl, $newUrl, $nginxConf); - } - - $this->unsecure($oldUrl); - - $this->secure( - $newUrl, - $nginxConf - ); - } - } - - /** - * Get all the URLs that are currently secured. - */ - public function secured(): Collection - { - return collect($this->files->scandir($this->certificatesPath())) - ->map(function ($file) { - return str_replace(['.key', '.csr', '.crt', '.conf'], '', $file); - })->unique()->values(); - } - - /** - * Secure the given host with TLS. - */ - public function secure(string $url, string $stub = null): void - { - if (is_null($stub)) { - $stub = $this->prepareConf($url, true); - } - $this->unsecure($url); - - $this->files->ensureDirExists($this->certificatesPath(), user()); - - $this->createCertificate($url); - - $this->createSecureNginxServer($url, $stub); - } - - /** - * Unsecure the given URL so that it will use HTTP again. - */ - public function unsecure(string $url, bool $preserveUnsecureConfig = false): void - { - $stub = null; - if ($this->files->exists($this->certificatesPath().'/'.$url.'.crt')) { - if ($preserveUnsecureConfig) { - $stub = $this->prepareConf($url); - } - - $this->files->unlink($this->nginxPath().'/'.$url); - - $this->files->unlink($this->certificatesPath().'/'.$url.'.conf'); - $this->files->unlink($this->certificatesPath().'/'.$url.'.key'); - $this->files->unlink($this->certificatesPath().'/'.$url.'.csr'); - $this->files->unlink($this->certificatesPath().'/'.$url.'.crt'); - - $this->cli->run(sprintf('certutil -d sql:$HOME/.pki/nssdb -D -n "%s"', $url)); - $this->cli->run(sprintf('certutil -d $HOME/.mozilla/firefox/*.default -D -n "%s"', $url)); - } - if ($stub) { - $this->put($url, $stub); - } - } - - /** - * Regenerate all secured file configurations. - */ - public function regenerateSecuredSitesConfig(): void - { - $this->secured()->each(function ($url) { - $this->createSecureNginxServer($url); - }); - } - /** * Get the site URL from a directory if it's a valid Valet site. + * @throws \DomainException */ public function getSiteUrl(string $directory): string { - $tld = $this->config->read()['domain']; + $tld = $this->config->get('domain'); if ($directory == '.' || $directory == './') { // Allow user to use dot as current directory site `--site=.` - $directory = $this->host(getcwd()); + $directory = basename((string)getcwd()); } - $directory = str_replace('.'.$tld, '', $directory); // Remove .tld from site name if it was provided + $directory = str_replace('.' . $tld, '', $directory); // Remove .tld from site name if it was provided - if (!$this->parked()->merge($this->links())->where('site', $directory)->count() > 0) { + $servedSites = $this->servedSites(); + if (!$servedSites->has($directory)) { throw new \DomainException("The [{$directory}] site could not be found in Valet's site list."); } - return $directory.'.'.$tld; - } - - /** - * Create new nginx config or modify existing nginx config to isolate this site - * to a custom version of PHP. - */ - public function isolate(string $url, string $phpVersion, bool $secure = false): void - { - $stub = $secure ? __DIR__.'/../stubs/secure.isolated.valet.conf' : __DIR__.'/../stubs/isolated.valet.conf'; - - // Isolate specific variables - $siteConf = strArrayReplace([ - 'VALET_FPM_SOCKET_FILE' => PhpFpmFacade::fpmSocketFile($phpVersion), - 'VALET_ISOLATED_PHP_VERSION' => $phpVersion, - ], $this->files->get($stub)); - - if ($secure) { - $this->secure($url, $siteConf); - } else { - $this->put($url, $siteConf); - } - } - - /** - * Remove PHP Version isolation from a specific site. - */ - 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 - if ($this->files->exists($this->certificatesPath().'/'.$valetSite.'.crt')) { - $this->createSecureNginxServer($valetSite); - } else { - // When site doesn't have SSL, we can remove the custom nginx config file to remove isolation - $this->files->unlink($this->nginxPath($valetSite)); - } - } - - /** - * Extract PHP version of exising nginx config. - */ - public function customPhpVersion(string $url, string $siteConf = null, bool $returnDecimal = false): ?string - { - if (is_null($siteConf) && !$this->files->exists($this->nginxPath($url))) { - return null; - } - - $siteConf = $siteConf ?: $this->files->get($this->nginxPath($url)); - if (strpos($siteConf, '# '.ISOLATED_PHP_VERSION) !== false) { - preg_match('/^# ISOLATED_PHP_VERSION=(.*?)\n/m', $siteConf, $version); - if ($returnDecimal) { - return $version[1]; - } - - return preg_replace("/[^\d]*/", '', $version[1]); // Example output: "74" or "81" - } - - return null; + return $directory . '.' . $tld; } /** @@ -389,8 +61,10 @@ public function customPhpVersion(string $url, string $siteConf = null, bool $ret */ public function phpRcVersion(string $site): ?string { - if ($site = $this->parked()->merge($this->links())->where('site', $site)->first()) { - $path = data_get($site, 'path').'/.valetphprc'; + $servedSites = $this->servedSites(); + if ($servedSites->has($site)) { + $sitePath = $servedSites->get($site); + $path = $sitePath . '/.valetphprc'; if ($this->files->exists($path)) { return PhpFpmFacade::normalizePhpVersion(trim($this->files->get($path))); @@ -400,410 +74,34 @@ public function phpRcVersion(string $site): ?string } /** - * Extract Proxy pass of exising nginx config. - */ - private function getProxyPass(string $url, string $siteConf = null): ?string - { - if (is_null($siteConf) && !$this->files->exists($this->nginxPath($url))) { - return null; - } - - $siteConf = $siteConf ?: $this->files->get($this->nginxPath($url)); - preg_match('/proxy_pass (?.*?);/m', $siteConf, $matches); - - return $matches['host'] ?? null; - } - - private function getNginxConf(string $url): ?string - { - if (!$this->files->exists($this->nginxPath().'/'.$url)) { - return null; - } - - return $this->files->get($this->nginxPath().'/'.$url); - } - - /** - * Prepare Nginx Conf based on existing config file. - **/ - private function prepareConf(string $url, bool $requireSecure = false): ?string - { - if (!$this->files->exists($this->nginxPath($url))) { - return null; - } - - $existingConf = $this->files->get($this->nginxPath($url)); - - preg_match('/# valet stub: (?secure)?(?:\.)?(?.*?).valet.conf/m', $existingConf, $stubDetail); - - if (empty($stubDetail['stub'])) { - return null; - } - - if ($stubDetail['stub'] === 'proxy') { - // Find proxy_pass from existingConf. - $proxyPass = $this->getProxyPass($url, $existingConf); - if (!$proxyPass) { - return null; - } - $stub = $requireSecure ? - __DIR__.'/../stubs/secure.proxy.valet.conf' : - __DIR__.'/../stubs/proxy.valet.conf'; - $stub = $this->files->get($stub); - - return strArrayReplace([ - 'VALET_PROXY_HOST' => $proxyPass, - ], $stub); - } - - if ($stubDetail['stub'] === 'isolated') { - $phpVersion = $this->customPhpVersion($url, $existingConf, true); - // empty($stubDetail['tls']) || We can use this statement if needed. - $stub = $requireSecure ? - __DIR__.'/../stubs/secure.isolated.valet.conf' : - __DIR__.'/../stubs/isolated.valet.conf'; - $stub = $this->files->get($stub); - // Isolate specific variables - return strArrayReplace([ - 'VALET_FPM_SOCKET_FILE' => PhpFpmFacade::fpmSocketFile($phpVersion), - 'VALET_ISOLATED_PHP_VERSION' => $phpVersion, - ], $stub); - } - - return null; - } - - /** - * Identify whether a site is for a proxy by reading the host name from its config file. - */ - private function getProxyHostForSite(string $site, string $configContents = null): ?string - { - $siteConf = $configContents ?: $this->getSiteConfigFileContents($site); - - if (empty($siteConf)) { - return null; - } - - $host = null; - if (preg_match('~proxy_pass\s+(?https?://.*)\s*;~', $siteConf, $patterns)) { - $host = trim($patterns['host']); - } - - return $host; - } - - /** - * Get the path to Nginx site configuration files. - */ - private function nginxPath(string $additionalPath = null): string - { - return $this->valetHomePath().'/Nginx'.($additionalPath ? '/'.$additionalPath : ''); - } - - public function valetHomePath(): string - { - return VALET_HOME_PATH; - } - - /** - * Create the given nginx host. - */ - private function put(string $url, string $siteConf): void - { - $this->unsecure($url); - - $this->files->ensureDirExists($this->nginxPath(), user()); - - $siteConf = strArrayReplace( - [ - 'VALET_HOME_PATH' => $this->valetHomePath(), - 'VALET_SERVER_PATH' => VALET_SERVER_PATH, - 'VALET_STATIC_PREFIX' => VALET_STATIC_PREFIX, - 'VALET_SITE' => $url, - 'VALET_HTTP_PORT' => $this->config->get('port', 80), - 'VALET_HTTPS_PORT' => $this->config->get('https_port', 443), - ], - $siteConf - ); - - $this->files->putAsUser( - $this->nginxPath($url), - $siteConf - ); - } - - private function getSiteConfigFileContents(string $site, string $suffix = null): ?string - { - $config = $this->config->read(); - $suffix = $suffix ?: '.'.$config['domain']; - $file = str_replace($suffix, '', $site).$suffix; - - return $this->files->exists($this->nginxPath($file)) ? $this->files->get($this->nginxPath($file)) : null; - } - - /** - * Get all certificates from config folder. - */ - private function getCertificates(string $path = null): Collection - { - $path = $path ?: $this->certificatesPath(); - - return collect($this->files->scanDir($path))->filter(function ($value) { - return ends_with($value, '.crt'); - })->map(function ($cert) { - return substr($cert, 0, -9); - })->flip(); - } - - /** - * Get list of links and present them formatted. - */ - private function getLinks(string $path, Collection $certs): Collection - { - $config = $this->config->read(); - - $httpPort = $this->httpSuffix(); - $httpsPort = $this->httpsSuffix(); - - return collect($this->files->scanDir($path))->mapWithKeys(function ($site) use ($path) { - return [$site => $this->files->readLink($path.'/'.$site)]; - })->map(function ($path, $site) use ($certs, $config, $httpPort, $httpsPort) { - $secured = $certs->has($site); - - $url = ($secured ? 'https' : 'http').'://'.$site.'.'.$config['domain'].($secured ? $httpsPort : $httpPort); - $phpVersion = $this->getPhpVersion($site.'.'.$config['domain']); - - return [ - 'site' => $site, - 'secured' => $secured ? ' X' : '', - 'url' => $url, - 'path' => $path, - 'phpVersion' => $phpVersion, - ]; - }); - } - - /** - * Return http port suffix. - */ - private function httpSuffix(): string - { - $port = $this->config->get('port', 80); - - return ($port == 80) ? '' : ':'.$port; - } - - /** - * Return https port suffix. + * List of all sites served by valet + * @return Collection */ - private function httpsSuffix(): string + private function servedSites(): Collection { - $port = $this->config->get('https_port', 443); + $parkedSites = []; - return ($port == 443) ? '' : ':'.$port; - } - - /** - * Create and trust a certificate for the given URL. - */ - private function createCertificate(string $url): void - { - $keyPath = $this->certificatesPath().'/'.$url.'.key'; - $csrPath = $this->certificatesPath().'/'.$url.'.csr'; - $crtPath = $this->certificatesPath().'/'.$url.'.crt'; - $confPath = $this->certificatesPath().'/'.$url.'.conf'; - - $this->buildCertificateConf($confPath, $url); - $this->createPrivateKey($keyPath); - $this->createSigningRequest($url, $keyPath, $csrPath, $confPath); - - $this->cli->runAsUser(sprintf( - 'openssl x509 -req -sha256 -days 365 -in %s -signkey %s -out %s -extensions v3_req -extfile %s', - $csrPath, - $keyPath, - $crtPath, - $confPath - )); - - $this->trustCertificate($crtPath, $url); - } - - /** - * Create the private key for the TLS certificate. - */ - private function createPrivateKey(string $keyPath): void - { - $this->cli->runAsUser(sprintf('openssl genrsa -out %s 2048', $keyPath)); - } - - /** - * Create the signing request for the TLS certificate. - */ - private function createSigningRequest(string $url, string $keyPath, string $csrPath, string $confPath): void - { - $this->cli->runAsUser(sprintf( - 'openssl req -new -key %s -out %s -subj "/C=US/ST=MN/O=Valet/localityName=Valet/commonName=%s/organizationalUnitName=Valet/emailAddress=valet/" -config %s -passin pass:', - $keyPath, - $csrPath, - $url, - $confPath - )); - } - - /** - * Build the SSL config for the given URL. - */ - private function buildCertificateConf(string $path, string $url): void - { - $config = str_replace('VALET_DOMAIN', $url, $this->files->get(__DIR__.'/../stubs/openssl.conf')); - $this->files->putAsUser($path, $config); - } - - /** - * Trust the given certificate file in the Mac Keychain. - */ - private function trustCertificate(string $crtPath, string $url): void - { - $this->cli->run(sprintf( - 'certutil -d sql:$HOME/.pki/nssdb -A -t TC -n "%s" -i "%s"', - $url, - $crtPath - )); - - $this->cli->run(sprintf( - 'certutil -d $HOME/.mozilla/firefox/*.default -A -t TC -n "%s" -i "%s"', - $url, - $crtPath - )); - - $this->cli->run(sprintf( - 'certutil -d $HOME/snap/firefox/common/.mozilla/firefox/*.default -A -t TC -n "%s" -i "%s"', - $url, - $crtPath - )); - } - - private function createSecureNginxServer(string $url, string $stub = null): void - { - $this->files->putAsUser( - $this->nginxPath($url), - $this->buildSecureNginxServer($url, $stub) - ); - } - - /** - * Build the TLS secured Nginx server for the given URL. - */ - private function buildSecureNginxServer(string $url, ?string $stub = null): string - { - $stub = ($stub ?: $this->files->get(__DIR__.'/../stubs/secure.valet.conf')); - $path = $this->certificatesPath(); - - return strArrayReplace( - [ - 'VALET_HOME_PATH' => $this->valetHomePath(), - 'VALET_SERVER_PATH' => VALET_SERVER_PATH, - 'VALET_STATIC_PREFIX' => VALET_STATIC_PREFIX, - 'VALET_SITE' => $url, - 'VALET_CERT' => $path.'/'.$url.'.crt', - 'VALET_KEY' => $path.'/'.$url.'.key', - 'VALET_HTTP_PORT' => $this->config->get('port', 80), - 'VALET_HTTPS_PORT' => $this->config->get('https_port', 443), - 'VALET_REDIRECT_PORT' => $this->httpsSuffix(), - 'VALET_FPM_SOCKET_FILE' => PhpFpmFacade::fpmSocketFile(PhpFpmFacade::getCurrentVersion()), - ], - $stub - ); - } - - /** - * Get the path to the linked Valet sites. - */ - private function sitesPath(): string - { - return $this->valetHomePath().'/Sites'; - } - - /** - * Get the path to the Valet TLS certificates. - */ - private function certificatesPath(): string - { - return $this->valetHomePath().'/Certificates'; - } - - /** - * Get list of sites and return them formatted - * Will work for symlink and normal site paths. - */ - private function getSites(string $path, Collection $certs): Collection - { - $config = $this->config->read(); - - $this->files->ensureDirExists($path, user()); - - return collect($this->files->scandir($path))->mapWithKeys(function ($site) use ($path) { - $sitePath = $path.'/'.$site; - - if ($this->files->isLink($sitePath)) { - $realPath = $this->files->readLink($sitePath); - } else { - $realPath = $this->files->realpath($sitePath); - } - - return [$site => $realPath]; - })->filter(function ($path) { - return $this->files->isDir($path); - })->map(function ($path, $site) use ($certs, $config) { - $secured = $certs->has($site); - $url = ($secured ? 'https' : 'http').'://'.$site.'.'.$config['domain']; - $phpVersion = $this->getPhpVersion($site.'.'.$config['domain']); - - return [ - 'site' => $site, - 'secured' => $secured ? ' X' : '', - 'url' => $url, - 'path' => $path, - 'phpVersion' => $phpVersion, - ]; - }); - } - - /** - * Get the PHP version for the given site. - */ - private function getPhpVersion(string $url): string - { - $defaultPhpVersion = PhpFpmFacade::getCurrentVersion(); - $phpVersion = PhpFpmFacade::normalizePhpVersion($this->customPhpVersion($url)); - if (empty($phpVersion)) { - $phpVersion = PhpFpmFacade::normalizePhpVersion($defaultPhpVersion); - } - - return $phpVersion; - } - - private function parked(): Collection - { - $certs = $this->getCertificates(); - - $links = $this->getSites($this->sitesPath(), $certs); - - $config = $this->config->read(); - $parkedLinks = collect(); - foreach (array_reverse($config['paths']) as $path) { + /** @var array $parkedPaths */ + $parkedPaths = $this->config->get('paths', []); + foreach ($parkedPaths as $path) { if ($path === $this->sitesPath()) { continue; } - // Only merge on the parked sites that don't interfere with the linked sites - $sites = $this->getSites($path, $certs)->filter(function ($site, $key) use ($links) { - return !$links->has($key); - }); + $sites = $this->files->scandir($path); + foreach ($sites as $site) { + if ($this->files->isDir($path . '/' . $site)) { + $parkedSites[$site] = $path . '/' . $site; + } + } + } - $parkedLinks = $parkedLinks->merge($sites); + // Get sites from links + $linkedSites = $this->files->scandir($this->sitesPath()); + foreach ($linkedSites as $linkedSite) { + $parkedSites[$linkedSite] = $this->files->realpath($this->sitesPath($linkedSite)); } - return $parkedLinks; + return collect($parkedSites); } } diff --git a/cli/Valet/SiteIsolate.php b/cli/Valet/SiteIsolate.php new file mode 100644 index 0000000..7b8d8c7 --- /dev/null +++ b/cli/Valet/SiteIsolate.php @@ -0,0 +1,232 @@ +pm = $pm; + $this->config = $config; + $this->files = $filesystem; + $this->siteSecure = $siteSecure; + $this->site = $site; + } + + /** + * Isolate a given directory to use a specific version of PHP. + */ + public function isolateDirectory(string $directory, string $version, bool $secure = false): bool + { + try { + $site = $this->site->getSiteUrl($directory); + + $version = PhpFpmFacade::normalizePhpVersion($version); + $this->validateIsolationVersion($version); + + $fpmName = $this->pm->getPhpFpmName($version); + if (!$this->pm->installed($fpmName)) { + PhpFpmFacade::install($version); + } + + $oldCustomPhpVersion = $this->isolatedPhpVersion($site); + + $this->isolate($site, $version, $secure); + + if ($oldCustomPhpVersion) { + PhpFpmFacade::stopIfUnused($oldCustomPhpVersion); + } + + PhpFpmFacade::restart($version); + NginxFacade::restart(); + + $this->addBinFileToConfig($version, $directory); + } catch (DomainException $exception) { + Writer::error($exception->getMessage()); + return false; + } + + return true; + } + + /** + * Remove PHP version isolation for a given directory. + */ + public function unIsolateDirectory(string $directory): void + { + $site = $this->site->getSiteUrl($directory); + + $oldCustomPhpVersion = $this->isolatedPhpVersion($site); + + $this->removeIsolation($site); + + if ($oldCustomPhpVersion) { + PhpFpmFacade::stopIfUnused($oldCustomPhpVersion); + } + NginxFacade::restart(); + + $this->removeBinFromConfig($directory); + } + + /** + * List isolated directories with version. + */ + public function isolatedDirectories(): Collection + { + $securedSites = $this->siteSecure->secured(); + + return NginxFacade::configuredSites()->filter(function ($item) { + return str_contains($this->files->get($this->nginxPath($item)), ISOLATED_PHP_VERSION); + })->map(function ($site) use ($securedSites) { + $secured = $securedSites->contains($site); + + $url = \sprintf( + '%s://%s', + $secured ? 'https' : 'http', + $site + ); + + return [ + 'url' => $url, + 'secured' => $secured ? '✓' : '✕', + 'version' => PhpFpmFacade::normalizePhpVersion((string)$this->isolatedPhpVersion($site)) + ]; + }); + } + + /** + * Extract PHP version of exising nginx config. + */ + public function isolatedPhpVersion(string $url): ?string + { + if (!$this->files->exists($this->nginxPath($url))) { + return null; + } + + $siteConf = $this->files->get($this->nginxPath($url)); + if (strpos($siteConf, '# ' . ISOLATED_PHP_VERSION) !== false) { + preg_match('/^# ISOLATED_PHP_VERSION=(.*?)\n/m', $siteConf, $version); + + return $version[1]; + } + + return null; + } + + /** + * Create new nginx config or modify existing nginx config to isolate this site + * to a custom version of PHP. + */ + private function isolate(string $url, string $phpVersion, bool $secure = false): void + { + $stub = $secure ? + VALET_ROOT_PATH . '/cli/stubs/secure.isolated.valet.conf' + : VALET_ROOT_PATH . '/cli/stubs/isolated.valet.conf'; + + // Isolate specific variables + $siteConf = strArrayReplace([ + 'VALET_FPM_SOCKET_FILE' => PhpFpmFacade::fpmSocketFile($phpVersion), + 'VALET_ISOLATED_PHP_VERSION' => $phpVersion, + ], $this->files->get($stub)); + + if ($secure) { + $this->siteSecure->secure($url, $siteConf); + } else { + $siteConf = $this->siteSecure->buildUnsecureNginxServer($url, $siteConf); + + $this->files->putAsUser( + $this->nginxPath($url), + $siteConf + ); + } + } + + /** + * Remove PHP Version isolation from a specific site. + */ + private function removeIsolation(string $siteName): 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 + if ($this->files->exists($this->certificatesPath() . '/' . $siteName . '.crt')) { + $conf = $this->siteSecure->buildSecureNginxServer($siteName); + $this->files->putAsUser($this->nginxPath($siteName), $conf); + + return; + } + + // When site doesn't have SSL, we can remove the custom nginx config file to remove isolation + $this->files->unlink($this->nginxPath($siteName)); + } + + /** + * Validate PHP version for isolation process. + */ + private function validateIsolationVersion(string $version): void + { + if (!in_array($version, PhpFpm::ISOLATION_SUPPORTED_PHP_VERSIONS)) { + throw new DomainException( + sprintf( + "Invalid version [%s] used. Supported versions are: %s", + $version, + implode(', ', PhpFpm::ISOLATION_SUPPORTED_PHP_VERSIONS) + ) + ); + } + } + + private function addBinFileToConfig(string $version, string $directoryName): void + { + $directoryName = $this->removeTld($directoryName); + $binaryFile = DevToolsFacade::getBin('php' . $version, ['/usr/local/bin/php']); + /** @var array $isolatedConfig */ + $isolatedConfig = $this->config->get('isolated_versions', []); + + $isolatedConfig[$directoryName] = $binaryFile; + $this->config->set('isolated_versions', $isolatedConfig); + } + + private function removeBinFromConfig(string $directoryName): void + { + $directoryName = $this->removeTld($directoryName); + /** @var array $isolatedConfig */ + $isolatedConfig = $this->config->get('isolated_versions', []); + if (isset($isolatedConfig[$directoryName])) { + unset($isolatedConfig[$directoryName]); + $this->config->set('isolated_versions', $isolatedConfig); + } + } + + private function removeTld(string $domainName): string + { + /** @var string $tld */ + $tld = $this->config->get('domain'); + if (str_ends_with($domainName, \sprintf('.%s', $tld))) { + $domainName = str_replace(\sprintf('.%s', $tld), '', $domainName); + } + + return $domainName; + } +} diff --git a/cli/Valet/SiteLink.php b/cli/Valet/SiteLink.php new file mode 100644 index 0000000..03715f8 --- /dev/null +++ b/cli/Valet/SiteLink.php @@ -0,0 +1,108 @@ +files = $filesystem; + $this->config = $config; + $this->siteSecure = $siteSecure; + } + + /** + * Link the current working directory with the given name. + */ + public function link(string $target, string $link): string + { + $linkPath = $this->sitesPath(); + $this->files->ensureDirExists($linkPath, user()); + + $this->config->addPath($linkPath, true); + + $this->files->symlinkAsUser($target, $linkPath . '/' . $link); + + return $linkPath . '/' . $link; + } + + /** + * Unlink the given symbolic link. + */ + public function unlink(string $name): void + { + $path = $this->sitesPath() . '/' . $name; + if ($this->files->exists($path)) { + $this->files->unlink($path); + } + } + + /** + * Pretty print out all links in Valet. + * @return Collection> + */ + public function links(): Collection + { + $certsPath = $this->certificatesPath(); + $path = $this->sitesPath(); + + $this->files->ensureDirExists($certsPath, user()); + + $securedSites = $this->siteSecure->secured(); + + /** @var string $domain */ + $domain = $this->config->get('domain'); + + $httpPort = $this->httpSuffix(); + $httpsPort = $this->httpsSuffix(); + + return collect($this->files->scandir($path))->mapWithKeys(function ($site) use ($path) { + return [$site => $this->files->readLink($path . '/' . $site)]; + })->map(function ($path, $site) use ($securedSites, $domain, $httpPort, $httpsPort) { + $secured = $securedSites->contains($site . '.' . $domain); + + $url = \sprintf( + '%s://%s.%s%s', + $secured ? 'https' : 'http', + $site, + $domain, + $secured ? $httpsPort : $httpPort + ); + + return [ + 'url' => $url, + 'secured' => $secured ? '✓' : '✕', + 'path' => $path, + ]; + }); + } + + /** + * Return http port suffix. + */ + private function httpSuffix(): string + { + $port = $this->config->get('port', 80); + + return ($port == 80) ? '' : ':' . $port; + } + + /** + * Return https port suffix. + */ + private function httpsSuffix(): string + { + $port = $this->config->get('https_port', 443); + + return ($port == 443) ? '' : ':' . $port; + } +} diff --git a/cli/Valet/SiteProxy.php b/cli/Valet/SiteProxy.php new file mode 100644 index 0000000..eb1aee2 --- /dev/null +++ b/cli/Valet/SiteProxy.php @@ -0,0 +1,124 @@ +files = $filesystem; + $this->config = $config; + $this->siteSecure = $siteSecure; + } + + /** + * Build the Nginx proxy config for the specified domain. + * @throws \InvalidArgumentException + */ + 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)); + } + + $domain = $this->config->get('domain'); + + if (!str_ends_with($url, '.' . $domain)) { + $url .= '.' . $domain; + } + + $siteConf = $this->files->get( + $secure + ? VALET_ROOT_PATH . '/cli/stubs/secure.proxy.valet.conf' + : VALET_ROOT_PATH . '/cli/stubs/proxy.valet.conf' + ); + + // Proxy specific variables + $siteConf = strArrayReplace( + [ + 'VALET_PROXY_HOST' => $host, + ], + $siteConf + ); + + if ($secure) { + $this->siteSecure->secure($url, $siteConf); + } else { + $siteConf = $this->siteSecure->buildUnsecureNginxServer($url, $siteConf); + + $this->files->putAsUser( + $this->nginxPath($url), + $siteConf + ); + } + } + + /** + * Get all sites which are proxies (not Links, and contain proxy_pass directive). + */ + public function proxies(): Collection + { + $nginxPath = $this->nginxPath(); + $domain = $this->config->get('domain'); + + $securedSites = $this->siteSecure->secured(); + + if (!$this->files->exists($nginxPath)) { + return collect(); + } + + return collect($this->files->scandir($nginxPath)) + ->filter(function ($site) use ($domain) { + // keep sites that match our TLD + return str_ends_with($site, '.' . $domain); + })->mapWithKeys(function ($site) use ($domain) { + $host = $this->getProxyHostForSite($site) ?: '(other)'; + + return [$site => $host]; + })->reject(function ($host) { + // If proxy host is null, it may be just a normal SSL stub, or something else; + // either way we exclude it from the list + return $host === '(other)'; + })->map(function ($host, $site) use ($securedSites) { + $secured = $securedSites->contains($site); + $url = ($secured ? 'https' : 'http') . '://' . $site; + + return [ + 'url' => $url, + 'secured' => $secured ? '✓' : '✕', + 'path' => $host, + ]; + }); + } + + /** + * Identify whether a site is for a proxy by reading the host name from its config file. + */ + private function getProxyHostForSite(string $siteName): ?string + { + if ($this->files->exists($this->nginxPath($siteName)) === false) { + return null; + } + + $siteConf = $this->files->get($this->nginxPath($siteName)); + preg_match('~proxy_pass\s+(?https?://.*)\s*;~', $siteConf, $matches); + + if (!isset($matches['host'])) { + return null; + } + + return trim($matches['host']); + } +} diff --git a/cli/Valet/SiteSecure.php b/cli/Valet/SiteSecure.php new file mode 100644 index 0000000..0a33e32 --- /dev/null +++ b/cli/Valet/SiteSecure.php @@ -0,0 +1,407 @@ +files = $filesystem; + $this->cli = $cli; + $this->config = $config; + } + + /** + * Secure the given host with TLS. + */ + public function secure(string $url, string $stub = null): void + { + if ($stub === null) { + $stub = $this->prepareConf($url, true); + } + + $this->files->ensureDirExists($this->caPath(), user()); + + $this->files->ensureDirExists($this->certificatesPath(), user()); + + $caExpireInDate = (new \DateTime())->diff(new \DateTime("+20 years")); + $expiryInDays = (int)$caExpireInDate->format('%a'); // 20 years in days + $this->createCa($expiryInDays); + + $certificateExpireInDate = (new \DateTime())->diff(new \DateTime("+1 year")); + $certificateExpireInDays = (int)$certificateExpireInDate->format('%a'); // 20 years in days + $this->createCertificate($url, $certificateExpireInDays); + + $this->files->putAsUser( + $this->nginxPath($url), + $this->buildSecureNginxServer($url, $stub) + ); + } + + /** + * Unsecure the given URL so that it will use HTTP again. + */ + public function unsecure(string $url, bool $preserveUnsecureConfig = false): void + { + $stub = null; + if ($this->files->exists($this->certificatesPath($url . '.crt'))) { + if ($preserveUnsecureConfig) { + $stub = $this->prepareConf($url); + } + + $this->files->unlink($this->nginxPath($url)); + + $this->files->unlink($this->certificatesPath($url . '.conf')); + $this->files->unlink($this->certificatesPath($url . '.key')); + $this->files->unlink($this->certificatesPath($url . '.csr')); + $this->files->unlink($this->certificatesPath($url . '.crt')); + } + + if ($stub) { + $stub = $this->buildUnsecureNginxServer($url, $stub); + + $this->files->putAsUser( + $this->nginxPath($url), + $stub + ); + } + } + + /** + * Get all the URLs that are currently secured. + * @return Collection + */ + public function secured(): Collection + { + return collect($this->files->scandir($this->certificatesPath())) + ->map(function ($file) { + return str_replace(['.key', '.csr', '.crt', '.conf'], '', $file); + })->unique()->values(); + } + + /** + * Regenerate all secured file configurations. + */ + public function regenerateSecuredSitesConfig(): void + { + $this->secured()->each(function (string $url) { + $this->files->putAsUser( + $this->nginxPath($url), + $this->buildSecureNginxServer($url) + ); + }); + } + + /** + * Re-secure all currently secured sites with a fresh domain. + */ + public function reSecureForNewDomain(string $oldDomain, string $domain): void + { + if (!$this->files->exists($this->certificatesPath())) { + return; + } + + $secured = $this->secured(); + + foreach ($secured as $oldUrl) { + $newUrl = str_replace('.' . $oldDomain, '.' . $domain, $oldUrl); + $hasConf = $this->files->exists($this->nginxPath($oldUrl)); + $nginxConf = null; + if ($hasConf) { + $nginxConf = $this->files->get($this->nginxPath($oldUrl)); + $nginxConf = str_replace($oldUrl, $newUrl, $nginxConf); + } + + $this->unsecure($oldUrl); + + $this->secure($newUrl, $nginxConf); + } + } + + /** + * 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. + * @throws \Exception + */ + private function createCa(int $caExpireInDays): void + { + $caPemPath = $this->caPath($this->caCertificatePem); + $caKeyPath = $this->caPath($this->caCertificateKey); + + if ($this->files->exists($caKeyPath) && $this->files->exists($caPemPath)) { + $this->trustCa($caPemPath); + return; + } + + if ($this->files->exists($caKeyPath)) { + $this->files->unlink($caKeyPath); + } + if ($this->files->exists($caPemPath)) { + $this->files->unlink($caPemPath); + } + + $this->unTrustCa(); + + $subject = sprintf( + '/C=/ST=/O=%s/localityName=/commonName=%s/organizationalUnitName=Developers/emailAddress=%s/', + $this->caCertificateOrganization, + $this->caCertificateCommonName, + $this->certificateDummyEmail, + ); + $this->cli->runAsUser( + sprintf( + 'openssl req -new -newkey rsa:2048 -days %s -nodes -x509 -subj "%s" -keyout "%s" -out "%s"', + $caExpireInDays, + $subject, + $caKeyPath, + $caPemPath + ) + ); + $this->trustCa($caPemPath); + } + + /** + * Trust the given root certificate file in the macOS Keychain. + * @throws \Exception + */ + private function unTrustCa(): void + { + $this->files->remove(\sprintf('%s%s.crt', $this->caCertificatePath, $this->caCertificatePem)); + $this->cli->run('sudo update-ca-certificates'); + } + + /** + * Trust the given root certificate file in the macOS Keychain. + */ + private function trustCa(string $caPemPath): void + { + $this->files->copy($caPemPath, sprintf('%s%s.crt', $this->caCertificatePath, $this->caCertificatePem)); + $this->cli->run('sudo update-ca-certificates'); + + $this->cli->runAsUser(sprintf( + 'certutil -d sql:$HOME/.pki/nssdb -A -t TC -n "%s" -i "%s"', + $this->caCertificateOrganization, + $caPemPath + )); + + $this->cli->runAsUser(sprintf( + 'certutil -d $HOME/.mozilla/firefox/*.default -A -t TC -n "%s" -i "%s"', + $this->caCertificateOrganization, + $caPemPath + )); + + $this->cli->runAsUser(sprintf( + 'certutil -d $HOME/snap/firefox/common/.mozilla/firefox/*.default -A -t TC -n "%s" -i "%s"', + $this->caCertificateOrganization, + $caPemPath + )); + } + + /** + * Create and trust a certificate for the given URL. + */ + private function createCertificate(string $url, int $certificateExpireInDays = 368): void + { + $caPemPath = $this->caPath($this->caCertificatePem); + $caKeyPath = $this->caPath($this->caCertificateKey); + $caSrlPath = $this->caPath($this->caCertificateSrl); + + $keyPath = $this->certificatesPath() . '/' . $url . '.key'; + $csrPath = $this->certificatesPath() . '/' . $url . '.csr'; + $crtPath = $this->certificatesPath() . '/' . $url . '.crt'; + $confPath = $this->certificatesPath() . '/' . $url . '.conf'; + + $this->generateCertificateConf($confPath, $url); + $this->cli->runAsUser(sprintf('openssl genrsa -out %s 2048', $keyPath)); + + $subject = sprintf( + '/C=/ST=/O=/localityName=/commonName=%s/organizationalUnitName=/emailAddress=%s/', + $url, + $this->certificateDummyEmail, + ); + $this->cli->runAsUser(sprintf( + 'openssl req -new -key %s -out %s -subj "%s" -config %s', + $keyPath, + $csrPath, + $subject, + $confPath + )); + + $caSrlParam = '-CAserial "' . $caSrlPath . '"'; + if (! $this->files->exists($caSrlPath)) { + $caSrlParam .= ' -CAcreateserial'; + } + + $this->cli->run(sprintf( + 'openssl x509 -req -sha256 -days %s -CA "%s" -CAkey "%s" %s -in %s -out %s -extensions v3_req -extfile %s', + $certificateExpireInDays, + $caPemPath, + $caKeyPath, + $caSrlParam, + $csrPath, + $crtPath, + $confPath + )); + } + + /** + * Build the TLS secured Nginx server for the given URL. + */ + public function buildSecureNginxServer(string $url, ?string $stub = null): string + { + $stub = ($stub ?: $this->files->get(VALET_ROOT_PATH . '/cli/stubs/secure.valet.conf')); + $path = $this->certificatesPath(); + + return strArrayReplace( + [ + 'VALET_HOME_PATH' => VALET_HOME_PATH, + 'VALET_SERVER_PATH' => VALET_SERVER_PATH, + 'VALET_STATIC_PREFIX' => VALET_STATIC_PREFIX, + 'VALET_SITE' => $url, + 'VALET_CERT' => $path . '/' . $url . '.crt', + 'VALET_KEY' => $path . '/' . $url . '.key', + 'VALET_HTTP_PORT' => $this->config->get('port', 80), + 'VALET_HTTPS_PORT' => $this->config->get('https_port', 443), + 'VALET_REDIRECT_PORT' => $this->httpsSuffix(), + 'VALET_FPM_SOCKET_FILE' => PhpFpmFacade::fpmSocketFile(PhpFpmFacade::getCurrentVersion()), + ], + $stub + ); + } + + /** + * Build the TLS secured Nginx server for the given URL. + */ + public function buildUnsecureNginxServer(string $url, string $stub): string + { + $this->files->ensureDirExists($this->nginxPath(), user()); + + return strArrayReplace( + [ + 'VALET_HOME_PATH' => VALET_HOME_PATH, + 'VALET_SERVER_PATH' => VALET_SERVER_PATH, + 'VALET_STATIC_PREFIX' => VALET_STATIC_PREFIX, + 'VALET_SITE' => $url, + 'VALET_HTTP_PORT' => $this->config->get('port', 80), + 'VALET_HTTPS_PORT' => $this->config->get('https_port', 443), + ], + $stub + ); + } + + /** + * Prepare Nginx Conf based on existing config file. + */ + private function prepareConf(string $url, bool $secure = false): ?string + { + if (!$this->files->exists($this->nginxPath($url))) { + return null; + } + + $existingConf = $this->files->get($this->nginxPath($url)); + + preg_match('/# valet stub: (?secure)?\.?(?.*?).valet.conf/m', $existingConf, $stubDetail); + + if (empty($stubDetail['stub'])) { + return null; + } + + if ($stubDetail['stub'] === 'proxy') { + // Find proxy_pass from existingConf. + $proxyPass = $this->getProxyPass($url, $existingConf); + if (!$proxyPass) { + return null; + } + $stub = $secure ? + VALET_ROOT_PATH . '/cli/stubs/secure.proxy.valet.conf' : + VALET_ROOT_PATH . '/cli/stubs/proxy.valet.conf'; + $stub = $this->files->get($stub); + + return strArrayReplace([ + 'VALET_PROXY_HOST' => $proxyPass, + ], $stub); + } + + if ($stubDetail['stub'] === 'isolated') { + $phpVersion = $this->isolatedPhpVersion($existingConf); + // empty($stubDetail['tls']) || We can use this statement if needed. + $stub = $secure ? + VALET_ROOT_PATH . '/cli/stubs/secure.isolated.valet.conf' : + VALET_ROOT_PATH . '/cli/stubs/isolated.valet.conf'; + $stub = $this->files->get($stub); + // Isolate specific variables + return strArrayReplace([ + 'VALET_FPM_SOCKET_FILE' => PhpFpmFacade::fpmSocketFile($phpVersion), + 'VALET_ISOLATED_PHP_VERSION' => $phpVersion, + ], $stub); + } + + return null; + } + + /** + * Extract Proxy pass of exising nginx config. + */ + private function getProxyPass(string $url, string $siteConf = null): ?string + { + if ($siteConf === null && !$this->files->exists($this->nginxPath($url))) { + return null; + } + + $siteConf = $siteConf ?: $this->files->get($this->nginxPath($url)); + preg_match('/proxy_pass (?.*?);/m', $siteConf, $matches); + + return $matches['host'] ?? null; + } + + /** + * Build the SSL config for the given URL. + */ + private function generateCertificateConf(string $path, string $url): void + { + $config = str_replace('VALET_DOMAIN', $url, $this->files->get(VALET_ROOT_PATH . '/cli/stubs/openssl.conf')); + $this->files->putAsUser($path, $config); + } + + /** + * Return https port suffix. + */ + private function httpsSuffix(): string + { + $port = $this->config->get('https_port', 443); + + return ($port == 443) ? '' : ':' . $port; + } + + /** + * Extract PHP version of exising nginx config. + */ + private function isolatedPhpVersion(string $siteConf): string + { + if (str_contains($siteConf, '# ' . ISOLATED_PHP_VERSION)) { + preg_match('/^# ISOLATED_PHP_VERSION=(.*?)\n/m', $siteConf, $version); + return $version[1]; + } + + return ''; + } +} diff --git a/cli/Valet/Traits/Paths.php b/cli/Valet/Traits/Paths.php new file mode 100644 index 0000000..03906ec --- /dev/null +++ b/cli/Valet/Traits/Paths.php @@ -0,0 +1,38 @@ +cli->run('ln -snf '.realpath(__DIR__.'/../../valet').' '.$this->valetBin); + $this->cli->run('ln -snf ' . realpath(VALET_ROOT_PATH . '/valet') . ' ' . $this->valetBin); + } + + /** + * Symlink the Valet Bash script into the user's local bin. + */ + public function symlinkPhpToUsersBin(): void + { + $fallbackBin = '/usr/bin/php'; + $phpBin = $_SERVER['_'] ?? $fallbackBin; + $phpBin = $this->files->realpath($phpBin); + if ($phpBin !== VALET_ROOT_PATH . 'php') { + ConfigurationFacade::set('fallback_binary', $phpBin); + } else { + ConfigurationFacade::set('fallback_binary', $fallbackBin); + } + + $this->cli->run('ln -snf ' . realpath(VALET_ROOT_PATH . '/php') . ' ' . $this->phpBin); } /** @@ -68,24 +70,25 @@ public function symlinkToUsersBin(): void public function uninstall(): void { $this->files->unlink($this->valetBin); - $this->files->unlink($this->sudoers); + $this->files->unlink($this->phpBin); } /** * Get the paths to all the Valet extensions. + * @return array */ public function extensions(): array { - if (!$this->files->isDir(VALET_HOME_PATH.'/Extensions')) { + if (!$this->files->isDir(VALET_HOME_PATH . '/Extensions')) { return []; } - return collect($this->files->scandir(VALET_HOME_PATH.'/Extensions')) + return collect($this->files->scandir(VALET_HOME_PATH . '/Extensions')) ->reject(function ($file) { - return is_dir($file); + return $this->files->isDir($file); }) ->map(function ($file) { - return VALET_HOME_PATH.'/Extensions/'.$file; + return VALET_HOME_PATH . '/Extensions/' . $file; }) ->values()->all(); } @@ -96,7 +99,8 @@ public function extensions(): array */ public function onLatestVersion(string $currentVersion): bool { - $response = Request::get($this->github)->send(); + $response = RequestFacade::get($this->github)->send(); + $currentVersion = str_replace('v', '', $currentVersion); $latestVersion = isset($response->body->tag_name) ? trim($response->body->tag_name) : 'v1.0.0'; $latestVersion = str_replace('v', '', $latestVersion); @@ -111,7 +115,7 @@ public function onLatestVersion(string $currentVersion): bool */ public function getLatestVersion() { - $response = Request::get($this->github)->send(); + $response = RequestFacade::get($this->github)->send(); return isset($response->body->tag_name) ? trim($response->body->tag_name) : false; } @@ -125,6 +129,93 @@ public function environmentSetup(): void $this->packageManagerSetup(); } + /** + * Migrate ~/.valet directory to ~/.config/valet directory + */ + public function migrateConfig(): void + { + $newHomePath = VALET_HOME_PATH; + $oldHomePath = OLD_VALET_HOME_PATH; + + // Check if new config home already exists, then skip the process + if ($this->files->isDir($newHomePath)) { + return; + } + + // Fetch FPM running process + $fpmVersions = $this->getRunningFpmVersions($oldHomePath); + + // Stop running fpm services + if (count($fpmVersions)) { + foreach ($fpmVersions as $fpmVersion) { + PhpFpmFacade::stop($fpmVersion); + } + } + + // Copy directory + $this->files->copyDirectory($oldHomePath, $newHomePath); + + // Replace $oldHomePath to $newHomePath in Certificates, Valet.conf file + $this->updateNginxConfFiles(); + + // Update phpfpm's socket file path in config + PhpFpmFacade::updateHomePath($oldHomePath, $newHomePath); + + // Start fpm services again + if (count($fpmVersions)) { + foreach ($fpmVersions as $fpmVersion) { + PhpFpmFacade::restart($fpmVersion); + } + } else { + PhpFpmFacade::restart(); + } + + NginxFacade::restart(); + + Writer::info('Valet home directory is migrated successfully! Please re-run your command'); + Writer::info(\sprintf('New home directory: %s', $newHomePath)); + Writer::info(\sprintf('Please remove %s directory manually', $oldHomePath)); + exit; + } + + private function updateNginxConfFiles(): void + { + $newHomePath = VALET_HOME_PATH; + $oldHomePath = OLD_VALET_HOME_PATH; + $nginxPath = $newHomePath . '/Nginx'; + + $siteConfigs = $this->files->scandir($nginxPath); + foreach ($siteConfigs as $siteConfig) { + $filePath = \sprintf('%s/%s', $nginxPath, $siteConfig); + $content = $this->files->get($filePath); + $content = str_replace($oldHomePath, $newHomePath, $content); + $this->files->put($filePath, $content); + } + + $sitesAvailableConf = $this->files->get(Nginx::SITES_AVAILABLE_CONF); + $sitesAvailableConf = str_replace($oldHomePath, $newHomePath, $sitesAvailableConf); + $this->files->put(Nginx::SITES_AVAILABLE_CONF, $sitesAvailableConf); + + $nginxConfig = $this->files->get(Nginx::NGINX_CONF); + $nginxConfig = str_replace($oldHomePath, $newHomePath, $nginxConfig); + $this->files->put(Nginx::SITES_AVAILABLE_CONF, $nginxConfig); + } + + private function getRunningFpmVersions(string $homePath): array + { + $runningVersions = []; + + $files = $this->files->scandir($homePath); + foreach ($files as $file) { + preg_match('/valet(\d)(\d)\.sock/', $file, $matches); + if (count($matches) >= 2) { + $runningVersions[] = \sprintf('%d.%d', $matches[1], $matches[2]); + } + } + + return $runningVersions; + } + /** * Configure package manager. */ diff --git a/cli/Valet/ValetRedis.php b/cli/Valet/ValetRedis.php index 3c42d54..42f6104 100644 --- a/cli/Valet/ValetRedis.php +++ b/cli/Valet/ValetRedis.php @@ -26,10 +26,6 @@ class ValetRedis /** * Create a new PHP FPM class instance. * - * @param PackageManager $pm - * @param ServiceManager $sm - * @param CommandLine $cli - * * @return void */ public function __construct(PackageManager $pm, ServiceManager $sm, CommandLine $cli) @@ -44,8 +40,9 @@ public function __construct(PackageManager $pm, ServiceManager $sm, CommandLine */ public function install(): void { - $this->pm->ensureInstalled($this->pm->redisPackageName); - $this->sm->enable($this->pm->redisPackageName); + $packageName = $this->pm->packageName('redis'); + $this->pm->ensureInstalled($packageName); + $this->sm->enable($packageName); } /** @@ -53,7 +50,7 @@ public function install(): void */ public function installed(): bool { - return $this->pm->installed($this->pm->redisPackageName); + return $this->pm->installed($this->pm->packageName('redis')); } /** @@ -61,7 +58,7 @@ public function installed(): bool */ public function restart(): void { - $this->sm->restart($this->pm->redisPackageName); + $this->sm->restart($this->pm->packageName('redis')); } /** @@ -69,7 +66,7 @@ public function restart(): void */ public function stop(): void { - $this->sm->stop($this->pm->redisPackageName); + $this->sm->stop($this->pm->packageName('redis')); } /** diff --git a/cli/app.php b/cli/app.php index dbd8f3d..717572b 100644 --- a/cli/app.php +++ b/cli/app.php @@ -1,15 +1,13 @@ command('install [--ignore-selinux] [--mariadb]', function ($ignoreSELinux, $mariaDB) { - passthru(dirname(__FILE__).'/scripts/update.sh'); // Clean up cruft +$app->command('install [--ignore-selinux]', function ($ignoreSELinux) { + Writer::info('Installing valet services'); + + passthru(dirname(__FILE__) . '/scripts/update.sh'); // Clean up cruft Requirements::setIgnoreSELinux($ignoreSELinux)->check(); Configuration::install(); Nginx::install(); PhpFpm::install(); - DnsMasq::install(Configuration::read()['domain']); - Valet::symlinkToUsersBin(); + DnsMasq::install(Configuration::get('domain')); Mailpit::install(); ValetRedis::install(); Nginx::restart(); - Mysql::install($mariaDB); + Mysql::install(); + Ngrok::install(); + Valet::symlinkToUsersBin(); - output(PHP_EOL.'Valet installed successfully!'); + Writer::info('Valet installed successfully!'); + + $canLinkValetPhp = Writer::confirm('Do you want to link valet\'s php binary?', true); + if ($canLinkValetPhp) { + Valet::symlinkPhpToUsersBin(); + } + + if ($canLinkValetPhp) { + Writer::info('Valet executable php helper is linked to /usr/local/bin/php.'); + } })->descriptions('Install the Valet services', [ '--ignore-selinux' => 'Skip SELinux checks', ]); - /** * Most commands are available only if valet is installed. */ @@ -92,7 +102,7 @@ Mailpit::restart(); Mysql::restart(); ValetRedis::restart(); - info('Valet services have been started.'); + Writer::info('Valet services have been started.'); return; } @@ -126,7 +136,7 @@ } } - info('Specified Valet services have been started.'); + Writer::info('Specified Valet services have been started.'); })->descriptions('Start the Valet services'); /** @@ -140,7 +150,7 @@ Mailpit::restart(); Mysql::restart(); ValetRedis::restart(); - info('Valet services have been restarted.'); + Writer::info('Valet services have been restarted.'); return; } @@ -175,7 +185,7 @@ } } - info('Specified Valet services have been restarted.'); + Writer::info('Specified Valet services have been restarted.'); })->descriptions('Restart the Valet services'); /** @@ -188,7 +198,7 @@ Mailpit::stop(); Mysql::stop(); ValetRedis::stop(); - info('Valet services have been stopped.'); + Writer::info('Valet services have been stopped.'); return; } @@ -219,7 +229,7 @@ } } - info('Specified Valet services have been stopped.'); + Writer::info('Specified Valet services have been stopped.'); })->descriptions('Stop the Valet services'); /** @@ -233,7 +243,7 @@ Configuration::uninstall(); Valet::uninstall(); - info('Valet has been uninstalled.'); + Writer::info('Valet has been uninstalled.'); })->descriptions('Uninstall the Valet services'); /** @@ -247,54 +257,54 @@ /** * Determine if this is the latest release of Valet. */ - $app->command('is-latest', function () { - if (Valet::onLatestVersion(VALET_VERSION)) { - output('YES'); + $app->command('is-latest', function () use ($version) { + if (Valet::onLatestVersion($version)) { + Writer::info('YES'); } else { - output('NO'); + Writer::info('NO'); } })->descriptions('Determine if this is the latest version of Valet'); /** * Determine if this is the latest release of Valet. */ - $app->command('update', function () { - $script = dirname(__FILE__).'/scripts/update.sh'; + $app->command('update', function () use ($version) { + $script = dirname(__FILE__) . '/scripts/update.sh'; - if (Valet::onLatestVersion(VALET_VERSION)) { - info('You have the latest version of Valet Linux'); + if (Valet::onLatestVersion($version)) { + Writer::info('You have the latest version of Valet Linux+'); passthru($script); } else { - warning('There is a new release of Valet Linux'); - warning('Updating now...'); + Writer::warn('There is a new release of Valet Linux+'); + Writer::warn('Updating now...'); $latestVersion = Valet::getLatestVersion(); if ($latestVersion) { - passthru($script." update $latestVersion"); + passthru($script . " update $latestVersion"); } else { - passthru($script.' update'); + passthru($script . ' update'); } } - })->descriptions('Update Valet Linux and clean up cruft'); + })->descriptions('Update Valet Linux+ and clean up cruft'); /** * Get or set the domain currently being used by Valet. */ $app->command('domain [domain]', function ($domain = null) { if ($domain === null) { - info(Configuration::read()['domain']); + Writer::info(sprintf('Your current Valet domain is [%s].', Configuration::get('domain'))); return; } DnsMasq::updateDomain($domain = trim($domain, '.')); - $oldDomain = Configuration::read()['domain']; + $oldDomain = Configuration::get('domain'); - Configuration::updateKey('domain', $domain); - Site::resecureForNewDomain($oldDomain, $domain); + Configuration::set('domain', $domain); + SiteSecure::reSecureForNewDomain($oldDomain, $domain); PhpFpm::restart(); Nginx::restart(); - info('Your Valet domain has been updated to ['.$domain.'].'); + Writer::info('Your Valet domain has been updated to [' . $domain . '].'); })->descriptions('Get or set the domain used for Valet sites'); /** @@ -302,8 +312,8 @@ */ $app->command('port [port] [--https]', function ($port, $https) { if ($port === null) { - info('Current Nginx port (HTTP): '.Configuration::get('port', 80)); - info('Current Nginx port (HTTPS): '.Configuration::get('https_port', 443)); + Writer::info('Current Nginx port (HTTP): ' . Configuration::get('port', 80)); + Writer::info('Current Nginx port (HTTPS): ' . Configuration::get('https_port', 443)); return; } @@ -311,19 +321,19 @@ $port = trim($port); if ($https) { - Configuration::updateKey('https_port', $port); + Configuration::set('https_port', $port); } else { Nginx::updatePort($port); - Configuration::updateKey('port', $port); + Configuration::set('port', $port); } - Site::regenerateSecuredSitesConfig(); + SiteSecure::regenerateSecuredSitesConfig(); Nginx::restart(); PhpFpm::restart(); $protocol = $https ? 'HTTPS' : 'HTTP'; - info("Your Nginx $protocol port has been updated to [$port]."); + Writer::info("Your Nginx $protocol port has been updated to [$port]."); })->descriptions('Get or set the port number used for Valet sites'); /** @@ -333,9 +343,9 @@ $driver = ValetDriver::assign(getcwd(), basename(getcwd()), '/'); if ($driver) { - info('This site is served by ['.get_class($driver).'].'); + Writer::info('This site is served by [' . get_class($driver) . '].'); } else { - warning('Valet could not determine which driver to use for this site.'); + Writer::warn('Valet could not determine which driver to use for this site.'); } })->descriptions('Determine which Valet driver serves the current working directory'); @@ -343,21 +353,26 @@ * Add the current working directory to paths configuration. */ $app->command('park [path]', function ($path = null) { - Configuration::addPath($path ?: getcwd()); + $path = $path ?: getcwd(); + Configuration::addPath($path); - info(($path === null ? 'This' : "The [$path]")." directory has been added to Valet's paths."); + Writer::info("The [$path] directory has been added to Valet's paths."); })->descriptions('Register the current working (or specified) directory with Valet'); /** * Display all the registered paths. */ $app->command('paths', function () { - $paths = Configuration::read()['paths']; + $paths = Configuration::get('paths'); if (count($paths) > 0) { - info(json_encode($paths, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $paths = array_map(function ($path) { + return [$path]; + }, $paths); + + Writer::table(['Path'], $paths); } else { - warning('No paths have been registered.'); + Writer::warn('No paths have been registered.'); } })->descriptions('Get all of the paths registered with Valet'); @@ -365,17 +380,43 @@ * Remove the current working directory from paths configuration. */ $app->command('forget [path]', function ($path = null) { - Configuration::removePath($path ?: getcwd()); + $path = $path ?: getcwd(); + Configuration::removePath($path); - info(($path === null ? 'This' : "The [$path]")." directory has been removed from Valet's paths."); + Writer::info("The [$path] directory has been removed from Valet's paths."); })->descriptions('Remove the current working (or specified) directory from Valet\'s list of paths'); /** * Create Nginx proxy config for the specified domain. */ - $app->command('proxy domain host [--secure]', function ($domain, $host, $secure) { - Site::proxyCreate($domain, $host, $secure); + $app->command('proxy [domain] [host] [--secure]', function ($domain, $host, $secure) { + if ($domain === null) { + Writer::error('Please provide domain'); + return; + } + + if ($host === null) { + Writer::error('Please provide host'); + return; + } + + if (!preg_match('~^https?://.*$~', $host)) { + Writer::error(sprintf('"%s" is not a valid URL', $host)); + return; + } + + $tld = Configuration::get('domain'); + + if (!str_ends_with($domain, '.' . $tld)) { + $domain .= '.' . $tld; + } + + SiteProxy::proxyCreate($domain, $host, $secure); Nginx::restart(); + + $protocol = $secure ? 'https' : 'http'; + + Writer::info('Valet will now proxy [' . $protocol . '://' . $domain . '] traffic to [' . $host . '].'); })->descriptions('Create an Nginx proxy site for the specified host. Useful for docker, node etc.', [ '--secure' => 'Create a proxy with a trusted TLS certificate', ]); @@ -383,136 +424,139 @@ /** * Delete Nginx proxy config. */ - $app->command('unproxy domain', function ($domain) { - Site::proxyDelete($domain); + $app->command('unproxy [domain]', function ($domain) { + if ($domain === null) { + Writer::error('Please provide domain'); + return; + } + + $tld = Configuration::get('domain'); + if (!str_ends_with($domain, '.' . $tld)) { + $domain .= '.' . $tld; + } + + SiteSecure::unsecure($domain); Nginx::restart(); + + Writer::info('Valet will no longer proxy [' . $domain . '].'); })->descriptions('Delete an Nginx proxy config.'); /** * Display all the sites that are proxies. */ $app->command('proxies', function () { - $proxies = Site::proxies(); + $proxies = SiteProxy::proxies(); - table(['URL', 'SSL', 'Host'], $proxies->all()); + Writer::table(['URL', 'SSL', 'Host'], $proxies->all()); })->descriptions('Display all of the proxy sites'); /** * Register a symbolic link with Valet. */ $app->command('link [name]', function ($name) { - $linkPath = Site::link(getcwd(), $name = $name ?: basename(getcwd())); + $name = $name ?: basename(getcwd()); + $linkPath = SiteLink::link(getcwd(), $name); - info('A ['.$name.'] symbolic link has been created in ['.$linkPath.'].'); + Writer::info('A [' . $name . '] symbolic link has been created in [' . $linkPath . '].'); })->descriptions('Link the current working directory to Valet'); /** * Unlink a link from the Valet links directory. */ $app->command('unlink [name]', function ($name) { - Site::unlink($name = $name ?: basename(getcwd())); + $name = $name ?: basename(getcwd()); + SiteLink::unlink($name); - info('The ['.$name.'] symbolic link has been removed.'); + Writer::info('The [' . $name . '] symbolic link has been removed.'); })->descriptions('Remove the specified Valet link'); /** * Display all the registered symbolic links. */ $app->command('links', function () { - $links = Site::links(); + $links = SiteLink::links(); - table(['Site', 'SSL', 'URL', 'Path', 'PHP Version'], $links->all()); + Writer::table(['URL', 'SSL', 'Path'], $links->all()); })->descriptions('Display all of the registered Valet links'); /** * Secure the given domain with a trusted TLS certificate. */ $app->command('secure [domain]', function ($domain = null) { - $url = ($domain ?: Site::host(getcwd())).'.'.Configuration::read()['domain']; + $url = ($domain ?: basename(getcwd())); + $url = Configuration::parseDomain($url); - Site::secure($url); + SiteSecure::secure($url); Nginx::restart(); - info('The ['.$url.'] site has been secured with a fresh TLS certificate.'); + Writer::info('The [' . $url . '] site has been secured with a fresh TLS certificate.'); })->descriptions('Secure the given domain with a trusted TLS certificate'); /** * Stop serving the given domain over HTTPS and remove the trusted TLS certificate. */ $app->command('unsecure [domain]', function ($domain = null) { - $url = ($domain ?: Site::host(getcwd())).'.'.Configuration::read()['domain']; + $url = ($domain ?: basename(getcwd())); + $url = Configuration::parseDomain($url); - Site::unsecure($url, true); + SiteSecure::unsecure($url, true); Nginx::restart(); - info('The ['.$url.'] site will now serve traffic over HTTP.'); + Writer::info('The [' . $url . '] site will now serve traffic over HTTP.'); })->descriptions('Stop serving the given domain over HTTPS and remove the trusted TLS certificate'); /** * Determine if the site is secured or not. */ $app->command('secured [site]', function ($site) { - if (Site::secured()->contains($site)) { - info("$site is secured."); - return 1; - } + $site = $site ?: basename(getcwd()); + $site = Configuration::parseDomain($site); - info("$site is not secured."); - return 0; - })->descriptions('Determine if the site is secured or not'); - - /** - * Register a subdomain link. - */ - $app->command('subdomain:create [name] [--secure]', function ($name, $secure) { - $name = $name ?: 'www'; - Site::link(getcwd(), $name.'.'.basename(getcwd())); - - if ($secure) { - $this->runCommand('secure '.$name); + if (SiteSecure::secured()->contains($site)) { + Writer::info("$site is secured."); + return; } - $domain = Configuration::read()['domain']; - - info('Subdomain '.$name.'.'.basename(getcwd()).'.'.$domain.' created'); - })->descriptions('Create a subdomains'); - /** - * Unregister a subdomain link. - */ - $app->command('subdomain:remove [name]', function ($name) { - $name = $name ?: 'www'; - Site::unlink($name.'.'.basename(getcwd())); - $domain = Configuration::read()['domain']; - info('Subdomain '.$name.'.'.basename(getcwd()).'.'.$domain.' removed'); - })->descriptions('Remove a subdomains'); - - /** - * List subdomains. - */ - $app->command('subdomain:list', function () { - $links = Site::links(); - table(['Site', 'SSL', 'URL', 'Path'], $links->all()); - })->descriptions('List all subdomains'); + Writer::info("$site is not secured."); + })->descriptions('Determine if the site is secured or not'); /** * Change the PHP version to the desired one. */ - $app->command('use [preferredVersion] [--update-cli] [--ignore-ext] [--ignore-update]', function ( + $app->command('use [preferredVersion] [--update-cli] [--ignore-ext]', function ( $preferredVersion = null, $updateCli = null, - $ignoreExt = null, - $ignoreUpdate = null + $ignoreExt = null ) { - info('Changing php version...'); - PhpFpm::switchVersion($preferredVersion, $updateCli, $ignoreExt, $ignoreUpdate); - info('php version successfully changed!'); + $preferredVersion = PhpFpm::normalizePhpVersion($preferredVersion); + $isValid = PhpFpm::validateVersion($preferredVersion); + if (!$isValid) { + Writer::error( + sprintf( + "Invalid version [%s] used. Supported versions are: %s", + $preferredVersion, + implode(', ', \Valet\PhpFpm::SUPPORTED_PHP_VERSIONS) + ) + ); + Writer::info( + sprintf( + 'You can still use any version from [%s] list using `valet isolate` command', + implode(', ', \Valet\PhpFpm::ISOLATION_SUPPORTED_PHP_VERSIONS) + ) + ); + return; + } + + PhpFpm::switchVersion($preferredVersion, $updateCli, $ignoreExt); + Writer::info(sprintf('PHP version successfully changed to [%s]', $preferredVersion)); })->descriptions( - 'Set the PHP version to use, enter "default" or leave empty to use version: ' - .PhpFpm::getCurrentVersion(), + sprintf( + 'Set the PHP version to use, enter "default" or leave empty to use version: %s', + PHP_VERSION + ), [ '--update-cli' => 'Updates CLI version as well', '--ignore-ext' => 'Installs extension with selected php version', - '--ignore-update' => 'Ignores self package update. Works with --update-cli flag.', ] ); @@ -520,63 +564,73 @@ * List MySQL Database. */ $app->command('db:list', function () { - Mysql::listDatabases(); + $databases = Mysql::getDatabases(); + + Writer::table(['Database'], $databases); })->descriptions('List all available database in MySQL/MariaDB'); /** * Create new database in MySQL. */ $app->command('db:create [databaseName]', function ($databaseName) { - Mysql::createDatabase($databaseName); + $databaseName = $databaseName ?: basename((string)getcwd()); + + $isCreated = Mysql::createDatabase($databaseName); + if ($isCreated) { + Writer::info(sprintf('Database [%s] created successfully', $databaseName)); + } })->descriptions('Create new database in MySQL/MariaDB'); /** * Drop database in MySQL. */ - $app->command('db:drop [databaseName] [-y|--yes]', function (Input $input, $output, $databaseName) { - $helper = $this->getHelperSet()->get('question'); - $defaults = $input->getOptions(); - if (!$defaults['yes']) { - $question = new ConfirmationQuestion('Are you sure you want to delete the database? [y/N] ', false); - if (!$helper->ask($input, $output, $question)) { - warning('Aborted'); + $app->command('db:drop [databaseName] [-y|--yes]', function ($databaseName, $yes) { + $databaseName = $databaseName ?: basename((string)getcwd()); + + if (!$yes) { + $confirm = Writer::confirm(sprintf('Are you sure you want to delete [%s] database?', $databaseName)); + if (!$confirm) { + Writer::warn('Aborted'); return; } } - Mysql::dropDatabase($databaseName); + $isDropped = Mysql::dropDatabase($databaseName); + if ($isDropped) { + Writer::info(sprintf('Database [%s] dropped successfully', $databaseName)); + } })->descriptions('Drop given database from MySQL/MariaDB'); /** * Reset database in MySQL. */ - $app->command('db:reset [databaseName] [-y|--yes]', function (Input $input, $output, $databaseName) { - $helper = $this->getHelperSet()->get('question'); - $defaults = $input->getOptions(); - if (!$defaults['yes']) { - $question = new ConfirmationQuestion('Are you sure you want to reset the database? [y/N] ', false); - if (!$helper->ask($input, $output, $question)) { - warning('Aborted'); + $app->command('db:reset [databaseName] [-y|--yes]', function ($databaseName, $yes) { + $databaseName = $databaseName ?: basename((string)getcwd()); + + if (!$yes) { + $confirm = Writer::confirm(sprintf('Are you sure you want to reset [%s] database?', $databaseName)); + if (!$confirm) { + Writer::warn('Aborted'); return; } } $dropDB = Mysql::dropDatabase($databaseName); if (!$dropDB) { - warning('Error resetting database'); + Writer::warn('Error resetting database'); return; } - $databaseName = Mysql::createDatabase($databaseName); + $isCreated = Mysql::createDatabase($databaseName); - if (!$databaseName) { - warning('Error resetting database'); + if (!$isCreated) { + Writer::warn('Error resetting database'); return; } - info("Database [$databaseName] reset successfully"); + Writer::info(sprintf('Database [%s] reset successfully', $databaseName)); })->descriptions('Clear all tables for given database in MySQL/MariaDB'); /** @@ -584,44 +638,37 @@ * * @throws Exception */ - $app->command('db:import [databaseName] [dumpFile]', function (Input $input, $output, $databaseName, $dumpFile) { - $helper = $this->getHelperSet()->get('question'); - info('Importing database...'); + $app->command('db:import [databaseName] [dumpFile]', function ($databaseName, $dumpFile) { if (!$databaseName) { - throw new DatabaseException('Please provide database name'); + Writer::error('Please provide database name'); + return; } if (!$dumpFile) { - throw new DatabaseException('Please provide a dump file'); - } - if (!file_exists($dumpFile)) { - throw new DatabaseException("Unable to locate [$dumpFile]"); + Writer::error('Please provide a dump file path'); + return; } - $isExistsDatabase = false; - // check if database already exists. - if (Mysql::isDatabaseExists($databaseName)) { - $question = new ConfirmationQuestion( - 'Database already exists are you sure you want to continue? [y/N] ', - false - ); - if (!$helper->ask($input, $output, $question)) { - warning('Aborted'); - return; - } - $isExistsDatabase = true; + if (!Filesystem::exists($dumpFile)) { + Writer::error(sprintf('Unable to locate [%s]', $dumpFile)); + return; } + Writer::info('Importing database...'); - Mysql::importDatabase($dumpFile, $databaseName, $isExistsDatabase); + Mysql::importDatabase($dumpFile, $databaseName); + + Writer::info(sprintf('Database [%s] imported successfully', $databaseName)); })->descriptions('Import dump file for selected database in MySQL/MariaDB'); /** * Export database in MySQL. */ - $app->command('db:export [databaseName] [--sql]', function (Input $input, $databaseName) { - info('Exporting database...'); - $defaults = $input->getOptions(); - $data = Mysql::exportDatabase($databaseName, $defaults['sql']); - info("Database [{$data['database']}] exported into file {$data['filename']}"); + $app->command('db:export [databaseName] [--sql]', function ($databaseName, $sql) { + Writer::info('Exporting database...'); + $databaseName = $databaseName ?: basename((string)getcwd()); + + $data = Mysql::exportDatabase($databaseName, $sql); + + Writer::info(sprintf("Database [%s] exported into file %s", $data['database'], $data['filename'])); })->descriptions('Export selected MySQL/MariaDB database'); /** @@ -668,17 +715,26 @@ */ $app->command('isolate [phpVersion] [--site=] [--secure]', function ($phpVersion, $site, $secure) { if (!$site) { - $site = basename(getcwd()); + $site = basename((string)getcwd()); } - if (is_null($phpVersion) && $phpVersion = Site::phpRcVersion($site)) { - info("Found '$site/.valetphprc' specifying version: $phpVersion"); + if ($phpVersion === null && $phpVersion = Site::phpRcVersion($site)) { + Writer::info("Found '$site/.valetphprc' specifying version: $phpVersion"); } - PhpFpm::isolateDirectory($site, $phpVersion, $secure); + if ($phpVersion === null) { + Writer::warn('Please select version to isolate'); + return; + } + + $isSuccess = SiteIsolate::isolateDirectory($site, $phpVersion, $secure); + + if ($isSuccess) { + Writer::info(sprintf('The site [%s] is now using %s.', $site, $phpVersion)); + } })->descriptions('Change the version of PHP used by Valet to serve the current working directory', [ 'phpVersion' => 'The PHP version you want to use; e.g php@8.1', - '--site' => 'Specify the site to isolate (e.g. if the site isn\'t linked as its directory name)', + '--site' => 'Specify the site to isolate (e.g. if the site isn\'t linked as its directory name)', '--secure' => 'Create a isolated site with a trusted TLS certificate', ]); @@ -687,10 +743,12 @@ */ $app->command('unisolate [--site=]', function ($site = null) { if (!$site) { - $site = basename(getcwd()); + $site = basename((string)getcwd()); } - PhpFpm::unIsolateDirectory($site); + SiteIsolate::unIsolateDirectory($site); + + Writer::info(sprintf('The site [%s] is now using the default PHP version.', $site)); })->descriptions('Stop customizing the version of PHP used by Valet to serve the current working directory', [ '--site' => 'Specify the site to un-isolate (e.g. if the site isn\'t linked as its directory name)', ]); @@ -699,24 +757,24 @@ * List isolated sites. */ $app->command('isolated', function () { - $sites = PhpFpm::isolatedDirectories(); + $sites = SiteIsolate::isolatedDirectories(); - table(['Path', 'PHP Version'], $sites->all()); + Writer::table(['URL', 'SSL', 'PHP Version'], $sites->all()); })->descriptions('List all sites using isolated versions of PHP.'); /** * Get the PHP executable path for a site. */ $app->command('which-php [site]', function ($site) { - $phpVersion = Site::customPhpVersion( - Site::host($site ?: getcwd()).'.'.Configuration::read()['domain'] - ); + $site = basename($site ?: (string)getcwd()); + $domain = Configuration::parseDomain($site); + $phpVersion = SiteIsolate::isolatedPhpVersion($domain); if (!$phpVersion) { $phpVersion = Site::phpRcVersion($site ?: basename(getcwd())); } - info(PhpFpm::getPhpExecutablePath($phpVersion)); + echo PhpFpm::getPhpExecutablePath($phpVersion); })->descriptions('Get the PHP executable path for a given site', [ 'site' => 'The site to get the PHP executable path for', ]); @@ -725,7 +783,7 @@ * Proxy commands through to an isolated site's version of PHP. */ $app->command('php [--site=] [command]', function () { - warning( + Writer::warn( 'It looks like you are running `cli/valet.php` directly; please use the `valet` script in the project root instead.' ); @@ -738,7 +796,7 @@ * Proxy commands through to an isolated site's version of Composer. */ $app->command('composer [--site=] [command]', function () { - warning('It looks like you are running `cli/valet.php` directly; + Writer::warn('It looks like you are running `cli/valet.php` directly; please use the `valet` script in the project root instead.'); })->descriptions("Proxy Composer commands with isolated site's PHP executable", [ 'command' => "Composer command to run with isolated site's PHP executable", @@ -749,16 +807,20 @@ * Open the current directory in the browser. */ $app->command('open [domain]', function ($domain = null) { - $url = 'http://'.($domain ?: Site::host(getcwd())).'.'.Configuration::read()['domain'].'/'; + $url = sprintf( + 'http://%s.%s/', + $domain ?: basename(getcwd()), + Configuration::get('domain') + ); - passthru('xdg-open '.escapeshellarg($url)); + passthru('xdg-open ' . escapeshellarg($url)); })->descriptions('Open the site for the current (or specified) directory in your browser'); /** * Generate a publicly accessible URL for your project. */ $app->command('share', function () { - warning( + Writer::warn( 'It looks like you are running `cli/valet.php` directly, please use the `valet` script in the project root instead.' ); @@ -768,7 +830,7 @@ * Echo the currently tunneled URL. */ $app->command('fetch-share-url', function () { - output(Ngrok::currentTunnelUrl()); + echo Ngrok::currentTunnelUrl(); })->descriptions('Get the URL to the current Ngrok tunnel'); /** @@ -776,9 +838,13 @@ */ $app->command('ngrok-auth [authtoken]', function ($authtoken) { if (!$authtoken) { - throw new NgrokException('Missing arguments to authenticate ngrok. Use: "valet ngrok-auth [authtoken]"'); + Writer::error('Please provide ngrok auth token'); + return; } + Ngrok::setAuthToken($authtoken); + + Writer::info('Ngrok authentication token set.'); })->descriptions('Set authentication token for ngrok'); } diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php index 0c0782b..c6a42c1 100644 --- a/cli/includes/helpers.php +++ b/cli/includes/helpers.php @@ -5,9 +5,6 @@ use Exception; use Illuminate\Container\Container; use Illuminate\Contracts\Container\BindingResolutionException; -use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Console\Output\OutputInterface; /** * Define constants. @@ -15,18 +12,19 @@ if (! defined('VALET_HOME_PATH')) { if (testing()) { define('VALET_HOME_PATH', __DIR__.'/../../tests/config/valet'); + define('OLD_VALET_HOME_PATH', __DIR__.'/../../tests/old-config/valet'); + } else { define('VALET_HOME_PATH', $_SERVER['HOME'].'/.config/valet'); + define('OLD_VALET_HOME_PATH', $_SERVER['HOME'].'/.valet'); } } -define('OLD_VALET_HOME_PATH', $_SERVER['HOME'].'/.valet'); if (! defined('VALET_STATIC_PREFIX')) { define('VALET_STATIC_PREFIX', '41c270e4-5535-4daa-b23e-c269744c2f45'); } define('VALET_LOOPBACK', '127.0.0.1'); -define('VALET_ROOT_PATH', realpath(__DIR__.'/../../')); //TODO: Check if it is in user -define('VALET_BIN_PATH', realpath(__DIR__.'/../../bin/')); //TODO: Check if it is in user +define('VALET_ROOT_PATH', realpath(__DIR__.'/../../')); define('VALET_SERVER_PATH', realpath(__DIR__.'/../../server.php')); define('ISOLATED_PHP_VERSION', 'ISOLATED_PHP_VERSION'); @@ -38,66 +36,6 @@ function testing(): bool return strpos($_SERVER['SCRIPT_NAME'], 'phpunit') !== false; } -/** - * Set or get a global console writer. - * @throws BindingResolutionException - */ -function writer(?OutputInterface $writer = null): ?OutputInterface -{ - $container = Container::getInstance(); - - if (! $writer) { - if (! $container->bound('writer')) { - $container->instance('writer', new ConsoleOutput()); - } - - return $container->make('writer'); - } - - $container->instance('writer', $writer); - - return null; -} -/** - * Output the given text to the console. - */ -function info(string $output): void -{ - output(''.$output.''); -} - -/** - * Output the given text to the console. - */ -function warning(string $output): void -{ - output(''.$output.''); -} - -/** - * Output a table to the console. - */ -function table(array $headers = [], array $rows = []): void -{ - $table = new Table(new ConsoleOutput()); - - $table->setHeaders($headers)->setRows($rows); - - $table->render(); -} - -/** - * Output the given text to the console. - */ -function output(string $output): void -{ - if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { - return; - } - - (new ConsoleOutput())->writeln($output); -} - if (!function_exists('resolve')) { /** * Resolve the given class from the container. @@ -110,26 +48,6 @@ function resolve(string $class) } } -if (!function_exists('endsWith')) { - /** - * Determine if a given string ends with a given substring. - */ - function endsWith(string $haystack, string $needle): bool - { - return substr($haystack, -strlen($needle)) === $needle; - } -} - -if (!function_exists('startsWith')) { - /** - * Determine if a given string starts with a given substring. - */ - function startsWith(string $haystack, string $needle): bool - { - return $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; - } -} - /** * Swap the given class implementation in the container. * @param mixed $instance @@ -183,7 +101,7 @@ function tap($value, callable $callback) /** * Get the user. */ -function user(): string // TODO: Validate user function +function user(): string { if (!isset($_SERVER['SUDO_USER'])) { return $_SERVER['USER']; diff --git a/cli/includes/require-drivers.php b/cli/includes/require-drivers.php index 97f17d6..0f1547a 100644 --- a/cli/includes/require-drivers.php +++ b/cli/includes/require-drivers.php @@ -1,10 +1,14 @@ | "$CONFIG" + fi + fi +} + +if [[ "$1" = "update" ]] +then + if [[ "$2" ]] + then + composer global update "genesisweb/valet-linux-plus:$2" + else + composer global update "genesisweb/valet-linux-plus" + fi + valet install +fi + +fix-config diff --git a/cli/stubs/proxy.valet.conf b/cli/stubs/proxy.valet.conf index e4e727f..7dd4ea2 100644 --- a/cli/stubs/proxy.valet.conf +++ b/cli/stubs/proxy.valet.conf @@ -18,8 +18,6 @@ server { access_log off; error_log "VALET_HOME_PATH/Log/VALET_SITE-error.log"; - error_page 404 "VALET_SERVER_PATH"; - location / { proxy_pass VALET_PROXY_HOST; proxy_set_header Host $host; diff --git a/cli/stubs/secure.isolated.valet.conf b/cli/stubs/secure.isolated.valet.conf index d202cb9..9b0880a 100644 --- a/cli/stubs/secure.isolated.valet.conf +++ b/cli/stubs/secure.isolated.valet.conf @@ -9,11 +9,11 @@ server { server { listen VALET_HTTPS_PORT ssl http2; + listen 88; server_name VALET_SITE www.VALET_SITE *.VALET_SITE; root /; charset utf-8; client_max_body_size 128M; - http2_push_preload on; location /VALET_STATIC_PREFIX/ { internal; diff --git a/cli/stubs/secure.proxy.valet.conf b/cli/stubs/secure.proxy.valet.conf index b8e8241..bfee1c1 100644 --- a/cli/stubs/secure.proxy.valet.conf +++ b/cli/stubs/secure.proxy.valet.conf @@ -8,12 +8,12 @@ server { server { listen VALET_HTTPS_PORT ssl http2; + listen 88; #listen VALET_LOOPBACK:443 ssl http2; # valet loopback server_name VALET_SITE www.VALET_SITE *.VALET_SITE; root /; charset utf-8; client_max_body_size 128M; - http2_push_preload on; location /VALET_STATIC_PREFIX/ { internal; diff --git a/cli/valet.php b/cli/valet.php index af259cc..cbf99e7 100644 --- a/cli/valet.php +++ b/cli/valet.php @@ -1,13 +1,17 @@ #!/usr/bin/env php run(); } catch (Exception $e) { - warning($e->getMessage()); + Writer::error($e->getMessage()); } diff --git a/composer.json b/composer.json index 202692a..5a2f7c3 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,6 @@ ], "license": "MIT", "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com" - }, { "name": "Uttam Rabadiya", "email": "mail@uttam.dev" @@ -44,19 +40,24 @@ "ext-pdo": "*", "ext-posix": "*", "ext-json": "*", - "php": "^7.1|^7.2|^7.3||^8.0|^8.1|^8.2|^8.3", - "illuminate/container": "~5.1|^6.0|^7.0|^8.0|^9.0|^10.0", - "mnapoli/silly": "~1.1", - "symfony/process": "^3.0|^4.0|^5.0|^6.0", - "nategood/httpful": "~0.2", - "tightenco/collect": "^5.3|^6.0|^7.0|^8.0" + "ext-mbstring": "*", + "ext-xml": "*", + "php": "^8.2|^8.3", + "illuminate/container": "^11.0", + "mnapoli/silly": "^1.9", + "symfony/process": "^7.0", + "nategood/httpful": "^0.3", + "illuminate/collections": "^11.4", + "uttamrabadiya/console-components": "^1.0.1" }, "require-dev": { "mockery/mockery": "^1.2.3", "phpunit/phpunit": "~5.5|^9.0", "phpstan/phpstan": "^1.4", "squizlabs/php_codesniffer": "^3.9", - "friendsofphp/php-cs-fixer": "^3.3" + "friendsofphp/php-cs-fixer": "^3.3", + "symfony/var-dumper": "^7.0", + "ext-zip": "*" }, "scripts": { "post-install-cmd": [ diff --git a/php b/php new file mode 100755 index 0000000..b93471c --- /dev/null +++ b/php @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e + +DEFAULT_PHP_BINARY="/usr/bin/php" +CONFIG_FILE="$HOME/.config/valet/config.json" + +# Check if the JSON file exists +if [ -f "$CONFIG_FILE" ]; then + IS_PWD_MATCHED=false + + # Read the paths array from the JSON file + paths=$(jq -r '.paths | .[]' "$CONFIG_FILE") + + # Loop through the paths and print each one + for path in $paths + do + if [[ "$PWD" == "$path"* ]]; then + IS_PWD_MATCHED=true + break + fi + done + if [ $IS_PWD_MATCHED == true ]; then + SITE_NAME=$( basename $PWD ); + SELECTED_PHP=$DEFAULT_PHP_BINARY + if jq -e '.isolated_versions | length > 0' "$CONFIG_FILE" >/dev/null; then + if [ "$(jq -r ".isolated_versions[\"$SITE_NAME\"]" "$CONFIG_FILE")" != "null" ]; then + SELECTED_PHP=$(jq -r ".isolated_versions[\"$SITE_NAME\"]" "$CONFIG_FILE") + elif [ "$(jq -r ".fallback_binary" "$CONFIG_FILE")" != "null" ]; then + SELECTED_PHP=$(jq -r ".fallback_binary" "$CONFIG_FILE") + fi + fi + else + SELECTED_PHP=$(jq -r ".fallback_binary" "$CONFIG_FILE") + fi +else + SELECTED_PHP=$DEFAULT_PHP_BINARY +fi + +if ! [ -f "$SELECTED_PHP" ]; then + SELECTED_PHP=$DEFAULT_PHP_BINARY +fi + +# shellcheck disable=SC2145 +eval "$SELECTED_PHP ${@@Q}" diff --git a/phpstan-errors b/phpstan-errors deleted file mode 100644 index bb81b2b..0000000 --- a/phpstan-errors +++ /dev/null @@ -1,410 +0,0 @@ - ------ ---------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Configuration.php - ------ ---------------------------------------------------------------------------------------------------------------- - 124 Parameter #1 $config of method Valet\Configuration::write() expects array, mixed given. - 127 Unable to resolve the template type TKey in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 127 Unable to resolve the template type TValue in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 144 Parameter #1 $config of method Valet\Configuration::write() expects array, mixed given. - 145 Unable to resolve the template type TKey in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 145 Unable to resolve the template type TValue in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 160 Parameter #1 $config of method Valet\Configuration::write() expects array, mixed given. - 161 Unable to resolve the template type TKey in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 161 Unable to resolve the template type TValue in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 162 Parameter #1 $path of method Valet\Filesystem::isDir() expects string, mixed given. - 170 Method Valet\Configuration::read() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 172 Method Valet\Configuration::read() should return array but returns mixed. - 203 Method Valet\Configuration::updateKey() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 205 Method Valet\Configuration::updateKey() should return array but returns mixed. - 214 Method Valet\Configuration::write() has parameter $config with no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 236 Part $domain (mixed) of encapsed string cannot be cast to string. - 237 Parameter #3 ...$values of function sprintf expects bool|float|int|string|null, mixed given. - ------ ---------------------------------------------------------------------------------------------------------------- - - ------ ---------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/Specific/CraftValetDriver.php - ------ ---------------------------------------------------------------------------------------------------------------------------- - 20 Method Valet\Drivers\Specific\CraftValetDriver::frontControllerDirectory() has parameter $sitePath with no type specified. - ------ ---------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/Specific/DrupalValetDriver.php - ------ ------------------------------------------------------------------------------------------------------------------------------------------- - 37 Offset 'extension' does not exist on array{dirname?: string, basename: string, extension?: string, filename: string}. - 76 Method Valet\Drivers\Specific\DrupalValetDriver::addSubdirectory() has parameter $sitePath with no type specified. - 98 Method Valet\Drivers\Specific\DrupalValetDriver::possibleSubdirectories() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - ------ ------------------------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------- - Line cli/Valet/Drivers/Specific/KirbyValetDriver.php - ------ ------------------------------------------------- - 57 Variable $indexPath might not be defined. - ------ ------------------------------------------------- - - ------ ----------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/Specific/Magento2ValetDriver.php - ------ ----------------------------------------------------------------------------------------- - 28 Parameter #1 $haystack of function strpos expects string, string|null given. - 29 Parameter #3 $subject of function preg_replace expects array|string, string|null given. - 34 Parameter #1 $haystack of function strpos expects string, string|null given. - ------ ----------------------------------------------------------------------------------------- - - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/Specific/StatamicV2ValetDriver.php - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - 76 Variable $indexPath might not be defined. - 97 Method Valet\Drivers\Specific\StatamicV2ValetDriver::getLocales() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 125 Cannot access offset 'path' on array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false. - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/Specific/StatamicValetDriver.php - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - 29 Method Valet\Drivers\Specific\StatamicValetDriver::frontControllerPath() should return string but returns string|null. - 35 Method Valet\Drivers\Specific\StatamicValetDriver::getStaticPath() has no return type specified. - 44 Cannot access offset 'path' on array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false. - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------------------ - Line cli/Valet/Drivers/Specific/Typo3ValetDriver.php - ------ ------------------------------------------------------------------------------------------------------------------------------ - 24 Property Valet\Drivers\Specific\Typo3ValetDriver::$documentRoot has no type specified. - 36 Property Valet\Drivers\Specific\Typo3ValetDriver::$forbiddenUriPatterns has no type specified. - 87 Parameter #1 $uri of method Valet\Drivers\Specific\Typo3ValetDriver::isAccessAuthorized() expects string, string|null given. - ------ ------------------------------------------------------------------------------------------------------------------------------ - - ------ ---------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/Specific/WordPressValetDriver.php - ------ ---------------------------------------------------------------------------------------------------------------------- - 25 Parameter #3 $uri of method Valet\Drivers\BasicValetDriver::frontControllerPath() expects string, string|null given. - 32 Method Valet\Drivers\Specific\WordPressValetDriver::forceTrailingSlash() has parameter $uri with no type specified. - ------ ---------------------------------------------------------------------------------------------------------------------- - - ------ --------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Drivers/ValetDriver.php - ------ --------------------------------------------------------------------------------------------------------------------- - 55 Instantiated class LocalValetDriver not found. - 💡 Learn more at https://phpstan.org/user-guide/discovering-symbols - 86 Method Valet\Drivers\ValetDriver::driversIn() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 99 Cannot access offset 0 on mixed. - 101 Cannot access offset 0 on mixed. - 101 Parameter #1 $path of function basename expects string, mixed given. - 110 Method Valet\Drivers\ValetDriver::specificDrivers() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 120 Method Valet\Drivers\ValetDriver::customDrivers() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 213 Parameter #1 $json of function json_decode expects string, string|false given. - 219 Cannot access offset 'require' on mixed. - 219 Cannot access offset string on mixed. - ------ --------------------------------------------------------------------------------------------------------------------- - - ------ ---------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Facades/Facade.php - ------ ---------------------------------------------------------------------------------------------------------------------------- - 26 Method Valet\Facades\Facade::__callStatic() has parameter $parameters with no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 30 Parameter #1 $callback of function call_user_func_array expects callable(): mixed, array{mixed, string} given. - ------ ---------------------------------------------------------------------------------------------------------------------------- - - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Filesystem.php - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------- - 130 Method Valet\Filesystem::get() should return string but returns string|false. - 144 Method Valet\Filesystem::put() should return string but returns int<0, max>|false. - 335 Method Valet\Filesystem::realpath() should return string but returns string|false. - 357 Parameter #1 $filename of function is_link expects string, string|false given. - 358 Parameter #1 $path of function readlink expects string, string|false given. - 361 Method Valet\Filesystem::readLink() should return string but returns string|false. - 393 Method Valet\Filesystem::scandir() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 395 Parameter #1 $value of function collect expects Illuminate\Contracts\Support\Arrayable|iterable|null, array|false given. - 406 Method Valet\Filesystem::toIterator() has parameter $files with no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 406 Method Valet\Filesystem::toIterator() return type has no value type specified in iterable type Traversable. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 408 Instanceof between array|string and Traversable will always evaluate to false. - 💡 Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your phpstan.neon. - ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------- - Line cli/Valet/Mailpit.php - ------ ------------------------------------------------------------------- - 71 Part $domain (mixed) of encapsed string cannot be cast to string. - 72 Part $domain (mixed) of encapsed string cannot be cast to string. - 177 Part $domain (mixed) of encapsed string cannot be cast to string. - ------ ------------------------------------------------------------------- - - ------ ---------------------------------------------------------------------------------------------------------- - Line cli/Valet/Mysql.php - ------ ---------------------------------------------------------------------------------------------------------- - 49 Access to an undefined property Valet\Contracts\PackageManager::$mysqlPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 50 Access to an undefined property Valet\Contracts\PackageManager::$mysqlPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 52 Access to an undefined property Valet\Contracts\PackageManager::$mariaDBPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 53 Access to an undefined property Valet\Contracts\PackageManager::$mariaDBPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 60 Method Valet\Mysql::install() has no return type specified. - 60 Method Valet\Mysql::install() has parameter $useMariaDB with no type specified. - 65 Access to an undefined property Valet\Contracts\PackageManager::$mariaDBPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 65 Access to an undefined property Valet\Contracts\PackageManager::$mysqlPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 73 Access to an undefined property Valet\Contracts\PackageManager::$mariaDBPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 74 Access to an undefined property Valet\Contracts\PackageManager::$mysqlPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 80 Access to an undefined property Valet\Contracts\PackageManager::$mysqlPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 81 Access to an undefined property Valet\Contracts\PackageManager::$mariaDBPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 235 Cannot call method execute() on bool|PDOStatement|null. - 237 Cannot call method rowCount() on bool|PDOStatement|null. - 243 Method Valet\Mysql::exportDatabase() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 318 Method Valet\Mysql::getDatabases() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 326 Cannot call method fetchAll() on PDOStatement|true. - 339 Method Valet\Mysql::query() never returns void so it can be removed from the return type. - 402 Method Valet\Mysql::getSystemDatabases() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 432 Access to an undefined property Valet\Contracts\PackageManager::$mariaDBPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 467 Method Valet\Mysql::getCredentials() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - ------ ---------------------------------------------------------------------------------------------------------- - - ------ --------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Nginx.php - ------ --------------------------------------------------------------------------------------------------------------------------------------------- - 144 Method Valet\Nginx::configuredSites() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - ------ --------------------------------------------------------------------------------------------------------------------------------------------- - - ------ --------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Ngrok.php - ------ --------------------------------------------------------------------------------------------------------------------- - 36 Method Valet\Ngrok::currentTunnelUrl() should return string|null but returns mixed. - 61 Method Valet\Ngrok::findHttpTunnelUrl() has parameter $tunnels with no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - ------ --------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PackageManagers/Apt.php - ------ ------------------------------------------------------------------------------------------------------------------------- - 37 Constant Valet\PackageManagers\Apt::PHP_FPM_PATTERN_BY_VERSION type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 51 Method Valet\PackageManagers\Apt::packages() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 119 Offset string on array{} in empty() does not exist. - 120 Offset string does not exist on array{}. - ------ ------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PackageManagers/Dnf.php - ------ ------------------------------------------------------------------------------------------------------------------------- - 37 Constant Valet\PackageManagers\Dnf::PHP_FPM_PATTERN_BY_VERSION type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 113 Offset string on array{} in empty() does not exist. - 114 Offset string does not exist on array{}. - ------ ------------------------------------------------------------------------------------------------------------------------- - - ------ --------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PackageManagers/Eopkg.php - ------ --------------------------------------------------------------------------------------------------------------------------- - 37 Constant Valet\PackageManagers\Eopkg::PHP_FPM_PATTERN_BY_VERSION type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 51 Method Valet\PackageManagers\Eopkg::packages() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 119 Offset string on array{} in empty() does not exist. - 120 Offset string does not exist on array{}. - ------ --------------------------------------------------------------------------------------------------------------------------- - - ------ -------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PackageManagers/PackageKit.php - ------ -------------------------------------------------------------------------------------------------------------------------------- - 37 Constant Valet\PackageManagers\PackageKit::PHP_FPM_PATTERN_BY_VERSION type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 51 Method Valet\PackageManagers\PackageKit::packages() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 119 Offset string on array{} in empty() does not exist. - 120 Offset string does not exist on array{}. - ------ -------------------------------------------------------------------------------------------------------------------------------- - - ------ ---------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PackageManagers/Pacman.php - ------ ---------------------------------------------------------------------------------------------------------------------------- - 37 Constant Valet\PackageManagers\Pacman::PHP_FPM_PATTERN_BY_VERSION type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 51 Method Valet\PackageManagers\Pacman::packages() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 122 Offset string on array{} in empty() does not exist. - 123 Offset string does not exist on array{}. - 126 Parameter #2 $replace of function str_replace expects array|string, string|null given. - 136 Parameter #2 $replace of function str_replace expects array|string, string|null given. - ------ ---------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PackageManagers/Yum.php - ------ ------------------------------------------------------------------------------------------------------------------------- - 37 Constant Valet\PackageManagers\Yum::PHP_FPM_PATTERN_BY_VERSION type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 113 Offset string on array{} in empty() does not exist. - 114 Offset string does not exist on array{}. - ------ ------------------------------------------------------------------------------------------------------------------------- - - ------ -------------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/PhpFpm.php - ------ -------------------------------------------------------------------------------------------------------------------------------------------------- - 16 Property Valet\PhpFpm::$config has no type specified. - 17 Property Valet\PhpFpm::$pm has no type specified. - 18 Property Valet\PhpFpm::$sm has no type specified. - 19 Property Valet\PhpFpm::$cli has no type specified. - 20 Property Valet\PhpFpm::$files has no type specified. - 21 Property Valet\PhpFpm::$site has no type specified. - 22 Property Valet\PhpFpm::$nginx has no type specified. - 147 Method Valet\PhpFpm::restart() has parameter $version with no type specified. - 155 Method Valet\PhpFpm::stop() has parameter $version with no type specified. - 163 Method Valet\PhpFpm::status() has parameter $version with no type specified. - 218 Method Valet\PhpFpm::isolatedDirectories() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 243 Method Valet\PhpFpm::normalizePhpVersion() has parameter $version with no type specified. - 398 Method Valet\PhpFpm::utilizedPhpVersions() should return array but returns array. - 455 Method Valet\PhpFpm::getExtensionPrefix() has parameter $version with no type specified. - ------ -------------------------------------------------------------------------------------------------------------------------------------------------- - - ------ --------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Server.php - ------ --------------------------------------------------------------------------------------------------------------- - 10 Property Valet\Server::$config type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 12 Method Valet\Server::__construct() has parameter $config with no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 36 Method Valet\Server::show404() has no return type specified. - 46 Method Valet\Server::showDirectoryListing() has no return type specified. - 57 Parameter #1 $array of function usort expects TArray of array, array|false given. - 69 Parameter #2 $array of function array_map expects array, array|false given. - 80 Method Valet\Server::hostIsIpAddress() should return bool but returns int|false. - 148 Parameter #1 $haystack of function strpos expects string, string|null given. - 149 Parameter #2 $string of function explode expects string, string|null given. - 152 Method Valet\Server::allowWildcardDnsDomains() should return string but returns string|null. - ------ --------------------------------------------------------------------------------------------------------------- - - ------ --------------------------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/ServiceManagers/LinuxService.php - ------ --------------------------------------------------------------------------------------------------------------------------------------------------------------- - 175 Method Valet\ServiceManagers\LinuxService::getRealService() should return string but returns mixed. - 175 Parameter #1 $value of function collect expects Illuminate\Contracts\Support\Arrayable<(int|string), mixed>|iterable<(int|string), mixed>|null, string given. - 175 Unable to resolve the template type TKey in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 175 Unable to resolve the template type TValue in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - ------ --------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - Line cli/Valet/ServiceManagers/Systemd.php - ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - 181 Method Valet\ServiceManagers\Systemd::getRealService() should return string but returns mixed. - 181 Parameter #1 $value of function collect expects Illuminate\Contracts\Support\Arrayable<(int|string), mixed>|iterable<(int|string), mixed>|null, string given. - 181 Unable to resolve the template type TKey in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 181 Unable to resolve the template type TValue in call to function collect - 💡 See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type - 182 Parameter #1 $callback of method Illuminate\Support\Collection<(int|string),mixed>::first() expects (callable(mixed, int|string): bool)|null, Closure(mixed): (int<0, max>|false) given. - 183 Part $service (mixed) of encapsed string cannot be cast to string. - ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - - ------ -------------------------------------------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Site.php - ------ -------------------------------------------------------------------------------------------------------------------------------------------------- - 71 Method Valet\Site::links() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 85 Method Valet\Site::proxies() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 108 Possibly invalid array key type (array|string). - 219 Parameter #3 $subject of function str_replace expects array|string, mixed given. - 220 Parameter #1 $url of method Valet\Site::getNginxConf() expects string, mixed given. - 222 Parameter #1 $search of function str_replace expects array|string, mixed given. - 225 Parameter #1 $url of method Valet\Site::unsecure() expects string, mixed given. - 237 Method Valet\Site::secured() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 294 Parameter #1 $url of method Valet\Site::createSecureNginxServer() expects string, mixed given. - 306 Parameter #1 $path of method Valet\Site::host() expects string, string|false given. - 536 Method Valet\Site::getCertificates() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 550 Method Valet\Site::getLinks() has parameter $certs with generic class Illuminate\Support\Collection but does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 550 Method Valet\Site::getLinks() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 728 Method Valet\Site::getSites() has parameter $certs with generic class Illuminate\Support\Collection but does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 728 Method Valet\Site::getSites() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - 775 Method Valet\Site::parked() return type with generic class Illuminate\Support\Collection does not specify its types: TKey, TValue - 💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. - ------ -------------------------------------------------------------------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------- - Line cli/Valet/Valet.php - ------ ------------------------------------------------------------------------------------------------------------- - 36 Property Valet\Valet::$phpBin has no type specified. - 212 Method Valet\Valet::getRunningFpmVersions() return type has no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - 241 Method Valet\Valet::getAvailablePackageManager() should return class-string but returns string. - 249 Cannot call method isAvailable() on mixed. - 269 Method Valet\Valet::getAvailableServiceManager() should return class-string but returns string. - 273 Cannot call method isAvailable() on mixed. - ------ ------------------------------------------------------------------------------------------------------------- - - ------ -------------------------------------------------------------------------------------- - Line cli/Valet/ValetRedis.php - ------ -------------------------------------------------------------------------------------- - 47 Access to an undefined property Valet\Contracts\PackageManager::$redisPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 48 Access to an undefined property Valet\Contracts\PackageManager::$redisPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 56 Access to an undefined property Valet\Contracts\PackageManager::$redisPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 64 Access to an undefined property Valet\Contracts\PackageManager::$redisPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - 72 Access to an undefined property Valet\Contracts\PackageManager::$redisPackageName. - 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property - ------ -------------------------------------------------------------------------------------- - - ------ ------------------------------------------------------------------------------------------------------------------- - Line cli/app.php - ------ ------------------------------------------------------------------------------------------------------------------- - 338 Parameter #1 $path of function basename expects string, string|false given. - 338 Parameter #1 $sitePath of static method Valet\Drivers\ValetDriver::assign() expects string, string|false given. - 446 Parameter #1 $path of function basename expects string, string|false given. - 446 Parameter #1 $target of static method Valet\Facades\Site::link() expects string, string|false given. - 455 Parameter #1 $path of function basename expects string, string|false given. - 473 Parameter #1 $path of static method Valet\Facades\Site::host() expects string, string|false given. - 486 Parameter #1 $path of static method Valet\Facades\Site::host() expects string, string|false given. - 499 Parameter #1 $path of static method Valet\Facades\Site::host() expects string, string|false given. - 731 Parameter #1 $site of static method Valet\Facades\Configuration::parseDomain() expects string, string|null given. - 735 Parameter #1 $path of function basename expects string, string|false given. - 771 Parameter #1 $path of static method Valet\Facades\Site::host() expects string, string|false given. - ------ ------------------------------------------------------------------------------------------------------------------- - - ------ ----------------------------------------------------------------------------------------------------------------------- - Line cli/includes/helpers.php - ------ ----------------------------------------------------------------------------------------------------------------------- - 127 Function Valet\strArrayReplace() has parameter $searchAndReplace with no value type specified in iterable type array. - 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type - ------ ----------------------------------------------------------------------------------------------------------------------- - - [ERROR] Found 183 errors - diff --git a/phpstan.neon b/phpstan.neon index 165952f..4ed4682 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,5 @@ parameters: paths: - cli - tests + ignoreErrors: + - '#Method .*? with no value type specified in iterable type array#' diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..2aa1809 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,34 @@ + + + + + + + + + cli/Valet + + + + + ./tests + + + + + + diff --git a/server.php b/server.php index 852f975..fcdd946 100644 --- a/server.php +++ b/server.php @@ -41,7 +41,7 @@ $siteName = $server->siteNameFromHttpHost($_SERVER['HTTP_HOST']); $valetSitePath = $server->sitePath($siteName); -if (is_null($valetSitePath) && is_null($valetSitePath = $server->defaultSitePath())) { +if ($valetSitePath === null && is_null($valetSitePath = $server->defaultSitePath())) { Server::show404(); } diff --git a/tests/CliTest.php b/tests/CliTest.php new file mode 100644 index 0000000..8e26127 --- /dev/null +++ b/tests/CliTest.php @@ -0,0 +1,1198 @@ +tester->run(['command' => 'domain']); + + $this->tester->assertCommandIsSuccessful(); + $domain = ConfigurationFacade::get('domain'); + + $this->assertSame('test', $domain); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString('Your current Valet domain is [test]', $output->fetch()); + } + + /** + * @test + */ + public function itWillSetDomainInConfig(): void + { + Writer::fake(); + $dnsmasq = Mockery::mock(DnsMasq::class); + $dnsmasq->shouldReceive('updateDomain')->once()->with('localhost'); + swap(DnsMasq::class, $dnsmasq); + + $config = Mockery::mock(Configuration::class); + $config->shouldReceive('get')->with('domain')->andReturn('test')->once(); + $config->shouldReceive('set')->with('domain', 'localhost')->once(); + swap(Configuration::class, $config); + + $siteSecure = Mockery::mock(SiteSecure::class); + $siteSecure->shouldReceive('reSecureForNewDomain')->with('test', 'localhost')->once(); + swap(SiteSecure::class, $siteSecure); + + $phpFpm = Mockery::mock(PhpFpm::class); + $phpFpm->shouldReceive('restart')->withNoArgs()->once(); + swap(PhpFpm::class, $phpFpm); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->withNoArgs()->once(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'domain', 'domain' => 'localhost']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString('Your Valet domain has been updated to [localhost]', $output->fetch()); + } + + /** + * @test + */ + public function itWillReadNginxPortFromConfig(): void + { + Writer::fake(); + + $this->tester->run(['command' => 'port']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString('Current Nginx port (HTTP): 80', $content); + $this->assertStringContainsString('Current Nginx port (HTTPS): 443', $content); + } + + public function nginxPortDataProvider(): array + { + return [ + [8443, true, 'https_port', 'Your Nginx HTTPS port has been updated to [8443]'], + [88, false, 'port', 'Your Nginx HTTP port has been updated to [88]'], + ]; + } + + /** + * @test + * @dataProvider nginxPortDataProvider + */ + public function itWillUpdateNginxPortSuccessfully( + int $port, + bool $isHttps, + string $updateKey, + string $expectedOutput + ): void { + Writer::fake(); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->withNoArgs()->once(); + + if ($isHttps === false) { + $nginx->shouldReceive('updatePort')->with($port)->once(); + } + + swap(Nginx::class, $nginx); + + $config = Mockery::mock(Configuration::class); + $config->shouldReceive('set')->with($updateKey, $port)->once(); + swap(Configuration::class, $config); + + $siteSecure = Mockery::mock(SiteSecure::class); + $siteSecure->shouldReceive('regenerateSecuredSitesConfig')->withNoArgs()->once(); + swap(SiteSecure::class, $siteSecure); + + $phpFpm = Mockery::mock(PhpFpm::class); + $phpFpm->shouldReceive('restart')->withNoArgs()->once(); + swap(PhpFpm::class, $phpFpm); + + $this->tester->run(['command' => 'port', 'port' => $port, '--https' => $isHttps]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString($expectedOutput, $output->fetch()); + } + + /** + * @test + */ + public function itWillValidateWhichCommand(): void + { + Writer::fake(); + + $this->tester->run(['command' => 'which']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString('This site is served by [Valet\Drivers\BasicValetDriver]', $output->fetch()); + } + + public function parkedDirectoryProvider(): array + { + return [ + ['/test/directory', '/test/directory'], + [null, getcwd()], + ]; + } + + /** + * @test + * @dataProvider parkedDirectoryProvider + */ + public function itWillParkDirectoryToConfig(?string $directory, string $expectedDirectory): void + { + Writer::fake(); + + $config = Mockery::mock(Configuration::class); + $config->shouldReceive('addPath')->with($expectedDirectory)->once(); + swap(Configuration::class, $config); + + $this->tester->run(['command' => 'park', 'path' => $directory]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + \sprintf('The [%s] directory has been added to Valet\'s paths.', $expectedDirectory), + $output->fetch() + ); + } + + public function directoryProvider(): array + { + return [ + [null, 'No paths have been registered.'], + ['/test/directory', '/test/directory'], + ]; + } + + /** + * @test + * @dataProvider directoryProvider + */ + public function itWillReadParkedDirectories(?string $path, string $expectedMessage): void + { + Writer::fake(); + + if ($path !== null) { + ConfigurationFacade::addPath($path); + } + + $this->tester->run(['command' => 'paths']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + /** + * @test + * @dataProvider parkedDirectoryProvider + */ + public function itWillForgetParkedDirectory(?string $directory, string $expectedDirectory): void + { + Writer::fake(); + + $config = Mockery::mock(Configuration::class); + $config->shouldReceive('removePath')->with($expectedDirectory)->once(); + swap(Configuration::class, $config); + + $this->tester->run(['command' => 'forget', 'path' => $directory]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + \sprintf('The [%s] directory has been removed from Valet\'s paths.', $expectedDirectory), + $output->fetch() + ); + } + + public function nginxProxyDataProvider(): array + { + return [ + [ + 'mails', + 'http://localhost:8045', + true, + 'mails.test', + 'Valet will now proxy [https://mails.test] traffic to [http://localhost:8045]' + ], + [ + 'withtld.test', + 'http://localhost:8045', + true, + 'withtld.test', + 'Valet will now proxy [https://withtld.test] traffic to [http://localhost:8045]' + ], + [ + 'mails', + 'http://localhost:8045', + false, + 'mails.test', + 'Valet will now proxy [http://mails.test] traffic to [http://localhost:8045]' + ] + ]; + } + + /** + * @test + * @dataProvider nginxProxyDataProvider + */ + public function itWillCreateNginxProxy( + string $domain, + string $host, + bool $isSecure, + string $expectedDomain, + string $expectedMessage + ): void { + Writer::fake(); + + $siteProxy = Mockery::mock(SiteProxy::class); + $siteProxy->shouldReceive('proxyCreate')->with($expectedDomain, $host, $isSecure)->once(); + swap(SiteProxy::class, $siteProxy); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->withNoArgs()->once(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'proxy', 'domain' => $domain, 'host' => $host, '--secure' => $isSecure]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + public function invalidProxyDataProvider(): array + { + return [ + [ + [ + 'domain' => null, + ], + 'Please provide domain' + ], + [ + [ + 'host' => null, + ], + 'Please provide host' + ], + [ + [ + 'host' => 'invalid-url', + ], + '"invalid-url" is not a valid URL' + ], + ]; + } + + /** + * @test + * @dataProvider invalidProxyDataProvider + */ + public function itWillFailToProxyDomainWhenValidParametersNotAvailable( + array $overrides, + string $expectedMessage + ): void { + Writer::fake(); + + $domain = array_key_exists('domain', $overrides) ? $overrides['domain'] : 'mails'; + $host = array_key_exists('host', $overrides) ? $overrides['host'] : 'http://127.0.0.1:8025'; + + $site = Mockery::mock(Site::class); + $site->shouldReceive('proxyDelete')->never(); + swap(Site::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->never(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'proxy', 'domain' => $domain, 'host' => $host]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + public function nginxUnproxyDataProvider(): array + { + return [ + [ + 'mails', + 'mails.test', + 'Valet will no longer proxy [mails.test]' + ], + [ + 'withtld.test', + 'withtld.test', + 'Valet will no longer proxy [withtld.test]' + ] + ]; + } + + /** + * @test + * @dataProvider nginxUnproxyDataProvider + */ + public function itWillRemoveNginxProxy(string $domain, string $expectedDomain, string $expectedMessage): void + { + Writer::fake(); + + $siteSecure = Mockery::mock(SiteSecure::class); + $siteSecure->shouldReceive('unsecure')->with($expectedDomain)->once(); + swap(SiteSecure::class, $siteSecure); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->withNoArgs()->once(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'unproxy', 'domain' => $domain]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + public function invalidUnproxyDataProvider(): array + { + return [ + [ + [ + 'domain' => null, + ], + 'Please provide domain' + ], + ]; + } + + /** + * @test + * @dataProvider invalidUnproxyDataProvider + */ + public function itWillFailToUnproxyDomainWhenValidParametersNotAvailable( + array $overrides, + string $expectedMessage + ): void { + Writer::fake(); + + $domain = array_key_exists('domain', $overrides) ? $overrides['domain'] : 'mails'; + + $site = Mockery::mock(Site::class); + $site->shouldReceive('proxyDelete')->never(); + swap(Site::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->never(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'unproxy', 'domain' => $domain]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + /** + * @test + */ + public function itWillListProxies(): void + { + Writer::fake(); + + $siteProxies = Mockery::mock(SiteProxy::class); + $siteProxies->shouldReceive('proxies')->withNoArgs()->once()->andReturn(collect([ + [ + 'url' => 'https://mails.localhost', + 'secured' => '✓', + 'path' => 'http://127.0.0.1:8045', + ], + [ + 'url' => 'http://docker-host.localhost', + 'secured' => '✕', + 'path' => 'http://127.0.0.1:8888', + ], + ])); + swap(SiteProxy::class, $siteProxies); + + $this->tester->run(['command' => 'proxies']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'https://mails.localhost | ✓ | http://127.0.0.1:8045', + $content + ); + $this->assertStringContainsString( + 'http://docker-host.localhost | ✕ | http://127.0.0.1:8888', + $content + ); + } + + /** + * @test + */ + public function itWillLinkCwdToNginx(): void + { + Writer::fake(); + + $currentDirectory = getcwd(); + $domainName = basename($currentDirectory); + + $siteLink = Mockery::mock(SiteLink::class); + $siteLink->shouldReceive('link') + ->with($currentDirectory, $domainName) + ->once() + ->andReturn('direct-path'); + swap(SiteLink::class, $siteLink); + + $this->tester->run(['command' => 'link']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'A [' . $domainName . '] symbolic link has been created in [direct-path]', + $content + ); + } + + /** + * @test + */ + public function itWillUnlinkCwdToNginx(): void + { + Writer::fake(); + + $domainName = basename(getcwd()); + + $siteLink = Mockery::mock(SiteLink::class); + $siteLink->shouldReceive('unlink')->with($domainName)->once(); + swap(SiteLink::class, $siteLink); + + $this->tester->run(['command' => 'unlink']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'The [' . $domainName . '] symbolic link has been removed', + $content + ); + } + + /** + * @test + */ + public function itWillListLinks(): void + { + Writer::fake(); + + $siteLink = Mockery::mock(SiteLink::class); + $siteLink->shouldReceive('links')->withNoArgs()->once()->andReturn(collect([ + [ + 'url' => 'http://scripts.test', + 'secured' => '✕', + 'path' => '/test/path', + ], + ])); + swap(SiteLink::class, $siteLink); + + $this->tester->run(['command' => 'links']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'http://scripts.test | ✕ | /test/path', + $content + ); + } + + public function secureNginxDomainProvider(): array + { + return [ + [ + [ + 'domain' => 'test-domain', + ], + 'The [test-domain.test] site has been secured with a fresh TLS certificate', + ], + ]; + } + + /** + * @test + * @dataProvider secureNginxDomainProvider + */ + public function itWillSecureNginxDomain(array $overrides, string $expectedMessage): void + { + Writer::fake(); + + $domain = array_key_exists('domain', $overrides) ? $overrides['domain'] : 'test-domain'; + + $siteSecure = Mockery::mock(SiteSecure::class); + $siteSecure->shouldReceive('secure')->with($domain . '.test')->once(); + swap(SiteSecure::class, $siteSecure); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->withNoArgs()->once(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'secure', 'domain' => $domain]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + public function unsecureNginxDomainProvider(): array + { + return [ + [ + [ + 'domain' => 'test-domain', + ], + 'The [test-domain.test] site will now serve traffic over HTTP', + ], + ]; + } + + /** + * @test + * @dataProvider unsecureNginxDomainProvider + */ + public function itWillUnsecureNginxDomain(array $overrides, string $expectedMessage): void + { + Writer::fake(); + + $domain = array_key_exists('domain', $overrides) ? $overrides['domain'] : 'test-domain'; + + $siteSecure = Mockery::mock(SiteSecure::class); + $siteSecure->shouldReceive('unsecure')->with($domain . '.test', true)->once(); + swap(SiteSecure::class, $siteSecure); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->withNoArgs()->once(); + swap(Nginx::class, $nginx); + + $this->tester->run(['command' => 'unsecure', 'domain' => $domain]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + public function securedNginxDomainProvider(): array + { + return [ + [ + [ + 'domain' => 'domain-1', + ], + 'domain-1.test is secured', + ], + [ + [ + 'domain' => null, + ], + 'valet-linux-plus.test is secured', + ], + [ + [ + 'domain' => 'unsecure-domain.test', + ], + 'unsecure-domain.test is not secured', + ], + ]; + } + + /** + * @test + * @dataProvider securedNginxDomainProvider + */ + public function itWillListSecuredNginxDomains(array $overrides, string $expectedMessage): void + { + Writer::fake(); + + $domain = array_key_exists('domain', $overrides) ? $overrides['domain'] : 'test-domain'; + + $siteSecured = Mockery::mock(SiteSecure::class); + $siteSecured->shouldReceive('secured')->withNoArgs()->once()->andReturn(collect([ + 'domain-1.test', + 'domain-2.test', + 'valet-linux-plus.test', + ])); + swap(SiteSecure::class, $siteSecured); + + $this->tester->run(['command' => 'secured', 'site' => $domain]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + $expectedMessage, + $output->fetch() + ); + } + + /** + * @test + */ + public function itWillChangePhpVersion(): void + { + Writer::fake(); + + $phpFpm = Mockery::mock(PhpFpm::class); + $phpFpm->shouldReceive('normalizePhpVersion')->with('8.2')->andReturn('8.2'); + $phpFpm->shouldReceive('validateVersion')->with('8.2')->andReturnTrue(); + $phpFpm->shouldReceive('switchVersion')->with('8.2', true, false)->once(); + swap(PhpFpm::class, $phpFpm); + + $this->tester->run(['command' => 'use', 'preferredVersion' => '8.2', '--update-cli' => true]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString( + 'PHP version successfully changed to [8.2]', + $output->fetch() + ); + } + + /** + * @test + */ + public function itWillHandleInvalidPhpVersion(): void + { + Writer::fake(); + + $phpFpm = Mockery::mock(PhpFpm::class); + $phpFpm->shouldReceive('normalizePhpVersion')->with('7.2')->andReturn('7.2'); + $phpFpm->shouldReceive('validateVersion')->with('7.2')->andReturnFalse(); + $phpFpm->shouldReceive('switchVersion')->never(); + swap(PhpFpm::class, $phpFpm); + + $this->tester->run(['command' => 'use', 'preferredVersion' => '7.2', '--update-cli' => true]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'Invalid version [7.2] used. Supported versions are: 8.2, 8.3', + $content + ); + $this->assertStringContainsString( + \sprintf( + 'You can still use any version from [%s] list using `valet isolate` command', + '7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3' + ), + $content + ); + } + + /** + * @test + */ + public function itWillListDatabases(): void + { + Writer::fake(); + + $mysql = Mockery::mock(Mysql::class); + $mysql->shouldReceive('getDatabases')->withNoArgs()->andReturn([['database1'], ['database2']]); + swap(Mysql::class, $mysql); + + $this->tester->run(['command' => 'db:list']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'database1', + $content + ); + $this->assertStringContainsString( + 'database2', + $content + ); + } + + public function databaseNamesProvider(): array + { + return [ + [ + 'database1', + 'database1', + ], + [ + null, + basename((string)getcwd()) + ] + ]; + } + + /** + * @test + * @dataProvider databaseNamesProvider + */ + public function itWillCreateDatabase(?string $databaseName, string $expectedDatabaseName): void + { + Writer::fake(); + + $mysql = Mockery::mock(Mysql::class); + $mysql->shouldReceive('createDatabase')->with($expectedDatabaseName)->andReturnTrue(); + swap(Mysql::class, $mysql); + + $this->tester->run(['command' => 'db:create', 'databaseName' => $databaseName]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + \sprintf('Database [%s] created successfully', $expectedDatabaseName), + $content + ); + } + + /** + * @test + * @dataProvider databaseNamesProvider + */ + public function itWillDropDatabase(?string $databaseName, string $expectedDatabaseName): void + { + Writer::fake(); + + $mysql = Mockery::mock(Mysql::class); + $mysql->shouldReceive('dropDatabase')->with($expectedDatabaseName)->andReturnTrue(); + swap(Mysql::class, $mysql); + + $this->tester->run(['command' => 'db:drop', 'databaseName' => $databaseName, '-y' => true]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + \sprintf('Database [%s] dropped successfully', $expectedDatabaseName), + $content + ); + } + + /** + * @test + * @dataProvider databaseNamesProvider + */ + public function itWillResetDatabase(?string $databaseName, string $expectedDatabaseName): void + { + Writer::fake(); + + $mysql = Mockery::mock(Mysql::class); + $mysql->shouldReceive('dropDatabase')->with($expectedDatabaseName)->andReturnTrue(); + $mysql->shouldReceive('createDatabase')->with($expectedDatabaseName)->andReturnTrue(); + swap(Mysql::class, $mysql); + + $this->tester->run(['command' => 'db:reset', 'databaseName' => $databaseName, '-y' => true]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + \sprintf('Database [%s] reset successfully', $expectedDatabaseName), + $content + ); + } + + /** + * @test + */ + public function itWillImportDatabase(): void + { + Writer::fake(); + + $databaseName = 'database1'; + $dumpFilePath = '/path/to/sql-file'; + $mysql = Mockery::mock(Mysql::class); + $mysql->shouldReceive('importDatabase')->with($dumpFilePath, $databaseName); + swap(Mysql::class, $mysql); + + $fileSystem = Mockery::mock(Filesystem::class); + $fileSystem->shouldReceive('exists')->with($dumpFilePath)->andReturnTrue(); + swap(Filesystem::class, $fileSystem); + + $this->tester->run(['command' => 'db:import', 'databaseName' => $databaseName, 'dumpFile' => $dumpFilePath]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + 'Importing database...', + $content + ); + $this->assertStringContainsString( + \sprintf('Database [%s] imported successfully', $databaseName), + $content + ); + } + + /** + * @test + */ + public function itWillExportDatabase(): void + { + Writer::fake(); + + $databaseName = 'database1'; + $mysql = Mockery::mock(Mysql::class); + $mysql->shouldReceive('exportDatabase')->with($databaseName, true)->andReturn([ + 'database' => $databaseName, + 'filename' => 'database1.sql', + ]); + swap(Mysql::class, $mysql); + + $this->tester->run(['command' => 'db:export', 'databaseName' => $databaseName, '--sql' => true]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + \sprintf('Database [%s] exported into file database1.sql', $databaseName), + $content + ); + } + + public function isolateDirectoryDataProvider(): array + { + return [ + [ + '7.4', + 'info.test', + 'info.test', + true, + ], + [ + '7.4', + null, + basename((string)getcwd()), + true, + ], + ]; + } + + /** + * @test + * @dataProvider isolateDirectoryDataProvider + */ + public function itWillIsolatePhpVersion( + string $phpVersion, + ?string $siteName, + string $expectedSiteName, + bool $isSecure + ): void { + Writer::fake(); + + $siteIsolate = Mockery::mock(SiteIsolate::class); + $siteIsolate->shouldReceive('isolateDirectory') + ->with($expectedSiteName, $phpVersion, $isSecure) + ->once() + ->andReturnTrue(); + swap(SiteIsolate::class, $siteIsolate); + + $arguments = [ + 'phpVersion' => $phpVersion, + '--secure' => $isSecure + ]; + if ($siteName) { + $arguments['--site'] = $siteName; + } + + $this->tester->run([ + 'command' => 'isolate', + ...$arguments + ]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + sprintf('The site [%s] is now using %s.', $expectedSiteName, $phpVersion), + $content + ); + } + + /** + * @test + */ + public function itWillFailWhenRequireArgumentIsNotAvailable(): void + { + Writer::fake(); + + $siteIsolate = Mockery::mock(SiteIsolate::class); + $siteIsolate->shouldReceive('isolateDirectory')->never(); + swap(SiteIsolate::class, $siteIsolate); + + $this->tester->run(['command' => 'isolate']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + sprintf('Please select version to isolate'), + $content + ); + } + + public function unIsolateDirectoryDataProvider(): array + { + return [ + [ + 'info.test', + 'info.test', + ], + [ + null, + basename((string)getcwd()), + ], + ]; + } + + /** + * @test + * @dataProvider unIsolateDirectoryDataProvider + */ + public function itWillUnIsolateDirectory(?string $domainName, string $expectedDomainName): void + { + Writer::fake(); + + $siteIsolate = Mockery::mock(SiteIsolate::class); + $siteIsolate->shouldReceive('unIsolateDirectory')->with($expectedDomainName)->once()->andReturnTrue(); + swap(SiteIsolate::class, $siteIsolate); + + $arguments = []; + if ($domainName) { + $arguments['--site'] = $domainName; + } + $this->tester->run(['command' => 'unisolate', ...$arguments]); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString( + sprintf('The site [%s] is now using the default PHP version.', $expectedDomainName), + $content + ); + } + + /** + * @test + */ + public function itWillListIsolatedDirectories(): void + { + Writer::fake(); + + $siteIsolate = Mockery::mock(SiteIsolate::class); + $siteIsolate->shouldReceive('isolatedDirectories') + ->withNoArgs() + ->andReturn( + collect([ + [ + 'url' => 'fpm-site.test', + 'secured' => '✓', + 'version' => '7.2', + ], + [ + 'url' => 'second.test', + 'secured' => '✕', + 'version' => '8.1' + ] + ]) + )->once(); + swap(SiteIsolate::class, $siteIsolate); + + $this->tester->run(['command' => 'isolated']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString('fpm-site.test | ✓ | 7.2', $content); + $this->assertStringContainsString('second.test | ✕ | 8.1', $content); + } + + /** + * @test + */ + public function itWillStoreNgrokAuthToken(): void + { + Writer::fake(); + + $ngrok = Mockery::mock(Ngrok::class); + $ngrok->shouldReceive('setAuthToken') + ->with('auth-token')->once(); + swap(Ngrok::class, $ngrok); + + $this->tester->run(['command' => 'ngrok-auth', 'authtoken' => 'auth-token']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString('Ngrok authentication token set', $content); + } + + /** + * @test + */ + public function itWillThrowErrorWhenTokenNotProvided(): void + { + Writer::fake(); + + $ngrok = Mockery::mock(Ngrok::class); + $ngrok->shouldReceive('setAuthToken')->never(); + swap(Ngrok::class, $ngrok); + + $this->tester->run(['command' => 'ngrok-auth']); + + $this->tester->assertCommandIsSuccessful(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $content = $output->fetch(); + $this->assertStringContainsString('Please provide ngrok auth token', $content); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..f1846ef --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,51 @@ +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 + $files = new Filesystem(); + if ($files->isDir(VALET_HOME_PATH)) { + $files->remove(VALET_HOME_PATH); + } + + Configuration::install(); + + // Keep this file empty, as it's tailed in a test + $files->touch(VALET_HOME_PATH . '/Log/nginx-error.log'); + + require __DIR__ . '/../cli/app.php'; + + /** @var Application $app */ + $this->app = $app; + $this->app->setAutoExit(false); + $this->tester = new ApplicationTester($app); + } +} diff --git a/tests/Unit/ConfigurationTest.php b/tests/Unit/ConfigurationTest.php new file mode 100644 index 0000000..3a09e84 --- /dev/null +++ b/tests/Unit/ConfigurationTest.php @@ -0,0 +1,426 @@ +filesystem = \Mockery::mock(Filesystem::class); + $this->configuration = new Configuration($this->filesystem); + } + + /** + * @test + */ + public function itWillInstallConfigurationSuccessfully(): void + { + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH, user()); + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with(VALET_HOME_PATH.'/Drivers') + ->andReturn(false); + $this->filesystem + ->shouldReceive('mkdirAsUser') + ->once() + ->with(VALET_HOME_PATH.'/Drivers') + ->andReturn(false); + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH.'/cli/stubs/SampleValetDriver.php') + ->once() + ->andReturn('Sample Valet Driver Content'); + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with(VALET_HOME_PATH.'/Drivers/SampleValetDriver.php', 'Sample Valet Driver Content'); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Sites', user()); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Extensions', user()); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Log', user()); + $this->filesystem + ->shouldReceive('touch') + ->once() + ->with(VALET_HOME_PATH.'/Log/nginx-error.log'); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Certificates', user()); + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(false); + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with( + VALET_HOME_PATH.'/config.json', + json_encode( + $this->defaultConfig(), + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + $this->filesystem + ->shouldReceive('chown') + ->once() + ->with(VALET_HOME_PATH.'/config.json', user()) + ->andReturn(false); + + $this->configuration->install(); + } + + /** + * @test + */ + public function itWillNotOverrideWhenAlreadyInstalled(): void + { + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH, user()); + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with(VALET_HOME_PATH.'/Drivers') + ->andReturn(true); + $this->filesystem + ->shouldNotReceive('mkdirAsUser'); + $this->filesystem + ->shouldNotReceive('get') + ->with(VALET_ROOT_PATH.'/cli/stubs/SampleValetDriver.php'); + $this->filesystem + ->shouldNotReceive('putAsUser') + ->with(VALET_HOME_PATH.'/Drivers/SampleValetDriver.php', 'Sample Valet Driver Content'); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Sites', user()); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Extensions', user()); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Log', user()); + $this->filesystem + ->shouldReceive('touch') + ->once() + ->with(VALET_HOME_PATH.'/Log/nginx-error.log'); + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH.'/Certificates', user()); + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(true); + $this->filesystem + ->shouldNotReceive('putAsUser') + ->with( + VALET_HOME_PATH.'/config.json', + json_encode( + $this->defaultConfig(), + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + $this->filesystem + ->shouldReceive('chown') + ->once() + ->with(VALET_HOME_PATH.'/config.json', user()) + ->andReturn(false); + + $this->configuration->install(); + } + + /** + * @test + */ + public function itWillUninstallSuccessfully(): void + { + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with(VALET_HOME_PATH) + ->andReturn(true); + $this->filesystem + ->shouldReceive('remove') + ->once() + ->with(VALET_HOME_PATH); + + $this->configuration->uninstall(); + } + + /** + * @test + */ + public function itWillNotUninstallWhenDirectoryNotExist(): void + { + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with(VALET_HOME_PATH) + ->andReturn(false); + $this->filesystem + ->shouldNotReceive('remove') + ->with(VALET_HOME_PATH); + + $this->configuration->uninstall(); + } + + /** + * @test + */ + public function itWillAddPathToConfig(): void + { + $defaultConfig = $this->defaultConfig(); + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(json_encode($defaultConfig)); + + $defaultConfig['paths'] = ['new_path']; + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + VALET_HOME_PATH.'/config.json', + json_encode( + $defaultConfig, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + + $this->configuration->addPath('new_path'); + } + + /** + * @test + */ + public function itWillRemovePathFromConfig(): void + { + $defaultConfig = $this->defaultConfig(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(json_encode([...$defaultConfig, 'paths' => ['new_path']])); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + VALET_HOME_PATH.'/config.json', + json_encode( + $defaultConfig, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + + $this->configuration->removePath('new_path'); + } + + /** + * @test + */ + public function itWillRemoveBrokenPathsFromConfig(): void + { + $defaultConfig = $this->defaultConfig(); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(json_encode([...$defaultConfig, 'paths' => ['new_path']])); + + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with('new_path') + ->andReturnFalse(); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + VALET_HOME_PATH.'/config.json', + json_encode( + $defaultConfig, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + + $this->configuration->prune(); + } + + public function configDataProvider(): array + { + return [ + [ + 'domain', + null, + 'test', + ], + [ + 'paths', + null, + [], + ], + [ + 'port', + null, + '80', + ], + [ + 'mysql', + [], + [], + ] + ]; + } + + /** + * @test + * @dataProvider configDataProvider + */ + public function itWillGetValueFromConfig(string $configKey, mixed $defaultValue, mixed $expectedValue): void + { + $defaultConfig = $this->defaultConfig(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(json_encode($defaultConfig)); + + $value = $this->configuration->get($configKey, $defaultValue); + + $this->assertSame($expectedValue, $value); + } + + public function updateConfigDataProvider(): array + { + return [ + [ + 'domain', + 'new_domain', + ], + [ + 'paths', + ['test_path', 'new_path'], + ], + [ + 'mysql', + ['user' => 'mysql', 'password' => 'password'], + ] + ]; + } + + /** + * @test + * @dataProvider updateConfigDataProvider + */ + public function itWillSetValueForConfig(string $configKey, mixed $updateValue): void + { + $defaultConfig = $this->defaultConfig(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(json_encode($defaultConfig)); + + $updatedConfig = [...$defaultConfig, ...[$configKey => $updateValue]]; + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with( + VALET_HOME_PATH.'/config.json', + json_encode( + $updatedConfig, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + + $value = $this->configuration->set($configKey, $updateValue); + + $this->assertSame($updatedConfig, $value); + } + + public function domainDataProvider(): array + { + return [ + [ + 'test', + 'test.test', + ], + [ + 'site.test', + 'site.test', + ], + [ + 'site.localhost', + 'site.localhost.test', + ] + ]; + } + + /** + * @test + * @dataProvider domainDataProvider + */ + public function itWillParseDomain(string $siteName, string $expectedDomain): void + { + $defaultConfig = $this->defaultConfig(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH.'/config.json') + ->andReturn(json_encode($defaultConfig)); + + $domain = $this->configuration->parseDomain($siteName); + + $this->assertSame($expectedDomain, $domain); + } + + private function defaultConfig(): array + { + return [ + 'domain' => 'test', + 'paths' => [], + 'port' => '80', + ]; + } +} diff --git a/tests/Unit/DevToolsTest.php b/tests/Unit/DevToolsTest.php new file mode 100644 index 0000000..36fda8a --- /dev/null +++ b/tests/Unit/DevToolsTest.php @@ -0,0 +1,204 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->commandLine = Mockery::mock(CommandLine::class); + $this->filesystem = Mockery::mock(Filesystem::class); + + $this->devTools = new DevTools( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem + ); + } + + /** + * @test + */ + public function itWillGetBinForGivenServiceUsingWhichCommand(): void + { + $service = 'service_name'; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('which ' . $service, $path); + return '/path/to/bin/service_name'; + }); + + $binPath = $this->devTools->getBin($service); + + $this->assertSame('/path/to/bin/service_name', $binPath); + } + + /** + * @test + */ + public function itWillGetBinForGivenServiceUsingLocateCommand(): void + { + $service = 'service_name'; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('which ' . $service, $path); + $callback(1, ''); + }); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('locate --regex bin/' . $service. '$', $path); + return "/path/to/bin/service_name\n/second-path/bin/service_name\n/third-path/bin/service_name\n"; + }); + + $binPath = $this->devTools->getBin($service); + + $this->assertSame('/path/to/bin/service_name', $binPath); + } + + /** + * @test + */ + public function itWillGetBinForGivenServiceAndExcludeGivenServiceByWhichCommand(): void + { + $service = 'service_name'; + $excludedServices = ['/path/to/bin/service_name']; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('which ' . $service, $path); + return '/path/to/bin/service_name'; + }); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('locate --regex bin/' . $service. '$', $path); + return "/path/to/bin/service_name\n/second-path/bin/service_name\n/third-path/bin/service_name\n"; + }); + + $binPath = $this->devTools->getBin($service, $excludedServices); + + $this->assertSame('/second-path/bin/service_name', $binPath); + } + + /** + * @test + */ + public function itWillGetBinForGivenServiceAndExcludeGivenServiceByLocateCommand(): void + { + $service = 'service_name'; + $excludedServices = ['/path/to/bin/service_name']; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('which ' . $service, $path); + $callback(1, ''); + }); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('locate --regex bin/' . $service. '$', $path); + return "/path/to/bin/service_name\n/second-path/bin/service_name\n/third-path/bin/service_name\n"; + }); + + $binPath = $this->devTools->getBin($service, $excludedServices); + + $this->assertSame('/second-path/bin/service_name', $binPath); + } + + /** + * @test + */ + public function itWillRunServiceWhenAvailable(): void + { + $service = 'service_name'; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('which ' . $service, $path); + return '/path/to/bin/service_name'; + }); + + $this->commandLine + ->shouldReceive('quietly') + ->once() + ->with('/path/to/bin/service_name /path/to/folder'); + + $this->devTools->run('/path/to/folder', $service); + } + + /** + * @test + */ + public function itWillShowOutputWhenServiceNotAvailableToRun(): void + { + Writer::fake(); + $service = 'service_name'; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('which ' . $service, $path); + $callback(1, ''); + }); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($path, $callback) use ($service) { + $this->assertSame('locate --regex bin/' . $service. '$', $path); + return ""; + }); + + $this->commandLine + ->shouldNotReceive('quietly'); + + $this->devTools->run('/path/to/folder', $service); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString('service_name not available', $output->fetch()); + } +} diff --git a/tests/Unit/DnsMasqTest.php b/tests/Unit/DnsMasqTest.php new file mode 100644 index 0000000..9a39d76 --- /dev/null +++ b/tests/Unit/DnsMasqTest.php @@ -0,0 +1,328 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->filesystem = Mockery::mock(Filesystem::class); + $this->commandLine = Mockery::mock(CommandLine::class); + + $this->dnsMasq = new DnsMasq( + $this->packageManager, + $this->serviceManager, + $this->filesystem, + $this->commandLine + ); + } + + /** + * @test + */ + public function itWillInstallSuccessfully(): void + { + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with('dnsmasq'); + + $this->serviceManager + ->shouldReceive('enable') + ->once() + ->with('dnsmasq'); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/etc/NetworkManager/conf.d'); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/etc/dnsmasq.d'); + + $this->filesystem + ->shouldReceive('uncommentLine') + ->once() + ->with('IGNORE_RESOLVCONF', '/etc/default/dnsmasq'); + + $this->filesystem + ->shouldReceive('isLink') + ->once() + ->with('/etc/resolv.conf') + ->andReturnFalse(); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function (string $command): string { + $this->assertSame('chattr -i /etc/resolv.conf', $command); + return ''; + }); + + $this->filesystem + ->shouldReceive('remove') + ->once() + ->with('/opt/valet-linux'); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/opt/valet-linux'); + + $this->serviceManager + ->shouldReceive('removeValetDns') + ->once(); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with('/etc/rc.local') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('restore') + ->once() + ->with('/etc/rc.local'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with('/etc/dnsmasq.d/network-manager'); + + $this->filesystem + ->shouldReceive('backup') + ->once() + ->with('/etc/dnsmasq.conf'); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH.'/cli/stubs/dnsmasq.conf') + ->andReturn('dnsmasq.conf content'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with('/etc/dnsmasq.conf', 'dnsmasq.conf content'); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH.'/cli/stubs/dnsmasq_options') + ->andReturn('dnsmasq_options content'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with('/etc/dnsmasq.d/options', 'dnsmasq_options content'); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH.'/cli/stubs/networkmanager.conf') + ->andReturn('networkmanager.conf content'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with('/etc/NetworkManager/conf.d/valet.conf', 'networkmanager.conf content'); + + $this->serviceManager + ->shouldReceive('disabled') + ->with('systemd-resolved') + ->andReturnFalse(); + + $this->serviceManager + ->shouldReceive('disable') + ->with('systemd-resolved'); + + $this->serviceManager + ->shouldReceive('stop') + ->with('systemd-resolved'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + '/etc/dnsmasq.d/valet', + 'address=/.test/127.0.0.1'.PHP_EOL.'server=1.1.1.1'.PHP_EOL.'server=8.8.8.8'.PHP_EOL + ); + + $this->serviceManager + ->shouldReceive('restart') + ->with('dnsmasq'); + + $this->dnsMasq->install('test'); + } + + /** + * @test + */ + public function itWillUninstallSuccessfully(): void + { + Writer::fake(); + + $this->serviceManager + ->shouldReceive('removeValetDns') + ->once(); + + $this->commandLine + ->shouldReceive('passthru') + ->with('rm -rf /opt/valet-linux'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with('/etc/dnsmasq.d/valet'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with('/etc/dnsmasq.d/options'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with('/etc/NetworkManager/conf.d/valet.conf'); + + $this->filesystem + ->shouldReceive('restore') + ->once() + ->with('/etc/systemd/resolved.conf'); + + $this->filesystem + ->shouldReceive('isLink') + ->once() + ->with('/etc/resolv.conf') + ->andReturnFalse(); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function (string $command): string { + $this->assertSame('chattr -i /etc/resolv.conf', $command); + return ''; + }); + + $this->filesystem + ->shouldReceive('restore') + ->once() + ->with('/etc/rc.local'); + + $this->commandLine + ->shouldReceive('passthru') + ->once() + ->with('rm -f /etc/resolv.conf'); + + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('systemd-resolved'); + + $this->serviceManager + ->shouldReceive('start') + ->once() + ->with('systemd-resolved'); + + $this->filesystem + ->shouldReceive('symlink') + ->once() + ->with('/run/systemd/resolve/resolv.conf', '/etc/resolv.conf'); + + $this->filesystem + ->shouldReceive('restore') + ->once() + ->with('/etc/dnsmasq.conf'); + + $this->filesystem + ->shouldReceive('commentLine') + ->once() + ->with('IGNORE_RESOLVCONF', '/etc/default/dnsmasq'); + + $this->packageManager + ->shouldReceive('restartNetworkManager') + ->once() + ->withNoArgs(); + + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with('dnsmasq'); + + $this->dnsMasq->uninstall(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + + $this->assertStringContainsString('Valet DNS changes have been rolled back', $output->fetch()); + } + + /** + * @test + */ + public function itWillStopServiceSuccessfully(): void + { + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('dnsmasq'); + + $this->dnsMasq->stop(); + } + + /** + * @test + */ + public function itWillRestartServiceSuccessfully(): void + { + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with('dnsmasq'); + + $this->dnsMasq->restart(); + } + + /** + * @test + */ + public function itWillUpdateDomainSuccessfully(): void + { + $domain = 'local'; + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + '/etc/dnsmasq.d/valet', + 'address=/.'.$domain.'/127.0.0.1'.PHP_EOL.'server=1.1.1.1'.PHP_EOL.'server=8.8.8.8'.PHP_EOL + ); + + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with('dnsmasq'); + + $this->dnsMasq->updateDomain($domain); + } +} diff --git a/tests/Unit/MailpitTest.php b/tests/Unit/MailpitTest.php new file mode 100644 index 0000000..dc3855a --- /dev/null +++ b/tests/Unit/MailpitTest.php @@ -0,0 +1,211 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->filesystem = Mockery::mock(Filesystem::class); + $this->commandLine = Mockery::mock(CommandLine::class); + + $this->config = Mockery::mock(Configuration::class); + swap(Configuration::class, $this->config); + + $this->siteProxy = Mockery::mock(SiteProxy::class); + swap(SiteProxy::class, $this->siteProxy); + + $this->siteSecure = Mockery::mock(SiteSecure::class); + swap(SiteSecure::class, $this->siteSecure); + + $this->mailpit = new Mailpit( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem + ); + } + + /** + * @test + */ + public function itWillInstallSuccessfully(): void + { + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturnUsing(function ($command) { + $this->assertSame('which mailpit', $command); + return false; + }); + + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with('curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh | bash'); + + $this->serviceManager + ->shouldReceive('isSystemd') + ->once() + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/init/mailpit') + ->andReturn('service file content'); + + $this->filesystem + ->shouldReceive('put') + ->once() + ->with('/etc/systemd/system/mailpit.service', 'service file content'); + + $this->serviceManager + ->shouldReceive('enable') + ->once() + ->with('mailpit'); + + $this->config + ->shouldReceive('get') + ->twice() + ->with('domain') + ->andReturn('test'); + + $this->siteProxy + ->shouldReceive('proxyCreate') + ->once() + ->with('mails.test', 'http://localhost:8025', true); + + $this->serviceManager + ->shouldReceive('start') + ->once() + ->with('mailpit'); + + $this->serviceManager + ->shouldReceive('disabled') + ->once() + ->with('mailhog') + ->andReturnFalse(); + + $this->serviceManager + ->shouldReceive('disable') + ->once() + ->with('mailhog'); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with('/opt/valet-linux/mailhog') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('remove') + ->once() + ->with('/opt/valet-linux/mailhog'); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . "/Nginx/mailhog.test") + ->andReturnTrue(); + + $this->siteSecure + ->shouldReceive('unsecure') + ->once() + ->with('mailhog.test'); + + $this->mailpit->install(); + } + + /** + * @test + */ + public function itWillStartServiceSuccessfully(): void + { + $this->serviceManager + ->shouldReceive('start') + ->once() + ->with('mailpit'); + + $this->mailpit->start(); + } + + /** + * @test + */ + public function itWillRestartServiceSuccessfully(): void + { + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with('mailpit'); + + $this->mailpit->restart(); + } + + /** + * @test + */ + public function itWillStopServiceSuccessfully(): void + { + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('mailpit'); + + $this->mailpit->stop(); + } + + /** + * @test + */ + public function itWillPrintStatus(): void + { + $this->serviceManager + ->shouldReceive('printStatus') + ->once() + ->with('mailpit'); + + $this->mailpit->status(); + } + + /** + * @test + */ + public function itWillUninstallServiceSuccessfully(): void + { + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('mailpit'); + + $this->mailpit->uninstall(); + } +} diff --git a/tests/Unit/MockResponse/github_response.json b/tests/Unit/MockResponse/github_response.json new file mode 100644 index 0000000..c73bb63 --- /dev/null +++ b/tests/Unit/MockResponse/github_response.json @@ -0,0 +1,41 @@ +{ + "url": "https://api.github.com/repos/genesisweb/valet-linux-plus/releases/150098134", + "assets_url": "https://api.github.com/repos/genesisweb/valet-linux-plus/releases/150098134/assets", + "upload_url": "https://uploads.github.com/repos/genesisweb/valet-linux-plus/releases/150098134/assets{?name,label}", + "html_url": "https://github.com/genesisweb/valet-linux-plus/releases/tag/1.6.9", + "id": 150098134, + "author": { + "login": "uttamrabadiya", + "id": 48178542, + "node_id": "MDQ6VXNlcjQ4MTc4NTQy", + "avatar_url": "https://avatars.githubusercontent.com/u/48178542?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/uttamrabadiya", + "html_url": "https://github.com/uttamrabadiya", + "followers_url": "https://api.github.com/users/uttamrabadiya/followers", + "following_url": "https://api.github.com/users/uttamrabadiya/following{/other_user}", + "gists_url": "https://api.github.com/users/uttamrabadiya/gists{/gist_id}", + "starred_url": "https://api.github.com/users/uttamrabadiya/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/uttamrabadiya/subscriptions", + "organizations_url": "https://api.github.com/users/uttamrabadiya/orgs", + "repos_url": "https://api.github.com/users/uttamrabadiya/repos", + "events_url": "https://api.github.com/users/uttamrabadiya/events{/privacy}", + "received_events_url": "https://api.github.com/users/uttamrabadiya/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDODQn61c4I8lDW", + "tag_name": "1.6.9", + "target_commitish": "master", + "name": "1.6.9", + "draft": false, + "prerelease": false, + "created_at": "2024-04-07T07:07:57Z", + "published_at": "2024-04-07T07:10:08Z", + "assets": [ + + ], + "tarball_url": "https://api.github.com/repos/genesisweb/valet-linux-plus/tarball/1.6.9", + "zipball_url": "https://api.github.com/repos/genesisweb/valet-linux-plus/zipball/1.6.9", + "body": "Fixes:\r\n\r\n- Updated supported version\r\n- Updated ngrok binary" +} diff --git a/tests/Unit/MockResponse/ngrok_response.json b/tests/Unit/MockResponse/ngrok_response.json new file mode 100644 index 0000000..33c10b5 --- /dev/null +++ b/tests/Unit/MockResponse/ngrok_response.json @@ -0,0 +1 @@ +{"tunnels":[{"name":"command_line","ID":"204020b4b272cf3ed13630aa5c18772b","uri":"/api/tunnels/command_line","public_url":"https://33e2-2405-201-2024-a899-720-a588-f13d-82fe.ngrok-free.app","proto":"http","config":{"addr":"http://info.localhost:80","inspect":true},"metrics":{"conns":{"count":0,"gauge":0,"rate1":0,"rate5":0,"rate15":0,"p50":0,"p90":0,"p95":0,"p99":0},"http":{"count":0,"rate1":0,"rate5":0,"rate15":0,"p50":0,"p90":0,"p95":0,"p99":0}}}],"uri":"/api/tunnels"} diff --git a/tests/Unit/MysqlTest.php b/tests/Unit/MysqlTest.php new file mode 100644 index 0000000..e82992a --- /dev/null +++ b/tests/Unit/MysqlTest.php @@ -0,0 +1,378 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->commandLine = Mockery::mock(CommandLine::class); + $this->filesystem = Mockery::mock(Filesystem::class); + $this->config = Mockery::mock(Configuration::class); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mysql') + ->once() + ->andReturn('mysql-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mariadb') + ->once() + ->andReturn('mariadb-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mariadb-server') + ->once() + ->andReturnFalse(); + + $this->mysql = new Mysql( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config + ); + } + + public function packageDataProvider(): array + { + return [ + 'mysql' => [ + false, + 'mysql', + 'mysql-server', + ], + 'mariadb' => [ + true, + 'mariadb', + 'mariadb-server', + ], + ]; + } + + /** + * @test + * @dataProvider packageDataProvider + */ + public function itWillInstallSuccessfully(bool $useMariaDB, string $packageName, string $packageServerName): void + { + Writer::fake(); + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + $phpFpm->shouldReceive('getCurrentVersion')->once()->andReturn('8.2'); + + $this->packageManager + ->shouldReceive('packageName') + ->with($packageName) + ->once() + ->andReturn($packageServerName); + + $this->packageManager + ->shouldReceive('ensureInstalled') + ->with('php8.2-mysql') + ->once(); + + $this->packageManager + ->shouldReceive('installed') + ->with($packageServerName) + ->once() + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('installOrFail') + ->with($packageServerName) + ->once() + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mariadb') + ->twice() + ->andReturn('mariadb-server'); + + $this->serviceManager + ->shouldReceive('enable') + ->with($packageName) + ->once() + ->andReturnFalse(); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->andReturn(''); + + $this->config + ->shouldReceive('get') + ->with('mysql', []) + ->once() + ->andReturn([]); + + $this->config + ->shouldReceive('set') + ->with('mysql', ['user' => 'valet', 'password' => '']) + ->once() + ->andReturn([]); + + $this->mysql->install($useMariaDB); + } + + /** + * @test + */ + public function itWillNotOverrideWhenInstalled() + { + $this->packageManager + ->shouldReceive('packageName') + ->with('mysql') + ->once() + ->andReturn('mysql-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnTrue(); + + $mysql = new Mysql( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config + ); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnTrue(); + + $this->config + ->shouldReceive('get') + ->with('mysql', []) + ->once() + ->andReturn(['user' => 'valet', 'password' => 'valet-password']); + + $this->packageManager + ->shouldNotReceive('installOrFail') + ->with('mysql-server'); + $this->serviceManager + ->shouldNotReceive('enable') + ->with('mysql'); + + $mysql->install(); + } + + /** + * @test + */ + public function itWillNotOverrideWhenMariaDbInstalled() + { + $this->packageManager + ->shouldReceive('packageName') + ->with('mysql') + ->once() + ->andReturn('mysql-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mariadb') + ->once() + ->andReturn('mariadb-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mariadb-server') + ->once() + ->andReturnTrue(); + + $mysql = new Mysql( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config + ); + + $this->packageManager + ->shouldReceive('installed') + ->with('mariadb-server') + ->once() + ->andReturnTrue(); + + $this->config + ->shouldReceive('get') + ->with('mysql', []) + ->once() + ->andReturn(['user' => 'valet', 'password' => 'valet-password']); + + $this->packageManager + ->shouldNotReceive('installOrFail') + ->with('mariadb-server'); + $this->serviceManager + ->shouldNotReceive('enable') + ->with('mariadb'); + + $mysql->install(true); + } + + /** + * @test + */ + public function itWillStopService(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->with('mysql') + ->once() + ->andReturn('mysql-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnTrue(); + + $mysql = new Mysql( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config + ); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mariadb') + ->once() + ->andReturn('mariadb-server'); + + $this->serviceManager + ->shouldReceive('stop') + ->with('mysql') + ->once() + ->andReturnTrue(); + + $mysql->stop(); + } + + /** + * @test + */ + public function itWillRestartService(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->with('mysql') + ->once() + ->andReturn('mysql-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnTrue(); + + $mysql = new Mysql( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config + ); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mariadb') + ->once() + ->andReturn('mariadb-server'); + + $this->serviceManager + ->shouldReceive('restart') + ->with('mysql') + ->once() + ->andReturnTrue(); + + $mysql->restart(); + } + + /** + * @test + */ + public function itWillUninstallService(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->with('mysql') + ->once() + ->andReturn('mysql-server'); + + $this->packageManager + ->shouldReceive('installed') + ->with('mysql-server') + ->once() + ->andReturnTrue(); + + $mysql = new Mysql( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config + ); + + $this->packageManager + ->shouldReceive('packageName') + ->with('mariadb') + ->once() + ->andReturn('mariadb-server'); + + $this->serviceManager + ->shouldReceive('stop') + ->with('mysql') + ->once() + ->andReturnTrue(); + + $mysql->uninstall(); + } +} diff --git a/tests/Unit/NginxTest.php b/tests/Unit/NginxTest.php new file mode 100644 index 0000000..92a6239 --- /dev/null +++ b/tests/Unit/NginxTest.php @@ -0,0 +1,388 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->filesystem = Mockery::mock(Filesystem::class); + $this->commandLine = Mockery::mock(CommandLine::class); + $this->config = Mockery::mock(Configuration::class); + $this->siteSecure = Mockery::mock(SiteSecure::class); + + $this->nginx = new Nginx( + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->config, + $this->siteSecure + ); + } + + /** + * @test + */ + public function itWillInstallSuccessfully(): void + { + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with('nginx'); + + $this->serviceManager + ->shouldReceive('enable') + ->once() + ->with('nginx'); + + $this->packageManager + ->shouldReceive('installed') + ->with('apache2') + ->once() + ->andReturnTrue(); + + $this->serviceManager + ->shouldReceive('disabled') + ->once() + ->with('apache2') + ->andReturnFalse(); + + $this->serviceManager + ->shouldReceive('disable') + ->once() + ->with('apache2'); + + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('apache2'); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/etc/nginx/sites-available'); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/etc/nginx/sites-enabled'); + + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('nginx'); + + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/nginx.conf') + ->once() + ->andReturn('nginx.conf content'); + + $this->commandLine + ->shouldReceive('run') + ->with('cat /lib/systemd/system/nginx.service') + ->once() + ->andReturn('pid /run/nginx.pid'); + + $this->filesystem + ->shouldReceive('backup') + ->with('/etc/nginx/nginx.conf') + ->once(); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with('/etc/nginx/nginx.conf', 'nginx.conf content') + ->once(); + + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/valet.conf') + ->once() + ->andReturn('valet.conf content'); + + $this->config + ->shouldReceive('get') + ->with('port') + ->once() + ->andReturn('80'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with('/etc/nginx/sites-available/valet.conf', 'valet.conf content') + ->once(); + + $this->filesystem + ->shouldReceive('exists') + ->with('/etc/nginx/sites-enabled/default') + ->once() + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('unlink') + ->with('/etc/nginx/sites-enabled/default') + ->once(); + + $this->commandLine + ->shouldReceive('run') + ->with('ln -snf /etc/nginx/sites-available/valet.conf /etc/nginx/sites-enabled/valet.conf') + ->once(); + + $this->filesystem + ->shouldReceive('backup') + ->with('/etc/nginx/fastcgi_params') + ->once(); + + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/fastcgi_params') + ->once() + ->andReturn('fastcgi_params content'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with('/etc/nginx/fastcgi_params', 'fastcgi_params content') + ->once(); + + $this->filesystem + ->shouldReceive('isDir') + ->with(VALET_HOME_PATH . '/Nginx') + ->once() + ->andReturnFalse(); + + $this->filesystem + ->shouldReceive('mkdirAsUser') + ->with(VALET_HOME_PATH . '/Nginx') + ->once(); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with(VALET_HOME_PATH . '/Nginx/.keep', "\n") + ->once(); + + $this->config + ->shouldReceive('get') + ->with('domain') + ->once() + ->andReturn('test'); + + $this->siteSecure + ->shouldReceive('reSecureForNewDomain') + ->with('test', 'test') + ->once(); + + $this->nginx->install(); + } + + /** + * @test + */ + public function itWillUpdatePort(): void + { + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/valet.conf') + ->once() + ->andReturn('valet.conf content VALET_PORT'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with('/etc/nginx/sites-available/valet.conf', 'valet.conf content 88') + ->once(); + + $this->nginx->updatePort('88'); + } + + /** + * @test + */ + public function itWillRestartService(): void + { + $this->serviceManager + ->shouldReceive('restart') + ->with('nginx') + ->once(); + + $this->nginx->restart(); + } + + /** + * @test + */ + public function itWillStopService(): void + { + $this->serviceManager + ->shouldReceive('stop') + ->with('nginx') + ->once(); + + $this->nginx->stop(); + } + + /** + * @test + */ + public function itWillPrintStatusOfService(): void + { + $this->serviceManager + ->shouldReceive('printStatus') + ->with('nginx') + ->once(); + + $this->nginx->status(); + } + + /** + * @test + */ + public function itWillUninstall(): void + { + $this->serviceManager + ->shouldReceive('stop') + ->with('nginx') + ->once(); + + $this->filesystem + ->shouldReceive('restore') + ->with('/etc/nginx/nginx.conf') + ->once(); + + $this->filesystem + ->shouldReceive('restore') + ->with('/etc/nginx/fastcgi_params') + ->once(); + + $this->filesystem + ->shouldReceive('unlink') + ->with('/etc/nginx/sites-enabled/valet.conf') + ->once(); + + $this->filesystem + ->shouldReceive('unlink') + ->with('/etc/nginx/sites-available/valet.conf') + ->once(); + + $this->filesystem + ->shouldReceive('exists') + ->with('/etc/nginx/sites-available/default') + ->once() + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('symlink') + ->with('/etc/nginx/sites-available/default', '/etc/nginx/sites-enabled/default') + ->once(); + + $this->nginx->uninstall(); + } + + /** + * @test + */ + public function itWillListConfiguredSites(): void + { + $this->filesystem + ->shouldReceive('scandir') + ->with(VALET_HOME_PATH . '/Nginx') + ->once() + ->andReturn(['site1', 'site2', 'site3', '.hiddenSite']); + + $output = $this->nginx->configuredSites(); + + $this->assertSame(['site1', 'site2', 'site3'], $output->all()); + } + + /** + * @test + */ + public function itWillInstallServerByGivenPhpVersion(): void + { + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + + $phpFpm->shouldReceive('socketFileName') + ->with('8.2') + ->once() + ->andReturn('valet82.sock'); + + $this->config + ->shouldReceive('get') + ->with('port') + ->once() + ->andReturn('80'); + + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/valet.conf') + ->once() + ->andReturn('valet.conf content VALET_FPM_SOCKET_FILE VALET_PORT'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + '/etc/nginx/sites-available/valet.conf', + \sprintf('valet.conf content %s 80', VALET_HOME_PATH . '/valet82.sock') + ) + ->once(); + + $this->filesystem + ->shouldReceive('exists') + ->with('/etc/nginx/sites-enabled/default') + ->once() + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('unlink') + ->with('/etc/nginx/sites-enabled/default') + ->once(); + + $this->commandLine + ->shouldReceive('run') + ->with('ln -snf /etc/nginx/sites-available/valet.conf /etc/nginx/sites-enabled/valet.conf') + ->once(); + + $this->filesystem + ->shouldReceive('backup') + ->with('/etc/nginx/fastcgi_params') + ->once(); + + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/fastcgi_params') + ->once() + ->andReturn('fastcgi_params content'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with('/etc/nginx/fastcgi_params', 'fastcgi_params content') + ->once(); + + $this->nginx->installServer('8.2'); + } +} diff --git a/tests/Unit/NgrokTest.php b/tests/Unit/NgrokTest.php new file mode 100644 index 0000000..dd071fe --- /dev/null +++ b/tests/Unit/NgrokTest.php @@ -0,0 +1,197 @@ +commandLine = Mockery::mock(CommandLine::class); + $this->filesystem = Mockery::mock(Filesystem::class); + + $this->ngrok = new Ngrok( + $this->commandLine, + $this->filesystem + ); + } + + /** + * @test + */ + public function itWillInstallSuccessfully(): void + { + Writer::fake(); + + $request = Mockery::mock(Request::class); + swap(Request::class, $request); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(\sprintf('%s/bin/ngrok', VALET_ROOT_PATH)) + ->andReturnFalse(); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(\sprintf('%s/bin', VALET_ROOT_PATH), user()); + + $request->shouldReceive('get') + ->with('https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz') + ->once() + ->andReturnSelf(); + + $request->shouldReceive('send') + ->withNoArgs() + ->once() + ->andReturn(new Response('ngrok-body-content', 'headers 0', $request)); + + $this->filesystem + ->shouldReceive('putAsUser') + ->with( + \sprintf('%s/bin/ngrok-v3-stable-linux-amd64.tgz', VALET_ROOT_PATH), + 'ngrok-body-content' + ) + ->once(); + + \Valet\Facades\Filesystem::ensureDirExists(\sprintf('%s/bin', VALET_ROOT_PATH), user()); + $this->createFakeZip(\sprintf('%s/bin/ngrok-v3-stable-linux-amd64.tgz', VALET_ROOT_PATH)); + + $this->filesystem + ->shouldReceive('remove') + ->with( + \sprintf('%s/bin/ngrok-v3-stable-linux-amd64.tgz', VALET_ROOT_PATH) + ) + ->once(); + + $this->ngrok->install(); + + unlink(\sprintf('%s/bin/ngrok-v3-stable-linux-amd64.tgz', VALET_ROOT_PATH)); + unlink(\sprintf('%s/bin/sample_file', VALET_ROOT_PATH)); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + $content = $output->fetch(); + $this->assertStringContainsString('Ngrok', $content); + $this->assertStringContainsString('Installing', $content); + } + + /** + * @test + */ + public function itWillNotOverrideWhenAlreadyInstalled(): void + { + Writer::fake(); + + $request = Mockery::mock(Request::class); + swap(Request::class, $request); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(\sprintf('%s/bin/ngrok', VALET_ROOT_PATH)) + ->andReturnTrue(); + + $this->filesystem + ->shouldNotReceive('ensureDirExists'); + + $request->shouldNotReceive('get') + ->with('https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz'); + + $request->shouldNotReceive('send'); + + $this->filesystem + ->shouldNotReceive('putAsUser'); + + $this->filesystem + ->shouldNotReceive('remove'); + + $this->ngrok->install(); + + /** @var BufferedOutput $output */ + $output = Writer::output(); + $content = $output->fetch(); + $this->assertStringNotContainsString('Ngrok', $content); + $this->assertStringNotContainsString('Installing', $content); + } + + /** + * @test + */ + public function itWillFetchCurrentTunnelUrl(): void + { + $request = Mockery::mock(Request::class); + swap(Request::class, $request); + Httpful::register(Mime::JSON, new JsonHandler(array('decode_as_array' => false))); + + $request->shouldReceive('get') + ->once() + ->with('http://127.0.0.1:4040/api/tunnels') + ->andReturnSelf(); + + $mockResponse = $this->getFile('ngrok_response.json'); + $request->shouldReceive('send') + ->once() + ->withNoArgs() + ->andReturn(new Response((string) $mockResponse, "HTTP/1.1 200 OK\r\n +Content-Type: application/json\r\n +Date: Tue, 14 May 2024 12:13:28 GMT\r\n +Content-Length: 477 +", $request)); + + $output = $this->ngrok->currentTunnelUrl(); + + $this->assertSame('https://33e2-2405-201-2024-a899-720-a588-f13d-82fe.ngrok-free.app', $output); + } + + /** + * @test + */ + public function itWillSetNgrokAuthToken(): void + { + $this->commandLine + ->shouldReceive('run') + ->once() + ->with( + \sprintf('%s/bin/ngrok config add-authtoken test-token', VALET_ROOT_PATH) + ); + + $this->ngrok->setAuthToken('test-token'); + } + + private function createFakeZip(string $file): void + { + $zip = new \ZipArchive(); + $zip->open($file, \ZipArchive::CREATE); + $zip->addFromString('sample_file', 'sample content'); + $zip->close(); + } + + private function getFile(string $fileName): string|false + { + return file_get_contents(__DIR__ . '/MockResponse/' . $fileName); + } +} diff --git a/tests/Unit/PhpFpmTest.php b/tests/Unit/PhpFpmTest.php new file mode 100644 index 0000000..ca8b25a --- /dev/null +++ b/tests/Unit/PhpFpmTest.php @@ -0,0 +1,656 @@ +config = Mockery::mock(Configuration::class); + $this->commandLine = Mockery::mock(CommandLine::class); + $this->packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->filesystem = Mockery::mock(Filesystem::class); + $this->site = Mockery::mock(Site::class); + $this->nginx = Mockery::mock(Nginx::class); + + $this->phpFpm = new PhpFpm( + $this->config, + $this->packageManager, + $this->serviceManager, + $this->commandLine, + $this->filesystem, + $this->site, + $this->nginx + ); + } + + public function versionProvider(): array + { + return [ + [ + '8.2', + '8.2', + 'valet82.sock', + '8.0', + ], + [ + '82', + '8.2', + 'valet82.sock', + '8.0', + ], + [ + 'php-8.2', + '8.2', + 'valet82.sock', + '8.0', + ], + [ + 'php8.2', + '8.2', + 'valet82.sock', + '8.0', + ], + [ + 'php@8.2', + '8.2', + 'valet82.sock', + '8.0', + ], + [ + '8.3', + '8.3', + 'valet83.sock', + '8.0', + ], + [ + '83', + '8.3', + 'valet83.sock', + '8.0', + ], + [ + 'php-8.3', + '8.3', + 'valet83.sock', + '8.0', + ], + [ + 'php8.3', + '8.3', + 'valet83.sock', + '8.0', + ], + [ + 'php@8.3', + '8.3', + 'valet83.sock', + '8.0', + ], + ]; + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillInstallSuccessfully( + string $version, + string $expectedVersion, + string $expectedSocketFileName + ): void { + $fpmName = \sprintf('php%s-fpm', $expectedVersion); + $prefix = \sprintf('php%s-', $expectedVersion); + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->times(3) + ->with($expectedVersion) + ->andReturn($fpmName); + + $this->packageManager + ->shouldReceive('installed') + ->once() + ->with($fpmName) + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with($fpmName) + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('getPhpExtensionPrefix') + ->once() + ->with($expectedVersion) + ->andReturn($prefix); + + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with( + \sprintf( + '%1$scli %1$smysql %1$sgd %1$szip %1$sxml %1$scurl %1$smbstring %1$spgsql %1$sintl %1$sposix', + $prefix + ) + ); + + $this->serviceManager + ->shouldReceive('enable') + ->once() + ->with($fpmName); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/var/log', user()); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/fpm.conf') + ->andReturn('fpm.conf content VALET_USER VALET_GROUP VALET_FPM_SOCKET_FILE'); + + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with('/etc/php/' . $expectedVersion . '/fpm/pool.d') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with( + '/etc/php/' . $expectedVersion . '/fpm/pool.d/valet.conf', + \sprintf('fpm.conf content %s %s %s', user(), group(), VALET_HOME_PATH . '/' . $expectedSocketFileName), + ) + ->andReturn('fpm.conf content VALET_USER VALET_GROUP VALET_FPM_SOCKET_FILE'); + + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with($fpmName); + + $this->phpFpm->install($version); + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillUninstallSuccessfully(string $version, string $expectedVersion): void + { + $this->filesystem + ->shouldReceive('isDir') + ->with('/etc/php/' . $expectedVersion . '/fpm/pool.d') + ->once() + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('exists') + ->with('/etc/php/' . $expectedVersion . '/fpm/pool.d/valet.conf') + ->once() + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('unlink') + ->with('/etc/php/' . $expectedVersion . '/fpm/pool.d/valet.conf') + ->once(); + + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->with($expectedVersion) + ->once() + ->andReturn('php' . $expectedVersion . '-fpm'); + + $this->serviceManager + ->shouldReceive('stop') + ->with('php' . $expectedVersion . '-fpm') + ->once(); + + $this->phpFpm->uninstall($version); + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillSwitchVersionSuccessfully( + string $version, + string $expectedVersion, + string $expectedSocketFileName, + string $currentVersion + ): void { + Writer::fake(); + $this->config + ->shouldReceive('get') + ->with('php_version', PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION) + ->once() + ->andReturn($currentVersion); + + $fpmName = \sprintf('php%s-fpm', $expectedVersion); + $prefix = \sprintf('php%s-', $expectedVersion); + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->times(6) + ->with($expectedVersion) + ->andReturn($fpmName); + + $this->packageManager + ->shouldReceive('installed') + ->once() + ->with($fpmName) + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with($fpmName) + ->andReturnFalse(); + + $this->packageManager + ->shouldReceive('getPhpExtensionPrefix') + ->once() + ->with($expectedVersion) + ->andReturn($prefix); + + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with( + \sprintf( + '%1$scli %1$smysql %1$sgd %1$szip %1$sxml %1$scurl %1$smbstring %1$spgsql %1$sintl %1$sposix', + $prefix + ) + ); + + $this->serviceManager + ->shouldReceive('enable') + ->once() + ->with($fpmName); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with('/var/log', user()); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/fpm.conf') + ->andReturn('fpm.conf content VALET_USER VALET_GROUP VALET_FPM_SOCKET_FILE'); + + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with( + '/etc/php/' . $expectedVersion . '/fpm/pool.d', + ) + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with( + '/etc/php/' . $expectedVersion . '/fpm/pool.d/valet.conf', + \sprintf('fpm.conf content %s %s %s', user(), group(), VALET_HOME_PATH . '/' . $expectedSocketFileName), + ) + ->andReturn('fpm.conf content VALET_USER VALET_GROUP VALET_FPM_SOCKET_FILE'); + + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with($fpmName); + + $this->serviceManager + ->expects('disabled') + ->once() + ->with($fpmName) + ->andReturnTrue(); + + $this->serviceManager + ->expects('enable') + ->once() + ->with($fpmName); + + $this->config + ->shouldReceive('set') + ->once() + ->with('php_version', $expectedVersion); + + $configuredSites = [ + 'mails.test' => [ + 'socket_file' => 'valet74.sock', + ], + 'site1.test' => [ + 'socket_file' => 'valet73.sock', + ], + 'valetsite.test' => [ + 'socket_file' => 'valet70.sock', + ], + ]; + $this->nginx + ->shouldReceive('configuredSites') + ->twice() + ->andReturn(collect(array_keys($configuredSites))); + + foreach ($configuredSites as $configuredSite => $siteData) { + $this->filesystem + ->shouldReceive('get') + ->twice() + ->with(VALET_HOME_PATH . '/Nginx/' . $configuredSite) + ->andReturn('content unix:' . $siteData['socket_file']); + + $this->filesystem + ->shouldReceive('put') + ->once() + ->with( + VALET_HOME_PATH . '/Nginx/' . $configuredSite, + 'content unix:' . VALET_HOME_PATH . '/' . $expectedSocketFileName + ); + } + + $this->config + ->shouldReceive('get') + ->with('php_version', PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION) + ->once() + ->andReturn($expectedVersion); + + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->with($currentVersion) + ->once() + ->andReturn('php' . $currentVersion . '-fpm'); + + $this->serviceManager + ->shouldReceive('stop') + ->with('php' . $currentVersion . '-fpm') + ->once(); + + $this->nginx + ->shouldReceive('installServer') + ->once() + ->andReturn($expectedVersion); + + $this->nginx + ->shouldReceive('restart') + ->once(); + + $this->serviceManager + ->shouldReceive('printStatus') + ->with($fpmName) + ->once(); + + $this->commandLine + ->shouldReceive('run') + ->with('update-alternatives --set php /usr/bin/php' . $expectedVersion) + ->once(); + + $this->phpFpm->switchVersion($version, true); + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillRestartSuccessfully(string $version, string $expectedVersion): void + { + $fpmName = 'php' . $expectedVersion . '-fpm'; + + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->with($expectedVersion) + ->andReturn($fpmName); + + $this->serviceManager + ->shouldReceive('restart') + ->with($fpmName) + ->once(); + + $this->phpFpm->restart($expectedVersion); + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillStopSuccessfully(string $version, string $expectedVersion): void + { + $fpmName = 'php' . $expectedVersion . '-fpm'; + + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->with($expectedVersion) + ->andReturn($fpmName); + + $this->serviceManager + ->shouldReceive('stop') + ->with($fpmName) + ->once(); + + $this->phpFpm->stop($expectedVersion); + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillGetStatusSuccessfully(string $version, string $expectedVersion): void + { + $fpmName = 'php' . $expectedVersion . '-fpm'; + + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->with($expectedVersion) + ->andReturn($fpmName); + + $this->serviceManager + ->shouldReceive('printStatus') + ->with($fpmName) + ->once(); + + $this->phpFpm->status($expectedVersion); + } + + public function socketFileVersionProvider(): array + { + return [ + [ + '8.2', + 'valet82.sock', + ], + [ + '8.1', + 'valet81.sock', + ], + [ + '8.3', + 'valet83.sock', + ], + ]; + } + + /** + * @test + * @dataProvider socketFileVersionProvider + */ + public function itWillGetSocketFileName(string $version, string $expectedSocket): void + { + $socketFile = $this->phpFpm->socketFileName($version); + + $this->assertSame($expectedSocket, $socketFile); + } + + /** + * @test + * @dataProvider versionProvider + */ + public function itWillNormalizePhpVersion(string $phpVersion, string $expectedVersion): void + { + $normalizedVersion = $this->phpFpm->normalizePhpVersion($phpVersion); + + $this->assertSame($expectedVersion, $normalizedVersion); + } + + public function invalidVersionProvider(): array + { + return [ + [ + 'invalid', + ], + [ + '8', + ], + [ + 'invalid-8.0', + ], + ]; + } + + /** + * @test + * @dataProvider invalidVersionProvider + */ + public function itWillReturnBlankStringWhenInvalidStringGiven(string $version): void + { + $normalizedVersion = $this->phpFpm->normalizePhpVersion($version); + + $this->assertSame('', $normalizedVersion); + } + + public function executableVersionProvider(): array + { + return [ + [ + '8.1', + 'valet81.sock', + ], + [ + '8.2', + 'valet82.sock', + ], + ]; + } + + /** + * @test + * @dataProvider executableVersionProvider + */ + public function itWillGetExecutablePath(string $version): void + { + $devTools = Mockery::mock(DevTools::class); + swap(DevTools::class, $devTools); + + $devTools->shouldReceive('getBin') + ->with('php' . $version, ['/usr/local/bin/php']) + ->once() + ->andReturn('/usr/bin/php' . $version); + + $binFile = $this->phpFpm->getPhpExecutablePath($version); + + $this->assertSame('/usr/bin/php' . $version, $binFile); + } + + /** + * @test + * @dataProvider executableVersionProvider + */ + public function itWillGetFpmSocketFile(string $version, string $expectedSocketFile): void + { + $socketFile = $this->phpFpm->fpmSocketFile($version); + + $this->assertSame(VALET_HOME_PATH . '/' . $expectedSocketFile, $socketFile); + } + + public function versionDataProvider(): array + { + return [ + [ + '8.2', + ], + [ + '8.3', + ] + ]; + } + + /** + * @test + * @dataProvider versionDataProvider + */ + public function itWillValidateVersion(string $version): void + { + $isValid = $this->phpFpm->validateVersion($version); + + $this->assertTrue($isValid); + } + + public function deprecatedVersionDataProvider(): array + { + return [ + [ + '7.0', + ], + [ + '7.1', + ], + [ + '7.2', + ], + [ + '7.3', + ], + [ + '7.4', + ], + [ + '8.0', + ], + [ + '8.1', + ], + ]; + } + + /** + * @test + * @dataProvider deprecatedVersionDataProvider + */ + public function itWillValidateDeprecatedVersion(string $version): void + { + $isValid = $this->phpFpm->validateVersion($version); + + $this->assertFalse($isValid); + } +} diff --git a/tests/Unit/RequirementsTest.php b/tests/Unit/RequirementsTest.php new file mode 100644 index 0000000..93c9fc9 --- /dev/null +++ b/tests/Unit/RequirementsTest.php @@ -0,0 +1,72 @@ +commandLine = Mockery::mock(CommandLine::class); + + $this->requirements = new Requirements( + $this->commandLine + ); + } + + /** + * @test + */ + public function itWillVerifyIfSELinuxIsEnabled(): void + { + $this->commandLine + ->shouldReceive('run') + ->with('sestatus') + ->once() + ->andReturn('@SELinux status: disabled'); + + $this->requirements->setIgnoreSELinux(false); + $this->requirements->check(); + } + + /** + * @test + */ + public function itWillThrowExceptionWhenSELinuxIsEnabledAndEnforcing(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('SELinux is in enforcing mode'); + + $this->commandLine + ->shouldReceive('run') + ->with('sestatus') + ->once() + ->andReturn("SELinux status: enabled\nCurrent mode: enforcing"); + + $this->requirements->setIgnoreSELinux(false); + $this->requirements->check(); + } + + /** + * @test + */ + public function itWillSkipCheckingSELinuxWhenIgnored(): void + { + $this->commandLine + ->shouldNotReceive('run') + ->with('sestatus'); + + $this->requirements->setIgnoreSELinux(); + $this->requirements->check(); + } +} diff --git a/tests/Unit/SiteIsolateTest.php b/tests/Unit/SiteIsolateTest.php new file mode 100644 index 0000000..d8bf53e --- /dev/null +++ b/tests/Unit/SiteIsolateTest.php @@ -0,0 +1,344 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->config = Mockery::mock(Configuration::class); + $this->filesystem = Mockery::mock(Filesystem::class); + $this->siteSecure = Mockery::mock(SiteSecure::class); + $this->site = Mockery::mock(Site::class); + + $this->siteIsolate = new SiteIsolate( + $this->packageManager, + $this->config, + $this->filesystem, + $this->siteSecure, + $this->site + ); + } + + /** + * @test + */ + public function itWillIsolateDirectory(): void + { + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + $nginx = Mockery::mock(Nginx::class); + swap(Nginx::class, $nginx); + $devTools = Mockery::mock(DevTools::class); + swap(DevTools::class, $devTools); + + $this->site + ->shouldReceive('getSiteUrl') + ->with('site') + ->once() + ->andReturn('site.test'); + $phpFpm->shouldReceive('normalizePhpVersion') + ->with('7.2') + ->once() + ->andReturn('7.2'); + + $this->packageManager + ->shouldReceive('getPhpFpmName') + ->with('7.2') + ->once() + ->andReturn('php7.2-fpm'); + + $this->packageManager + ->shouldReceive('installed') + ->with('php7.2-fpm') + ->once() + ->andReturnFalse(); + + $phpFpm->shouldReceive('install') + ->with('7.2') + ->once(); + + // Isolated PHP Version + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test') + ->andReturn("# ISOLATED_PHP_VERSION=7.1\nNginx content"); + // Isolated PHP Version END + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/secure.isolated.valet.conf') + ->andReturn('secure stub content'); + + $phpFpm->shouldReceive('fpmSocketFile') + ->with('7.2') + ->once() + ->andReturn('/var/run/php/php7.2-fpm.sock'); + + $this->siteSecure + ->shouldReceive('secure') + ->once() + ->with('site.test', 'secure stub content'); + + $phpFpm->shouldReceive('stopIfUnused') + ->with('7.1') + ->once(); + + $phpFpm->shouldReceive('restart') + ->with('7.2') + ->once(); + + $nginx->shouldReceive('restart') + ->withNoArgs() + ->once(); + + $this->config + ->shouldReceive('get') + ->once() + ->with('domain') + ->andReturn('test'); + + $devTools->shouldReceive('getBin') + ->once() + ->with('php7.2', ['/usr/local/bin/php']) + ->andReturn('/usr/bin/php7.2'); + + $this->config + ->shouldReceive('get') + ->once() + ->with('isolated_versions', []) + ->andReturn([]); + + $this->config + ->shouldReceive('set') + ->once() + ->with( + 'isolated_versions', + [ + 'site' => '/usr/bin/php7.2', + ] + ); + + $return = $this->siteIsolate->isolateDirectory('site', '7.2', true); + + $this->assertTrue($return); + } + + /** + * @test + */ + public function itWillUnIsolateDirectory(): void + { + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + $nginx = Mockery::mock(Nginx::class); + swap(Nginx::class, $nginx); + + $this->site + ->shouldReceive('getSiteUrl') + ->with('site') + ->once() + ->andReturn('site.test'); + + // Isolated PHP Version + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test') + ->andReturn("# ISOLATED_PHP_VERSION=7.2\nNginx content"); + // Isolated PHP Version END + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.crt') + ->andReturnTrue(); + + $this->siteSecure + ->shouldReceive('buildSecureNginxServer') + ->once() + ->with('site.test') + ->andReturn('site conf'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test', 'site conf'); + + $phpFpm->shouldReceive('stopIfUnused') + ->with('7.2') + ->once(); + + $nginx->shouldReceive('restart') + ->withNoArgs() + ->once(); + + $this->config + ->shouldReceive('get') + ->once() + ->with('domain') + ->andReturn('test'); + + $this->config + ->shouldReceive('get') + ->with('isolated_versions', []) + ->once() + ->andReturn(['site' => '/usr/local/bin/php7.2']); + + $this->config + ->shouldReceive('set') + ->with('isolated_versions', []) + ->once(); + + $this->siteIsolate->unIsolateDirectory('site'); + } + + /** + * @test + */ + public function itWillListIsolatedDirectories(): void + { + $nginx = Mockery::mock(Nginx::class); + swap(Nginx::class, $nginx); + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + + $this->siteSecure + ->shouldReceive('secured') + ->withNoArgs() + ->once() + ->andReturn(collect([ + 'site1.test' + ])); + + $dummySites = ['site1.test', 'site2.test']; + $nginx->shouldReceive('configuredSites') + ->withNoArgs() + ->once() + ->andReturn(collect($dummySites)); + + foreach ($dummySites as $dummySite) { + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $dummySite) + ->andReturn('ISOLATED_PHP_VERSION'); + + // Isolated PHP Version + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $dummySite) + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $dummySite) + ->andReturn("# ISOLATED_PHP_VERSION=7.2\nNginx content"); + // Isolated PHP Version END + + $phpFpm->shouldReceive('normalizePhpVersion') + ->with('7.2') + ->once() + ->andReturn('7.2'); + } + + $response = $this->siteIsolate->isolatedDirectories(); + $this->assertSame([ + [ + 'url' => 'https://site1.test', + 'secured' => '✓', + 'version' => '7.2', + ], + [ + 'url' => 'http://site2.test', + 'secured' => '✕', + 'version' => '7.2', + ], + ], $response->all()); + } + + /** + * @test + */ + public function itWillGetIsolatedPhpVersion(): void + { + $site = 'site.test'; + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $site) + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $site) + ->andReturn("# ISOLATED_PHP_VERSION=7.2\nNginx content"); + + $version = $this->siteIsolate->isolatedPhpVersion($site); + $this->assertSame('7.2', $version); + } + + /** + * @test + */ + public function itWillReturnNullWhenVersionNotFound(): void + { + $site = 'site.test'; + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $site) + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $site) + ->andReturn('Nginx content'); + + $version = $this->siteIsolate->isolatedPhpVersion($site); + $this->assertNull($version); + } +} diff --git a/tests/Unit/SiteLinkTest.php b/tests/Unit/SiteLinkTest.php new file mode 100644 index 0000000..0089222 --- /dev/null +++ b/tests/Unit/SiteLinkTest.php @@ -0,0 +1,160 @@ +filesystem = Mockery::mock(Filesystem::class); + $this->config = Mockery::mock(Configuration::class); + $this->siteSecure = Mockery::mock(SiteSecure::class); + + $this->siteLink = new SiteLink( + $this->filesystem, + $this->config, + $this->siteSecure + ); + } + + /** + * @test + */ + public function itWillLinkSite(): void + { + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH . '/Sites', user()); + + $this->config + ->shouldReceive('addPath') + ->once() + ->with(VALET_HOME_PATH . '/Sites', true); + + $this->filesystem + ->shouldReceive('symlinkAsUser') + ->once() + ->with('/test/home/path', VALET_HOME_PATH . '/Sites/path'); + + $host = $this->siteLink->link('/test/home/path', 'path'); + + $this->assertEquals(VALET_HOME_PATH . '/Sites/path', $host); + } + + /** + * @test + */ + public function itWillUnlinkSite(): void + { + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Sites/path') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with(VALET_HOME_PATH . '/Sites/path'); + + $this->siteLink->unlink('path'); + + $this->assertTrue(true); + } + + /** + * @test + */ + public function itWillNotUnlinkWhenSiteNotLinked(): void + { + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Sites/path') + ->andReturnFalse(); + + $this->filesystem + ->shouldNotReceive('unlink'); + + $this->siteLink->unlink('path'); + + $this->assertTrue(true); + } + + /** + * @test + */ + public function itWillLoadLinks(): void + { + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH . '/Certificates', user()); + + $this->siteSecure + ->shouldReceive('secured') + ->once() + ->withNoArgs() + ->andReturn(collect([ + 'site1.test' + ])); + + $this->config + ->shouldReceive('get') + ->once() + ->with('domain') + ->andReturn('test'); + + $this->config + ->shouldReceive('get') + ->once() + ->with('port', 80) + ->andReturn(80); + + $this->config + ->shouldReceive('get') + ->once() + ->with('https_port', 443) + ->andReturn(443); + + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with(VALET_HOME_PATH . '/Sites') + ->andReturn(['path']); + + $this->filesystem + ->shouldReceive('readLink') + ->once() + ->with(VALET_HOME_PATH . '/Sites/path') + ->andReturn('/test/home/path'); + + $links = $this->siteLink->links(); + + $this->assertSame([ + 'path' => [ + 'url' => 'http://path.test', + 'secured' => '✕', + 'path' => '/test/home/path', + ], + ], $links->toArray()); + } +} diff --git a/tests/Unit/SiteProxyTest.php b/tests/Unit/SiteProxyTest.php new file mode 100644 index 0000000..f15181b --- /dev/null +++ b/tests/Unit/SiteProxyTest.php @@ -0,0 +1,115 @@ +filesystem = Mockery::mock(Filesystem::class); + $this->config = Mockery::mock(Configuration::class); + $this->siteSecure = Mockery::mock(SiteSecure::class); + + $this->siteProxy = new SiteProxy( + $this->filesystem, + $this->config, + $this->siteSecure + ); + } + + /** + * @test + */ + public function itWillCreateProxy(): void + { + $this->config + ->shouldReceive('get') + ->once() + ->with('domain') + ->andReturn('test'); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/secure.proxy.valet.conf') + ->andReturn('VALET_PROXY_HOST'); + + $this->siteSecure + ->shouldReceive('secure') + ->once() + ->with('site.test', 'http://localhost:8000'); + + $this->siteProxy->proxyCreate('site', 'http://localhost:8000', true); + } + + /** + * @test + */ + public function itWillLoadProxies(): void + { + $this->config + ->shouldReceive('get') + ->once() + ->with('domain') + ->andReturn('test'); + + $this->siteSecure + ->shouldReceive('secured') + ->once() + ->withNoArgs() + ->andReturn(collect([ + 'site1.test' + ])); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with(VALET_HOME_PATH . '/Nginx') + ->andReturn([ + 'site1.test' + ]); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site1.test') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site1.test') + ->andReturn('proxy_pass http://localhost:8025;'); + + $response = $this->siteProxy->proxies(); + + $this->assertSame([ + 'site1.test' => [ + 'url' => 'https://site1.test', + 'secured' => '✓', + 'path' => 'http://localhost:8025', + ], + ], $response->all()); + } +} diff --git a/tests/Unit/SiteSecureTest.php b/tests/Unit/SiteSecureTest.php new file mode 100644 index 0000000..6232237 --- /dev/null +++ b/tests/Unit/SiteSecureTest.php @@ -0,0 +1,367 @@ +filesystem = Mockery::mock(Filesystem::class); + $this->commandLine = Mockery::mock(CommandLine::class); + $this->config = Mockery::mock(Configuration::class); + + $this->siteSecure = new SiteSecure( + $this->filesystem, + $this->commandLine, + $this->config + ); + } + + /** + * @test + */ + public function itWillSecureNewDomain(): void + { + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test') + ->andReturnFalse(); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH . '/CA', user()); + + $this->filesystem + ->shouldReceive('ensureDirExists') + ->once() + ->with(VALET_HOME_PATH . '/Certificates', user()); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem') + ->andReturnFalse(); + + $this->filesystem + ->shouldReceive('exists') + ->twice() + ->with(VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.key') + ->andReturnFalse(); + + $this->filesystem + ->shouldReceive('remove') + ->once() + ->with('/usr/local/share/ca-certificates/ValetLinuxCASelfSigned.pem.crt') + ->andReturnFalse(); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->with('sudo update-ca-certificates'); + + $caExpireInDate = (new \DateTime())->diff(new \DateTime("+20 years")); + $expiryInDays = (int)$caExpireInDate->format('%a'); // 20 years in days + $subject = sprintf( + '/C=/ST=/O=%s/localityName=/commonName=%s/organizationalUnitName=Developers/emailAddress=%s/', + 'Valet Linux CA Self Signed Organization', + 'Valet Linux CA Self Signed CN', + 'certificate@valet.linux', + ); + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with(sprintf( + 'openssl req -new -newkey rsa:2048 -days %s -nodes -x509 -subj "%s" -keyout "%s" -out "%s"', + $expiryInDays, + $subject, + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.key', + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem' + )); + + $this->filesystem + ->shouldReceive('copy') + ->once() + ->with( + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem', + '/usr/local/share/ca-certificates/ValetLinuxCASelfSigned.pem.crt' + ); + + $this->commandLine + ->shouldReceive('run') + ->once() + ->with('sudo update-ca-certificates'); + + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with(sprintf( + 'certutil -d sql:$HOME/.pki/nssdb -A -t TC -n "%s" -i "%s"', + 'Valet Linux CA Self Signed Organization', + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem' + )); + + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with(sprintf( + 'certutil -d $HOME/.mozilla/firefox/*.default -A -t TC -n "%s" -i "%s"', + 'Valet Linux CA Self Signed Organization', + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem' + )); + + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with(sprintf( + 'certutil -d $HOME/snap/firefox/common/.mozilla/firefox/*.default -A -t TC -n "%s" -i "%s"', + 'Valet Linux CA Self Signed Organization', + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem' + )); + + $certificateExpireInDate = (new \DateTime())->diff(new \DateTime("+1 year")); + $certificateExpireInDays = (int)$certificateExpireInDate->format('%a'); // 20 years in days + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/openssl.conf') + ->andReturn('SSL conf'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.conf', 'SSL conf'); + + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with(\sprintf( + 'openssl genrsa -out %s 2048', + VALET_HOME_PATH . '/Certificates/site.test.key' + )); + + $subject = sprintf( + '/C=/ST=/O=/localityName=/commonName=%s/organizationalUnitName=/emailAddress=%s/', + 'site.test', + 'certificate@valet.linux', + ); + $this->commandLine + ->shouldReceive('runAsUser') + ->once() + ->with(\sprintf( + 'openssl req -new -key %s -out %s -subj "%s" -config %s', + VALET_HOME_PATH . '/Certificates/site.test.key', + VALET_HOME_PATH . '/Certificates/site.test.csr', + $subject, + VALET_HOME_PATH . '/Certificates/site.test.conf' + )); + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.srl') + ->andReturnFalse(); + + $caSrlParam = '-CAserial "' . VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.srl" -CAcreateserial'; + + $this->commandLine + ->shouldReceive('run') + ->once() + ->with(sprintf( + 'openssl x509 -req -sha256 -days %s -CA "%s" -CAkey "%s" %s -in %s -out %s -extensions v3_req -extfile %s', + $certificateExpireInDays, + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.pem', + VALET_HOME_PATH . '/CA/ValetLinuxCASelfSigned.key', + $caSrlParam, + VALET_HOME_PATH . '/Certificates/site.test.csr', + VALET_HOME_PATH . '/Certificates/site.test.crt', + VALET_HOME_PATH . '/Certificates/site.test.conf' + )); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with(VALET_ROOT_PATH . '/cli/stubs/secure.valet.conf') + ->andReturn('Nginx Conf'); + + $this->config + ->shouldReceive('get') + ->once() + ->with('port', 80) + ->andReturn(80); + + $this->config + ->shouldReceive('get') + ->twice() + ->with('https_port', 443) + ->andReturn(443); + + $phpFpm->shouldReceive('getCurrentVersion') + ->once() + ->withNoArgs() + ->andReturn('8.3'); + + $phpFpm->shouldReceive('fpmSocketFile') + ->once() + ->with('8.3') + ->andReturn(VALET_HOME_PATH . '/' . 'valet83.sock'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test', 'Nginx Conf'); + + $this->siteSecure->secure('site.test'); + } + + /** + * @test + */ + public function itWillUnsecureSite(): void + { + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.crt') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/site.test'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.conf'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.key'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.csr'); + + $this->filesystem + ->shouldReceive('unlink') + ->once() + ->with(VALET_HOME_PATH . '/Certificates/site.test.crt'); + + $this->siteSecure->unsecure('site.test'); + } + + /** + * @test + */ + public function itWillListSecuredSites(): void + { + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with(VALET_HOME_PATH . '/Certificates') + ->andReturn([ + 'site.test.crt', + 'site.test.csr', + 'site.test.key', + 'site.test.conf', + 'site2.test.crt', + 'site2.test.csr', + 'site2.test.key', + 'site2.test.conf', + ]); + + $sites = $this->siteSecure->secured(); + + $this->assertSame(['site.test', 'site2.test'], $sites->toArray()); + } + + /** + * @test + */ + public function itWillRegenerateSecuredSites(): void + { + $phpFpm = Mockery::mock(PhpFpm::class); + swap(PhpFpm::class, $phpFpm); + + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with(VALET_HOME_PATH . '/Certificates') + ->andReturn([ + 'site.test.crt', + 'site.test.csr', + 'site.test.key', + 'site.test.conf', + 'site2.test.crt', + 'site2.test.csr', + 'site2.test.key', + 'site2.test.conf', + ]); + foreach (['site.test', 'site2.test'] as $site) { + + $this->filesystem + ->shouldReceive('get') + ->with(VALET_ROOT_PATH . '/cli/stubs/secure.valet.conf') + ->once() + ->andReturn('Nginx Conf'); + + $this->config + ->shouldReceive('get') + ->once() + ->with('port', 80) + ->andReturn(80); + + $this->config + ->shouldReceive('get') + ->twice() + ->with('https_port', 443) + ->andReturn(443); + + $phpFpm->shouldReceive('getCurrentVersion') + ->once() + ->withNoArgs() + ->andReturn('8.3'); + + $phpFpm->shouldReceive('fpmSocketFile') + ->once() + ->with('8.3') + ->andReturn(VALET_HOME_PATH . '/' . 'valet83.sock'); + + $this->filesystem + ->shouldReceive('putAsUser') + ->once() + ->with(VALET_HOME_PATH . '/Nginx/' . $site, 'Nginx Conf'); + } + + $this->siteSecure->regenerateSecuredSitesConfig(); + + $this->assertTrue(true); + } +} diff --git a/tests/Unit/SiteTest.php b/tests/Unit/SiteTest.php new file mode 100644 index 0000000..b5298e2 --- /dev/null +++ b/tests/Unit/SiteTest.php @@ -0,0 +1,175 @@ +config = Mockery::mock(Configuration::class); + $this->commandLine = Mockery::mock(CommandLine::class); + $this->filesystem = Mockery::mock(Filesystem::class); + + $this->site = new Site( + $this->config, + $this->commandLine, + $this->filesystem + ); + } + + /** + * @test + */ + public function itWillPruneLinks(): void + { + $this->filesystem + ->shouldReceive('ensureDirExists') + ->with(VALET_HOME_PATH . '/Sites', user()) + ->once(); + + $this->filesystem + ->shouldReceive('removeBrokenLinksAt') + ->with(VALET_HOME_PATH . '/Sites') + ->once(); + + $this->site->pruneLinks(); + + $this->assertTrue(true); + } + + /** + * @test + */ + public function itWillGetSiteUrl(): void + { + $this->config + ->shouldReceive('get') + ->once() + ->with('domain') + ->andReturn('test'); + + // servedSites + $this->config + ->shouldReceive('get') + ->once() + ->with('paths', []) + ->andReturn(['path1']); + + $dummySites = ['test', 'site2']; + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with('path1') + ->andReturn($dummySites); + + foreach ($dummySites as $dummySite) { + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with('path1/' . $dummySite) + ->andReturnTrue(); + } + + $nginxSites = ['proxy1', 'proxy2']; + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with(VALET_HOME_PATH . '/Sites') + ->andReturn($nginxSites); + + foreach ($nginxSites as $nginxSite) { + $this->filesystem + ->shouldReceive('realpath') + ->once() + ->with(VALET_HOME_PATH . '/Sites/' . $nginxSite) + ->andReturn('/real/path/' . $nginxSite); + } + + $output = $this->site->getSiteUrl('test'); + $this->assertSame('test.test', $output); + } + + /** + * @test + */ + public function itWillGetPhpRcVersion(): void + { + // servedSites + $this->config + ->shouldReceive('get') + ->once() + ->with('paths', []) + ->andReturn(['path1']); + + $dummySites = ['test', 'site2']; + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with('path1') + ->andReturn($dummySites); + + foreach ($dummySites as $dummySite) { + $this->filesystem + ->shouldReceive('isDir') + ->once() + ->with('path1/' . $dummySite) + ->andReturnTrue(); + } + + $nginxSites = ['proxy1', 'proxy2']; + $this->filesystem + ->shouldReceive('scandir') + ->once() + ->with(VALET_HOME_PATH . '/Sites') + ->andReturn($nginxSites); + + foreach ($nginxSites as $nginxSite) { + $this->filesystem + ->shouldReceive('realpath') + ->once() + ->with(VALET_HOME_PATH . '/Sites/' . $nginxSite) + ->andReturn('/real/path/' . $nginxSite); + } + + $this->filesystem + ->shouldReceive('exists') + ->once() + ->with('path1/test/.valetphprc') + ->andReturnTrue(); + + $this->filesystem + ->shouldReceive('get') + ->once() + ->with('path1/test/.valetphprc') + ->andReturn(' 8.2 '); + + $phpFpm = Mockery::mock(PhpFpm::class); + $phpFpm->shouldReceive('normalizePhpVersion') + ->once() + ->with('8.2') + ->andReturn('8.2'); + swap(PhpFpm::class, $phpFpm); + + $version = $this->site->phpRcVersion('test'); + $this->assertSame('8.2', $version); + } +} diff --git a/tests/Unit/ValetRedisTest.php b/tests/Unit/ValetRedisTest.php new file mode 100644 index 0000000..bb37e55 --- /dev/null +++ b/tests/Unit/ValetRedisTest.php @@ -0,0 +1,136 @@ +packageManager = Mockery::mock(PackageManager::class); + $this->serviceManager = Mockery::mock(ServiceManager::class); + $this->commandLine = Mockery::mock(CommandLine::class); + + $this->redis = new ValetRedis( + $this->packageManager, + $this->serviceManager, + $this->commandLine + ); + } + + /** + * @test + */ + public function itWillInstallSuccessfully(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->once() + ->with('redis') + ->andReturn('redis-server'); + + $this->packageManager + ->shouldReceive('ensureInstalled') + ->once() + ->with('redis-server'); + + $this->serviceManager + ->shouldReceive('enable') + ->once() + ->with('redis-server'); + + $this->redis->install(); + } + + /** + * @test + */ + public function itWillValidateIfPackageIsInstalled(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->once() + ->with('redis') + ->andReturn('redis-server'); + + $this->packageManager + ->shouldReceive('installed') + ->once() + ->with('redis-server') + ->andReturnTrue(); + + $isInstalled = $this->redis->installed(); + $this->assertTrue($isInstalled); + } + + /** + * @test + */ + public function itWillRestartServiceSuccessfully(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->once() + ->with('redis') + ->andReturn('redis-server'); + + $this->serviceManager + ->shouldReceive('restart') + ->once() + ->with('redis-server'); + + $this->redis->restart(); + } + + /** + * @test + */ + public function itWillStopServiceSuccessfully(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->once() + ->with('redis') + ->andReturn('redis-server'); + + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('redis-server'); + + $this->redis->stop(); + } + + /** + * @test + */ + public function itWillUninstallServiceSuccessfully(): void + { + $this->packageManager + ->shouldReceive('packageName') + ->once() + ->with('redis') + ->andReturn('redis-server'); + + $this->serviceManager + ->shouldReceive('stop') + ->once() + ->with('redis-server'); + + $this->redis->uninstall(); + } +} diff --git a/tests/Unit/ValetTest.php b/tests/Unit/ValetTest.php new file mode 100644 index 0000000..1b7dfee --- /dev/null +++ b/tests/Unit/ValetTest.php @@ -0,0 +1,218 @@ +commandLine = Mockery::mock(CommandLine::class); + $this->filesystem = Mockery::mock(Filesystem::class); + + $this->valet = new Valet( + $this->commandLine, + $this->filesystem + ); + } + + /** + * @test + */ + public function itWillLinkValetBinFileToSystemPath(): void + { + $this->commandLine + ->shouldReceive('run') + ->with('ln -snf ' . realpath(VALET_ROOT_PATH . '/valet') . ' /usr/local/bin/valet') + ->once(); + + $this->valet->symlinkToUsersBin(); + + $this->assertTrue(true); + } + + /** + * @test + */ + public function itWillLinkPhpBinFileToSystemPath(): void + { + $config = Mockery::mock(Configuration::class); + swap(Configuration::class, $config); + $execPath = $_SERVER['_'] ?? '/usr/bin/php'; + + $this->filesystem + ->shouldReceive('realpath') + ->with($execPath) + ->andReturn('/usr/bin/php'); + + $config->shouldReceive('set') + ->with('fallback_binary', '/usr/bin/php') + ->once(); + + $this->commandLine + ->shouldReceive('run') + ->with('ln -snf ' . realpath(VALET_ROOT_PATH . '/php') . ' /usr/local/bin/php') + ->once(); + + $this->valet->symlinkPhpToUsersBin(); + + $this->assertTrue(true); + } + + /** + * @test + */ + public function itWillUninstallSuccessfully(): void + { + $this->filesystem + ->shouldReceive('unlink') + ->with('/usr/local/bin/valet') + ->once(); + + $this->filesystem + ->shouldReceive('unlink') + ->with('/usr/local/bin/php') + ->once(); + + $this->valet->uninstall(); + + $this->assertTrue(true); + } + + /** + * @test + */ + public function itWillLoadExtensionsFromHomeDir(): void + { + $this->filesystem + ->shouldReceive('isDir') + ->with(VALET_HOME_PATH . '/Extensions') + ->once() + ->andReturn(true); + + $dummyExtensions = ['ext-1.php', 'ext-2.php']; + $this->filesystem + ->shouldReceive('scandir') + ->with(VALET_HOME_PATH . '/Extensions') + ->once() + ->andReturn($dummyExtensions); + + foreach ($dummyExtensions as $dummyExtension) { + $this->filesystem + ->shouldReceive('isDir') + ->with($dummyExtension) + ->once() + ->andReturnFalse(); + } + + $extensions = $this->valet->extensions(); + + $this->assertSame([ + VALET_HOME_PATH . '/Extensions/ext-1.php', + VALET_HOME_PATH . '/Extensions/ext-2.php', + ], $extensions); + } + + public function versionDataProvider(): array + { + return [ + [ + '2.0.0', + true, + ], + [ + 'v1.1.0', + false, + ], + ]; + } + + /** + * @test + * @dataProvider versionDataProvider + */ + public function itWillCompareLatestVersion(string $version, bool $expectedResponse): void + { + + $request = Mockery::mock(Request::class); + swap(Request::class, $request); + Httpful::register(Mime::JSON, new JsonHandler(array('decode_as_array' => false))); + + $request->shouldReceive('get') + ->with('https://api.github.com/repos/genesisweb/valet-linux-plus/releases/latest') + ->once() + ->andReturnSelf(); + + $mockResponse = $this->getFile('github_response.json'); + $request->shouldReceive('send') + ->once() + ->withNoArgs() + ->andReturn(new Response((string) $mockResponse, "HTTP/1.1 200 OK\r\n +Content-Type: application/json\r\n +Date: Tue, 14 May 2024 12:13:28 GMT\r\n +Content-Length: 477 +", $request)); + + $isLatest = $this->valet->onLatestVersion($version); + + $this->assertSame($expectedResponse, $isLatest); + } + + /** + * @test + */ + public function itWillGetLatestVersion(): void + { + + $request = Mockery::mock(Request::class); + swap(Request::class, $request); + Httpful::register(Mime::JSON, new JsonHandler(array('decode_as_array' => false))); + + $request->shouldReceive('get') + ->with('https://api.github.com/repos/genesisweb/valet-linux-plus/releases/latest') + ->once() + ->andReturnSelf(); + + $mockResponse = $this->getFile('github_response.json'); + $request->shouldReceive('send') + ->once() + ->withNoArgs() + ->andReturn(new Response((string) $mockResponse, "HTTP/1.1 200 OK\r\n +Content-Type: application/json\r\n +Date: Tue, 14 May 2024 12:13:28 GMT\r\n +Content-Length: 477 +", $request)); + + $response = $this->valet->getLatestVersion(); + + $this->assertSame('1.6.9', $response); + } + + private function getFile(string $fileName): string|false + { + return file_get_contents(__DIR__ . '/MockResponse/' . $fileName); + } +} diff --git a/valet b/valet index 9f213e7..977b2cf 100755 --- a/valet +++ b/valet @@ -3,7 +3,7 @@ set -e SOURCE="${BASH_SOURCE[0]}" -SUDO_COMMANDS="uninstall install start restart stop unsecure secure use isolate unisolate" +SUDO_COMMANDS="uninstall install start restart stop unsecure secure use isolate unisolate proxy unproxy" HOME_PATH=$HOME function check_dependencies() { @@ -21,9 +21,9 @@ function check_dependencies() { done if [[ $msg != '' ]]; then - printf "${RED}You have missing Valet dependencies:\n" - printf "$msg" - printf "\nPlease refer to https://valetlinux.plus/ on how to install them.${NC}\n" + printf "%sYou have missing Valet dependencies:\n" "${RED}" + printf "%s" "$msg" + printf "\nPlease refer to https://valetlinux.plus/ on how to install them.%s\n" "${NC}" exit 1 fi } @@ -33,9 +33,9 @@ function verify_ngrok_auth() { then if [[ -f "${HOME}/.ngrok2/ngrok.yml" ]] then - sudo -u $USER "$DIR/bin/ngrok" config upgrade --relocate + sudo -u "$USER" "$DIR/bin/ngrok" config upgrade --relocate else - printf "Please register for an ngrok account at: https://dashboard.ngrok.com/signup and install your authtoken.${NC}\n" + printf "Please register for an ngrok account at: https://dashboard.ngrok.com/signup and install your authtoken.%s\n" "${NC}" printf "Valet can help you to install the authtoken, use 'valet ngrok-auth {authtoken}' command.\n" exit 1 fi @@ -47,7 +47,7 @@ function verify_ngrok_auth() { # do it in pure Bash. So, we'll call into PHP CLI here to resolve. if [[ -L $SOURCE ]] then - DIR=$(php -r "echo dirname(realpath('$SOURCE'));") + DIR=$( dirname "$( readlink -f "$SOURCE" )" ) else DIR="$( cd "$( dirname "$SOURCE" )" && pwd )" fi @@ -57,7 +57,7 @@ fi # Valet CLI script which is written in PHP. Will use PHP to do it. if [[ ! -f "$DIR/cli/valet.php" ]] then - DIR=$(php -r "echo realpath('$DIR/../genesisweb/valet-linux-plus');") + DIR="$( cd "$DIR/../genesisweb/valet-linux-plus" && pwd )" fi # If the command is one of the commands that requires "sudo" privileges @@ -91,11 +91,11 @@ then verify_ngrok_auth HOST="${PWD##*/}" - DOMAIN=$(cat "$HOME/.valet/config.json" | jq -r ".domain") - PORT=$(cat "$HOME/.valet/config.json" | jq -r ".port") + DOMAIN=$(cat "$HOME/.config/valet/config.json" | jq -r ".domain") + PORT=$(cat "$HOME/.config/valet/config.json" | jq -r ".port") - for linkname in ~/.valet/Sites/*; do - if [[ "$(readlink ${linkname})" = "$PWD" ]] + for linkname in ~/.config/valet/Sites/*; do + if [[ "$(readlink "${linkname}")" = "$PWD" ]] then HOST="${linkname##*/}" fi @@ -103,14 +103,14 @@ then # Decide the correct PORT to use according if the site has a secure # config or not. - if grep --no-messages --quiet 443 ~/.valet/Nginx/$HOST* + if grep --no-messages --quiet 443 ~/.config/valet/Nginx/"$HOST"* then PORT=88 fi # Fetch Ngrok URL In Background bash "$DIR/cli/scripts/fetch-share-url.sh" & - sudo -u $USER "$DIR/bin/ngrok" http "$HOST.$DOMAIN:$PORT" --host-header=rewrite + sudo -u "$USER" "$DIR/bin/ngrok" http "$HOST.$DOMAIN:$PORT" --host-header=rewrite exit # Proxy PHP commands to the "php" executable on the isolated site @@ -118,9 +118,9 @@ elif [[ "$1" = "php" ]] then if [[ $2 == *"--site="* ]]; then SITE=${2#*=} - $(php "$DIR/cli/valet.php" which-php "$SITE") "${@:3}" + $(/usr/bin/php "$DIR/cli/valet.php" which-php "$SITE") "${@:3}" else - $(php "$DIR/cli/valet.php" which-php) "${@:2}" + $(/usr/bin/php "$DIR/cli/valet.php" which-php) "${@:2}" fi exit @@ -131,10 +131,10 @@ then if [[ $2 == *"--site="* ]]; then SITE=${2#*=} # shellcheck disable=SC2046 - $(php "$DIR/cli/valet.php" which-php "$SITE") $(which composer) "${@:3}" + $(/usr/bin/php "$DIR/cli/valet.php" which-php "$SITE") $(which composer) "${@:3}" else # shellcheck disable=SC2046 - $(php "$DIR/cli/valet.php" which-php) $(which composer) "${@:2}" + $(/usr/bin/php "$DIR/cli/valet.php" which-php) $(which composer) "${@:2}" fi exit @@ -143,5 +143,5 @@ then # and let it handle the request. These are commands which can be run # without sudo and don't require taking over terminals like Ngrok. else - php "$DIR/cli/valet.php" "$@" + /usr/bin/php "$DIR/cli/valet.php" "$@" fi