Skip to content
Merged
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
12 changes: 11 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@ jobs:
symfony-versions:
- '6.4.*'
- '7.0.*'
doctrine-migrations:
- "3.6.*"
- "3.9.*"
doctrine-fixtures-bundle:
- "3.5.*"
- "4.0.*"
include:
- description: 'Log Code Coverage'
php: '8.3'
symfony-versions: '^7.0'
doctrine-orm-versions: '^3.0'
doctrine-migrations: '^3.6'
doctrine-fixtures-bundle: '^3.5'
coverage: xdebug

name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} Doctrine ${{ matrix.doctrine-orm-versions }} ${{ matrix.description }}
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 }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -63,6 +71,8 @@ jobs:
run: |
composer require symfony/framework-bundle:${{ matrix.symfony-versions }} --no-update --no-scripts
composer require doctrine/orm:${{ matrix.doctrine-orm-versions }} --no-update --no-scripts
composer require --dev doctrine/migrations:${{ matrix.doctrine-migrations }} --no-update --no-scripts
composer require --dev doctrine/doctrine-fixtures-bundle:${{ matrix.doctrine-fixtures-bundle }} --no-update --no-scripts
composer require --dev symfony/yaml:${{ matrix.symfony-versions }} --no-update --no-scripts

- name: Install dependencies
Expand Down
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Kernel extends BaseKernel
public function boot(): void
{
parent::boot();

SchemaConnection::setSchemaResolver(
$this->getContainer()->get(BaggageSchemaResolver::class),
);
Expand Down Expand Up @@ -86,7 +86,59 @@ schema_context:
* When Doctrine connects to PostgreSQL, it sets the search_path to the specified schema.
* If the schema does not exist or DB is not PostgreSQL, an exception is thrown.

## Testing
## Optional Commands

The bundle provides three optional commands for schema management that can be registered in your services configuration:

### Schema Drop Command
Drops a PostgreSQL schema and all its objects:

```yaml
# config/services.yaml
services:
SharedServices\Command\Doctrine\DoctrineSchemaDropCommand: ~
```

Usage:
```bash
php bin/console doctrine:schema:delete <schema_name>
```

### Schema Migrations Command
Runs Doctrine migrations within a specific schema. Creates the schema if it doesn't exist:

```yaml
# config/services.yaml
services:
SharedServices\Command\Doctrine\DoctrineSchemaMigrationsMigrateCommand:
arguments:
- '@doctrine_migrations.migrate_command'
```

Usage:
```bash
php bin/console doctrine:schema:migrations:migrate <schema_name> [options]
```

### Schema Fixtures Load Command
Loads Doctrine fixtures within a specific schema:

```yaml
# config/services.yaml
services:
SharedServices\Command\Doctrine\DoctrineSchemaFixturesLoadCommand:
arguments:
- '@doctrine.fixtures_load_command'
```

Usage:
```bash
php bin/console doctrine:schema:fixtures:load <schema_name> [options]
```

**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.

## Testing
To run tests:
```bash
vendor/bin/phpunit
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
"macpaw/schema-context-bundle": "^1.1"
},
"require-dev": {
"doctrine/migrations": "^3.6",
"doctrine/doctrine-fixtures-bundle": "^3.5 || ^4.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "3.7.*"
"squizlabs/php_codesniffer": "3.7.*",
"dg/bypass-finals": "^1.9"
},
"config": {
"allow-plugins": {
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
colors="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
>
Expand Down
60 changes: 60 additions & 0 deletions src/Command/Doctrine/AbstractDoctrineSchemaCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;

use Doctrine\DBAL\Connection;
use Error;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;

abstract class AbstractDoctrineSchemaCommand extends Command
{
public function __construct(
string $commandName,
protected readonly Connection $connection,
) {
parent::__construct($commandName);
}

protected function configure(): void
{
$this->addArgument(
'schema',
InputArgument::REQUIRED,
'The schema name.',
);

parent::configure();
}

protected function getSchemaFromInput(InputInterface $input): string
{
$schema = $input->getArgument('schema');

if (!is_string($schema) || $schema === '') {
throw new Error('Schema name must be a non-empty string');
}

return $schema;
}

protected function isSchemaExist(string $schema): bool
{
$exists = $this->connection->fetchOne(
'SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ?)',
[$schema],
);

return (bool) $exists;
}

protected function switchToSchema(string $schema): void
{
$quotedSchema = $this->connection->quoteIdentifier($schema);

$this->connection->executeStatement("SET search_path TO {$quotedSchema}");
}
}
101 changes: 101 additions & 0 deletions src/Command/Doctrine/AbstractNestingDoctrineSchemaCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;

use Doctrine\DBAL\Connection;
use Error;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

abstract class AbstractNestingDoctrineSchemaCommand extends AbstractDoctrineSchemaCommand
{
public function __construct(
string $commandName,
private readonly Command $parentCommand,
Connection $connection,
) {
parent::__construct($commandName, $connection);
}

protected function configure(): void
{
parent::configure();

foreach ($this->parentCommand->getDefinition()->getArguments() as $argument) {
$this->addArgument(
$argument->getName(),
$argument->isRequired() ? InputArgument::REQUIRED : InputArgument::OPTIONAL,
$argument->getDescription(),
$argument->getDefault(),
);
}

foreach ($this->parentCommand->getDefinition()->getOptions() as $option) {
$this->addOption(
$option->getName(),
$option->getShortcut(),
$option->isValueRequired() ? InputOption::VALUE_REQUIRED : InputOption::VALUE_OPTIONAL,
$option->getDescription(),
$option->getDefault(),
);
}
}

protected function runCommand(string $commandName, InputInterface $input, OutputInterface $output): int
{
$application = $this->getApplication();

if ($application === null) {
throw new Error('Application is not available');
}

$command = $application->find($commandName);

$arguments = [];
foreach ($input->getArguments() as $name => $value) {
if ($value === null) {
continue;
}

if ($name === 'schema' || $name === 'command') {
continue;
}

if ($this->getDefinition()->getArguments()[$name]->getDefault() === $value) {
continue;
}

$arguments[$name] = $value;
}

$options = [];
foreach ($input->getOptions() as $name => $value) {
if ($value === null) {
continue;
}

if ($this->getDefinition()->getOptions()[$name]->getDefault() === $value) {
continue;
}

$options['--' . $name] = $value;
}

$commandInput = new ArrayInput([
...$arguments,
...$options,
]);

if ($input->getOption('no-interaction') === true) {
$commandInput->setInteractive(false);
}

return $command->run($commandInput, $output);
}
}
32 changes: 32 additions & 0 deletions src/Command/Doctrine/DoctrineSchemaDropCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;

use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class DoctrineSchemaDropCommand extends AbstractDoctrineSchemaCommand
{
public function __construct(Connection $connection)
{
parent::__construct('doctrine:schema:delete', $connection);
}

protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
$schema = $this->getSchemaFromInput($input);

$output->writeln("<info>Drop schema '{$schema}'...<info>");

$quotedSchema = $this->connection->quoteIdentifier($schema);
$this->connection->executeStatement(sprintf('DROP SCHEMA IF EXISTS %s CASCADE', $quotedSchema));

return Command::SUCCESS;
}
}
55 changes: 55 additions & 0 deletions src/Command/Doctrine/DoctrineSchemaFixturesLoadCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Macpaw\PostgresSchemaBundle\Command\Doctrine;

use Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;

class DoctrineSchemaFixturesLoadCommand extends AbstractNestingDoctrineSchemaCommand
{
public function __construct(
LoadDataFixturesDoctrineCommand $parentCommand,
Connection $connection,
) {
parent::__construct('doctrine:schema:fixtures:load', $parentCommand, $connection);
}

protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
try {
$schema = $this->getSchemaFromInput($input);

if (!$this->isSchemaExist($schema)) {
$output->writeln("<error>Schema '{$schema}' doesn't exist</error>");

return Command::FAILURE;
}

$this->switchToSchema($schema);

$output->writeln("<info>Load fixtures for '{$schema}'...</info>");

$returnCode = $this->runCommand('doctrine:fixtures:load', $input, $output);

if ($returnCode !== Command::SUCCESS) {
$output->writeln("<error>Fixtures load failed with return code: {$returnCode}</error>");

return Command::FAILURE;
}
} catch (Throwable $e) {
$output->writeln("<error>Error executing fixtures load: {$e->getMessage()}</error>");

return Command::FAILURE;
}

return Command::SUCCESS;
}
}
Loading
Loading