Skip to content

Commit e6a7eaa

Browse files
Merge pull request #8 from MacPaw/develop
Release
2 parents 818cb74 + 4851555 commit e6a7eaa

15 files changed

+583
-7
lines changed

.github/workflows/ci.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,22 @@ jobs:
2121
symfony-versions:
2222
- '6.4.*'
2323
- '7.0.*'
24+
doctrine-migrations:
25+
- "3.6.*"
26+
- "3.9.*"
27+
doctrine-fixtures-bundle:
28+
- "3.5.*"
29+
- "4.0.*"
2430
include:
2531
- description: 'Log Code Coverage'
2632
php: '8.3'
2733
symfony-versions: '^7.0'
2834
doctrine-orm-versions: '^3.0'
35+
doctrine-migrations: '^3.6'
36+
doctrine-fixtures-bundle: '^3.5'
2937
coverage: xdebug
3038

31-
name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} Doctrine ${{ matrix.doctrine-orm-versions }} ${{ matrix.description }}
39+
name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} Doctrine ${{ matrix.doctrine-orm-versions }} Doctrine migrations ${{ matrix.doctrine-migrations }} Doctrine fixtures bundle ${{ matrix.doctrine-fixtures-bundle }} ${{ matrix.description }}
3240
steps:
3341
- name: Checkout
3442
uses: actions/checkout@v4
@@ -63,6 +71,8 @@ jobs:
6371
run: |
6472
composer require symfony/framework-bundle:${{ matrix.symfony-versions }} --no-update --no-scripts
6573
composer require doctrine/orm:${{ matrix.doctrine-orm-versions }} --no-update --no-scripts
74+
composer require --dev doctrine/migrations:${{ matrix.doctrine-migrations }} --no-update --no-scripts
75+
composer require --dev doctrine/doctrine-fixtures-bundle:${{ matrix.doctrine-fixtures-bundle }} --no-update --no-scripts
6676
composer require --dev symfony/yaml:${{ matrix.symfony-versions }} --no-update --no-scripts
6777
6878
- name: Install dependencies

