Skip to content
191 changes: 191 additions & 0 deletions src/NewCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use Throwable;

use function Illuminate\Filesystem\join_paths;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
Expand Down Expand Up @@ -87,6 +90,8 @@ protected function interact(InputInterface $input, OutputInterface $output)

$this->ensureExtensionsAreAvailable($input, $output);

$this->checkForUpdate($input, $output);

if (! $input->getArgument('name')) {
$input->setArgument('name', text(
label: 'What is the name of your project?',
Expand Down Expand Up @@ -199,6 +204,192 @@ protected function ensureExtensionsAreAvailable(InputInterface $input, OutputInt
);
}

/**
* Check for newer version of the installer package.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function checkForUpdate(InputInterface $input, OutputInterface $output)
{
Comment on lines +214 to +215
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this return early if ! $input->isInteractive()?

$package = 'laravel/installer';
$version = $this->getApplication()->getVersion();
$versionData = $this->getLatestVersionData($package);

if ($versionData === false) {
return;
}

$data = json_decode($versionData, true);
$latestVersion = ltrim($data['packages'][$package][0]['version'], 'v');

if (version_compare($version, $latestVersion) !== -1) {
return;
}

$output->writeln(" <bg=yellow;fg=black> WARN </> A new version of the Laravel installer is available. You have version {$version} installed, the latest version is {$latestVersion}.");

$laravelInstallerPath = (new ExecutableFinder())->find('laravel') ?? '';
$isHerd = str_contains($laravelInstallerPath, DIRECTORY_SEPARATOR.'Herd'.DIRECTORY_SEPARATOR);
// Intalled via php.new
$isHerdLite = str_contains($laravelInstallerPath, DIRECTORY_SEPARATOR.'herd-lite'.DIRECTORY_SEPARATOR);

if ($isHerd) {
$this->confirmUpdateAndContinue(
'To update, open <options=bold>Herd</> > <options=bold>Settings</> > <options=bold>PHP</> > <options=bold>Laravel Installer</> '
.'and click the <options=bold>"Update"</> button.',
$input,
$output
);

return;
}

if ($isHerdLite) {
$message = match (PHP_OS_FAMILY) {
'Windows' => 'Set-ExecutionPolicy Bypass -Scope Process -Force; '
.'[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; '
."iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows'))",
'Darwin' => '/bin/bash -c "$(curl -fsSL https://php.new/install/mac)"',
default => '/bin/bash -c "$(curl -fsSL https://php.new/install/linux)"',
};

$output->writeln('');
$output->writeln(' To update, run the following command in your terminal:');

$this->confirmUpdateAndContinue($message, $input, $output);

return;
}

if (confirm(label: 'Would you like to update now?')) {
$this->runCommands(['composer global update laravel/installer'], $input, $output);
$this->proxyLaravelNew($input, $output);
}
Comment on lines +266 to +269
Copy link
Contributor

Choose a reason for hiding this comment

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

There are more ways the installer can be installed besides composer and Herd. Obviously these are the major ones (probably 95%+ of users) but it might be reasonable to somehow check if laravel is actually installed via composer.

Not sure how this'd be done on Windows but on Mac/Linux I think this could be as simple as checking that which laravel starts with composer global config home 2>/dev/null.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually using which is dumb, we can get the path to the currently running script (which is what matters here) using realpath($_SERVER['SCRIPT_FILENAME']).

So the check should probably be just: str_starts_with(realpath($_SERVER['SCRIPT_FILENAME']), $composerHome) where $composerHome is the stdout of composer global config home (can also just use --quiet).

}

/**
* Allow the user to update the Laravel Installer and continue.
*
* @param string $message
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function confirmUpdateAndContinue(string $message, InputInterface $input, OutputInterface $output): void
{
$output->writeln('');
$output->writeln(" {$message}");

$updated = confirm(
label: 'Would you like to update now?',
yes: 'I have updated',
no: 'Not now',
);

if (! $updated) {
return;
}

$this->proxyLaravelNew($input, $output);
}

/**
* Proxy the command to the globally installed Laravel Installer.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function proxyLaravelNew(InputInterface $input, OutputInterface $output): void
{
$output->writeln('');
$this->runCommands(['laravel '.$input], $input, $output, workingPath: getcwd());
exit;
}

/**
* Get the latest version of the installer package from Packagist.
*
* @param string $package
* @return string|false
*/
protected function getLatestVersionData(string $package): string|false
{
$packagePrefix = str_replace('/', '-', $package);
$cachedPath = join_paths(sys_get_temp_dir(), $packagePrefix.'-version-check.json');
$lastModifiedPath = join_paths(sys_get_temp_dir(), $packagePrefix.'-last-modified');

$cacheExists = file_exists($cachedPath);
$lastModifiedExists = file_exists($lastModifiedPath);

$cacheLastWrittenAt = $cacheExists ? filemtime($cachedPath) : 0;
$lastModifiedResponse = $lastModifiedExists ? file_get_contents($lastModifiedPath) : null;

if ($cacheLastWrittenAt > time() - 86400) {
// Cache is less than 24 hours old, use it
return file_get_contents($cachedPath);
}

$curl = curl_init();

$headers = ['User-Agent: Laravel Installer'];

if ($lastModifiedResponse) {
$headers[] = "If-Modified-Since: {$lastModifiedResponse}";
}

curl_setopt_array($curl, [
CURLOPT_URL => "https://repo.packagist.org/p2/{$package}.json",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 3,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => true,
]);

try {
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$error = curl_error($curl);
curl_close($curl);
} catch (Throwable $e) {
return false;
}

if ($error) {
return false;
}

$responseHeaders = substr($response, 0, $headerSize);
$result = substr($response, $headerSize);

$lastModifiedFromResponse = null;

if (preg_match('/^Last-Modified:\s*(.+)$/mi', $responseHeaders, $matches)) {
$lastModifiedFromResponse = trim($matches[1]);
}

file_put_contents($lastModifiedPath, $lastModifiedFromResponse);

if ($httpCode === 304 && $cacheExists) {
touch($cachedPath);

return file_get_contents($cachedPath);
}

if ($httpCode === 200 && $result !== false) {
file_put_contents($cachedPath, $result);

return $result;
}

return ($cacheExists) ? file_get_contents($cachedPath) : false;
}

/**
* Execute the command.
*
Expand Down