├── LICENSE.md ├── README.md ├── bin └── laravel ├── composer.json └── src ├── Concerns ├── ConfiguresPrompts.php └── InteractsWithHerdOrValet.php └── NewCommand.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Installer 2 | 3 | Build Status 4 | Total Downloads 5 | Latest Stable Version 6 | License 7 | 8 | ## Official Documentation 9 | 10 | Documentation for installing Laravel can be found on the [Laravel website](https://laravel.com/docs#creating-a-laravel-project). 11 | 12 | ## Contributing 13 | 14 | Thank you for considering contributing to the Installer! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 15 | 16 | ## Code of Conduct 17 | 18 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 19 | 20 | ## Security Vulnerabilities 21 | 22 | Please review [our security policy](https://github.com/laravel/installer/security/policy) on how to report security vulnerabilities. 23 | 24 | ## License 25 | 26 | Laravel Installer is open-sourced software licensed under the [MIT license](LICENSE.md). 27 | -------------------------------------------------------------------------------- /bin/laravel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new Laravel\Installer\Console\NewCommand); 12 | 13 | $app->run(); 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/installer", 3 | "description": "Laravel application installer.", 4 | "keywords": [ 5 | "laravel" 6 | ], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Taylor Otwell", 11 | "email": "taylor@laravel.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.2", 16 | "illuminate/filesystem": "^10.20|^11.0|^12.0", 17 | "illuminate/support": "^10.20|^11.0|^12.0", 18 | "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", 19 | "symfony/console": "^6.2|^7.0", 20 | "symfony/process": "^6.2|^7.0", 21 | "symfony/polyfill-mbstring": "^1.31" 22 | }, 23 | "require-dev": { 24 | "phpstan/phpstan": "^2.1", 25 | "phpunit/phpunit": "^10.4" 26 | }, 27 | "bin": [ 28 | "bin/laravel" 29 | ], 30 | "autoload": { 31 | "psr-4": { 32 | "Laravel\\Installer\\Console\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Laravel\\Installer\\Console\\Tests\\": "tests/" 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /src/Concerns/ConfiguresPrompts.php: -------------------------------------------------------------------------------- 1 | isInteractive() || PHP_OS_FAMILY === 'Windows'); 29 | 30 | TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid( 31 | fn () => (new SymfonyStyle($input, $output))->ask($prompt->label, $prompt->default ?: null) ?? '', 32 | $prompt->required, 33 | $prompt->validate, 34 | $output 35 | )); 36 | 37 | PasswordPrompt::fallbackUsing(fn (PasswordPrompt $prompt) => $this->promptUntilValid( 38 | fn () => (new SymfonyStyle($input, $output))->askHidden($prompt->label) ?? '', 39 | $prompt->required, 40 | $prompt->validate, 41 | $output 42 | )); 43 | 44 | ConfirmPrompt::fallbackUsing(fn (ConfirmPrompt $prompt) => $this->promptUntilValid( 45 | fn () => (new SymfonyStyle($input, $output))->confirm($prompt->label, $prompt->default), 46 | $prompt->required, 47 | $prompt->validate, 48 | $output 49 | )); 50 | 51 | SelectPrompt::fallbackUsing(fn (SelectPrompt $prompt) => $this->promptUntilValid( 52 | fn () => (new SymfonyStyle($input, $output))->choice($prompt->label, $prompt->options, $prompt->default), 53 | false, 54 | $prompt->validate, 55 | $output 56 | )); 57 | 58 | MultiSelectPrompt::fallbackUsing(function (MultiSelectPrompt $prompt) use ($input, $output) { 59 | if ($prompt->default !== []) { 60 | return $this->promptUntilValid( 61 | fn () => (new SymfonyStyle($input, $output))->choice($prompt->label, $prompt->options, implode(',', $prompt->default), true), 62 | $prompt->required, 63 | $prompt->validate, 64 | $output 65 | ); 66 | } 67 | 68 | return $this->promptUntilValid( 69 | fn () => collect((new SymfonyStyle($input, $output))->choice( 70 | $prompt->label, 71 | array_is_list($prompt->options) 72 | ? ['None', ...$prompt->options] 73 | : ['none' => 'None', ...$prompt->options], 74 | 'None', 75 | true) 76 | )->reject(array_is_list($prompt->options) ? 'None' : 'none')->all(), 77 | $prompt->required, 78 | $prompt->validate, 79 | $output 80 | ); 81 | }); 82 | 83 | SuggestPrompt::fallbackUsing(fn (SuggestPrompt $prompt) => $this->promptUntilValid( 84 | function () use ($prompt, $input, $output) { 85 | $question = new Question($prompt->label, $prompt->default); 86 | 87 | is_callable($prompt->options) 88 | ? $question->setAutocompleterCallback($prompt->options) 89 | : $question->setAutocompleterValues($prompt->options); 90 | 91 | return (new SymfonyStyle($input, $output))->askQuestion($question); 92 | }, 93 | $prompt->required, 94 | $prompt->validate, 95 | $output 96 | )); 97 | } 98 | 99 | /** 100 | * Prompt the user until the given validation callback passes. 101 | * 102 | * @param \Closure $prompt 103 | * @param bool|string $required 104 | * @param \Closure|null $validate 105 | * @param \Symfony\Component\Console\Output\OutputInterface $output 106 | * @return mixed 107 | */ 108 | protected function promptUntilValid($prompt, $required, $validate, $output) 109 | { 110 | while (true) { 111 | $result = $prompt(); 112 | 113 | if ($required && ($result === '' || $result === [] || $result === false)) { 114 | $output->writeln(''.(is_string($required) ? $required : 'Required.').''); 115 | 116 | continue; 117 | } 118 | 119 | if ($validate) { 120 | $error = $validate($result); 121 | 122 | if (is_string($error) && strlen($error) > 0) { 123 | $output->writeln("{$error}"); 124 | 125 | continue; 126 | } 127 | } 128 | 129 | return $result; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithHerdOrValet.php: -------------------------------------------------------------------------------- 1 | runOnValetOrHerd('paths'); 19 | 20 | $decodedOutput = json_decode($output); 21 | 22 | return is_array($decodedOutput) && in_array(dirname($directory), $decodedOutput); 23 | } 24 | 25 | /** 26 | * Runs the given command on the "herd" or "valet" CLI. 27 | * 28 | * @param string $command 29 | * @return string|false 30 | */ 31 | protected function runOnValetOrHerd(string $command) 32 | { 33 | foreach (['herd', 'valet'] as $tool) { 34 | $process = new Process([$tool, $command, '-v']); 35 | 36 | try { 37 | $process->run(); 38 | 39 | if ($process->isSuccessful()) { 40 | return trim($process->getOutput()); 41 | } 42 | } catch (ProcessStartFailedException) { 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/NewCommand.php: -------------------------------------------------------------------------------- 1 | setName('new') 45 | ->setDescription('Create a new Laravel application') 46 | ->addArgument('name', InputArgument::REQUIRED) 47 | ->addOption('dev', null, InputOption::VALUE_NONE, 'Install the latest "development" release') 48 | ->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a Git repository') 49 | ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The branch that should be created for a new repository', $this->defaultBranch()) 50 | ->addOption('github', null, InputOption::VALUE_OPTIONAL, 'Create a new repository on GitHub', false) 51 | ->addOption('organization', null, InputOption::VALUE_REQUIRED, 'The GitHub organization to create the new repository for') 52 | ->addOption('database', null, InputOption::VALUE_REQUIRED, 'The database driver your application will use') 53 | ->addOption('react', null, InputOption::VALUE_NONE, 'Install the React Starter Kit') 54 | ->addOption('vue', null, InputOption::VALUE_NONE, 'Install the Vue Starter Kit') 55 | ->addOption('livewire', null, InputOption::VALUE_NONE, 'Install the Livewire Starter Kit') 56 | ->addOption('livewire-class-components', null, InputOption::VALUE_NONE, 'Generate stand-alone Livewire class components') 57 | ->addOption('workos', null, InputOption::VALUE_NONE, 'Use WorkOS for authentication') 58 | ->addOption('pest', null, InputOption::VALUE_NONE, 'Install the Pest testing framework') 59 | ->addOption('phpunit', null, InputOption::VALUE_NONE, 'Install the PHPUnit testing framework') 60 | ->addOption('npm', null, InputOption::VALUE_NONE, 'Install and build NPM dependencies') 61 | ->addOption('using', null, InputOption::VALUE_OPTIONAL, 'Install a custom starter kit from a community maintained package') 62 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists'); 63 | } 64 | 65 | /** 66 | * Interact with the user before validating the input. 67 | * 68 | * @param \Symfony\Component\Console\Input\InputInterface $input 69 | * @param \Symfony\Component\Console\Output\OutputInterface $output 70 | * @return void 71 | */ 72 | protected function interact(InputInterface $input, OutputInterface $output) 73 | { 74 | parent::interact($input, $output); 75 | 76 | $this->configurePrompts($input, $output); 77 | 78 | $output->write(PHP_EOL.' _ _ 79 | | | | | 80 | | | __ _ _ __ __ ___ _____| | 81 | | | / _` | __/ _` \ \ / / _ \ | 82 | | |___| (_| | | | (_| |\ V / __/ | 83 | |______\__,_|_| \__,_| \_/ \___|_|'.PHP_EOL.PHP_EOL); 84 | 85 | $this->ensureExtensionsAreAvailable($input, $output); 86 | 87 | if (! $input->getArgument('name')) { 88 | $input->setArgument('name', text( 89 | label: 'What is the name of your project?', 90 | placeholder: 'E.g. example-app', 91 | required: 'The project name is required.', 92 | validate: function ($value) use ($input) { 93 | if (preg_match('/[^\pL\pN\-_.]/', $value) !== 0) { 94 | return 'The name may only contain letters, numbers, dashes, underscores, and periods.'; 95 | } 96 | 97 | if ($input->getOption('force') !== true) { 98 | try { 99 | $this->verifyApplicationDoesntExist($this->getInstallationDirectory($value)); 100 | } catch (RuntimeException $e) { 101 | return 'Application already exists.'; 102 | } 103 | } 104 | }, 105 | )); 106 | } 107 | 108 | if ($input->getOption('force') !== true) { 109 | $this->verifyApplicationDoesntExist( 110 | $this->getInstallationDirectory($input->getArgument('name')) 111 | ); 112 | } 113 | 114 | if (! $this->usingStarterKit($input)) { 115 | match (select( 116 | label: 'Which starter kit would you like to install?', 117 | options: [ 118 | 'none' => 'None', 119 | 'react' => 'React', 120 | 'vue' => 'Vue', 121 | 'livewire' => 'Livewire', 122 | ], 123 | default: 'none', 124 | )) { 125 | 'react' => $input->setOption('react', true), 126 | 'vue' => $input->setOption('vue', true), 127 | 'livewire' => $input->setOption('livewire', true), 128 | default => null, 129 | }; 130 | 131 | if ($this->usingLaravelStarterKit($input)) { 132 | match (select( 133 | label: 'Which authentication provider do you prefer?', 134 | options: [ 135 | 'laravel' => "Laravel's built-in authentication", 136 | 'workos' => 'WorkOS (Requires WorkOS account)', 137 | ], 138 | default: 'laravel', 139 | )) { 140 | 'laravel' => $input->setOption('workos', false), 141 | 'workos' => $input->setOption('workos', true), 142 | default => null, 143 | }; 144 | } 145 | 146 | if ($input->getOption('livewire') && ! $input->getOption('workos')) { 147 | $input->setOption('livewire-class-components', ! confirm( 148 | label: 'Would you like to use Laravel Volt?', 149 | default: true, 150 | )); 151 | } 152 | } 153 | 154 | if (! $input->getOption('phpunit') && ! $input->getOption('pest')) { 155 | $input->setOption('pest', select( 156 | label: 'Which testing framework do you prefer?', 157 | options: ['Pest', 'PHPUnit'], 158 | default: 'Pest', 159 | ) === 'Pest'); 160 | } 161 | } 162 | 163 | /** 164 | * Ensure that the required PHP extensions are installed. 165 | * 166 | * @param \Symfony\Component\Console\Input\InputInterface $input 167 | * @param \Symfony\Component\Console\Output\OutputInterface $output 168 | * @return void 169 | * 170 | * @throws \RuntimeException 171 | */ 172 | protected function ensureExtensionsAreAvailable(InputInterface $input, OutputInterface $output): void 173 | { 174 | $availableExtensions = get_loaded_extensions(); 175 | 176 | $missingExtensions = collect([ 177 | 'ctype', 178 | 'filter', 179 | 'hash', 180 | 'mbstring', 181 | 'openssl', 182 | 'session', 183 | 'tokenizer', 184 | ])->reject(fn ($extension) => in_array($extension, $availableExtensions)); 185 | 186 | if ($missingExtensions->isEmpty()) { 187 | return; 188 | } 189 | 190 | throw new \RuntimeException( 191 | sprintf('The following PHP extensions are required but are not installed: %s', $missingExtensions->join(', ', ', and ')) 192 | ); 193 | } 194 | 195 | /** 196 | * Execute the command. 197 | * 198 | * @param \Symfony\Component\Console\Input\InputInterface $input 199 | * @param \Symfony\Component\Console\Output\OutputInterface $output 200 | * @return int 201 | */ 202 | protected function execute(InputInterface $input, OutputInterface $output): int 203 | { 204 | $this->validateDatabaseOption($input); 205 | 206 | $name = rtrim($input->getArgument('name'), '/\\'); 207 | 208 | $directory = $this->getInstallationDirectory($name); 209 | 210 | $this->composer = new Composer(new Filesystem(), $directory); 211 | 212 | $version = $this->getVersion($input); 213 | 214 | if (! $input->getOption('force')) { 215 | $this->verifyApplicationDoesntExist($directory); 216 | } 217 | 218 | if ($input->getOption('force') && $directory === '.') { 219 | throw new RuntimeException('Cannot use --force option when using current directory for installation!'); 220 | } 221 | 222 | $composer = $this->findComposer(); 223 | $phpBinary = $this->phpBinary(); 224 | 225 | $createProjectCommand = $composer." create-project laravel/laravel \"$directory\" $version --remove-vcs --prefer-dist --no-scripts"; 226 | 227 | $starterKit = $this->getStarterKit($input); 228 | 229 | if ($starterKit) { 230 | $createProjectCommand = $composer." create-project {$starterKit} \"{$directory}\" --stability=dev"; 231 | 232 | if ($this->usingLaravelStarterKit($input) && $input->getOption('livewire-class-components')) { 233 | $createProjectCommand = str_replace(" {$starterKit} ", " {$starterKit}:dev-components ", $createProjectCommand); 234 | } 235 | 236 | if ($this->usingLaravelStarterKit($input) && $input->getOption('workos')) { 237 | $createProjectCommand = str_replace(" {$starterKit} ", " {$starterKit}:dev-workos ", $createProjectCommand); 238 | } 239 | } 240 | 241 | $commands = [ 242 | $createProjectCommand, 243 | $composer." run post-root-package-install -d \"$directory\"", 244 | $phpBinary." \"$directory/artisan\" key:generate --ansi", 245 | ]; 246 | 247 | if ($directory != '.' && $input->getOption('force')) { 248 | if (PHP_OS_FAMILY == 'Windows') { 249 | array_unshift($commands, "(if exist \"$directory\" rd /s /q \"$directory\")"); 250 | } else { 251 | array_unshift($commands, "rm -rf \"$directory\""); 252 | } 253 | } 254 | 255 | if (PHP_OS_FAMILY != 'Windows') { 256 | $commands[] = "chmod 755 \"$directory/artisan\""; 257 | } 258 | 259 | if (($process = $this->runCommands($commands, $input, $output))->isSuccessful()) { 260 | if ($name !== '.') { 261 | $this->replaceInFile( 262 | 'APP_URL=http://localhost', 263 | 'APP_URL='.$this->generateAppUrl($name, $directory), 264 | $directory.'/.env' 265 | ); 266 | 267 | [$database, $migrate] = $this->promptForDatabaseOptions($directory, $input); 268 | 269 | $this->configureDefaultDatabaseConnection($directory, $database, $name); 270 | 271 | if ($migrate) { 272 | if ($database === 'sqlite') { 273 | touch($directory.'/database/database.sqlite'); 274 | } 275 | 276 | $commands = [ 277 | trim(sprintf( 278 | $this->phpBinary().' artisan migrate %s', 279 | ! $input->isInteractive() ? '--no-interaction' : '', 280 | )), 281 | ]; 282 | 283 | $this->runCommands($commands, $input, $output, workingPath: $directory); 284 | } 285 | } 286 | 287 | if ($input->getOption('git') || $input->getOption('github') !== false) { 288 | $this->createRepository($directory, $input, $output); 289 | } 290 | 291 | if ($input->getOption('pest')) { 292 | $this->installPest($directory, $input, $output); 293 | } 294 | 295 | if ($input->getOption('github') !== false) { 296 | $this->pushToGitHub($name, $directory, $input, $output); 297 | $output->writeln(''); 298 | } 299 | 300 | $this->configureComposerDevScript($directory); 301 | 302 | if ($input->getOption('pest')) { 303 | $output->writeln(''); 304 | } 305 | 306 | $runNpm = $input->getOption('npm'); 307 | 308 | if (! $input->getOption('npm') && $input->isInteractive()) { 309 | $runNpm = confirm( 310 | label: 'Would you like to run npm install and npm run build?' 311 | ); 312 | } 313 | 314 | if ($runNpm) { 315 | $this->runCommands(['npm install', 'npm run build'], $input, $output, workingPath: $directory); 316 | } 317 | 318 | $output->writeln(" INFO Application ready in [{$name}]. You can start your local development using:".PHP_EOL); 319 | $output->writeln('cd '.$name.''); 320 | 321 | if (! $runNpm) { 322 | $output->writeln('npm install && npm run build'); 323 | } 324 | 325 | if ($this->isParkedOnHerdOrValet($directory)) { 326 | $url = $this->generateAppUrl($name, $directory); 327 | $output->writeln('➜ Open: '.$url.''); 328 | } else { 329 | $output->writeln('composer run dev'); 330 | } 331 | 332 | $output->writeln(''); 333 | $output->writeln(' New to Laravel? Check out our documentation. Build something amazing!'); 334 | $output->writeln(''); 335 | } 336 | 337 | return $process->getExitCode(); 338 | } 339 | 340 | /** 341 | * Return the local machine's default Git branch if set or default to `main`. 342 | * 343 | * @return string 344 | */ 345 | protected function defaultBranch() 346 | { 347 | $process = new Process(['git', 'config', '--global', 'init.defaultBranch']); 348 | 349 | $process->run(); 350 | 351 | $output = trim($process->getOutput()); 352 | 353 | return $process->isSuccessful() && $output ? $output : 'main'; 354 | } 355 | 356 | /** 357 | * Configure the default database connection. 358 | * 359 | * @param string $directory 360 | * @param string $database 361 | * @param string $name 362 | * @return void 363 | */ 364 | protected function configureDefaultDatabaseConnection(string $directory, string $database, string $name) 365 | { 366 | $this->pregReplaceInFile( 367 | '/DB_CONNECTION=.*/', 368 | 'DB_CONNECTION='.$database, 369 | $directory.'/.env' 370 | ); 371 | 372 | $this->pregReplaceInFile( 373 | '/DB_CONNECTION=.*/', 374 | 'DB_CONNECTION='.$database, 375 | $directory.'/.env.example' 376 | ); 377 | 378 | if ($database === 'sqlite') { 379 | $environment = file_get_contents($directory.'/.env'); 380 | 381 | // If database options aren't commented, comment them for SQLite... 382 | if (! str_contains($environment, '# DB_HOST=127.0.0.1')) { 383 | $this->commentDatabaseConfigurationForSqlite($directory); 384 | 385 | return; 386 | } 387 | 388 | return; 389 | } 390 | 391 | // Any commented database configuration options should be uncommented when not on SQLite... 392 | $this->uncommentDatabaseConfiguration($directory); 393 | 394 | $defaultPorts = [ 395 | 'pgsql' => '5432', 396 | 'sqlsrv' => '1433', 397 | ]; 398 | 399 | if (isset($defaultPorts[$database])) { 400 | $this->replaceInFile( 401 | 'DB_PORT=3306', 402 | 'DB_PORT='.$defaultPorts[$database], 403 | $directory.'/.env' 404 | ); 405 | 406 | $this->replaceInFile( 407 | 'DB_PORT=3306', 408 | 'DB_PORT='.$defaultPorts[$database], 409 | $directory.'/.env.example' 410 | ); 411 | } 412 | 413 | $this->replaceInFile( 414 | 'DB_DATABASE=laravel', 415 | 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), 416 | $directory.'/.env' 417 | ); 418 | 419 | $this->replaceInFile( 420 | 'DB_DATABASE=laravel', 421 | 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), 422 | $directory.'/.env.example' 423 | ); 424 | } 425 | 426 | /** 427 | * Determine if the application is using Laravel 11 or newer. 428 | * 429 | * @param string $directory 430 | * @return bool 431 | */ 432 | public function usingLaravelVersionOrNewer(int $usingVersion, string $directory): bool 433 | { 434 | $version = json_decode(file_get_contents($directory.'/composer.json'), true)['require']['laravel/framework']; 435 | $version = str_replace('^', '', $version); 436 | $version = explode('.', $version)[0]; 437 | 438 | return $version >= $usingVersion; 439 | } 440 | 441 | /** 442 | * Comment the irrelevant database configuration entries for SQLite applications. 443 | * 444 | * @param string $directory 445 | * @return void 446 | */ 447 | protected function commentDatabaseConfigurationForSqlite(string $directory): void 448 | { 449 | $defaults = [ 450 | 'DB_HOST=127.0.0.1', 451 | 'DB_PORT=3306', 452 | 'DB_DATABASE=laravel', 453 | 'DB_USERNAME=root', 454 | 'DB_PASSWORD=', 455 | ]; 456 | 457 | $this->replaceInFile( 458 | $defaults, 459 | collect($defaults)->map(fn ($default) => "# {$default}")->all(), 460 | $directory.'/.env' 461 | ); 462 | 463 | $this->replaceInFile( 464 | $defaults, 465 | collect($defaults)->map(fn ($default) => "# {$default}")->all(), 466 | $directory.'/.env.example' 467 | ); 468 | } 469 | 470 | /** 471 | * Uncomment the relevant database configuration entries for non SQLite applications. 472 | * 473 | * @param string $directory 474 | * @return void 475 | */ 476 | protected function uncommentDatabaseConfiguration(string $directory) 477 | { 478 | $defaults = [ 479 | '# DB_HOST=127.0.0.1', 480 | '# DB_PORT=3306', 481 | '# DB_DATABASE=laravel', 482 | '# DB_USERNAME=root', 483 | '# DB_PASSWORD=', 484 | ]; 485 | 486 | $this->replaceInFile( 487 | $defaults, 488 | collect($defaults)->map(fn ($default) => substr($default, 2))->all(), 489 | $directory.'/.env' 490 | ); 491 | 492 | $this->replaceInFile( 493 | $defaults, 494 | collect($defaults)->map(fn ($default) => substr($default, 2))->all(), 495 | $directory.'/.env.example' 496 | ); 497 | } 498 | 499 | /** 500 | * Determine the default database connection. 501 | * 502 | * @param string $directory 503 | * @param \Symfony\Component\Console\Input\InputInterface $input 504 | * @return array 505 | */ 506 | protected function promptForDatabaseOptions(string $directory, InputInterface $input) 507 | { 508 | $defaultDatabase = collect( 509 | $databaseOptions = $this->databaseOptions() 510 | )->keys()->first(); 511 | 512 | if ($this->usingStarterKit($input)) { 513 | // Starter kits will already be migrated in post composer create-project command... 514 | $migrate = false; 515 | 516 | $input->setOption('database', 'sqlite'); 517 | } 518 | 519 | if (! $input->getOption('database') && $input->isInteractive()) { 520 | $input->setOption('database', select( 521 | label: 'Which database will your application use?', 522 | options: $databaseOptions, 523 | default: $defaultDatabase, 524 | )); 525 | 526 | if ($input->getOption('database') !== 'sqlite') { 527 | $migrate = confirm( 528 | label: 'Default database updated. Would you like to run the default database migrations?' 529 | ); 530 | } else { 531 | $migrate = true; 532 | } 533 | } 534 | 535 | return [$input->getOption('database') ?? $defaultDatabase, $migrate ?? $input->hasOption('database')]; 536 | } 537 | 538 | /** 539 | * Get the available database options. 540 | * 541 | * @return array 542 | */ 543 | protected function databaseOptions(): array 544 | { 545 | return collect([ 546 | 'sqlite' => ['SQLite', extension_loaded('pdo_sqlite')], 547 | 'mysql' => ['MySQL', extension_loaded('pdo_mysql')], 548 | 'mariadb' => ['MariaDB', extension_loaded('pdo_mysql')], 549 | 'pgsql' => ['PostgreSQL', extension_loaded('pdo_pgsql')], 550 | 'sqlsrv' => ['SQL Server', extension_loaded('pdo_sqlsrv')], 551 | ]) 552 | ->sortBy(fn ($database) => $database[1] ? 0 : 1) 553 | ->map(fn ($database) => $database[0].($database[1] ? '' : ' (Missing PDO extension)')) 554 | ->all(); 555 | } 556 | 557 | /** 558 | * Validate the database driver input. 559 | * 560 | * @param \Symfony\Components\Console\Input\InputInterface $input 561 | */ 562 | protected function validateDatabaseOption(InputInterface $input) 563 | { 564 | if ($input->getOption('database') && ! in_array($input->getOption('database'), $drivers = ['mysql', 'mariadb', 'pgsql', 'sqlite', 'sqlsrv'])) { 565 | throw new \InvalidArgumentException("Invalid database driver [{$input->getOption('database')}]. Valid options are: ".implode(', ', $drivers).'.'); 566 | } 567 | } 568 | 569 | /** 570 | * Install Pest into the application. 571 | * 572 | * @param \Symfony\Component\Console\Input\InputInterface $input 573 | * @param \Symfony\Component\Console\Output\OutputInterface $output 574 | * @return void 575 | */ 576 | protected function installPest(string $directory, InputInterface $input, OutputInterface $output) 577 | { 578 | $composerBinary = $this->findComposer(); 579 | 580 | $commands = [ 581 | $composerBinary.' remove phpunit/phpunit --dev --no-update', 582 | $composerBinary.' require pestphp/pest pestphp/pest-plugin-laravel --no-update --dev', 583 | $composerBinary.' update', 584 | $this->phpBinary().' ./vendor/bin/pest --init', 585 | ]; 586 | 587 | $commands[] = $composerBinary.' require pestphp/pest-plugin-drift --dev'; 588 | $commands[] = $this->phpBinary().' ./vendor/bin/pest --drift'; 589 | $commands[] = $composerBinary.' remove pestphp/pest-plugin-drift --dev'; 590 | 591 | $this->runCommands($commands, $input, $output, workingPath: $directory, env: [ 592 | 'PEST_NO_SUPPORT' => 'true', 593 | ]); 594 | 595 | if ($this->usingStarterKit($input)) { 596 | $this->replaceInFile( 597 | './vendor/bin/phpunit', 598 | './vendor/bin/pest', 599 | $directory.'/.github/workflows/tests.yml', 600 | ); 601 | 602 | $contents = file_get_contents("$directory/tests/Pest.php"); 603 | 604 | $contents = str_replace( 605 | " // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)", 606 | " ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)", 607 | $contents, 608 | ); 609 | 610 | file_put_contents("$directory/tests/Pest.php", $contents); 611 | 612 | $directoryIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator("$directory/tests")); 613 | 614 | foreach ($directoryIterator as $testFile) { 615 | if ($testFile->isDir()) { 616 | continue; 617 | } 618 | 619 | $contents = file_get_contents($testFile); 620 | 621 | file_put_contents( 622 | $testFile, 623 | str_replace("\n\nuses(\Illuminate\Foundation\Testing\RefreshDatabase::class);", '', $contents), 624 | ); 625 | } 626 | } 627 | 628 | $this->commitChanges('Install Pest', $directory, $input, $output); 629 | } 630 | 631 | /** 632 | * Create a Git repository and commit the base Laravel skeleton. 633 | * 634 | * @param string $directory 635 | * @param \Symfony\Component\Console\Input\InputInterface $input 636 | * @param \Symfony\Component\Console\Output\OutputInterface $output 637 | * @return void 638 | */ 639 | protected function createRepository(string $directory, InputInterface $input, OutputInterface $output) 640 | { 641 | $branch = $input->getOption('branch') ?: $this->defaultBranch(); 642 | 643 | $commands = [ 644 | 'git init -q', 645 | 'git add .', 646 | 'git commit -q -m "Set up a fresh Laravel app"', 647 | "git branch -M {$branch}", 648 | ]; 649 | 650 | $this->runCommands($commands, $input, $output, workingPath: $directory); 651 | } 652 | 653 | /** 654 | * Commit any changes in the current working directory. 655 | * 656 | * @param string $message 657 | * @param string $directory 658 | * @param \Symfony\Component\Console\Input\InputInterface $input 659 | * @param \Symfony\Component\Console\Output\OutputInterface $output 660 | * @return void 661 | */ 662 | protected function commitChanges(string $message, string $directory, InputInterface $input, OutputInterface $output) 663 | { 664 | if (! $input->getOption('git') && $input->getOption('github') === false) { 665 | return; 666 | } 667 | 668 | $commands = [ 669 | 'git add .', 670 | "git commit -q -m \"$message\"", 671 | ]; 672 | 673 | $this->runCommands($commands, $input, $output, workingPath: $directory); 674 | } 675 | 676 | /** 677 | * Create a GitHub repository and push the git log to it. 678 | * 679 | * @param string $name 680 | * @param string $directory 681 | * @param \Symfony\Component\Console\Input\InputInterface $input 682 | * @param \Symfony\Component\Console\Output\OutputInterface $output 683 | * @return void 684 | */ 685 | protected function pushToGitHub(string $name, string $directory, InputInterface $input, OutputInterface $output) 686 | { 687 | $process = new Process(['gh', 'auth', 'status']); 688 | $process->run(); 689 | 690 | if (! $process->isSuccessful()) { 691 | $output->writeln(' WARN Make sure the "gh" CLI tool is installed and that you\'re authenticated to GitHub. Skipping...'.PHP_EOL); 692 | 693 | return; 694 | } 695 | 696 | $name = $input->getOption('organization') ? $input->getOption('organization')."/$name" : $name; 697 | $flags = $input->getOption('github') ?: '--private'; 698 | 699 | $commands = [ 700 | "gh repo create {$name} --source=. --push {$flags}", 701 | ]; 702 | 703 | $this->runCommands($commands, $input, $output, workingPath: $directory, env: ['GIT_TERMINAL_PROMPT' => 0]); 704 | } 705 | 706 | /** 707 | * Configure the Composer "dev" script. 708 | * 709 | * @param string $directory 710 | * @return void 711 | */ 712 | protected function configureComposerDevScript(string $directory): void 713 | { 714 | $this->composer->modify(function (array $content) { 715 | if (windows_os()) { 716 | $content['scripts']['dev'] = [ 717 | 'Composer\\Config::disableProcessTimeout', 718 | "npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"npm run dev\" --names='server,queue,vite'", 719 | ]; 720 | } 721 | 722 | return $content; 723 | }); 724 | } 725 | 726 | /** 727 | * Verify that the application does not already exist. 728 | * 729 | * @param string $directory 730 | * @return void 731 | */ 732 | protected function verifyApplicationDoesntExist($directory) 733 | { 734 | if ((is_dir($directory) || is_file($directory)) && $directory != getcwd()) { 735 | throw new RuntimeException('Application already exists!'); 736 | } 737 | } 738 | 739 | /** 740 | * Generate a valid APP_URL for the given application name. 741 | * 742 | * @param string $name 743 | * @param string $directory 744 | * @return string 745 | */ 746 | protected function generateAppUrl($name, $directory) 747 | { 748 | if (! $this->isParkedOnHerdOrValet($directory)) { 749 | return 'http://localhost:8000'; 750 | } 751 | 752 | $hostname = mb_strtolower($name).'.'.$this->getTld(); 753 | 754 | return $this->canResolveHostname($hostname) ? 'http://'.$hostname : 'http://localhost'; 755 | } 756 | 757 | /** 758 | * Get the starter kit repository, if any. 759 | * 760 | * @param \Symfony\Component\Console\Input\InputInterface $input 761 | * @return string|null 762 | */ 763 | protected function getStarterKit(InputInterface $input): ?string 764 | { 765 | return match (true) { 766 | $input->getOption('react') => 'laravel/react-starter-kit', 767 | $input->getOption('vue') => 'laravel/vue-starter-kit', 768 | $input->getOption('livewire') => 'laravel/livewire-starter-kit', 769 | default => $input->getOption('using'), 770 | }; 771 | } 772 | 773 | /** 774 | * Determine if a Laravel first-party starter kit has been chosen. 775 | * 776 | * @param \Symfony\Component\Console\Input\InputInterface $input 777 | * @return bool 778 | */ 779 | protected function usingLaravelStarterKit(InputInterface $input): bool 780 | { 781 | return $this->usingStarterKit($input) && 782 | str_starts_with($this->getStarterKit($input), 'laravel/'); 783 | } 784 | 785 | /** 786 | * Determine if a starter kit is being used. 787 | * 788 | * @param \Symfony\Component\Console\Input\InputInterface $input 789 | * @return bool 790 | */ 791 | protected function usingStarterKit(InputInterface $input) 792 | { 793 | return $input->getOption('react') || $input->getOption('vue') || $input->getOption('livewire') || $input->getOption('using'); 794 | } 795 | 796 | /** 797 | * Get the TLD for the application. 798 | * 799 | * @return string 800 | */ 801 | protected function getTld() 802 | { 803 | return $this->runOnValetOrHerd('tld') ?: 'test'; 804 | } 805 | 806 | /** 807 | * Determine whether the given hostname is resolvable. 808 | * 809 | * @param string $hostname 810 | * @return bool 811 | */ 812 | protected function canResolveHostname($hostname) 813 | { 814 | return gethostbyname($hostname.'.') !== $hostname.'.'; 815 | } 816 | 817 | /** 818 | * Get the installation directory. 819 | * 820 | * @param string $name 821 | * @return string 822 | */ 823 | protected function getInstallationDirectory(string $name) 824 | { 825 | return $name !== '.' ? getcwd().'/'.$name : '.'; 826 | } 827 | 828 | /** 829 | * Get the version that should be downloaded. 830 | * 831 | * @param \Symfony\Component\Console\Input\InputInterface $input 832 | * @return string 833 | */ 834 | protected function getVersion(InputInterface $input) 835 | { 836 | if ($input->getOption('dev')) { 837 | return 'dev-master'; 838 | } 839 | 840 | return ''; 841 | } 842 | 843 | /** 844 | * Get the composer command for the environment. 845 | * 846 | * @return string 847 | */ 848 | protected function findComposer() 849 | { 850 | return implode(' ', $this->composer->findComposer()); 851 | } 852 | 853 | /** 854 | * Get the path to the appropriate PHP binary. 855 | * 856 | * @return string 857 | */ 858 | protected function phpBinary() 859 | { 860 | $phpBinary = function_exists('Illuminate\Support\php_binary') 861 | ? \Illuminate\Support\php_binary() 862 | : (new PhpExecutableFinder)->find(false); 863 | 864 | return $phpBinary !== false 865 | ? ProcessUtils::escapeArgument($phpBinary) 866 | : 'php'; 867 | } 868 | 869 | /** 870 | * Run the given commands. 871 | * 872 | * @param array $commands 873 | * @param \Symfony\Component\Console\Input\InputInterface $input 874 | * @param \Symfony\Component\Console\Output\OutputInterface $output 875 | * @param string|null $workingPath 876 | * @param array $env 877 | * @return \Symfony\Component\Process\Process 878 | */ 879 | protected function runCommands($commands, InputInterface $input, OutputInterface $output, ?string $workingPath = null, array $env = []) 880 | { 881 | if (! $output->isDecorated()) { 882 | $commands = array_map(function ($value) { 883 | if (Str::startsWith($value, ['chmod', 'git', $this->phpBinary().' ./vendor/bin/pest'])) { 884 | return $value; 885 | } 886 | 887 | return $value.' --no-ansi'; 888 | }, $commands); 889 | } 890 | 891 | if ($input->getOption('quiet')) { 892 | $commands = array_map(function ($value) { 893 | if (Str::startsWith($value, ['chmod', 'git', $this->phpBinary().' ./vendor/bin/pest'])) { 894 | return $value; 895 | } 896 | 897 | return $value.' --quiet'; 898 | }, $commands); 899 | } 900 | 901 | $process = Process::fromShellCommandline(implode(' && ', $commands), $workingPath, $env, null, null); 902 | 903 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 904 | try { 905 | $process->setTty(true); 906 | } catch (RuntimeException $e) { 907 | $output->writeln(' WARN '.$e->getMessage().PHP_EOL); 908 | } 909 | } 910 | 911 | $process->run(function ($type, $line) use ($output) { 912 | $output->write(' '.$line); 913 | }); 914 | 915 | return $process; 916 | } 917 | 918 | /** 919 | * Replace the given file. 920 | * 921 | * @param string $replace 922 | * @param string $file 923 | * @return void 924 | */ 925 | protected function replaceFile(string $replace, string $file) 926 | { 927 | $stubs = dirname(__DIR__).'/stubs'; 928 | 929 | file_put_contents( 930 | $file, 931 | file_get_contents("$stubs/$replace"), 932 | ); 933 | } 934 | 935 | /** 936 | * Replace the given string in the given file. 937 | * 938 | * @param string|array $search 939 | * @param string|array $replace 940 | * @param string $file 941 | * @return void 942 | */ 943 | protected function replaceInFile(string|array $search, string|array $replace, string $file) 944 | { 945 | file_put_contents( 946 | $file, 947 | str_replace($search, $replace, file_get_contents($file)) 948 | ); 949 | } 950 | 951 | /** 952 | * Replace the given string in the given file using regular expressions. 953 | * 954 | * @param string|array $search 955 | * @param string|array $replace 956 | * @param string $file 957 | * @return void 958 | */ 959 | protected function pregReplaceInFile(string $pattern, string $replace, string $file) 960 | { 961 | file_put_contents( 962 | $file, 963 | preg_replace($pattern, $replace, file_get_contents($file)) 964 | ); 965 | } 966 | 967 | /** 968 | * Delete the given file. 969 | * 970 | * @param string $file 971 | * @return void 972 | */ 973 | protected function deleteFile(string $file) 974 | { 975 | unlink($file); 976 | } 977 | } 978 | --------------------------------------------------------------------------------