diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..3f2bb56
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,83 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches: [ main, develop ]
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php:
+ - '8.3'
+ - '8.4'
+ coverage: ['none']
+ doctrine-orm-versions:
+ - '^2.17'
+ - '^3.0'
+ symfony-versions:
+ - '6.4.*'
+ - '7.0.*'
+ include:
+ - description: 'Log Code Coverage'
+ php: '8.3'
+ symfony-versions: '^7.0'
+ doctrine-orm-versions: '^3.0'
+ coverage: xdebug
+
+ name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} Doctrine ${{ matrix.doctrine-orm-versions }} ${{ matrix.description }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: actions/cache@v4
+ with:
+ path: ~/.composer/cache/files
+ key: ${{ matrix.php }}-${{ matrix.symfony-versions }}
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: xdebug
+
+ - name: Add PHPUnit matcher
+ run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
+
+ - name: Set composer cache directory
+ id: composer-cache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache composer
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer-${{ hashFiles('composer.json') }}
+ restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer
+
+ - name: Update Symfony version
+ if: matrix.symfony-versions != ''
+ 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 symfony/yaml:${{ matrix.symfony-versions }} --no-update --no-scripts
+
+ - name: Install dependencies
+ run: composer install
+
+ - name: Run PHPUnit tests
+ run: vendor/bin/phpunit
+ if: matrix.coverage == 'none'
+
+ - name: PHPUnit tests and Log Code coverage
+ run: vendor/bin/phpunit --coverage-clover=coverage.xml
+ if: matrix.coverage == 'xdebug'
+
+ - name: Upload coverage reports to Codecov
+ if: matrix.coverage == 'xdebug'
+ uses: codecov/codecov-action@v4.0.1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml
new file mode 100644
index 0000000..248d5d0
--- /dev/null
+++ b/.github/workflows/security.yaml
@@ -0,0 +1,24 @@
+on:
+ pull_request:
+ push:
+ branches: [ main, develop ]
+
+jobs:
+ security-checker:
+ name: Security checker
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Download local-php-security-checker
+ run: curl -s -L -o local-php-security-checker https://github.com/fabpot/local-php-security-checker/releases/download/v1.0.0/local-php-security-checker_1.0.0_linux_amd64
+
+ - name: Run local-php-security-checker
+ run: chmod +x local-php-security-checker && ./local-php-security-checker
diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml
new file mode 100644
index 0000000..8796ae1
--- /dev/null
+++ b/.github/workflows/static-analysis.yaml
@@ -0,0 +1,55 @@
+name: Code style and static analysis
+
+on:
+ pull_request:
+ push:
+ branches: [ main, develop ]
+
+jobs:
+ php-cs-fixer:
+ name: PHP-CS-Fixer
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Run script
+ run: vendor/bin/phpcs
+
+ phpstan:
+ name: PHPStan
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Run script
+ run: vendor/bin/phpstan analyse
+
+ composer-validate:
+ name: Composer validate
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Run script
+ run: composer composer-validate
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1473148
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.idea
+/vendor/
+/composer.lock
+/.phpcs-cache
+
+###> phpunit/phpunit ###
+/phpunit.xml
+.phpunit.result.cache
+###< phpunit/phpunit ###
diff --git a/README.md b/README.md
index 948c599..502c9da 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,71 @@
-# postgres-schema-bundle
-Postgres schema bundle
+# Postgres Schema Bundle
+The **Postgres Schema Bundle** provides seamless multi-tenant schema support for PostgreSQL within Symfony applications. It automatically switches PostgreSQL `search_path` based on the current request context and ensures proper schema resolution across Doctrine and Messenger.## Installation
+
+## Features
+
+- Automatically sets PostgreSQL `search_path` from request headers.
+- Validates that the schema exists in the database.
+- Works only if the configured database driver is PostgreSQL.
+- Integrates with [Schema Context Bundle](https://github.com/macpaw/schema-context-bundle).
+- Compatible with Symfony Messenger and Doctrine ORM.
+
+## Installation
+Use Composer to install the bundle:
+```
+composer require macpaw/postgres-schema-bundle
+```
+
+### Applications that don't use Symfony Flex
+Enable the bundle by adding it to the list of registered bundles in ```config/bundles.php```
+
+```
+// config/bundles.php
+ ['all' => true],
+ Macpaw\SchemaContextBundle\PostgresSchemaBundle::class => ['all' => true],
+ // ...
+ ];
+```
+
+## Configuration
+
+You must tell Doctrine to use the SchemaConnection class as its DBAL connection class:
+
+```yaml
+# config/packages/doctrine.yaml
+doctrine:
+ dbal:
+ connections:
+ default:
+ wrapper_class: Macpaw\PostgresSchemaBundle\Doctrine\SchemaConnection
+```
+Make sure you configure the context bundle properly:
+
+See https://github.com/MacPaw/schema-context-bundle/blob/develop/README.md
+
+```yaml
+schema_context:
+ app_name: '%env(APP_NAME)%' # Application name
+ header_name: 'X-Tenant' # Request header to extract schema name
+ default_schema: 'public' # Default schema to fallback to
+ allowed_app_names: ['develop', 'staging', 'test'] # App names where schema context is allowed to change
+```
+
+## How it Works
+* A request comes in with a header like X-Tenant-Id: tenant123.
+* The SchemaRequestListener sets this schema in the 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
+To run tests:
+```bash
+vendor/bin/phpunit
+```
+## Contributing
+Feel free to open issues and submit pull requests.
+
+## License
+This bundle is released under the MIT license.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..0b6878f
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,23 @@
+# Security Policy
+
+## Reporting Security Issues
+If you believe you have found a security vulnerability in any MacPaw-owned repository, please report it to us through coordinated disclosure.
+
+Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
+
+Instead, please send an email to security[@]macpaw.com.
+
+Please include as much of the information listed below as you can to help us better understand and resolve the issue:
+
+- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
+- Full paths of source file(s) related to the manifestation of the issue
+- The location of the affected source code (tag/branch/commit or direct URL)
+- Any special configuration required to reproduce the issue
+- Step-by-step instructions to reproduce the issue
+- Proof-of-concept or exploit code (if possible)
+- Impact of the issue, including how an attacker might exploit the issue
+
+This information will help us triage your report more quickly.
+
+## Policy
+See MacPaw's [Vulnerability Disclosure Policy](https://macpaw.com/vulnerability-disclosure-policy)
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..68e4f78
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,50 @@
+{
+ "name": "macpaw/postgres-schema-bundle",
+ "description": "A Symfony bundle to add schema wrapper for postgres",
+ "type": "symfony-bundle",
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "Macpaw\\PostgresSchemaBundle\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Macpaw\\PostgresSchemaBundle\\Tests\\": "tests/"
+ }
+ },
+ "require": {
+ "php": ">=8.3",
+ "doctrine/orm": "^2.17 || ^3.0",
+ "symfony/doctrine-bridge": "^6.4 || ^7.0",
+ "doctrine/dbal": "^3.4",
+ "macpaw/schema-context-bundle": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^10.0",
+ "squizlabs/php_codesniffer": "3.7.*"
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
+ "scripts": {
+ "composer-validate": [
+ "composer validate"
+ ],
+ "cs": [
+ "vendor/bin/phpcs"
+ ],
+ "cs-fix": [
+ "vendor/bin/phpcbf"
+ ],
+ "phpstan": [
+ "vendor/bin/phpstan analyse"
+ ],
+ "phpunit": [
+ "vendor/bin/phpunit"
+ ]
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..d89bc03
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src/
+ tests/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..776ccd8
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,4 @@
+parameters:
+ level: max
+ paths:
+ - src
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..6344d8d
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,23 @@
+
+
+
+
+ ./tests
+
+
+
+
+
+
+
+
+
+ ./src
+
+
+
diff --git a/src/Doctrine/SchemaConnection.php b/src/Doctrine/SchemaConnection.php
new file mode 100644
index 0000000..e7982ac
--- /dev/null
+++ b/src/Doctrine/SchemaConnection.php
@@ -0,0 +1,56 @@
+getSchema();
+
+ if (!$schema) {
+ return $connection;
+ }
+
+ $this->ensurePostgreSql();
+ $this->applySearchPath($schema);
+
+ return $connection;
+ }
+
+ private function ensurePostgreSql(): void
+ {
+ $platform = $this->getDatabasePlatform();
+
+ if (!$platform instanceof PostgreSQLPlatform) {
+ throw new UnsupportedPlatformException(get_class($platform));
+ }
+ }
+
+ private function applySearchPath(string $schema): void
+ {
+ if ($this->_conn !== null) {
+ $this->_conn->exec('SET search_path TO ' . $schema);
+ }
+ }
+}
diff --git a/src/Exception/UnsupportedPlatformException.php b/src/Exception/UnsupportedPlatformException.php
new file mode 100644
index 0000000..9fb006e
--- /dev/null
+++ b/src/Exception/UnsupportedPlatformException.php
@@ -0,0 +1,15 @@
+createMock(DriverConnection::class);
+
+ $driverConnection->expects($this->once())
+ ->method('exec')
+ ->with('SET search_path TO test_schema');
+
+ $driver = $this->createMock(Driver::class);
+
+ $driver->method('connect')->willReturn($driverConnection);
+
+ $platform = new PostgreSQLPlatform();
+ $connection = $this->getMockBuilder(SchemaConnection::class)
+ ->setConstructorArgs([[], $driver, new Configuration(), new EventManager()])
+ ->onlyMethods(['getDatabasePlatform', 'fetchOne'])
+ ->getMock();
+
+ $connection->method('getDatabasePlatform')->willReturn($platform);
+ $connection->method('fetchOne')->willReturn(true);
+
+ $resolver = new SchemaResolver();
+
+ $resolver->setSchema('test_schema');
+
+ SchemaConnection::setSchemaResolver($resolver);
+
+ $result = $connection->connect();
+
+ self::assertTrue($result);
+ }
+
+ public function testConnectSkipsWhenNoSchema(): void
+ {
+ $driverConnection = $this->createMock(DriverConnection::class);
+
+ $driver = $this->createMock(Driver::class);
+
+ $driver->method('connect')->willReturn($driverConnection);
+
+ $connection = new SchemaConnection([], $driver, new Configuration());
+ $resolver = new SchemaResolver();
+
+ SchemaConnection::setSchemaResolver($resolver);
+
+ self::assertTrue($connection->connect());
+ }
+
+ public function testThrowsForUnsupportedPlatform(): void
+ {
+ $this->expectException(UnsupportedPlatformException::class);
+
+ $driverConnection = $this->createMock(DriverConnection::class);
+ $driver = $this->createMock(Driver::class);
+
+ $driver->method('connect')->willReturn($driverConnection);
+
+ $platform = new MySQLPlatform();
+ $connection = $this->getMockBuilder(SchemaConnection::class)
+ ->setConstructorArgs([[], $driver, new Configuration(), new EventManager()])
+ ->onlyMethods(['getDatabasePlatform'])
+ ->getMock();
+
+ $connection->method('getDatabasePlatform')->willReturn($platform);
+
+ $resolver = new SchemaResolver();
+
+ $resolver->setSchema('test_schema');
+
+ SchemaConnection::setSchemaResolver($resolver);
+
+ $connection->connect();
+ }
+}