├── 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 |
4 |
5 |
6 |
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 |
--------------------------------------------------------------------------------