diff --git a/src/Kickstart/Kickstart.php b/src/Kickstart/Kickstart.php new file mode 100644 index 00000000..c262e429 --- /dev/null +++ b/src/Kickstart/Kickstart.php @@ -0,0 +1,152 @@ +fs = $fs; + $this->project = new Project($projectPath, $hasTeams); + $this->stub = new Stub($template, $controllerType, $hasTeams); + } + + /** + * @return bool|int + * + * @throws InvalidArgumentException + */ + public function copyDraftToProject() + { + return $this->fs->put( + $this->project()->draftPath(), + $this->stub()->content() + ); + } + + /** + * @return void + * + * @throws FileNotFoundException + */ + public function copySeederToProject() + { + if (! $this->fs->exists($this->project()->draftPath())) { + throw new RuntimeException('The draft file does not exist in project'); + } + + if (! $this->fs->exists($this->stub()->seederPath())) { + throw new FileNotFoundException('The seeder stub does not exist'); + } + + $content = $this->fs->get($this->stub()->seederPath()); + + if (! $this->fs->put($this->project()->seederPath(), $content)) { + throw new RuntimeException('The seeder file could not be created'); + } + } + + /** + * @return void + */ + public function deleteGenericSeeders() + { + $finder = (new Finder()) + ->files() + ->in(dirname($this->project()->seederPath())) + ->notName([ + 'KickstartSeeder.php', + 'DatabaseSeeder.php', + ]); + + foreach ($finder as $genericSeederFile) { + if (false !== $path = $genericSeederFile->getRealPath()) { + $this->fs->delete($path); + } + } + } + + /** + * @return string|null + * + * @throws Throwable + */ + public function missingRequiredMigrationsMessage() + { + $migrationsDir = $this->project()->migrationsPath(); + + [$hasUserMigration, $hasTeamMigration] = [false, ! $this->project()->hasTeams()]; + foreach (scandir($migrationsDir) ?: [] as $fileName) { + if (str($fileName)->is('*create_users_table.php')) { + $hasUserMigration = true; + } + + if (str($fileName)->is('*create_teams_table.php')) { + $hasTeamMigration = true; + } + } + + if ($hasUserMigration && $hasTeamMigration) { + return null; + } + + $missingMigrations = collect(['user' => ! $hasUserMigration, 'team' => ! $hasTeamMigration]) + ->filter() + ->keys(); + + return sprintf('%s seeder bypassed: the %s %s %s missing', + $this->stub()->template(), + $missingMigrations->join(' and '), + Pluralizer::plural('migration', $missingMigrations), + $missingMigrations->count() > 1 ? 'are' : 'is' + ); + } + + /** + * @return Project + */ + public function project(): Project + { + return $this->project; + } + + /** + * @return Stub + */ + public function stub(): Stub + { + return $this->stub; + } +} diff --git a/src/Kickstart/Project.php b/src/Kickstart/Project.php new file mode 100644 index 00000000..f8fc1bc9 --- /dev/null +++ b/src/Kickstart/Project.php @@ -0,0 +1,76 @@ +basePath = $basePath; + $this->hasTeams = $hasTeams; + } + + /** + * @return string + * + * @throws InvalidArgumentException + */ + public function basePath() + { + throw_unless( + is_dir($this->basePath), + InvalidArgumentException::class, + "The path [{$this->basePath}] does not exist" + ); + + return $this->basePath; + } + + /** + * @return string + */ + public function draftPath() + { + return join_paths($this->basePath, 'draft.yaml'); + } + + /** + * @return bool + */ + public function hasTeams() + { + return $this->hasTeams; + } + + /** + * @return string + */ + public function migrationsPath() + { + return join_paths($this->basePath, 'database', 'migrations'); + } + + /** + * @return string + */ + public function seederPath() + { + return join_paths($this->basePath, 'database', 'seeders', 'KickstartSeeder.php'); + } +} diff --git a/src/Kickstart/Stub.php b/src/Kickstart/Stub.php new file mode 100644 index 00000000..facead47 --- /dev/null +++ b/src/Kickstart/Stub.php @@ -0,0 +1,223 @@ + + */ + private static array $modelNames = [ + 'blog' => ['Post', 'Comment'], + 'podcast' => ['Podcast', 'Episode', 'Genre'], + 'phone-book' => ['Person', 'Business', 'Phone', 'Address'], + ]; + + public function __construct(string $template, string $controllerType, bool $hasTeams) + { + $this->teams = $hasTeams; + $this->template = $template; + $this->controllerType = $controllerType; + $this->basePath = join_paths(dirname(__DIR__, 2), 'stubs', 'kickstart', $template); + } + + /** + * @return false|string + * + * @throws InvalidArgumentException + */ + public function content() + { + return str_replace( + '{{ controllers }}', + $this->controllerContent(), + file_get_contents($this->draftPath()), + ); + } + + /** + * @return 'none' | 'empty' | 'api' | 'web' + * + * @throws InvalidArgumentException + */ + public function controllerType() + { + throw_unless( + in_array($this->controllerType, ['none', 'empty', 'api', 'web']), + InvalidArgumentException::class, + "[{$this->controllerType}] is not a valid controller type" + ); + + return $this->controllerType; + } + + public function displayName() + { + return str($this->template())->replace('-', ' ')->title()->toString(); + } + + /** + * @return string + */ + public function draftPath() + { + return $this->teams + ? join_paths($this->basePath, 'draft-with-teams.yaml.stub') + : join_paths($this->basePath, 'draft.yaml.stub'); + } + + /** + * @return string[] + * + * @throws InvalidArgumentException + */ + public function modelNames() + { + return self::$modelNames[$this->template()]; + } + + /** + * @return string + */ + public function seederPath() + { + return join_paths($this->basePath, 'Seeder.php.stub'); + } + + /** + * @return string + * + * @throws InvalidArgumentException + */ + public function template() + { + static $validated; + + $template = $this->template; + + if ($validated) { + return $template; + } + + throw_unless( + in_array($template, ['blog', 'podcast', 'phone-book']), + InvalidArgumentException::class, + "[{$template}] is not listed as a valid kickstart template" + ); + + $expectedStubFiles = [ + 'draft.yaml.stub', + 'draft-with-teams.yaml.stub', + 'Seeder.php.stub', + ]; + + foreach ($expectedStubFiles as $stubFile) { + $stubPath = join_paths($this->basePath, $stubFile); + + throw_unless( + file_exists($stubPath), + InvalidArgumentException::class, + "The [{$stubFile}] stub file does not exist" + ); + } + + $validated = true; + + return $template; + } + + /** + * @return string + * + * @throws InvalidArgumentException + */ + private function controllerContent() + { + if ($this->controllerType() === 'empty') { + return $this->emptyControllersContent(); + } + + if ($this->controllerType() === 'api') { + return $this->apiControllersContent(); + } + + if ($this->controllerType() === 'web') { + return $this->webControllersContent(); + } + + return ''; + } + + /** + * @return string + */ + private function emptyControllersContent() + { + $result = 'controllers:'.PHP_EOL; + + foreach ($this->modelNames() as $model) { + $result .= " {$model}:".PHP_EOL; + $result .= ' resource: none'.PHP_EOL; + } + + return $result; + } + + /** + * @return string + */ + private function apiControllersContent() + { + $result = 'controllers:'.PHP_EOL; + foreach ($this->modelNames() as $model) { + $pluralResource = str($model)->lower()->plural(); + + $result .= " {$model}:".PHP_EOL; + $result .= ' resource: api'.PHP_EOL; + $result .= ' index:'.PHP_EOL; + $result .= " resource: 'paginate:{$pluralResource}'".PHP_EOL; + } + + return $result; + } + + /** + * @return string + */ + private function webControllersContent() + { + $result = 'controllers:'.PHP_EOL; + foreach ($this->modelNames() as $model) { + $result .= " {$model}:".PHP_EOL; + $result .= ' resource'.PHP_EOL; + } + + return $result; + } +} diff --git a/src/NewCommand.php b/src/NewCommand.php index e0744ece..b000493c 100644 --- a/src/NewCommand.php +++ b/src/NewCommand.php @@ -2,9 +2,12 @@ namespace Laravel\Installer\Console; +use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Composer; use Illuminate\Support\ProcessUtils; +use Illuminate\Support\Str; +use Laravel\Installer\Console\Kickstart\Kickstart; use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -13,6 +16,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; +use Throwable; use function Laravel\Prompts\confirm; use function Laravel\Prompts\multiselect; @@ -31,6 +35,11 @@ class NewCommand extends Command */ protected $composer; + /** + * @var Kickstart + */ + protected $kickstart; + /** * Configure the command options. * @@ -62,7 +71,8 @@ protected function configure() ->addOption('phpunit', null, InputOption::VALUE_NONE, 'Installs the PHPUnit testing framework') ->addOption('prompt-breeze', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Breeze should be installed (Deprecated)') ->addOption('prompt-jetstream', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Jetstream should be installed (Deprecated)') - ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists'); + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists') + ->addOption('kickstart', 'k', InputOption::VALUE_OPTIONAL, 'Generates scaffolding after the project is created for immediate prototyping and Eloquent interactions'); } /** @@ -144,6 +154,65 @@ protected function interact(InputInterface $input, OutputInterface $output) ) === 'Pest'); } + if (! version_compare(PHP_VERSION, '7.4.0', '>=')) { + $output->writeln(' WARN Kickstart options bypassed. PHP >= 7.4 is required'.PHP_EOL); + return; + } + + $kickstartTemplate = transform($input->getOption('kickstart'), Str::kebab(...)); + + if (! in_array($kickstartTemplate, ['none', 'blog', 'podcast', 'phone-book'])) { + $input->setOption('kickstart', select( + label: 'Would you like to kickstart this project with a pre-defined template?', + options: [ + 'none' => 'None', + 'blog' => 'Blog', + 'podcast' => 'Podcast', + 'phone-book' => 'Phone Book', + ], + default: 'none', + hint: implode(PHP_EOL.' ', [ + 'Eloquent Relationships Included:', + 'Blog: BelongsTo, HasMany', + 'Podcast: BelongsTo, HasMany, BelongsToMany', + 'Phone Book: BelongsTo, 1-to-M Polymorphic, M-to-M Polymorphic', + ]) + )); + } + + if ($input->getOption('kickstart') !== 'none') { + $stack = $input->getOption('stack'); + + if ($input->getOption('jet') || ( + $input->getOption('breeze') && ! in_array($stack, ['api', 'blade']) + )) { + $defaultControllerType = 'none'; + } elseif ($stack === 'blade') { + $defaultControllerType = 'web'; + } else { + $defaultControllerType = 'api'; + } + + $controllerType = select( + label: 'Which types of controllers would you like to kickstart with?', + options: [ + 'api' => 'API Resource Controllers', + 'web' => 'Resource Controllers', + 'none' => 'No Controllers', + 'empty' => 'Empty Controllers', + ], + default: $defaultControllerType, + hint: "The default is chosen based the project you're building" + ); + + $this->kickstart = new Kickstart( + $this->getInstallationDirectory($input->getArgument('name')), + $controllerType, + $input->getOption('kickstart'), + $input->getOption('teams') + ); + } + // if (! $input->getOption('git') && $input->getOption('github') === false && Process::fromShellCommandline('git --version')->run() === 0) { // $input->setOption('git', confirm(label: 'Would you like to initialize a Git repository?', default: false)); // } @@ -156,7 +225,7 @@ protected function interact(InputInterface $input, OutputInterface $output) * @param \Symfony\Component\Console\Output\OutputInterface $output * @return void * - * @throws \RuntimeException + * @throws RuntimeException */ protected function ensureExtensionsAreAvailable(InputInterface $input, OutputInterface $output): void { @@ -176,7 +245,7 @@ protected function ensureExtensionsAreAvailable(InputInterface $input, OutputInt return; } - throw new \RuntimeException( + throw new RuntimeException( sprintf('The following PHP extensions are required but are not installed: %s', $missingExtensions->join(', ', ', and ')) ); } @@ -230,6 +299,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands[] = "chmod 755 \"$directory/artisan\""; } + $migrate = false; if (($process = $this->runCommands($commands, $input, $output))->isSuccessful()) { if ($name !== '.') { $this->replaceInFile( @@ -270,6 +340,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->installPest($directory, $input, $output); } + if ($input->getOption('kickstart') !== 'none') { + $this->runKickstart($migrate, $input, $output); + } + if ($input->getOption('github') !== false) { $this->pushToGitHub($name, $directory, $input, $output); $output->writeln(''); @@ -1018,4 +1092,80 @@ protected function pregReplaceInFile(string $pattern, string $replace, string $f preg_replace($pattern, $replace, file_get_contents($file)) ); } + + /** + * @param bool $ranDefaultMigrations + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * + * @throws FileNotFoundException + * @throws Throwable + */ + private function runKickstart(bool $ranDefaultMigrations, InputInterface $input, OutputInterface $output) + { + $output->writeln(''); + $seed = ! $input->isInteractive() || ($ranDefaultMigrations && confirm( + label: 'Run the kickstart seeder for the '.$this->kickstart->stub()->displayName().' template?', + hint: 'Or, run manually: `php artisan db:seed --class=KickstartSeeder`' + )); + + if (! $this->kickstart->copyDraftToProject()) { + $output->writeln(' WARN Failed to copy draft.yaml to project. Kickstart disabled...'.PHP_EOL); + + return; + } + + $projectPath = $this->kickstart->project()->basePath(); + + $commands = array_filter([ + $this->installApiCommand($seed), + $this->findComposer().' require laravel-shift/blueprint jasonmccreary/laravel-test-assertions --dev', + "echo '{$projectPath}/draft.yaml' >> .gitignore", + "echo '{$projectPath}/.blueprint' >> .gitignore", + $this->phpBinary().' artisan vendor:publish --tag=blueprint-config', + $this->phpBinary()." artisan blueprint:build {$this->kickstart->project()->draftPath()}", + ]); + + $this->runCommands($commands, $input, $output, workingPath: $projectPath); + + $this->kickstart->deleteGenericSeeders(); + + $this->kickstart->copySeederToProject(); + + if ($seed) { + $this->runCommands([$this->phpBinary().' artisan migrate'], $input, $output, workingPath: $projectPath); + + if ($msg = $this->kickstart->missingRequiredMigrationsMessage()) { + $output->writeln(' WARN '.$msg.PHP_EOL); + } else { + $this->runCommands([$this->phpBinary().' artisan db:seed --class=KickstartSeeder'], $input, $output, workingPath: $projectPath); + } + } + + $this->commitChanges('Kickstart Complete', $projectPath, $input, $output); + + $output->writeln(' INFO Kickstart files created 🚀.'.PHP_EOL); + } + + /** + * @param bool $seed + * @return string|null + */ + private function installApiCommand(bool $seed) + { + if (! $this->usingLaravelVersionOrNewer(11, $this->kickstart->project()->basePath())) { + return null; + } + + if ($this->kickstart->stub()->controllerType() !== 'api') { + return null; + } + + if (! $seed) { + return $this->phpBinary().' artisan install:api --without-migration-prompt'; + } + + return $this->phpBinary().' artisan install:api --no-interaction'; + } } diff --git a/stubs/kickstart/blog/Seeder.php.stub b/stubs/kickstart/blog/Seeder.php.stub new file mode 100644 index 00000000..4d0d6e1b --- /dev/null +++ b/stubs/kickstart/blog/Seeder.php.stub @@ -0,0 +1,73 @@ +withoutForeignKeyConstraints($this->seedPosts(...)); + } + + private function seedPosts(): void + { + foreach ($this->users() as $user) { + Post::factory(static::$postCount) + ->for($user) + ->has(Comment::factory(static::$commentCount)) + ->sequence(function () { + return Lottery::odds(1/3) + ->winner(fn () => ['published_at' => now()]) + ->loser(fn () => ['published_at' => null]) + ->choose(); + }) + ->sequence(array_filter([ + 'team_id' => rescue(fn () => $user->teams->random()->getKey(), report: false), + ])) + ->create(); + } + } + + private function users(): EloquentCollection + { + $users = User::query() + ->take(static::$userCount) + ->get(); + + if ($users->isNotEmpty()) { + return $users; + } + + $userTeamQualifies = fn () => class_exists(Team::class) + && method_exists(User::class, 'teams') + && rescue(fn () => Arr::has(Team::factory()->raw(), ['name', 'user_id', 'personal_team']), report: false); + + return EloquentCollection::wrap( + User::factory(static::$userCount) + ->when($userTeamQualifies, function ($factory) { + return $factory->has(Team::factory()->state(fn (array $attributes, User $user) => [ + 'name' => $user->name ? "{$user->name}'s Team" : 'Personal Team', + 'user_id' => $user->getKey(), + 'personal_team' => true, + ])); + }) + ->create() + ); + } +} diff --git a/stubs/kickstart/blog/draft-with-teams.yaml.stub b/stubs/kickstart/blog/draft-with-teams.yaml.stub new file mode 100644 index 00000000..b46065b4 --- /dev/null +++ b/stubs/kickstart/blog/draft-with-teams.yaml.stub @@ -0,0 +1,26 @@ +# Beginner difficulty +# Blueprint documentation: https://blueprint.laravelshift.com +kickstart-template: blog + +models: + Post: + title: string index + content: text + published_at: timestamp nullable + user_id: unsignedBigInteger nullable foreign:users + team_id: unsignedBigInteger nullable foreign:teams + timestamps: true + relationships: + belongsTo: User, Team + hasMany: Comment + + Comment: + body: text + timestamps: true + post_id: unsignedBigInteger foreign:posts + relationships: + belongsTo: Post + +{{ controllers }} + +seeders: Post, Comment diff --git a/stubs/kickstart/blog/draft.yaml.stub b/stubs/kickstart/blog/draft.yaml.stub new file mode 100644 index 00000000..92da9dad --- /dev/null +++ b/stubs/kickstart/blog/draft.yaml.stub @@ -0,0 +1,25 @@ +# Beginner difficulty +# Blueprint documentation: https://blueprint.laravelshift.com +kickstart-template: blog + +models: + Post: + title: string index + content: text + published_at: timestamp nullable + user_id: unsignedBigInteger nullable foreign:users + timestamps: true + relationships: + belongsTo: User + hasMany: Comment + + Comment: + body: text + timestamps: true + post_id: unsignedBigInteger foreign:posts + relationships: + belongsTo: Post + +{{ controllers }} + +seeders: Post, Comment diff --git a/stubs/kickstart/phone-book/Seeder.php.stub b/stubs/kickstart/phone-book/Seeder.php.stub new file mode 100644 index 00000000..c8c19c69 --- /dev/null +++ b/stubs/kickstart/phone-book/Seeder.php.stub @@ -0,0 +1,167 @@ +phoneables = EloquentCollection::make(); + $this->addressables = EloquentCollection::make(); + + DB::getSchemaBuilder()->withoutForeignKeyConstraints(function () { + $this->seedPeople(); + $this->seedBusinesses(); + $this->seedAddresses(); + $this->seedPhones(); + }); + } + + private function seedPeople(): void + { + foreach ($this->users() as $user) { + $this->seedPerson($user); + } + + Collection::times(static::$peopleCount, fn () => $this->seedPerson()); + } + + private function seedBusinesses(): void + { + $businesses = EloquentCollection::wrap( + Business::factory(static::$businessCount) + ->sequence(function () { + return array_filter([ + 'name' => fake()->unique()->company(), + 'industry' => Arr::random(self::INDUSTRIES), + 'team_id' => rescue(fn () => Team::inRandomOrder()->first()->getKey(), report: false), + ]); + }) + ->create() + ); + + if (method_exists($businesses->first(), 'team')) { + $businesses->count() && $businesses->loadMissing('team'); + } + + $this->phoneables = $this->phoneables->merge($businesses); + $this->addressables = $this->addressables->merge($businesses); + } + + private function seedPerson(?User $user = null): void + { + $people = EloquentCollection::wrap(Person::factory() + ->when($user, function ($factory, $user) { + $team = rescue(fn () => $user->teams->random(), report: false); + + return $factory + ->for($user) + ->when($team, fn ($f) => $f->for($team->getKey())); + }) + ->create() + ); + + if (method_exists($people->first(), 'team')) { + $people->count() && $people->loadMissing('team'); + } + + $this->phoneables = $this->phoneables->merge($people); + $this->addressables = $this->addressables->merge($people); + } + + private function seedPhones(): void + { + foreach ($this->phoneables as $phoneable) { + $phones = Phone::factory() + ->count(static::$phoneCount) + ->sequence(function () { + return Arr::random([ + ['number' => fake()->e164PhoneNumber(), 'type' => 'mobile'], + ['number' => fake()->e164PhoneNumber(), 'type' => 'home'], + ['number' => fake()->e164PhoneNumber(), 'type' => 'work'], + ['number' => fake()->e164PhoneNumber(), 'type' => 'fax'], + ['number' => fake()->e164PhoneNumber(), 'type' => 'business'], + ]); + }) + ->create(); + + $phoneable->phones()->sync($phones); + } + } + + private function seedAddresses(): void + { + foreach ($this->addressables as $addressable) { + $addressable->address()->save( + Address::factory()->make(array_filter([ + 'city' => fake()->city(), + 'street' => fake()->streetName(), + 'state' => Arr::random(['NV', 'CA', 'OR', 'TX', 'UT']), + 'zip' => Str::limit(fake()->postcode(), 5, ''), + ])) + ); + } + } + + private function users(): EloquentCollection + { + $users = User::query() + ->take(static::$userCount) + ->get(); + + if ($users->isNotEmpty()) { + return $users; + } + + $userTeamQualifies = fn () => class_exists(Team::class) + && method_exists(User::class, 'teams') + && rescue(fn () => Arr::has(Team::factory()->raw(), ['name', 'user_id', 'personal_team']), report: false); + + return EloquentCollection::wrap( + User::factory(static::$userCount) + ->when($userTeamQualifies, function ($factory) { + return $factory->has(Team::factory()->state(fn (array $attributes, User $user) => [ + 'name' => $user->name ? "{$user->name}'s Team" : 'Personal Team', + 'user_id' => $user->getKey(), + 'personal_team' => true, + ])); + }) + ->create() + ); + } +} diff --git a/stubs/kickstart/phone-book/draft-with-teams.yaml.stub b/stubs/kickstart/phone-book/draft-with-teams.yaml.stub new file mode 100644 index 00000000..e03334bc --- /dev/null +++ b/stubs/kickstart/phone-book/draft-with-teams.yaml.stub @@ -0,0 +1,49 @@ +# Advanced difficulty +# Blueprint documentation: https://blueprint.laravelshift.com +kickstart-template: phone-book + +models: + Person: + name: string + email: string index + team_id: unsignedBigInteger nullable foreign:teams + user_id: unsignedBigInteger nullable foreign:users + timestamps: true + relationships: + belongsTo: User, Team + morphOne: Address + morphToMany: Phone + + Business: + name: string index + industry: string + team_id: unsignedBigInteger nullable foreign:teams + timestamps: true + relationships: + belongsTo: Team + morphOne: Address + morphToMany: Phone + + Phone: + number: string + type: string + timestamps: true + indexes: + - unique: number, type + relationships: + morphedByMany: Person, Business + + Address: + street: string index + city: string + state: string + zip: string index + timestamps: true + indexes: + - unique: addressable_id, addressable_type + relationships: + morphTo: addressable + +{{ controllers }} + +seeders: Person, Business, Phone, Address diff --git a/stubs/kickstart/phone-book/draft.yaml.stub b/stubs/kickstart/phone-book/draft.yaml.stub new file mode 100644 index 00000000..bdfa5cc7 --- /dev/null +++ b/stubs/kickstart/phone-book/draft.yaml.stub @@ -0,0 +1,46 @@ +# Advanced difficulty +# Blueprint documentation: https://blueprint.laravelshift.com +kickstart-template: phone-book + +models: + Person: + name: string + email: string index + user_id: unsignedBigInteger nullable foreign:users + timestamps: true + relationships: + belongsTo: User + morphOne: Address + morphToMany: Phone + + Business: + name: string index + industry: string + timestamps: true + relationships: + morphOne: Address + morphToMany: Phone + + Phone: + number: string + type: string + timestamps: true + indexes: + - unique: number, type + relationships: + morphedByMany: Person, Business + + Address: + street: string index + city: string + state: string + zip: string index + timestamps: true + indexes: + - unique: addressable_id, addressable_type + relationships: + morphTo: addressable + +{{ controllers }} + +seeders: Person, Business, Phone, Address diff --git a/stubs/kickstart/podcast/Seeder.php.stub b/stubs/kickstart/podcast/Seeder.php.stub new file mode 100644 index 00000000..2b56edc9 --- /dev/null +++ b/stubs/kickstart/podcast/Seeder.php.stub @@ -0,0 +1,140 @@ +genres = EloquentCollection::make(); + $this->podcasts = EloquentCollection::make(); + + DB::getSchemaBuilder()->withoutForeignKeyConstraints(function () { + $this->seedPodcasts(); + $this->seedGenres(); + $this->seedPodcastToGenres(); + }); + } + + private function seedPodcasts(): void + { + foreach ($this->users() as $user) { + $this->podcasts = $this->podcasts->concat( + EloquentCollection::wrap( + Podcast::factory(static::$podcastCount) + ->has(Episode::factory(static::$episodeCount)) + ->create(array_filter([ + 'team_id' => rescue(fn () => $user->teams->random()->getKey(), report: false), + ])) + ) + ); + } + } + + private function seedGenres(): void + { + $genresAvailable = self::GENRES_AVAILABLE; + + $this->genres = Genre::factory(self::$genreCount) + ->sequence(function () use (&$genresAvailable) { + return ['name' => array_shift($genresAvailable)]; + }) + ->create(); + } + + private function seedPodcastToGenres(): void + { + $this + ->podcasts + ->flatMap(function (Podcast $podcast) { + return $this + ->genres + ->shuffle() + ->take(static::$podcastGenreCount) + ->map(function (Genre $genre) use ($podcast) { + return (object) ['genre_id' => $genre->getKey(), 'podcast_id' => $podcast->getKey()]; + }) + ->all(); + }) + ->chunk(500) + ->each(function ($chunk) { + $chunk + ->map(fn($p) => (array)$p) + ->whenNotEmpty(function ($chunk) { + GenrePodcast::insert($chunk->toArray()); + }); + }); + } + + private function users(): EloquentCollection + { + $users = User::query() + ->take(static::$userCount) + ->get(); + + if ($users->isNotEmpty()) { + return $users; + } + + $userTeamQualifies = fn () => class_exists(Team::class) + && method_exists(User::class, 'teams') + && rescue(fn () => Arr::has(Team::factory()->raw(), ['name', 'user_id', 'personal_team']), report: false); + + return EloquentCollection::wrap( + User::factory(static::$userCount) + ->when($userTeamQualifies, function ($factory) { + return $factory->has(Team::factory()->state(fn (array $attributes, User $user) => array_filter([ + 'name' => $user->name ? "{$user->name}'s Team" : 'Personal Team', + 'user_id' => $user->getKey(), + 'personal_team' => true, + ]))); + }) + ->create() + ); + } +} diff --git a/stubs/kickstart/podcast/draft-with-teams.yaml.stub b/stubs/kickstart/podcast/draft-with-teams.yaml.stub new file mode 100644 index 00000000..d9eda2b3 --- /dev/null +++ b/stubs/kickstart/podcast/draft-with-teams.yaml.stub @@ -0,0 +1,42 @@ +# Intermediate difficulty +# Blueprint documentation: https://blueprint.laravelshift.com +kickstart-template: podcast + +models: + Podcast: + title: string index + description: text + team_id: unsignedBigInteger nullable foreign:teams + timestamps: true + relationships: + belongsTo: Team + hasMany: Episode + belongsToMany: Genre:&GenrePodcast + + Episode: + title: string index + description: text + podcast_id: unsignedBigInteger foreign:podcasts + timestamps: true + relationships: + belongsTo: Podcast + + Genre: + name: string index + timestamps: true + relationships: + belongsToMany: Podcast:&GenrePodcast + + GenrePodcast: + meta: + pivot: true + table: genre_podcast + genre_id: id + podcast_id: id + timestamps: false + indexes: + - unique: genre_id, podcast_id + +{{ controllers }} + +seeders: Podcast, Episode, Genre diff --git a/stubs/kickstart/podcast/draft.yaml.stub b/stubs/kickstart/podcast/draft.yaml.stub new file mode 100644 index 00000000..c0285d89 --- /dev/null +++ b/stubs/kickstart/podcast/draft.yaml.stub @@ -0,0 +1,40 @@ +# Intermediate difficulty +# Blueprint documentation: https://blueprint.laravelshift.com +kickstart-template: podcast + +models: + Podcast: + title: string index + description: text + timestamps: true + relationships: + hasMany: Episode + belongsToMany: Genre:&GenrePodcast + + Episode: + title: string index + description: text + podcast_id: unsignedBigInteger foreign:podcasts + timestamps: true + relationships: + belongsTo: Podcast + + Genre: + name: string index + timestamps: true + relationships: + belongsToMany: Podcast:&GenrePodcast + + GenrePodcast: + meta: + pivot: true + table: genre_podcast + genre_id: id + podcast_id: id + timestamps: false + indexes: + - unique: genre_id, podcast_id + +{{ controllers }} + +seeders: Podcast, Episode, Genre