README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Kernel extends BaseKernel
5858
public function boot(): void
5959
{
6060
parent::boot();
61-
61+
6262
SchemaConnection::setSchemaResolver(
6363
$this->getContainer()->get(BaggageSchemaResolver::class),
6464
);
@@ -86,7 +86,59 @@ schema_context:
8686
* When Doctrine connects to PostgreSQL, it sets the search_path to the specified schema.
8787
* If the schema does not exist or DB is not PostgreSQL, an exception is thrown.
8888

89-
## Testing
89+
## Optional Commands
90+
91+
The bundle provides three optional commands for schema management that can be registered in your services configuration:
92+
93+
### Schema Drop Command
94+
Drops a PostgreSQL schema and all its objects:
95+
96+
```yaml
97+
# config/services.yaml
98+
services:
99+
SharedServices\Command\Doctrine\DoctrineSchemaDropCommand: ~
100+
```
101+
102+
Usage:
103+
```bash
104+
php bin/console doctrine:schema:delete <schema_name>
105+
```
106+
107+
### Schema Migrations Command
108+
Runs Doctrine migrations within a specific schema. Creates the schema if it doesn't exist:
109+
110+
```yaml
111+
# config/services.yaml
112+
services:
113+
SharedServices\Command\Doctrine\DoctrineSchemaMigrationsMigrateCommand:
114+
arguments:
115+
- '@doctrine_migrations.migrate_command'
116+
```
117+
118+
Usage:
119+
```bash
120+
php bin/console doctrine:schema:migrations:migrate <schema_name> [options]
121+
```
122+
123+
### Schema Fixtures Load Command
124+
Loads Doctrine fixtures within a specific schema:
125+
126+
```yaml
127+
# config/services.yaml
128+
services:
129+
SharedServices\Command\Doctrine\DoctrineSchemaFixturesLoadCommand:
130+
arguments:
131+
- '@doctrine.fixtures_load_command'
132+
```
133+
134+
Usage:
135+
```bash
136+
php bin/console doctrine:schema:fixtures:load <schema_name> [options]
137+
```
138+
139+
**Note:** These commands are optional and should only be registered if you're using the corresponding Doctrine features (migrations and/or fixtures) in your project.
140+
141+
## Testing
90142
To run tests:
91143
```bash
92144
vendor/bin/phpunit

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
"macpaw/schema-context-bundle": "^1.1"
2222
},
2323
"require-dev": {
24+
"doctrine/migrations": "^3.6",
25+
"doctrine/doctrine-fixtures-bundle": "^3.5 || ^4.0",
2426
"phpstan/phpstan": "^1.10",
2527
"phpunit/phpunit": "^10.0",
26-
"squizlabs/php_codesniffer": "3.7.*"
28+
"squizlabs/php_codesniffer": "3.7.*",
29+
"dg/bypass-finals": "^1.9"
2730
},
2831
"config": {
2932
"allow-plugins": {

phpunit.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<phpunit
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4-
bootstrap="vendor/autoload.php"
4+
bootstrap="tests/bootstrap.php"
55
colors="true"
66
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
77
>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Error;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputArgument;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
13+
abstract class AbstractDoctrineSchemaCommand extends Command
14+
{
15+
public function __construct(
16+
string $commandName,
17+
protected readonly Connection $connection,
18+
) {
19+
parent::__construct($commandName);
20+
}
21+
22+
protected function configure(): void
23+
{
24+
$this->addArgument(
25+
'schema',
26+
InputArgument::REQUIRED,
27+
'The schema name.',
28+
);
29+
30+
parent::configure();
31+
}
32+
33+
protected function getSchemaFromInput(InputInterface $input): string
34+
{
35+
$schema = $input->getArgument('schema');
36+
37+
if (!is_string($schema) || $schema === '') {
38+
throw new Error('Schema name must be a non-empty string');
39+
}
40+
41+
return $schema;
42+
}
43+
44+
protected function isSchemaExist(string $schema): bool
45+
{
46+
$exists = $this->connection->fetchOne(
47+
'SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ?)',
48+
[$schema],
49+
);
50+
51+
return (bool) $exists;
52+
}
53+
54+
protected function switchToSchema(string $schema): void
55+
{
56+
$quotedSchema = $this->connection->quoteIdentifier($schema);
57+
58+
$this->connection->executeStatement("SET search_path TO {$quotedSchema}");
59+
}
60+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Error;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\ArrayInput;
11+
use Symfony\Component\Console\Input\InputArgument;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Input\InputOption;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
abstract class AbstractNestingDoctrineSchemaCommand extends AbstractDoctrineSchemaCommand
17+
{
18+
public function __construct(
19+
string $commandName,
20+
private readonly Command $parentCommand,
21+
Connection $connection,
22+
) {
23+
parent::__construct($commandName, $connection);
24+
}
25+
26+
protected function configure(): void
27+
{
28+
parent::configure();
29+
30+
foreach ($this->parentCommand->getDefinition()->getArguments() as $argument) {
31+
$this->addArgument(
32+
$argument->getName(),
33+
$argument->isRequired() ? InputArgument::REQUIRED : InputArgument::OPTIONAL,
34+
$argument->getDescription(),
35+
$argument->getDefault(),
36+
);
37+
}
38+
39+
foreach ($this->parentCommand->getDefinition()->getOptions() as $option) {
40+
$this->addOption(
41+
$option->getName(),
42+
$option->getShortcut(),
43+
$option->isValueRequired() ? InputOption::VALUE_REQUIRED : InputOption::VALUE_OPTIONAL,
44+
$option->getDescription(),
45+
$option->getDefault(),
46+
);
47+
}
48+
}
49+
50+
protected function runCommand(string $commandName, InputInterface $input, OutputInterface $output): int
51+
{
52+
$application = $this->getApplication();
53+
54+
if ($application === null) {
55+
throw new Error('Application is not available');
56+
}
57+
58+
$command = $application->find($commandName);
59+
60+
$arguments = [];
61+
foreach ($input->getArguments() as $name => $value) {
62+
if ($value === null) {
63+
continue;
64+
}
65+
66+
if ($name === 'schema' || $name === 'command') {
67+
continue;
68+
}
69+
70+
if ($this->getDefinition()->getArguments()[$name]->getDefault() === $value) {
71+
continue;
72+
}
73+
74+
$arguments[$name] = $value;
75+
}
76+
77+
$options = [];
78+
foreach ($input->getOptions() as $name => $value) {
79+
if ($value === null) {
80+
continue;
81+
}
82+
83+
if ($this->getDefinition()->getOptions()[$name]->getDefault() === $value) {
84+
continue;
85+
}
86+
87+
$options['--' . $name] = $value;
88+
}
89+
90+
$commandInput = new ArrayInput([
91+
...$arguments,
92+
...$options,
93+
]);
94+
95+
if ($input->getOption('no-interaction') === true) {
96+
$commandInput->setInteractive(false);
97+
}
98+
99+
return $command->run($commandInput, $output);
100+
}
101+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
12+
class DoctrineSchemaDropCommand extends AbstractDoctrineSchemaCommand
13+
{
14+
public function __construct(Connection $connection)
15+
{
16+
parent::__construct('doctrine:schema:delete', $connection);
17+
}
18+
19+
protected function execute(
20+
InputInterface $input,
21+
OutputInterface $output,
22+
): int {
23+
$schema = $this->getSchemaFromInput($input);
24+
25+
$output->writeln("<info>Drop schema '{$schema}'...<info>");
26+
27+
$quotedSchema = $this->connection->quoteIdentifier($schema);
28+
$this->connection->executeStatement(sprintf('DROP SCHEMA IF EXISTS %s CASCADE', $quotedSchema));
29+
30+
return Command::SUCCESS;
31+
}
32+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;
6+
7+
use Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand;
8+
use Doctrine\DBAL\Connection;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Throwable;
13+
14+
class DoctrineSchemaFixturesLoadCommand extends AbstractNestingDoctrineSchemaCommand
15+
{
16+
public function __construct(
17+
LoadDataFixturesDoctrineCommand $parentCommand,
18+
Connection $connection,
19+
) {
20+
parent::__construct('doctrine:schema:fixtures:load', $parentCommand, $connection);
21+
}
22+
23+
protected function execute(
24+
InputInterface $input,
25+
OutputInterface $output,
26+
): int {
27+
try {
28+
$schema = $this->getSchemaFromInput($input);
29+
30+
if (!$this->isSchemaExist($schema)) {
31+
$output->writeln("<error>Schema '{$schema}' doesn't exist</error>");
32+
33+
return Command::FAILURE;
34+
}
35+
36+
$this->switchToSchema($schema);
37+
38+
$output->writeln("<info>Load fixtures for '{$schema}'...</info>");
39+
40+
$returnCode = $this->runCommand('doctrine:fixtures:load', $input, $output);
41+
42+
if ($returnCode !== Command::SUCCESS) {
43+
$output->writeln("<error>Fixtures load failed with return code: {$returnCode}</error>");
44+
45+
return Command::FAILURE;
46+
}
47+
} catch (Throwable $e) {
48+
$output->writeln("<error>Error executing fixtures load: {$e->getMessage()}</error>");
49+
50+
return Command::FAILURE;
51+
}
52+
53+
return Command::SUCCESS;
54+
}
55+
}

0 commit comments

Comments
 (0)