feat: add database option to installer. updated: SYMFONY_DOCKER_REF by ili101 · Pull Request #3009 · api-platform/api-platform · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
11 changes: 11 additions & 0 deletions src/InstallerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ protected function configure(): void
->addOption('with-pwa', null, InputOption::VALUE_NEGATABLE, 'Include Next.js PWA (Symfony only)')
->addOption('with-admin', null, InputOption::VALUE_NEGATABLE, 'Include React-admin SPA')
->addOption('with-docker', null, InputOption::VALUE_NEGATABLE, 'Use Docker (Symfony only)')
->addOption('with-database', null, InputOption::VALUE_NEGATABLE, 'Create a Doctrine entity and database migration (Symfony only)')
->addOption('with-agents', null, InputOption::VALUE_NEGATABLE, 'Write AI agent instruction files AGENTS.md and CLAUDE.md (default: yes)')
->addOption('format', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'API formats (jsonld|jsonapi|hal); repeat for several')
->addOption('docs', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Documentation (swagger_ui|redoc|scalar); repeat for several, empty disables');
Expand Down Expand Up @@ -123,13 +124,19 @@ private function resolveOptions(SymfonyStyle $io, InputInterface $input, string
{
$withDocker = false;
$withPwa = false;
$withDatabase = false;

if (self::FRAMEWORK_SYMFONY === $framework) {
$dockerOption = $input->getOption('with-docker');
$withDocker = null !== $dockerOption
? (bool) $dockerOption
: (bool) $io->askQuestion(new ConfirmationQuestion('Use Docker?', true));

$databaseOption = $input->getOption('with-database');
$withDatabase = null !== $databaseOption
? (bool) $databaseOption
: $input->isInteractive() && (bool) $io->askQuestion(new ConfirmationQuestion('Use a database?', false));

$pwaOption = $input->getOption('with-pwa');
if (null !== $pwaOption) {
$withPwa = (bool) $pwaOption;
Expand All @@ -148,6 +155,9 @@ private function resolveOptions(SymfonyStyle $io, InputInterface $input, string
if (true === $input->getOption('with-pwa')) {
throw new InvalidArgumentException('--with-pwa is not supported with Laravel.');
}
if (true === $input->getOption('with-database')) {
throw new InvalidArgumentException('--with-database is not supported with Laravel.');
}
}

$withAdmin = $this->resolveWithAdmin($io, $input);
Expand All @@ -167,6 +177,7 @@ private function resolveOptions(SymfonyStyle $io, InputInterface $input, string
docs: $docs,
withAdmin: $withAdmin,
withAgents: $withAgents,
withDatabase: $withDatabase,
);
}

Expand Down
1 change: 1 addition & 0 deletions src/Scaffold/ScaffoldOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(
public array $docs,
public bool $withAdmin = false,
public bool $withAgents = true,
public bool $withDatabase = false,
) {
}
}
42 changes: 38 additions & 4 deletions src/Scaffold/SymfonyScaffold.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
final class SymfonyScaffold
{
private const SYMFONY_DOCKER_REPO = 'https://github.com/dunglas/symfony-docker';
public const SYMFONY_DOCKER_REF = '3c0d1772e807a2e54b6c9c53471ef25c5782e275';
public const SYMFONY_DOCKER_REF = 'bd4320fa303d506c8a1d0593f0e9a7ed45dc13fb';
private const DOCKER_FILES = [
'Dockerfile',
'.dockerignore',
Expand All @@ -30,6 +30,15 @@ final class SymfonyScaffold
'jsonapi' => 'application/vnd.api+json',
'hal' => 'application/hal+json',
];
private const DATABASE_SETUP_COMMANDS = [
['composer', 'require', 'symfony/maker-bundle', '--dev', '--no-interaction'],
['composer', 'require', 'orm', '--no-interaction'],
];
private const DATABASE_TEMPLATE_FILES = [
'database/Entity/Greetings.php' => 'src/Entity/Greetings.php',
'database/Repository/GreetingsRepository.php' => 'src/Repository/GreetingsRepository.php',
'database/Migrations/Version20260824200921.php' => 'migrations/Version20260824200921.php',
];

private readonly Filesystem $fs;
private readonly ProcessRunner $runner;
Expand Down Expand Up @@ -88,9 +97,13 @@ public function run(string $projectDir, string $projectName, ScaffoldOptions $op
$this->fs->mkdir($routesDir);
$this->fs->copy(Templates::path('routes.yaml'), $routesDir.'/api_platform.yaml', true);

$resourceDir = $apiDir.'/src/ApiResource';
$this->fs->mkdir($resourceDir);
$this->fs->copy(Templates::path('Greetings.php'), $resourceDir.'/Greetings.php', true);
if ($opts->withDatabase) {
$this->setupDatabase($apiDir, $opts->withDocker);
} else {
$resourceDir = $apiDir.'/src/ApiResource';
$this->fs->mkdir($resourceDir);
$this->fs->copy(Templates::path('Greetings.php'), $resourceDir.'/Greetings.php', true);
}

if ($opts->withPwa) {
$this->io->writeln('<info>Setting up Next.js PWA</info>');
Expand Down Expand Up @@ -168,6 +181,27 @@ private static function composerHasRequirement(string $apiDir, string $package):
return isset($json['require'][$package]) || isset($json['require-dev'][$package]);
}

private function setupDatabase(string $apiDir, bool $withDocker): void
{
$this->io->writeln('<info>Installing Doctrine database support</info>');
foreach (self::DATABASE_SETUP_COMMANDS as $command) {
$this->runner->run($command, $apiDir, self::databaseSetupEnvironment($withDocker));
}
foreach (self::DATABASE_TEMPLATE_FILES as $template => $destination) {
$destination = $apiDir.'/'.$destination;
$this->fs->mkdir(\dirname($destination));
$this->fs->copy(Templates::path($template), $destination, true);
}
}

/**
* @return array<string, string>
*/
private static function databaseSetupEnvironment(bool $withDocker): array
{
return ['SYMFONY_DOCKER' => $withDocker ? '1' : '0'];
}

private function setupDocker(string $apiDir): void
{
$this->io->writeln('<info>Fetching Docker infrastructure from symfony-docker</info>');
Expand Down
37 changes: 37 additions & 0 deletions templates/database/Entity/Greetings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\Repository\GreetingsRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: GreetingsRepository::class)]
#[ApiResource]
class Greetings
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\Column(length: 255)]
private ?string $name = null;

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(string $name): static
{
$this->name = $name;

return $this;
}
}
26 changes: 26 additions & 0 deletions templates/database/Migrations/Version20260824200921.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260824200921 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create the greetings table';
}

public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE greetings (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
}

public function down(Schema $schema): void
{
$this->addSql('DROP TABLE greetings');
}
}
18 changes: 18 additions & 0 deletions templates/database/Repository/GreetingsRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Repository;

