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