martin-georgiev/postgresql-for-doctrine
Adds PostgreSQL-specific power to Doctrine DBAL/ORM: rich native types (jsonb, arrays, ranges, network, geometric, etc.) plus DQL functions/operators for JSON and array querying. Supports PostgreSQL 9.4+ and PHP 8.2+.
This project supports devenv.sh for a consistent development environment:
Install the Nix package manager (if not already installed). For example, install Nix via the recommended multi-user installation:
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon
ℹ️ Nix lets you declaratively define environments. While the learning curve is steep, it enables reproducible installations. Consider using Nix Flakes and Home Manager.
Configure the Nix environment:
Enable nix command, and Flakes support:
grep --quiet '^extra-experimental-features = nix-command flakes' '/etc/nix/nix.conf' ||
sudo tee --append '/etc/nix/nix.conf' <<EOF
# Enable nix command and flakes
extra-experimental-features = nix-command flakes
EOF
Trust Cachix devenv packages cache:
grep --quiet '^extra-substituters = https://devenv.cachix.org' '/etc/nix/nix.conf' ||
sudo tee --append '/etc/nix/nix.conf' <<EOF
# Trust Cachix DevEnv
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
EOF
Restart nix-daemon to load the new configuration:
for GNU/Linux systems:
sudo systemctl 'restart' 'nix-daemon.service'
for macOS systems:
sudo launchctl kickstart -k system/org.nixos.nix-daemon
Install devenv.sh (if not already installed), by following Getting started @ devenv.sh:
by using nix command if available (recommended):
nix profile install nixpkgs#devenv
by using nix-env command (legacy; discouraged):
nix-env --install --attr devenv -f https://github.com/NixOS/nixpkgs/tarball/nixpkgs-unstable
Install direnv (if not already installed):
nix profile install nixpkgs#direnv nixpkgs#nix-direnv
Then hook direnv into your shell (once).
for bash, add this line to ~/.bashrc:
eval "$(direnv hook bash)"
for zsh, add this line to ~/.zshrc:
eval "$(direnv hook zsh)"
for other shells, see Setup @ direnv documentation.
Enter the development shell from the project's root:
with direnv (recommended):
direnv allow
without direnv:
devenv shell
Launch the PostgreSQL server, for running integration tests:
devenv up
The provided environment includes:
devenv up.ℹ️ Use devenv.local.nix to alter the development environment.
It's listed in .gitignore and not committed.
Using local-only plaintext secrets here is acceptable.
For example, this file:
# devenv.local.nix
{ pkgs, lib, config, inputs, ... }:
{
# https://devenv.sh/packages/
packages = with pkgs; [ harlequin ];
# https://devenv.sh/languages/
languages.php.version = "8.5";
# https://devenv.sh/basics/
env = {
POSTGRES_PASSWORD = "changeme";
POSTGRES_PORT = 45432;
};
}
The devenv.lock file pins the Nix inputs (package set and dependencies) used
by devenv.
This ensures reproducible development environments.
Update the devenv by:
Update dependencies:
devenv update
Commit the changes:
git add devenv.lock && git commit --message="chore: update devenv.lock"
For the sake of clear Git history and speedy review of your PR, please verify that the suggested changes are in line with the project's standards. Code style, static analysis, and file validation scripts are already provided and can easily be run from project's root:
Check for consistent code style:
composer check-code-style
Automatically apply fixes to the code style:
composer fix-code-style
Run static analysis for the currently configured level:
composer run-static-analysis
Run the full test suite:
composer run-all-tests
Extend MartinGeorgiev\Doctrine\DBAL\Types\BaseArray.
Give the new data type a unique name within your application.
Use the TYPE_NAME constant for that purpose.
Depending on the new data-type nature you may have to overwrite some of the following methods:
transformPostgresArrayToPHPArray()
transformArrayItemForPHP()
isValidArrayItemForDatabase()
Most new functions will likely have a signature very similar to those already implemented in the project. This means new functions probably require only extending the base class and decorating it with some behaviour. Here are the two main steps to follow:
MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction.setFunctionPrototype() and addNodeMapping()
to implement customizeFunction() for your new function class.Example:
<?php
declare(strict_types=1);
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
class ArrayAppend extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('array_append(%s, %s)');
$this->addNodeMapping('StringPrimary'); // corresponds to param №1 in the prototype set in setFunctionPrototype
$this->addNodeMapping('Literal'); // corresponds to param №2 in the prototype set in setFunctionPrototype
// Add more node mappings if needed.
}
}
⚠️ Beware: you cannot use ? (e.g. the ?? operator) as part of any
function prototype in Doctrine.
It causes query parsing failures.
This project has a rich, well-structured test suite consisting of fast unit tests and database-backed integration tests. Please follow the conventions below when adding or modifying tests.
Composer scripts:
composer run-unit-tests (uses ci/phpunit/config-unit.xml)composer run-integration-tests (uses ci/phpunit/config-integration.xml)composer run-all-testscomposer run-static-analysisIntegration tests require a PostgreSQL with PostGIS:
docker-compose up -ddocker-compose down -vdevenv upCoverage reports are written to var/logs/test-coverage/{unit|integration}/.
Keep unit tests fast and deterministic; use integration tests to validate behavior against the real database.
Test (e.g., PointTest, CidrTest)finalPHPUnit\Framework\TestCase or an existing base test class in Unit when availablesetUp() to create an AbstractPlatform mock and the subject under test when testing DBAL Types#[DataProvider] for bidirectional transformation scenarios (one provider used for both PHP->DB and DB->PHP tests)InvalidCidrForPHPException, InvalidRangeForDatabaseException)tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseRangeTestCasetests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseTimestampRangeTestCasePointTest, CidrTest, JsonbTestgetName()), conversions in both directions, and invalid inputstests/Unit/.../ORM/Query/AST/Functions/TestCase to assert DQL -> SQL transformationAnti-patterns to avoid in unit tests:
[@phpstan-ignore](https://github.com/phpstan-ignore)tests/Integration/MartinGeorgiev/TestCase
test, caches, and ensures PostGISArrayTypeTestCaseScalarTypeTestCaseRangeTypeTestCase (includes operator tests and assertRangeEquals)SpatialArrayTypeTestCase (ARRAY[...] insertion for WKT)MacaddrTypeTest, JsonbTypeTest, IntegerArrayTypeTest)protected function getTypeName(): string (Doctrine type name)protected function getPostgresTypeName(): string (column type)provideValidTransformations() where applicablerunDbalBindingRoundTrip($typeName, $columnType, $value) for round-tripsRangeTypeTestCase#[DataProvider('provideValidTransformations')] to can_handle_range_values()provideOperatorScenarios() returning [name, DQL, expectedIds]Anti-patterns to avoid in integration tests:
test schema created per test runCommonly used bases and what they provide:
BaseRangeTestCase: creation/formatting/boundary tests with abstract factory methodsBaseTimestampRangeTestCase: extends the above with timestamp-specific boundary and helperstests/Unit/.../DBAL/Types/BaseRangeTestCase: negative cases and conversions for range DBAL typestests/Unit/.../ORM/Query/AST/Functions/TestCase: DQL to SQL transformation checksTestCase: connection/schema setup, round-trip helper, assertionsArrayTypeTestCase, ScalarTypeTestCase, RangeTypeTestCase, SpatialArrayTypeTestCasePrefer extending these base classes over duplicating setup/utility code.
convertToPHPValue() should throw ...ForPHPExceptionconvertToDatabaseValue() should throw ...ForDatabaseExceptionexpectExceptionMessage() when the message is part of the contractassertRangeEquals() which compares string representation and emptinessfixtures/MartinGeorgiev/Doctrine/Entity and are registered via attributesRangeTypeTestCase’s createRangeOperatorsTable() and insert helpers)#[Test] and #[DataProvider] (PHPUnit 10)can_..., throws_..., dql_is_...Unit test for a DBAL Type (mock platform, bidirectional conversions):
final class InetTest extends TestCase
{
/**
* [@var](https://github.com/var) AbstractPlatform&MockObject
*/
private MockObject $platform;
private Inet $fixture;
protected function setUp(): void
{
$this->platform = $this->createMock(AbstractPlatform::class);
$this->fixture = new Inet();
}
#[DataProvider('provideValidTransformations')]
#[Test]
public function can_transform_from_php_value(?string $php, ?string $pg): void
{
$this->assertEquals($pg, $this->fixture->convertToDatabaseValue($php, $this->platform));
}
#[DataProvider('provideValidTransformations')]
#[Test]
public function can_transform_to_php_value(?string $php, ?string $pg): void
{
$this->assertEquals($php, $this->fixture->convertToPHPValue($pg, $this->platform));
}
}
Integration test for an array type:
final class TextArrayTypeTest extends ArrayTypeTestCase
{
protected function getTypeName(): string { return 'text[]'; }
protected function getPostgresTypeName(): string { return 'TEXT[]'; }
#[DataProvider('provideValidTransformations')] #[Test]
public function can_handle_array_values(string $name, array $value): void { parent::can_handle_array_values($name, $value); }
}
Range integration test:
final class Int4RangeTypeTest extends RangeTypeTestCase
{
protected function getTypeName(): string { return 'int4range'; }
protected function getPostgresTypeName(): string { return 'INT4RANGE'; }
#[DataProvider('provideValidTransformations')] #[Test]
public function can_handle_range_values(string $name, RangeValueObject $range): void { parent::can_handle_range_values($name, $range); }
}
If unsure which base to extend or how to structure a new test, mirror a nearby, similar test and keep changes minimal and consistent with the patterns above.
How can I help you explore Laravel packages today?