use App\Entity\Greetings;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends ServiceEntityRepository<Greetings>
*/
class GreetingsRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Greetings::class);
}
}
63 changes: 62 additions & 1 deletion tests/InstallerCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,65 @@ public function testRejectsWithPwaOnLaravel(): void
$this->assertStringContainsString('--with-pwa is not supported with Laravel', $tester->getDisplay());
}

public function testRejectsWithDatabaseOnLaravel(): void
{
$tester = $this->tester();
$tester->execute(
['name' => 'demo', '--framework' => 'laravel', '--with-database' => true],
['interactive' => false],
);
$this->assertSame(2, $tester->getStatusCode());
$this->assertStringContainsString('--with-database is not supported with Laravel', $tester->getDisplay());
}

public function testDatabaseOptionIsPropagatedOnSymfony(): void
{
$opts = $this->resolveOptions([
'name' => 'demo',
'--framework' => 'symfony',
'--with-docker' => false,
'--with-pwa' => false,
'--with-admin' => false,
'--with-database' => true,
]);

$this->assertTrue($opts->withDatabase);
}

public function testInteractiveDatabasePromptAppearsAfterDockerAndDefaultsToNo(): void
{
$command = new InstallerCommand();
$input = new ArrayInput([
'name' => 'demo',
'--framework' => 'symfony',
'--with-pwa' => false,
'--with-admin' => false,
'--format' => ['jsonld'],
'--docs' => ['swagger_ui'],
]);
$input->bind($command->getDefinition());
$input->setInteractive(true);

$stream = fopen('php://memory', 'r+');
if (false === $stream) {
throw new \RuntimeException('Could not open memory stream.');
}
fwrite($stream, "\nyes\n");
rewind($stream);
$input->setStream($stream);

$output = new BufferedOutput();
$io = new SymfonyStyle($input, $output);
$method = new \ReflectionMethod($command, 'resolveOptions');
$opts = $method->invoke($command, $io, $input, InstallerCommand::FRAMEWORK_SYMFONY);
$display = $output->fetch();

$this->assertTrue($opts->withDocker);
$this->assertTrue($opts->withDatabase);
$this->assertStringContainsString('Use a database? (yes/no) [no]', $display);
$this->assertLessThan(strpos($display, 'Use a database?'), strpos($display, 'Use Docker?'));
}

public function testAdminOptionIsAcceptedOnSymfony(): void
{
$command = new InstallerCommand();
Expand Down Expand Up @@ -243,7 +302,7 @@ public function testInteractiveMultiselectPromptsEmitNoPhpWarnings(): void
// Accept defaults for the two multiselect prompts (formats, docs).
$tester->setInputs(['', '']);
$tester->execute(
['name' => basename($tmp), '--framework' => 'symfony', '--with-docker' => false, '--with-pwa' => false, '--with-admin' => false],
['name' => basename($tmp), '--framework' => 'symfony', '--with-docker' => false, '--with-pwa' => false, '--with-admin' => false, '--with-database' => false],
['interactive' => true],
);
if (false !== $cwd) {
Expand Down Expand Up @@ -277,6 +336,7 @@ private function resolveDefaultOptions(): ScaffoldOptions
'--with-docker' => false,
'--with-pwa' => false,
'--with-admin' => false,
'--with-database' => false,
]);
}

Expand Down Expand Up @@ -308,6 +368,7 @@ private function resolveInteractiveDocs(string $answer): array
'--with-docker' => false,
'--with-pwa' => false,
'--with-admin' => false,
'--with-database' => false,
'--format' => ['jsonld'],
]);
$input->bind($command->getDefinition());
Expand Down
27 changes: 27 additions & 0 deletions tests/Scaffold/SymfonyScaffoldTest.php