diff --git a/.ci/pgsql_fixtures.sh b/.ci/pgsql_fixtures.sh
new file mode 100644
index 0000000..2c872a3
--- /dev/null
+++ b/.ci/pgsql_fixtures.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+echo "Configure PostgreSQL test database"
+
+psql -U postgres -c 'create database phpdb_test;'
+psql -U postgres -c "alter role postgres password 'postgres'"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..4368bd2
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+/.gitattributes export-ignore
+/.github/ export-ignore
+/.gitignore export-ignore
+/phpcs.xml export-ignore
+/renovate.json export-ignore
+/.laminas-ci.json export-ignore
+/.laminas-ci/ export-ignore
+/.ci/ export-ignore
+/phpunit.xml.dist export-ignore
+/test/ export-ignore
+/clover.xml export-ignore
\ No newline at end of file
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
new file mode 100644
index 0000000..289e5ff
--- /dev/null
+++ b/.github/workflows/continuous-integration.yml
@@ -0,0 +1,45 @@
+name: "Continuous Integration"
+
+on:
+ pull_request:
+ push:
+ branches:
+ tags:
+
+jobs:
+ matrix:
+ name: Generate job matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.matrix.outputs.matrix }}
+ steps:
+ - name: Gather CI configuration
+ id: matrix
+ uses: laminas/laminas-ci-matrix-action@v1
+
+ qa:
+ name: QA Checks
+ needs: [matrix]
+ runs-on: ${{ matrix.operatingSystem }}
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
+ steps:
+ - name: ${{ matrix.name }}
+ uses: laminas/laminas-continuous-integration-action@v1
+ with:
+ job: ${{ matrix.job }}
+ services:
+ postgres:
+ image: postgres
+ env:
+ POSTGRES_USER: 'gha'
+ POSTGRES_PASSWORD: 'password'
+ POSTGRES_DB: 'phpdb_test'
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 3
+ ports:
+ - 5432
\ No newline at end of file
diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml
new file mode 100644
index 0000000..a52f436
--- /dev/null
+++ b/.github/workflows/release-on-milestone-closed.yml
@@ -0,0 +1,15 @@
+name: "Automatic Releases"
+
+on:
+ milestone:
+ types:
+ - "closed"
+
+jobs:
+ release:
+ uses: laminas/workflow-automatic-releases/.github/workflows/release-on-milestone-closed.yml@1.x
+ secrets:
+ GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
+ GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
+ ORGANIZATION_ADMIN_TOKEN: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
+ SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0088c44
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+/.phpcs-cache
+/.phpstan-cache
+/phpstan.neon
+/.phpunit.cache
+/.phpunit.result.cache
+/phpunit.xml
+/vendor/
+/xdebug_filter.php
+/clover.xml
\ No newline at end of file
diff --git a/.laminas-ci.json b/.laminas-ci.json
new file mode 100644
index 0000000..10c530b
--- /dev/null
+++ b/.laminas-ci.json
@@ -0,0 +1,12 @@
+{
+ "additional_checks": [
+ {
+ "name": "PhpStan",
+ "job": {
+ "php": "8.2",
+ "dependencies": "latest",
+ "command": "composer require --dev phpstan/phpstan && vendor/bin/phpstan analyse"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.laminas-ci/phpunit.xml b/.laminas-ci/phpunit.xml
new file mode 100644
index 0000000..af92d7b
--- /dev/null
+++ b/.laminas-ci/phpunit.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ ./test/unit
+
+
+ ./test/integration
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.laminas-ci/pre-install.sh b/.laminas-ci/pre-install.sh
new file mode 100644
index 0000000..a3c153e
--- /dev/null
+++ b/.laminas-ci/pre-install.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+WORKING_DIRECTORY=$2
+JOB=$3
+PHP_VERSION=$(echo "${JOB}" | jq -r '.php')
+
+
+if [ ! -z "$GITHUB_BASE_REF" ] && [[ "$GITHUB_BASE_REF" =~ ^[0-9]+\.[0-9] ]]; then
+ readarray -td. TARGET_BRANCH_VERSION_PARTS <<<"${GITHUB_BASE_REF}.";
+ unset 'TARGET_BRANCH_VERSION_PARTS[-1]';
+ declare -a TARGET_BRANCH_VERSION_PARTS
+ MAJOR_OF_TARGET_BRANCH=${TARGET_BRANCH_VERSION_PARTS[0]}
+ MINOR_OF_TARGET_BRANCH=${TARGET_BRANCH_VERSION_PARTS[1]}
+
+ export COMPOSER_ROOT_VERISON="${MAJOR_OF_TARGET_BRANCH}.${MINOR_OF_TARGET_BRANCH}.99"
+ echo "Exported COMPOSER_ROOT_VERISON as ${COMPOSER_ROOT_VERISON}"
+fi
diff --git a/.laminas-ci/pre-run.sh b/.laminas-ci/pre-run.sh
new file mode 100644
index 0000000..660082a
--- /dev/null
+++ b/.laminas-ci/pre-run.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+set -e
+
+TEST_USER=$1
+WORKSPACE=$2
+JOB=$3
+
+COMMAND=$(echo "${JOB}" | jq -r '.command')
+
+if [[ ! ${COMMAND} =~ phpunit ]]; then
+ exit 0
+fi
+
+PHP_VERSION=$(echo "${JOB}" | jq -r '.php')
+
+# Install CI version of phpunit config
+cp .laminas-ci/phpunit.xml phpunit.xml
+
+# Install lsof (used in integration tests)
+apt update -qq
+apt install -yqq lsof
diff --git a/README.md b/README.md
index 5919748..5f93a4e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
-# axleus-repo-template
-Template repo for starting all new repo's
+# phpdb-adapter-pgsql
+
+PostgreSQL Adapter for PHPDb
diff --git a/bin/install-deps.sh b/bin/install-deps.sh
new file mode 100755
index 0000000..3101960
--- /dev/null
+++ b/bin/install-deps.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+composer validate && \
+composer --ignore-platform-reqs install \
+ --no-ansi --no-progress --no-scripts \
+ --classmap-authoritative --no-interaction \
+ --quiet
\ No newline at end of file
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..704f405
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,33 @@
+services:
+ php:
+ build:
+ context: ./
+ dockerfile: docker/php/Dockerfile
+ args:
+ - PHP_VERSION=${PHP_VERSION:-8.3.19}
+ volumes:
+ - ./:/var/www/html
+ depends_on:
+ - postgresql
+
+ postgresql:
+ build:
+ context: ./
+ dockerfile: docker/databases/postgresql/Dockerfile
+ args:
+ - VERSION=${VERSION:-17.4}
+ - ALPINE_VERSION=${ALPINE_VERSION:-3.21}
+ ports:
+ - "5432:5432"
+ volumes:
+ - ./test/integration/TestFixtures/pgsql.sql:/docker-entrypoint-initdb.d/pgsql.sql
+ environment:
+ - POSTGRES_DB=${POSTGRES_DB:-phpdb_test}
+ - POSTGRES_USER=${POSTGRES_USER:-postgres}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
+ healthcheck:
+ test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-phpdb_test}'"]
+ interval: 30s
+ timeout: 60s
+ retries: 5
+ start_period: 80s
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..e5f63c3
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,75 @@
+{
+ "name": "php-db/phpdb-adapter-pgsql",
+ "description": "PostgreSQL support for php-db",
+ "license": "BSD-3-Clause",
+ "keywords": [
+ "php-db",
+ "pgsql",
+ "db"
+ ],
+ "homepage": "https://php-db.dev",
+ "support": {
+ "issues": "https://github.com/php-db/phpdb-adapter-pgsql/issues",
+ "source": "https://github.com/php-db/phpdb-adapter-pgsql",
+ "forum": "https://github.com/php-db/phpdb-adapter-pgsql/discussions"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "config": {
+ "sort-packages": true,
+ "platform": {
+ "php": "8.2.99"
+ },
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
+ "extra": {
+ "laminas": {
+ "config-provider": "PhpDb\\Adapter\\Pgsql\\ConfigProvider"
+ }
+ },
+ "require": {
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0",
+ "php-db/phpdb": "^0.2.1"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "^3.0.1",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^11.5.15"
+ },
+ "suggest": {
+ "ext-pdo": "*",
+ "ext-pdo_pgsql": "*",
+ "ext-pgsql": "*",
+ "laminas/laminas-servicemanager": "Laminas\\ServiceManager component"
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpDb\\Adapter\\Pgsql\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "PhpDbTest\\Adapter\\Pgsql\\": "test/unit/",
+ "PhpDbIntegrationTest\\Adapter\\Pgsql\\": "test/integration/"
+ }
+ },
+ "scripts": {
+ "check": [
+ "@cs-check",
+ "@static-analysis",
+ "@test",
+ "@test-integration"
+ ],
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
+ "test": "phpunit --colors=always --testsuite \"unit test\"",
+ "test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
+ "test-integration": "phpunit --colors=always --testsuite \"integration test\"",
+ "static-analysis": "vendor/bin/phpstan analyse --memory-limit=256M",
+ "sa-generate-baseline": "vendor/bin/phpstan analyse --memory-limit=256M --generate-baseline",
+ "upload-coverage": "coveralls -v"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..0413512
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2611 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "93b90a40dd4fe8c39502da74c4585c56",
+ "packages": [
+ {
+ "name": "brick/varexporter",
+ "version": "0.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/varexporter.git",
+ "reference": "84b2a7a91f69aa5d079aec5a0a7256ebf2dceb6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/varexporter/zipball/84b2a7a91f69aa5d079aec5a0a7256ebf2dceb6b",
+ "reference": "84b2a7a91f69aa5d079aec5a0a7256ebf2dceb6b",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.2",
+ "phpunit/phpunit": "^9.3",
+ "psalm/phar": "5.21.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\VarExporter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()",
+ "keywords": [
+ "var_export"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/varexporter/issues",
+ "source": "https://github.com/brick/varexporter/tree/0.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2024-05-10T17:15:19+00:00"
+ },
+ {
+ "name": "laminas/laminas-servicemanager",
+ "version": "4.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-servicemanager.git",
+ "reference": "74da44d07e493b834347123242d0047976fb9932"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/74da44d07e493b834347123242d0047976fb9932",
+ "reference": "74da44d07e493b834347123242d0047976fb9932",
+ "shasum": ""
+ },
+ "require": {
+ "brick/varexporter": "^0.3.8 || ^0.4.0 || ^0.5.0",
+ "laminas/laminas-stdlib": "^3.19",
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
+ "psr/container": "^1.1 || ^2.0"
+ },
+ "conflict": {
+ "laminas/laminas-code": "<4.10.0",
+ "zendframework/zend-code": "<3.3.1"
+ },
+ "provide": {
+ "psr/container-implementation": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/package-versions-deprecated": "^1.11.99.5",
+ "friendsofphp/proxy-manager-lts": "^1.0.18",
+ "laminas/laminas-cli": "^1.11",
+ "laminas/laminas-coding-standard": "~3.0.1",
+ "laminas/laminas-container-config-test": "^1.0",
+ "mikey179/vfsstream": "^1.6.12",
+ "phpbench/phpbench": "^1.4.0",
+ "phpunit/phpunit": "^10.5.44",
+ "psalm/plugin-phpunit": "^0.19.2",
+ "symfony/console": "^6.4.17 || ^7.0",
+ "vimeo/psalm": "^6.2.0"
+ },
+ "suggest": {
+ "friendsofphp/proxy-manager-lts": "To handle lazy initialization of services",
+ "laminas/laminas-cli": "To consume CLI commands provided by this component"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "module": "Laminas\\ServiceManager",
+ "config-provider": "Laminas\\ServiceManager\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laminas\\ServiceManager\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Factory-Driven Dependency Injection Container",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "PSR-11",
+ "dependency-injection",
+ "di",
+ "dic",
+ "laminas",
+ "service-manager",
+ "servicemanager"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-servicemanager/issues",
+ "source": "https://github.com/laminas/laminas-servicemanager/tree/4.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2025-02-04T06:13:50+00:00"
+ },
+ {
+ "name": "laminas/laminas-stdlib",
+ "version": "3.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-stdlib.git",
+ "reference": "8974a1213be42c3e2f70b2c27b17f910291ab2f4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/8974a1213be42c3e2f70b2c27b17f910291ab2f4",
+ "reference": "8974a1213be42c3e2f70b2c27b17f910291ab2f4",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "conflict": {
+ "zendframework/zend-stdlib": "*"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "^3.0",
+ "phpbench/phpbench": "^1.3.1",
+ "phpunit/phpunit": "^10.5.38",
+ "psalm/plugin-phpunit": "^0.19.0",
+ "vimeo/psalm": "^5.26.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Laminas\\Stdlib\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "SPL extensions, array utilities, error handlers, and more",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "laminas",
+ "stdlib"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-stdlib/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-stdlib/issues",
+ "rss": "https://github.com/laminas/laminas-stdlib/releases.atom",
+ "source": "https://github.com/laminas/laminas-stdlib"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2024-10-29T13:46:07+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
+ },
+ "time": "2025-08-13T20:13:15+00:00"
+ },
+ {
+ "name": "php-db/phpdb",
+ "version": "0.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-db/phpdb.git",
+ "reference": "d221b024cb3aea77992f41a962913918301dc92e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-db/phpdb/zipball/d221b024cb3aea77992f41a962913918301dc92e",
+ "reference": "d221b024cb3aea77992f41a962913918301dc92e",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-servicemanager": "^4.0.0",
+ "laminas/laminas-stdlib": "^3.20.0",
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "conflict": {
+ "laminas/laminas-db": "*",
+ "zendframework/zend-db": "*"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "^3.0.1",
+ "laminas/laminas-eventmanager": "^3.14.0",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^11.5.15",
+ "rector/rector": "^2.0"
+ },
+ "suggest": {
+ "laminas/laminas-eventmanager": "Laminas\\EventManager component",
+ "laminas/laminas-hydrator": "(^5.0.0) Laminas\\Hydrator component for using HydratingResultSets"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "config-provider": "PhpDb\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpDb\\": "src/",
+ "CustomRule\\PHPUnit\\": "rector/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Database abstraction layer, SQL abstraction, result set abstraction, and RowDataGateway and TableDataGateway implementations",
+ "homepage": "https://php-db.dev",
+ "keywords": [
+ "db",
+ "laminas",
+ "mezzio",
+ "php-db"
+ ],
+ "support": {
+ "docs": "https://docs.php-db.dev/",
+ "issues": "https://github.com/php-db/phpdb/issues",
+ "source": "https://github.com/php-db/phpdb"
+ },
+ "time": "2025-10-07T08:36:48+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/composer-installer.git",
+ "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1",
+ "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.2",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.2",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "opensource@frenck.dev",
+ "homepage": "https://frenck.dev",
+ "role": "Open source developer"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcbf",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/composer-installer/issues",
+ "security": "https://github.com/PHPCSStandards/composer-installer/security/policy",
+ "source": "https://github.com/PHPCSStandards/composer-installer"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-07-17T20:45:56+00:00"
+ },
+ {
+ "name": "laminas/laminas-coding-standard",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-coding-standard.git",
+ "reference": "d4412caba9ed16c93cdcf301759f5ee71f9d9aea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-coding-standard/zipball/d4412caba9ed16c93cdcf301759f5ee71f9d9aea",
+ "reference": "d4412caba9ed16c93cdcf301759f5ee71f9d9aea",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0",
+ "php": "^7.4 || ^8.0",
+ "slevomat/coding-standard": "^8.15.0",
+ "squizlabs/php_codesniffer": "^3.10",
+ "webimpress/coding-standard": "^1.3"
+ },
+ "type": "phpcodesniffer-standard",
+ "autoload": {
+ "psr-4": {
+ "LaminasCodingStandard\\": "src/LaminasCodingStandard/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Laminas Coding Standard",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "Coding Standard",
+ "laminas"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-coding-standard/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-coding-standard/issues",
+ "rss": "https://github.com/laminas/laminas-coding-standard/releases.atom",
+ "source": "https://github.com/laminas/laminas-coding-standard"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2025-05-13T08:37:04+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^5.3.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
+ },
+ "time": "2025-08-30T15:50:23+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.30",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a4a7f159927983dd4f7c8020ed227d80b7f39d7d",
+ "reference": "a4a7f159927983dd4f7c8020ed227d80b7f39d7d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2025-10-02T16:07:52+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-phpunit",
+ "version": "2.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-phpunit.git",
+ "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc",
+ "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.1.18"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<7.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^5",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon",
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPUnit extensions and rules for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
+ "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.7"
+ },
+ "time": "2025-07-13T11:31:46+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "11.0.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^5.4.0",
+ "php": ">=8.2",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-text-template": "^4.0.1",
+ "sebastian/code-unit-reverse-lookup": "^4.0.1",
+ "sebastian/complexity": "^4.0.1",
+ "sebastian/environment": "^7.2.0",
+ "sebastian/lines-of-code": "^3.0.1",
+ "sebastian/version": "^5.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5.2"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-27T14:37:49+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "5.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-27T05:02:59+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:07:44+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:08:43+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "7.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:09:35+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "11.5.42",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c",
+ "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.2",
+ "phpunit/php-code-coverage": "^11.0.11",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-invoker": "^5.0.1",
+ "phpunit/php-text-template": "^4.0.1",
+ "phpunit/php-timer": "^7.0.1",
+ "sebastian/cli-parser": "^3.0.2",
+ "sebastian/code-unit": "^3.0.3",
+ "sebastian/comparator": "^6.3.2",
+ "sebastian/diff": "^6.0.2",
+ "sebastian/environment": "^7.2.1",
+ "sebastian/exporter": "^6.3.2",
+ "sebastian/global-state": "^7.0.2",
+ "sebastian/object-enumerator": "^6.0.1",
+ "sebastian/type": "^5.1.3",
+ "sebastian/version": "^5.0.2",
+ "staabm/side-effects-detector": "^1.0.5"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.42"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-28T12:09:13+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:41:36+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "security": "https://github.com/sebastianbergmann/code-unit/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-19T07:56:08+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:45:54+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "6.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/diff": "^6.0",
+ "sebastian/exporter": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.4"
+ },
+ "suggest": {
+ "ext-bcmath": "For comparing BcMath\\Number objects"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T08:07:46+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:49:50+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:53:05+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "7.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-21T11:55:47+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "6.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-24T06:12:51+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:57:36+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:58:38+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "6.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:00:13+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:01:32+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "6.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-13T04:42:22+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "5.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "security": "https://github.com/sebastianbergmann/type/security/policy",
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-09T06:55:48+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "security": "https://github.com/sebastianbergmann/version/security/policy",
+ "source": "https://github.com/sebastianbergmann/version/tree/5.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-09T05:16:32+00:00"
+ },
+ {
+ "name": "slevomat/coding-standard",
+ "version": "8.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slevomat/coding-standard.git",
+ "reference": "1dd80bf3b93692bedb21a6623c496887fad05fec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/1dd80bf3b93692bedb21a6623c496887fad05fec",
+ "reference": "1dd80bf3b93692bedb21a6623c496887fad05fec",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.1.2",
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpdoc-parser": "^2.3.0",
+ "squizlabs/php_codesniffer": "^3.13.4"
+ },
+ "require-dev": {
+ "phing/phing": "3.0.1|3.1.0",
+ "php-parallel-lint/php-parallel-lint": "1.4.0",
+ "phpstan/phpstan": "2.1.24",
+ "phpstan/phpstan-deprecation-rules": "2.0.3",
+ "phpstan/phpstan-phpunit": "2.0.7",
+ "phpstan/phpstan-strict-rules": "2.0.6",
+ "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.3.10"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SlevomatCodingStandard\\": "SlevomatCodingStandard/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
+ "keywords": [
+ "dev",
+ "phpcs"
+ ],
+ "support": {
+ "issues": "https://github.com/slevomat/coding-standard/issues",
+ "source": "https://github.com/slevomat/coding-standard/tree/8.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kukulich",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-13T08:53:30+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119",
+ "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-09-05T05:47:09+00:00"
+ },
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues",
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:36:25+00:00"
+ },
+ {
+ "name": "webimpress/coding-standard",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webimpress/coding-standard.git",
+ "reference": "6f6a1a90bd9e18fc8bee0660dd1d1ce68cf9fc53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webimpress/coding-standard/zipball/6f6a1a90bd9e18fc8bee0660dd1d1ce68cf9fc53",
+ "reference": "6f6a1a90bd9e18fc8bee0660dd1d1ce68cf9fc53",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.3 || ^8.0",
+ "squizlabs/php_codesniffer": "^3.10.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6.15"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "dev-master": "1.2.x-dev",
+ "dev-develop": "1.3.x-dev"
+ },
+ "autoload": {
+ "psr-4": {
+ "WebimpressCodingStandard\\": "src/WebimpressCodingStandard/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "description": "Webimpress Coding Standard",
+ "keywords": [
+ "Coding Standard",
+ "PSR-2",
+ "phpcs",
+ "psr-12",
+ "webimpress"
+ ],
+ "support": {
+ "issues": "https://github.com/webimpress/coding-standard/issues",
+ "source": "https://github.com/webimpress/coding-standard/tree/1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/michalbundyra",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-16T06:55:17+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "dev",
+ "stability-flags": {},
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "platform-dev": {},
+ "platform-overrides": {
+ "php": "8.2.99"
+ },
+ "plugin-api-version": "2.6.0"
+}
diff --git a/coveralls.yml b/coveralls.yml
new file mode 100644
index 0000000..bc71b62
--- /dev/null
+++ b/coveralls.yml
@@ -0,0 +1,2 @@
+coverage_clover: clover.xml
+json_path: coveralls-upload.json
diff --git a/docker/databases/postgresql/Dockerfile b/docker/databases/postgresql/Dockerfile
new file mode 100644
index 0000000..c4609c2
--- /dev/null
+++ b/docker/databases/postgresql/Dockerfile
@@ -0,0 +1,4 @@
+ARG VERSION=17.4
+ARG ALPINE_VERSION=3.21
+
+FROM postgres:${VERSION}-alpine${ALPINE_VERSION} AS base
\ No newline at end of file
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
new file mode 100644
index 0000000..eaaff2e
--- /dev/null
+++ b/docker/php/Dockerfile
@@ -0,0 +1,26 @@
+ARG PHP_VERSION=8.3.19
+
+FROM php:${PHP_VERSION}-apache-bookworm AS base
+
+# Copy Composer from the official Docker Hub repository to the local filesystem
+COPY --from=composer:2.8.6 /usr/bin/composer /usr/bin/
+
+# Install the database extensions for MySQL, PostgreSQL, and SQLite, their
+# dependencies, and any other tools that are required for the environment to be
+# used fully and completely.
+RUN apt-get update \
+ && apt-get install -y git \
+ && docker-php-ext-install mysqli pdo pdo_mysql \
+ && apt-get clean
+
+# Allow the www-data login so that it can run Composer instead of using root
+RUN usermod -s /usr/bin/bash www-data
+
+# Copy all of the files from the context to the current directory setting the
+# correct owner
+COPY --chown=www-data:www-data . .
+
+RUN chmod +x bin/install-deps.sh
+
+# Validate and install PHP's dependencies
+RUN su --preserve-environment www-data --command "bin/install-deps.sh"
\ No newline at end of file
diff --git a/docker/php/conf.d/error_reporting.ini b/docker/php/conf.d/error_reporting.ini
new file mode 100644
index 0000000..d040e65
--- /dev/null
+++ b/docker/php/conf.d/error_reporting.ini
@@ -0,0 +1 @@
+error_reporting=E_ALL
\ No newline at end of file
diff --git a/docker/php/conf.d/xdebug.ini b/docker/php/conf.d/xdebug.ini
new file mode 100644
index 0000000..e29c8bd
--- /dev/null
+++ b/docker/php/conf.d/xdebug.ini
@@ -0,0 +1,6 @@
+zend_extension=xdebug
+
+[xdebug]
+xdebug.mode=develop,debug
+xdebug.client_host=host.docker.internal
+xdebug.start_with_request=yes
\ No newline at end of file
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..38effcf
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src
+ test
+
+
+
+
+
+
+
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..096cae4
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,2 @@
+parameters:
+ ignoreErrors:
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..4b4e3f6
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,17 @@
+includes:
+ - phpstan-baseline.neon
+parameters:
+ level: 5
+ paths:
+ - src
+ - test
+ universalObjectCratesClasses:
+ - Laminas\Stdlib\ArrayObject
+ stubFiles:
+ - stubs/Laminas/ServiceManager/Factory/AbstractFactoryInterface.stub
+ - stubs/Laminas/ServiceManager/Factory/DelegatorFactoryInterface.stub
+ - stubs/Laminas/ServiceManager/Factory/FactoryInterface.stub
+ - stubs/Laminas/ServiceManager/Factory/InvokableFactory.stub
+ - stubs/Laminas/ServiceManager/Initializer/InitializerInterface.stub
+ - stubs/Psr/Container/ContainerInterface.stub
+ treatPhpDocTypesAsCertain: false
\ No newline at end of file
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..97ae618
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,38 @@
+
+
+
+
+ ./test/unit
+
+
+
+ ./test/integration
+
+
+
+
+
+ ./src
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..0c9fe21
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "local>php-db/.github:renovate-config"
+ ]
+}
\ No newline at end of file
diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php
new file mode 100644
index 0000000..19aa7af
--- /dev/null
+++ b/src/ConfigProvider.php
@@ -0,0 +1,34 @@
+ $this->getDependencies(),
+ ];
+ }
+
+ public function getDependencies(): array
+ {
+ return [
+ 'aliases' => [
+ PlatformInterface::class => Platform\Postgresql::class,
+ ],
+ 'factories' => [
+ AdapterInterface::class => Container\AdapterServiceFactory::class,
+ DriverInterface::class => Container\PdoDriverInterfaceFactory::class,
+ Platform\Postgresql::class => InvokableFactory::class,
+ ],
+ ];
+ }
+}
diff --git a/src/Container/AdapterInterfaceFactory.php b/src/Container/AdapterInterfaceFactory.php
new file mode 100644
index 0000000..602df73
--- /dev/null
+++ b/src/Container/AdapterInterfaceFactory.php
@@ -0,0 +1,44 @@
+has(ResultSetInterface::class)
+ ? $container->get(ResultSetInterface::class)
+ : null;
+
+ $profiler = $container->get(ProfilerInterface::class);
+
+ return new Adapter(
+ $container->get(DriverInterface::class),
+ $container->get(Platform\Postgresql::class),
+ $resultSetPrototype,
+ $profiler
+ );
+ }
+}
diff --git a/src/Container/AdapterManagerDelegator.php b/src/Container/AdapterManagerDelegator.php
new file mode 100644
index 0000000..742a6ae
--- /dev/null
+++ b/src/Container/AdapterManagerDelegator.php
@@ -0,0 +1,10 @@
+get('config')['db']);
+ }
+}
diff --git a/src/Container/PdoResultFactory.php b/src/Container/PdoResultFactory.php
new file mode 100644
index 0000000..59b7da6
--- /dev/null
+++ b/src/Container/PdoResultFactory.php
@@ -0,0 +1,10 @@
+isConnected()) {
+ $this->connect();
+ }
+
+ /** @var PDOStatement $result */
+ $result = $this->resource->query('main');
+ if ($result instanceof PDOStatement) {
+ return $result->fetchColumn();
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws Exception\InvalidConnectionParametersException
+ * @throws Exception\RuntimeException
+ */
+ #[Override]
+ public function connect(): ConnectionInterface&PdoConnectionInterface
+ {
+ if ($this->resource) {
+ return $this;
+ }
+
+ $dsn = $username = $password = $hostname = $database = null;
+ $options = [];
+ foreach ($this->connectionParameters as $key => $value) {
+ switch (strtolower($key)) {
+ case 'dsn':
+ $dsn = $value;
+ break;
+ case 'driver':
+ $value = strtolower((string) $value);
+ if (str_starts_with($value, 'pdo')) {
+ $pdoDriver = str_replace(['-', '_', ' '], '', $value);
+ $pdoDriver = substr($pdoDriver, 3) ?: '';
+ }
+ break;
+ case 'pdodriver':
+ $pdoDriver = (string) $value;
+ break;
+ case 'user':
+ case 'username':
+ $username = (string) $value;
+ break;
+ case 'pass':
+ case 'password':
+ $password = (string) $value;
+ break;
+ case 'host':
+ case 'hostname':
+ $hostname = (string) $value;
+ break;
+ case 'database':
+ case 'dbname':
+ $database = (string) $value;
+ break;
+ case 'unix_socket':
+ $unixSocket = (string) $value;
+ break;
+ case 'driver_options':
+ case 'options':
+ $value = (array) $value;
+ $options = array_diff_key($options, $value) + $value;
+ break;
+ default:
+ $options[$key] = $value;
+ break;
+ }
+ }
+
+ if (isset($hostname) && isset($unixSocket)) {
+ throw new Exception\InvalidConnectionParametersException(
+ 'Ambiguous connection parameters, both hostname and unix_socket parameters were set',
+ $this->connectionParameters
+ );
+ }
+
+ if (! isset($dsn) && isset($pdoDriver)) {
+ $dsn = $pdoDriver . ':' . $database;
+ } elseif (! isset($dsn)) {
+ throw new Exception\InvalidConnectionParametersException(
+ 'A dsn was not provided or could not be constructed from your parameters',
+ $this->connectionParameters
+ );
+ }
+
+ $this->dsn = $dsn;
+
+ try {
+ $this->resource = new PDO($dsn, $username, $password, $options);
+ $this->resource->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $this->driverName = strtolower($this->resource->getAttribute(PDO::ATTR_DRIVER_NAME));
+ } catch (PDOException $e) {
+ $code = $e->getCode();
+ if (! is_int($code)) {
+ $code = 0;
+ }
+ throw new Exception\RuntimeException('Connect Error: ' . $e->getMessage(), $code, $e);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $name
+ * @return string|null|false
+ */
+ #[Override]
+ public function getLastGeneratedValue($name = null): bool|int|string|null
+ {
+ try {
+ return $this->resource->lastInsertId($name);
+ } catch (\Exception) {
+ }
+
+ return false;
+ }
+}
diff --git a/src/Driver/Pdo/Pdo.php b/src/Driver/Pdo/Pdo.php
new file mode 100644
index 0000000..1aee71d
--- /dev/null
+++ b/src/Driver/Pdo/Pdo.php
@@ -0,0 +1,30 @@
+resultPrototype;
+ $result->initialize($resource, $this->connection->getLastGeneratedValue());
+
+ return $result;
+ }
+}
diff --git a/src/Driver/Pgsql/Connection.php b/src/Driver/Pgsql/Connection.php
new file mode 100644
index 0000000..03aba23
--- /dev/null
+++ b/src/Driver/Pgsql/Connection.php
@@ -0,0 +1,276 @@
+setConnectionParameters($connectionInfo);
+ } elseif ($connectionInfo instanceof PgSqlConnection || is_resource($connectionInfo)) {
+ $this->setResource($connectionInfo);
+ }
+ }
+
+ public function setResource(PgSqlConnection $resource): ConnectionInterface
+ {
+ $this->resource = $resource;
+
+ return $this;
+ }
+
+ /**
+ * old param type hint Pgsql $driver
+ */
+ public function setDriver(DriverInterface $driver): DriverAwareInterface
+ {
+ $this->driver = $driver;
+
+ return $this;
+ }
+
+ /**
+ * @return $this Provides a fluent interface
+ */
+ public function setType(?int $type): static
+ {
+ $invalidConectionType = $type !== PGSQL_CONNECT_FORCE_NEW;
+ if ($invalidConectionType) {
+ throw new Exception\InvalidArgumentException(
+ 'Connection type is not valid. (See: https://php.net/manual/en/function.pg-connect.php)'
+ );
+ }
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return null|string
+ */
+ public function getCurrentSchema(): bool|string
+ {
+ if (! $this->isConnected()) {
+ $this->connect();
+ }
+
+ $result = pg_query($this->resource, 'SELECT CURRENT_SCHEMA AS "currentschema"');
+ if ($result === false) {
+ return false;
+ }
+
+ return pg_fetch_result($result, 0, 'currentschema');
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws Exception\RuntimeException On failure.
+ */
+ public function connect(): static
+ {
+ if ($this->resource instanceof PgSqlConnection) {
+ return $this;
+ }
+
+ $connection = $this->getConnectionString();
+ set_error_handler(function ($number, $string) {
+ throw new Exception\RuntimeException(
+ self::class . '::connect: Unable to connect to database',
+ $number ?? 0,
+ new Exception\ErrorException($string, $number ?? 0)
+ );
+ });
+ try {
+ $this->resource = pg_connect($connection);
+ } finally {
+ restore_error_handler();
+ }
+
+ if ($this->resource === false) {
+ throw new Exception\RuntimeException(sprintf(
+ '%s: Unable to connect to database',
+ __METHOD__
+ ));
+ }
+
+ if (! empty($this->connectionParameters['charset'])) {
+ if (pg_set_client_encoding($this->resource, $this->connectionParameters['charset']) === -1) {
+ throw new Exception\RuntimeException(sprintf(
+ "%s: Unable to set client encoding '%s'",
+ __METHOD__,
+ $this->connectionParameters['charset']
+ ));
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isConnected(): bool
+ {
+ return $this->resource instanceof PgSqlConnection;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function disconnect(): static
+ {
+ // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
+ pg_close($this->resource);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function beginTransaction(): static
+ {
+ if ($this->inTransaction()) {
+ throw new Exception\RuntimeException('Nested transactions are not supported');
+ }
+
+ if (! $this->isConnected()) {
+ $this->connect();
+ }
+
+ pg_query($this->resource, 'BEGIN');
+ $this->inTransaction = true;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function commit(): static
+ {
+ if (! $this->isConnected()) {
+ $this->connect();
+ }
+
+ if (! $this->inTransaction()) {
+ return $this; // We ignore attempts to commit non-existing transaction
+ }
+
+ pg_query($this->resource, 'COMMIT');
+ $this->inTransaction = false;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function rollback(): static
+ {
+ if (! $this->isConnected()) {
+ throw new Exception\RuntimeException('Must be connected before you can rollback');
+ }
+
+ if (! $this->inTransaction()) {
+ throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback');
+ }
+
+ pg_query($this->resource, 'ROLLBACK');
+ $this->inTransaction = false;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws Exception\InvalidQueryException
+ * @return resource|ResultSetInterface
+ */
+ public function execute($sql): ResultInterface
+ {
+ if (! $this->isConnected()) {
+ $this->connect();
+ }
+
+ $this->profiler?->profilerStart($sql);
+
+ $resultResource = pg_query($this->resource, $sql);
+
+ $this->profiler?->profilerFinish();
+
+ // if the returnValue is something other than a pg result resource, bypass wrapping it
+ if ($resultResource === false) {
+ throw new Exception\InvalidQueryException(pg_last_error($this->resource));
+ }
+
+ return $this->driver->createResult($resultResource);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return string
+ */
+ public function getLastGeneratedValue($name = null): bool|int|string|null
+ {
+ if ($name === null) {
+ return null;
+ }
+ $result = pg_query(
+ $this->resource,
+ 'SELECT CURRVAL(\'' . str_replace('\'', '\\\'', $name) . '\') as "currval"'
+ );
+
+ return pg_fetch_result($result, 0, 'currval');
+ }
+
+ /**
+ * Get Connection String
+ */
+ private function getConnectionString(): string
+ {
+ $connectionParameters = array_filter((new PgsqlConfig())($this->connectionParameters));
+
+ return urldecode(http_build_query($connectionParameters, '', ' '));
+ }
+}
diff --git a/src/Driver/Pgsql/Pgsql.php b/src/Driver/Pgsql/Pgsql.php
new file mode 100644
index 0000000..c3fe265
--- /dev/null
+++ b/src/Driver/Pgsql/Pgsql.php
@@ -0,0 +1,155 @@
+ false,
+ ];
+
+ public function __construct(
+ protected readonly ConnectionInterface&Connection $connection,
+ protected readonly StatementInterface&Statement $statementPrototype,
+ protected readonly ResultInterface $resultPrototype,
+ array $options = []
+ ) {
+ $this->checkEnvironment();
+
+ //todo: verify this usage
+ $options = array_intersect_key(array_merge($this->options, $options), $this->options);
+
+ if ($this->connection instanceof DriverAwareInterface) {
+ $this->connection->setDriver($this);
+ }
+ if ($this->statementPrototype instanceof DriverAwareInterface) {
+ $this->statementPrototype->setDriver($this);
+ }
+ }
+
+ #[Override]
+ public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface
+ {
+ $this->profiler = $profiler;
+ if ($this->connection instanceof ProfilerAwareInterface) {
+ $this->connection->setProfiler($profiler);
+ }
+
+ if ($this->statementPrototype instanceof ProfilerAwareInterface) {
+ $this->statementPrototype->setProfiler($profiler);
+ }
+
+ return $this;
+ }
+
+ public function getProfiler(): ?ProfilerInterface
+ {
+ return $this->profiler;
+ }
+
+ #[Override]
+ public function checkEnvironment(): bool
+ {
+ if (! extension_loaded('pgsql')) {
+ throw new Exception\RuntimeException(
+ 'The PostgreSQL (pgsql) extension is required for this adapter but the extension is not loaded'
+ );
+ }
+ return true;
+ }
+
+ #[Override]
+ public function getConnection(): ConnectionInterface&Connection
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Create statement
+ *
+ * @param resource|string|null $sqlOrResource
+ */
+ #[Override]
+ public function createStatement($sqlOrResource = null): StatementInterface&Statement
+ {
+ $statement = clone $this->statementPrototype;
+
+ if (is_string($sqlOrResource)) {
+ $statement->setSql($sqlOrResource);
+ }
+
+ if (! $this->connection->isConnected()) {
+ $this->connection->connect();
+ }
+
+ $statement->initialize($this->connection->getResource());
+
+ return $statement;
+ }
+
+ /**
+ * Create result
+ *
+ * @param resource|PgSqlResult|PgSqlConnection $resource
+ */
+ #[Override]
+ public function createResult($resource): ResultInterface&Result
+ {
+ /** @var Result $result */
+ $result = clone $this->resultPrototype;
+ $result->initialize($resource, $this->connection->getLastGeneratedValue());
+
+ return $result;
+ }
+
+ public function getResultPrototype(): ResultInterface&Result
+ {
+ return $this->resultPrototype;
+ }
+
+ #[Override]
+ public function getPrepareType(): string
+ {
+ return self::PARAMETERIZATION_POSITIONAL;
+ }
+
+ #[Override]
+ public function formatParameterName(string $name, ?string $type = null): string
+ {
+ return '$#';
+ }
+
+ /**
+ * Get last generated value
+ */
+ #[Override]
+ public function getLastGeneratedValue(?string $name = null): int|string|false|null
+ {
+ return $this->connection->getLastGeneratedValue($name);
+ }
+}
diff --git a/src/Driver/Pgsql/PgsqlConfig.php b/src/Driver/Pgsql/PgsqlConfig.php
new file mode 100644
index 0000000..c759966
--- /dev/null
+++ b/src/Driver/Pgsql/PgsqlConfig.php
@@ -0,0 +1,69 @@
+ $value) {
+ $name = match (strtolower($name)) {
+ 'host', 'hostname' => 'host',
+ 'user', 'username' => 'user',
+ 'password', 'passwd', 'pw' => 'password',
+ 'database', 'dbname', 'db', 'schema' => 'dbname',
+ 'port' => 'port',
+ 'socket' => 'socket',
+ default => throw new InvalidArgumentException(
+ 'Unknown connection parameter "' . $name . '"'
+ ),
+ };
+ $connectionParameters[$name] = $value;
+ }
+
+ $connectionFilters = [
+ 'host' => [
+ 'filter' => FILTER_SANITIZE_URL,
+ 'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_EMPTY_STRING_NULL,
+ ],
+ 'user' => [
+ 'filter' => FILTER_SANITIZE_ENCODED,
+ 'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_EMPTY_STRING_NULL,
+ ],
+ 'password' => [
+ 'filter' => FILTER_SANITIZE_ENCODED,
+ 'flags' => FILTER_FLAG_EMPTY_STRING_NULL,
+ ],
+ 'database' => [
+ 'filter' => FILTER_SANITIZE_ENCODED,
+ 'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_EMPTY_STRING_NULL,
+ ],
+ 'socket' => [
+ 'filter' => FILTER_SANITIZE_ENCODED,
+ 'flags' => FILTER_FLAG_EMPTY_STRING_NULL,
+ ],
+ 'port' => [
+ 'filter' => FILTER_VALIDATE_INT,
+ 'flags' => FILTER_NULL_ON_FAILURE,
+ ],
+ ];
+
+ return filter_var_array($connectionParameters, $connectionFilters);
+ }
+}
diff --git a/src/Driver/Pgsql/Result.php b/src/Driver/Pgsql/Result.php
new file mode 100644
index 0000000..92831c6
--- /dev/null
+++ b/src/Driver/Pgsql/Result.php
@@ -0,0 +1,174 @@
+resource = $resource;
+ $this->count = pg_num_rows($this->resource);
+ $this->generatedValue = $generatedValue;
+ }
+
+ /**
+ * Current
+ *
+ * @return array|bool|mixed
+ */
+ #[ReturnTypeWillChange]
+ public function current()
+ {
+ if ($this->count === 0) {
+ return false;
+ }
+ return pg_fetch_assoc($this->resource, $this->position);
+ }
+
+ /**
+ * Next
+ *
+ * @return void
+ */
+ #[ReturnTypeWillChange]
+ public function next()
+ {
+ $this->position++;
+ }
+
+ /**
+ * Key
+ *
+ * @return int|mixed
+ */
+ #[ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->position;
+ }
+
+ /**
+ * Valid
+ *
+ * @return bool
+ */
+ #[ReturnTypeWillChange]
+ public function valid()
+ {
+ return $this->position < $this->count;
+ }
+
+ /**
+ * Rewind
+ *
+ * @return void
+ */
+ #[ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ /** todo: track this */
+ public function buffer(): void
+ {
+ }
+
+ /**
+ * Is buffered
+ */
+ public function isBuffered(): ?bool
+ {
+ return false;
+ }
+
+ public function isQueryResult(): bool
+ {
+ return pg_num_fields($this->resource) > 0;
+ }
+
+ /**
+ * Get affected rows
+ */
+ public function getAffectedRows(): int
+ {
+ return pg_affected_rows($this->resource);
+ }
+
+ /**
+ * Get generated value
+ *
+ * @return mixed|null
+ */
+ public function getGeneratedValue()
+ {
+ return $this->generatedValue;
+ }
+
+ /**
+ * Get resource
+ */
+ public function getResource()
+ {
+ // TODO: Implement getResource() method.
+ }
+
+ /**
+ * Count
+ *
+ * @return int The custom count as an integer.
+ */
+ #[ReturnTypeWillChange]
+ public function count()
+ {
+ return $this->count;
+ }
+
+ /**
+ * Get field count
+ */
+ public function getFieldCount(): int
+ {
+ return pg_num_fields($this->resource);
+ }
+}
diff --git a/src/Driver/Pgsql/Statement.php b/src/Driver/Pgsql/Statement.php
new file mode 100644
index 0000000..fcb26f1
--- /dev/null
+++ b/src/Driver/Pgsql/Statement.php
@@ -0,0 +1,216 @@
+driver = $driver;
+ return $this;
+ }
+
+ /**
+ * @return $this Provides a fluent interface
+ */
+ public function setProfiler(Profiler\ProfilerInterface $profiler): static
+ {
+ $this->profiler = $profiler;
+ return $this;
+ }
+
+ public function getProfiler(): ?Profiler\ProfilerInterface
+ {
+ return $this->profiler;
+ }
+
+ /**
+ * Initialize
+ *
+ * @param resource $pgsql
+ * @throws Exception\RuntimeException For invalid or missing postgresql connection.
+ */
+ public function initialize($pgsql): void
+ {
+ if (
+ ! $pgsql instanceof PgSqlConnection
+ && (
+ ! is_resource($pgsql)
+ || 'pgsql link' !== get_resource_type($pgsql)
+ )
+ ) {
+ throw new Exception\RuntimeException(sprintf(
+ '%s: Invalid or missing postgresql connection; received "%s"',
+ __METHOD__,
+ get_resource_type($pgsql)
+ ));
+ }
+
+ $this->pgsql = $pgsql;
+ }
+
+ /**
+ * Get resource
+ *
+ * @todo Implement this method
+ * phpcs:ignore Squiz.Commenting.FunctionComment.InvalidNoReturn
+ * @return resource
+ */
+ public function getResource()
+ {
+ return $this->resource;
+ }
+
+ /**
+ * Set sql
+ *
+ * @param string $sql
+ * @return $this Provides a fluent interface
+ */
+ public function setSql($sql): static
+ {
+ $this->sql = $sql;
+ return $this;
+ }
+
+ /**
+ * Get sql
+ */
+ public function getSql(): ?string
+ {
+ return $this->sql;
+ }
+
+ /**
+ * Set parameter container
+ *
+ * @return $this Provides a fluent interface
+ */
+ public function setParameterContainer(ParameterContainer $parameterContainer): static
+ {
+ $this->parameterContainer = $parameterContainer;
+ return $this;
+ }
+
+ /**
+ * Get parameter container
+ */
+ public function getParameterContainer(): ParameterContainer
+ {
+ return $this->parameterContainer;
+ }
+
+ /**
+ * Prepare
+ *
+ * @param string $sql
+ */
+ public function prepare($sql = null): StatementInterface
+ {
+ $sql = $sql ?: $this->sql;
+
+ $pCount = 1;
+ $sql = preg_replace_callback(
+ '#\$\##',
+ function () use (&$pCount) {
+ return '$' . $pCount++;
+ },
+ $sql
+ );
+
+ $this->sql = $sql;
+ $this->statementName = 'statement' . ++static::$statementIndex;
+ $this->resource = pg_prepare($this->pgsql, $this->statementName, $sql);
+ }
+
+ /**
+ * Is prepared
+ */
+ public function isPrepared(): bool
+ {
+ return isset($this->resource);
+ }
+
+ /**
+ * Execute
+ *
+ * @throws Exception\InvalidQueryException
+ * @return Result
+ */
+ public function execute(ParameterContainer|array|null $parameters = null): ?ResultInterface
+ {
+ if (! $this->isPrepared()) {
+ $this->prepare();
+ }
+
+ /** START Standard ParameterContainer Merging Block */
+ if (! $this->parameterContainer instanceof ParameterContainer) {
+ if ($parameters instanceof ParameterContainer) {
+ $this->parameterContainer = $parameters;
+ $parameters = null;
+ } else {
+ $this->parameterContainer = new ParameterContainer();
+ }
+ }
+
+ if (is_array($parameters)) {
+ $this->parameterContainer->setFromArray($parameters);
+ }
+
+ if ($this->parameterContainer->count() > 0) {
+ $parameters = $this->parameterContainer->getPositionalArray();
+ }
+ /** END Standard ParameterContainer Merging Block */
+
+ $this->profiler?->profilerStart($this);
+
+ $resultResource = pg_execute($this->pgsql, $this->statementName, (array) $parameters);
+
+ $this->profiler?->profilerFinish();
+
+ if ($resultResource === false) {
+ throw new Exception\InvalidQueryException(pg_last_error());
+ }
+
+ return $this->driver->createResult($resultResource);
+ }
+}
diff --git a/src/Metadata/Source/PostgresqlMetadata.php b/src/Metadata/Source/PostgresqlMetadata.php
new file mode 100644
index 0000000..45c4e3d
--- /dev/null
+++ b/src/Metadata/Source/PostgresqlMetadata.php
@@ -0,0 +1,363 @@
+data['schemas'])) {
+ return;
+ }
+ $this->prepareDataHierarchy('schemas');
+
+ $p = $this->adapter->getPlatform();
+
+ $sql = 'SELECT ' . $p->quoteIdentifier('schema_name')
+ . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'schemata'])
+ . ' WHERE ' . $p->quoteIdentifier('schema_name')
+ . ' != \'information_schema\''
+ . ' AND ' . $p->quoteIdentifier('schema_name') . " NOT LIKE 'pg_%'";
+
+ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE);
+
+ $schemas = [];
+ foreach ($results->toArray() as $row) {
+ $schemas[] = $row['schema_name'];
+ }
+
+ $this->data['schemas'] = $schemas;
+ }
+
+ #[Override]
+ protected function loadTableNameData(string $schema): void
+ {
+ if (isset($this->data['table_names'][$schema])) {
+ return;
+ }
+ $this->prepareDataHierarchy('table_names', $schema);
+
+ $p = $this->adapter->getPlatform();
+
+ $isColumns = [
+ ['t', 'table_name'],
+ ['t', 'table_type'],
+ ['v', 'view_definition'],
+ ['v', 'check_option'],
+ ['v', 'is_updatable'],
+ ];
+
+ array_walk($isColumns, function (&$c) use ($p) {
+ $c = $p->quoteIdentifierChain($c);
+ });
+
+ $sql = 'SELECT ' . implode(', ', $isColumns)
+ . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'tables']) . ' t'
+
+ . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'views']) . ' v'
+ . ' ON ' . $p->quoteIdentifierChain(['t', 'table_schema'])
+ . ' = ' . $p->quoteIdentifierChain(['v', 'table_schema'])
+ . ' AND ' . $p->quoteIdentifierChain(['t', 'table_name'])
+ . ' = ' . $p->quoteIdentifierChain(['v', 'table_name'])
+
+ . ' WHERE ' . $p->quoteIdentifierChain(['t', 'table_type'])
+ . ' IN (\'BASE TABLE\', \'VIEW\')';
+
+ if ($schema !== self::DEFAULT_SCHEMA) {
+ $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema'])
+ . ' = ' . $p->quoteTrustedValue($schema);
+ } else {
+ $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema'])
+ . ' != \'information_schema\'';
+ }
+
+ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE);
+
+ $tables = [];
+ foreach ($results->toArray() as $row) {
+ $tables[$row['table_name']] = [
+ 'table_type' => $row['table_type'],
+ 'view_definition' => $row['view_definition'],
+ 'check_option' => $row['check_option'],
+ 'is_updatable' => 'YES' === $row['is_updatable'],
+ ];
+ }
+
+ $this->data['table_names'][$schema] = $tables;
+ }
+
+ #[Override]
+ protected function loadColumnData(string $table, string $schema): void
+ {
+ if (isset($this->data['columns'][$schema][$table])) {
+ return;
+ }
+
+ $this->prepareDataHierarchy('columns', $schema, $table);
+
+ $platform = $this->adapter->getPlatform();
+
+ $isColumns = [
+ 'table_name',
+ 'column_name',
+ 'ordinal_position',
+ 'column_default',
+ 'is_nullable',
+ 'data_type',
+ 'character_maximum_length',
+ 'character_octet_length',
+ 'numeric_precision',
+ 'numeric_scale',
+ ];
+
+ array_walk($isColumns, function (&$c) use ($platform) {
+ $c = $platform->quoteIdentifier($c);
+ });
+
+ $sql = 'SELECT ' . implode(', ', $isColumns)
+ . ' FROM ' . $platform->quoteIdentifier('information_schema')
+ . $platform->getIdentifierSeparator() . $platform->quoteIdentifier('columns')
+ . ' WHERE ' . $platform->quoteIdentifier('table_schema')
+ . ' != \'information\''
+ . ' AND ' . $platform->quoteIdentifier('table_name')
+ . ' = ' . $platform->quoteTrustedValue($table);
+
+ if ($schema !== '__DEFAULT_SCHEMA__') {
+ $sql .= ' AND ' . $platform->quoteIdentifier('table_schema')
+ . ' = ' . $platform->quoteTrustedValue($schema);
+ }
+
+ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE);
+ $columns = [];
+ foreach ($results->toArray() as $row) {
+ $columns[$row['column_name']] = [
+ 'ordinal_position' => $row['ordinal_position'],
+ 'column_default' => $row['column_default'],
+ 'is_nullable' => 'YES' === $row['is_nullable'],
+ 'data_type' => $row['data_type'],
+ 'character_maximum_length' => $row['character_maximum_length'],
+ 'character_octet_length' => $row['character_octet_length'],
+ 'numeric_precision' => $row['numeric_precision'],
+ 'numeric_scale' => $row['numeric_scale'],
+ 'numeric_unsigned' => null,
+ 'erratas' => [],
+ ];
+ }
+
+ $this->data['columns'][$schema][$table] = $columns;
+ }
+
+ #[Override]
+ protected function loadConstraintData(string $table, string $schema): void
+ {
+ // phpcs:disable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps
+ if (isset($this->data['constraints'][$schema][$table])) {
+ return;
+ }
+
+ $this->prepareDataHierarchy('constraints', $schema, $table);
+
+ $isColumns = [
+ ['t', 'table_name'],
+ ['tc', 'constraint_name'],
+ ['tc', 'constraint_type'],
+ ['kcu', 'column_name'],
+ ['cc', 'check_clause'],
+ ['rc', 'match_option'],
+ ['rc', 'update_rule'],
+ ['rc', 'delete_rule'],
+ ['referenced_table_schema' => 'kcu2', 'table_schema'],
+ ['referenced_table_name' => 'kcu2', 'table_name'],
+ ['referenced_column_name' => 'kcu2', 'column_name'],
+ ];
+
+ $p = $this->adapter->getPlatform();
+
+ array_walk($isColumns, function (&$c) use ($p) {
+ $alias = key($c);
+ $c = $p->quoteIdentifierChain($c);
+ if (is_string($alias)) {
+ $c .= ' ' . $p->quoteIdentifier($alias);
+ }
+ });
+
+ $sql = 'SELECT ' . implode(', ', $isColumns)
+ . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'tables']) . ' t'
+
+ . ' INNER JOIN ' . $p->quoteIdentifierChain(['information_schema', 'table_constraints']) . ' tc'
+ . ' ON ' . $p->quoteIdentifierChain(['t', 'table_schema'])
+ . ' = ' . $p->quoteIdentifierChain(['tc', 'table_schema'])
+ . ' AND ' . $p->quoteIdentifierChain(['t', 'table_name'])
+ . ' = ' . $p->quoteIdentifierChain(['tc', 'table_name'])
+
+ . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'key_column_usage']) . ' kcu'
+ . ' ON ' . $p->quoteIdentifierChain(['tc', 'table_schema'])
+ . ' = ' . $p->quoteIdentifierChain(['kcu', 'table_schema'])
+ . ' AND ' . $p->quoteIdentifierChain(['tc', 'table_name'])
+ . ' = ' . $p->quoteIdentifierChain(['kcu', 'table_name'])
+ . ' AND ' . $p->quoteIdentifierChain(['tc', 'constraint_name'])
+ . ' = ' . $p->quoteIdentifierChain(['kcu', 'constraint_name'])
+
+ . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'check_constraints']) . ' cc'
+ . ' ON ' . $p->quoteIdentifierChain(['tc', 'constraint_schema'])
+ . ' = ' . $p->quoteIdentifierChain(['cc', 'constraint_schema'])
+ . ' AND ' . $p->quoteIdentifierChain(['tc', 'constraint_name'])
+ . ' = ' . $p->quoteIdentifierChain(['cc', 'constraint_name'])
+
+ . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'referential_constraints']) . ' rc'
+ . ' ON ' . $p->quoteIdentifierChain(['tc', 'constraint_schema'])
+ . ' = ' . $p->quoteIdentifierChain(['rc', 'constraint_schema'])
+ . ' AND ' . $p->quoteIdentifierChain(['tc', 'constraint_name'])
+ . ' = ' . $p->quoteIdentifierChain(['rc', 'constraint_name'])
+
+ . ' LEFT JOIN ' . $p->quoteIdentifierChain(['information_schema', 'key_column_usage']) . ' kcu2'
+ . ' ON ' . $p->quoteIdentifierChain(['rc', 'unique_constraint_schema'])
+ . ' = ' . $p->quoteIdentifierChain(['kcu2', 'constraint_schema'])
+ . ' AND ' . $p->quoteIdentifierChain(['rc', 'unique_constraint_name'])
+ . ' = ' . $p->quoteIdentifierChain(['kcu2', 'constraint_name'])
+ . ' AND ' . $p->quoteIdentifierChain(['kcu', 'position_in_unique_constraint'])
+ . ' = ' . $p->quoteIdentifierChain(['kcu2', 'ordinal_position'])
+
+ . ' WHERE ' . $p->quoteIdentifierChain(['t', 'table_name'])
+ . ' = ' . $p->quoteTrustedValue($table)
+ . ' AND ' . $p->quoteIdentifierChain(['t', 'table_type'])
+ . ' IN (\'BASE TABLE\', \'VIEW\')';
+
+ if ($schema !== self::DEFAULT_SCHEMA) {
+ $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema'])
+ . ' = ' . $p->quoteTrustedValue($schema);
+ } else {
+ $sql .= ' AND ' . $p->quoteIdentifierChain(['t', 'table_schema'])
+ . ' != \'information_schema\'';
+ }
+
+ $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(['tc', 'constraint_type'])
+ . " WHEN 'PRIMARY KEY' THEN 1"
+ . " WHEN 'UNIQUE' THEN 2"
+ . " WHEN 'FOREIGN KEY' THEN 3"
+ . " WHEN 'CHECK' THEN 4"
+ . " ELSE 5 END"
+ . ', ' . $p->quoteIdentifierChain(['tc', 'constraint_name'])
+ . ', ' . $p->quoteIdentifierChain(['kcu', 'ordinal_position']);
+
+ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE);
+
+ $name = null;
+ $constraints = [];
+ foreach ($results->toArray() as $row) {
+ if ($row['constraint_name'] !== $name) {
+ $name = $row['constraint_name'];
+ $constraints[$name] = [
+ 'constraint_name' => $name,
+ 'constraint_type' => $row['constraint_type'],
+ 'table_name' => $row['table_name'],
+ ];
+ if ('CHECK' === $row['constraint_type']) {
+ $constraints[$name]['check_clause'] = $row['check_clause'];
+ continue;
+ }
+ $constraints[$name]['columns'] = [];
+ $isFK = 'FOREIGN KEY' === $row['constraint_type'];
+ if ($isFK) {
+ $constraints[$name]['referenced_table_schema'] = $row['referenced_table_schema'];
+ $constraints[$name]['referenced_table_name'] = $row['referenced_table_name'];
+ $constraints[$name]['referenced_columns'] = [];
+ $constraints[$name]['match_option'] = $row['match_option'];
+ $constraints[$name]['update_rule'] = $row['update_rule'];
+ $constraints[$name]['delete_rule'] = $row['delete_rule'];
+ }
+ }
+ $constraints[$name]['columns'][] = $row['column_name'];
+ if ($isFK) {
+ $constraints[$name]['referenced_columns'][] = $row['referenced_column_name'];
+ }
+ }
+
+ $this->data['constraints'][$schema][$table] = $constraints;
+ // phpcs:enable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps
+ }
+
+ #[Override]
+ protected function loadTriggerData(string $schema): void
+ {
+ if (isset($this->data['triggers'][$schema])) {
+ return;
+ }
+
+ $this->prepareDataHierarchy('triggers', $schema);
+
+ $p = $this->adapter->getPlatform();
+
+ $isColumns = [
+ 'trigger_name',
+ 'event_manipulation',
+ 'event_object_catalog',
+ 'event_object_schema',
+ 'event_object_table',
+ 'action_order',
+ 'action_condition',
+ 'action_statement',
+ 'action_orientation',
+ ['action_timing' => 'condition_timing'],
+ ['action_reference_old_table' => 'condition_reference_old_table'],
+ ['action_reference_new_table' => 'condition_reference_new_table'],
+ 'created',
+ ];
+
+ array_walk($isColumns, function (&$c) use ($p) {
+ if (is_array($c)) {
+ $alias = key($c);
+ $c = $p->quoteIdentifierChain($c);
+ if (is_string($alias)) {
+ $c .= ' ' . $p->quoteIdentifier($alias);
+ }
+ } else {
+ $c = $p->quoteIdentifier($c);
+ }
+ });
+
+ $sql = 'SELECT ' . implode(', ', $isColumns)
+ . ' FROM ' . $p->quoteIdentifierChain(['information_schema', 'triggers'])
+ . ' WHERE ';
+
+ if ($schema !== self::DEFAULT_SCHEMA) {
+ $sql .= $p->quoteIdentifier('trigger_schema')
+ . ' = ' . $p->quoteTrustedValue($schema);
+ } else {
+ $sql .= $p->quoteIdentifier('trigger_schema')
+ . ' != \'information_schema\'';
+ }
+
+ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE);
+
+ $data = [];
+ foreach ($results->toArray() as $row) {
+ $row = array_change_key_case($row, CASE_LOWER);
+ $row['action_reference_old_row'] = 'OLD';
+ $row['action_reference_new_row'] = 'NEW';
+ if (null !== $row['created']) {
+ $row['created'] = new DateTime($row['created']);
+ }
+ $data[$row['trigger_name']] = $row;
+ }
+
+ $this->data['triggers'][$schema] = $data;
+ }
+}
diff --git a/src/Platform/Postgresql.php b/src/Platform/Postgresql.php
new file mode 100644
index 0000000..55fac46
--- /dev/null
+++ b/src/Platform/Postgresql.php
@@ -0,0 +1,121 @@
+quoteViaDriver($value);
+
+ return $quotedViaDriverValue ?? 'E' . parent::quoteValue($value);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param scalar $value
+ * @return string
+ */
+ #[Override]
+ public function quoteTrustedValue($value): string
+ {
+ $quotedViaDriverValue = $this->quoteViaDriver($value);
+
+ if ($quotedViaDriverValue === null) {
+ return 'E' . parent::quoteTrustedValue($value);
+ }
+
+ return $quotedViaDriverValue;
+ }
+
+ /**
+ * @param string $value
+ */
+ protected function quoteViaDriver($value): ?string
+ {
+ $resource = $this->driver instanceof DriverInterface
+ ? $this->driver->getConnection()->getResource()
+ : $this->driver;
+
+ if ($resource instanceof PgSqlConnection || is_resource($resource)) {
+ return '\'' . pg_escape_string($resource, $value) . '\'';
+ }
+
+ if ($resource instanceof PDO) {
+ return $resource->quote($value);
+ }
+
+ return null;
+ }
+
+ #[Override]
+ public function getSqlPlatformDecorator(): PlatformDecoratorInterface
+ {
+ return new SqlPlatformDecorator($this);
+ }
+}
diff --git a/stubs/Laminas/ServiceManager/Factory/AbstractFactoryInterface.stub b/stubs/Laminas/ServiceManager/Factory/AbstractFactoryInterface.stub
new file mode 100644
index 0000000..a129cb1
--- /dev/null
+++ b/stubs/Laminas/ServiceManager/Factory/AbstractFactoryInterface.stub
@@ -0,0 +1,19 @@
+|null $options
+ */
+ public function __invoke(
+ ContainerInterface $container,
+ string $name,
+ callable $callback,
+ ?array $options = null
+ ): mixed;
+}
diff --git a/stubs/Laminas/ServiceManager/Factory/FactoryInterface.stub b/stubs/Laminas/ServiceManager/Factory/FactoryInterface.stub
new file mode 100644
index 0000000..e1d6ac5
--- /dev/null
+++ b/stubs/Laminas/ServiceManager/Factory/FactoryInterface.stub
@@ -0,0 +1,21 @@
+|null $options
+ */
+ public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed;
+}
diff --git a/stubs/Laminas/ServiceManager/Factory/InvokableFactory.stub b/stubs/Laminas/ServiceManager/Factory/InvokableFactory.stub
new file mode 100644
index 0000000..47d7303
--- /dev/null
+++ b/stubs/Laminas/ServiceManager/Factory/InvokableFactory.stub
@@ -0,0 +1,22 @@
+|null $options
+ */
+ public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed
+ {
+ return null === $options ? new $requestedName() : new $requestedName($options);
+ }
+}
diff --git a/stubs/Laminas/ServiceManager/Initializer/InitializerInterface.stub b/stubs/Laminas/ServiceManager/Initializer/InitializerInterface.stub
new file mode 100644
index 0000000..345c9a3
--- /dev/null
+++ b/stubs/Laminas/ServiceManager/Initializer/InitializerInterface.stub
@@ -0,0 +1,19 @@
+markTestSkipped('pdo_pgsql integration tests are not enabled!');
+ }
+
+ $this->adapter = new Adapter([
+ 'driver' => 'pdo_pgsql',
+ 'database' => (string) getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_DATABASE'),
+ 'hostname' => (string) getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_HOSTNAME'),
+ 'username' => (string) getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_USERNAME'),
+ 'password' => (string) getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_PASSWORD'),
+ ]);
+
+ $this->hostname = (string) getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_HOSTNAME');
+ }
+}
diff --git a/test/integration/Adapter/Driver/Pdo/Postgresql/TableGatewayTest.php b/test/integration/Adapter/Driver/Pdo/Postgresql/TableGatewayTest.php
new file mode 100644
index 0000000..6d278a6
--- /dev/null
+++ b/test/integration/Adapter/Driver/Pdo/Postgresql/TableGatewayTest.php
@@ -0,0 +1,31 @@
+addFeature(new SequenceFeature('id', 'test_seq_id_seq'));
+
+ $tableGateway = new TableGateway($table, $this->getAdapter(), $featureSet);
+
+ $tableGateway->insert(['foo' => 'bar']);
+ self::assertSame(1, $tableGateway->getLastInsertValue());
+
+ $tableGateway->insert(['foo' => 'baz']);
+ self::assertSame(2, $tableGateway->getLastInsertValue());
+ }
+}
diff --git a/test/integration/Adapter/Platform/PostgresqlTest.php b/test/integration/Adapter/Platform/PostgresqlTest.php
new file mode 100644
index 0000000..7bec9ab
--- /dev/null
+++ b/test/integration/Adapter/Platform/PostgresqlTest.php
@@ -0,0 +1,88 @@
+ */
+ public array|\PDO $adapters = [];
+
+ #[Override]
+ protected function setUp(): void
+ {
+ if (! getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL')) {
+ $this->markTestSkipped(self::class . ' integration tests are not enabled!');
+ }
+ if (extension_loaded('pgsql')) {
+ $this->adapters['pgsql'] = pg_connect(
+ 'host=' . getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_HOSTNAME')
+ . ' dbname=' . getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_DATABASE')
+ . ' user=' . getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_USERNAME')
+ . ' password=' . getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_PASSWORD')
+ );
+ }
+ if (extension_loaded('pdo')) {
+ $this->adapters['pdo_pgsql'] = new \PDO(
+ 'pgsql:host=' . getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_HOSTNAME') . ';dbname='
+ . getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_DATABASE'),
+ getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_USERNAME'),
+ getenv('TESTS_LAMINAS_DB_ADAPTER_DRIVER_PGSQL_PASSWORD')
+ );
+ }
+ }
+
+ /**
+ * @return void
+ */
+ public function testQuoteValueWithPgsql()
+ {
+ if (
+ ! isset($this->adapters['pgsql'])
+ || (
+ ! $this->adapters['pgsql'] instanceof PgSqlConnection
+ && ! is_resource($this->adapters['pgsql'])
+ )
+ ) {
+ $this->markTestSkipped('Postgres (pgsql) not configured in unit test configuration file');
+ }
+ $pgsql = new Postgresql($this->adapters['pgsql']);
+ $value = $pgsql->quoteValue('value');
+ self::assertEquals('\'value\'', $value);
+
+ $pgsql = new Postgresql(new Pgsql\Pgsql(new Pgsql\Connection($this->adapters['pgsql'])));
+ $value = $pgsql->quoteValue('value');
+ self::assertEquals('\'value\'', $value);
+ }
+
+ /**
+ * @return void
+ */
+ public function testQuoteValueWithPdoPgsql()
+ {
+ if (! isset($this->adapters['pdo_pgsql']) || ! $this->adapters['pdo_pgsql'] instanceof \PDO) {
+ $this->markTestSkipped('Postgres (PDO_PGSQL) not configured in unit test configuration file');
+ }
+ $pgsql = new Postgresql($this->adapters['pdo_pgsql']);
+ $value = $pgsql->quoteValue('value');
+ self::assertEquals('\'value\'', $value);
+
+ $pgsql = new Postgresql(new Pdo\Pdo(new Pdo\Connection($this->adapters['pdo_pgsql'])));
+ $value = $pgsql->quoteValue('value');
+ self::assertEquals('\'value\'', $value);
+ }
+}
diff --git a/test/integration/FixtureLoader/FixtureLoaderInterface.php b/test/integration/FixtureLoader/FixtureLoaderInterface.php
new file mode 100644
index 0000000..a2cc82b
--- /dev/null
+++ b/test/integration/FixtureLoader/FixtureLoaderInterface.php
@@ -0,0 +1,12 @@
+connect();
+
+ $this->dropDatabase(); // closes connection
+
+ $this->connect();
+
+ if (
+ false === $this->pdo->exec(sprintf(
+ "CREATE DATABASE %s",
+ getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_DATABASE')
+ ))
+ ) {
+ throw new Exception(sprintf(
+ "I cannot create the PostgreSQL %s test database: %s",
+ getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_DATABASE'),
+ print_r($this->pdo->errorInfo(), true)
+ ));
+ }
+
+ // PostgreSQL cannot switch database on same connection.
+ $this->disconnect();
+
+ $this->connect(true);
+
+ if (false === $this->pdo->exec(file_get_contents($this->fixtureFile))) {
+ throw new Exception(sprintf(
+ "I cannot create the table for %s database. Check the %s file. %s ",
+ getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_DATABASE'),
+ $this->fixtureFile,
+ print_r($this->pdo->errorInfo(), true)
+ ));
+ }
+
+ $this->disconnect();
+ }
+
+ public function dropDatabase(): void
+ {
+ if (! $this->initialRun) {
+ // Not possible to drop in PostgreSQL.
+ // Connection is locking the database and trying to close it with unset()
+ // does not trigger garbage collector on time to actually close it to free the lock.
+ return;
+ }
+ $this->initialRun = false;
+
+ $this->connect();
+
+ $this->pdo->exec(sprintf(
+ "DROP DATABASE IF EXISTS %s",
+ getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_DATABASE')
+ ));
+
+ $this->disconnect();
+ }
+
+ /**
+ * @param bool $useDb add dbname using in dsn
+ */
+ protected function connect(bool $useDb = false): void
+ {
+ $dsn = 'pgsql:host=' . getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_HOSTNAME');
+
+ if ($useDb) {
+ $dsn .= ';dbname=' . getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_DATABASE');
+ }
+
+ $this->pdo = new PDO(
+ $dsn,
+ getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_USERNAME'),
+ getenv('TESTS_PHPDB_ADAPTER_DRIVER_PGSQL_PASSWORD')
+ );
+ }
+
+ protected function disconnect(): void
+ {
+ $this->pdo = null;
+ }
+}
diff --git a/test/integration/TestFixtures/pgsql.sql b/test/integration/TestFixtures/pgsql.sql
new file mode 100644
index 0000000..3fe43cc
--- /dev/null
+++ b/test/integration/TestFixtures/pgsql.sql
@@ -0,0 +1,29 @@
+DROP TABLE IF EXISTS test;
+CREATE TABLE IF NOT EXISTS test (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ value VARCHAR(255) NOT NULL
+);
+
+INSERT INTO test (name, value)
+VALUES
+('foo', 'bar'),
+('bar', 'baz');
+
+DROP TABLE IF EXISTS test_charset;
+CREATE TABLE IF NOT EXISTS test_charset (
+ id SERIAL PRIMARY KEY,
+ field$ VARCHAR(255) NOT NULL,
+ field_ VARCHAR(255) NOT NULL
+);
+
+INSERT INTO test_charset (field$, field_)
+VALUES
+('foo', 'bar'),
+('bar', 'baz');
+
+CREATE TABLE IF NOT EXISTS test_seq (
+ id SERIAL,
+ foo VARCHAR(255) NOT NULL,
+ CONSTRAINT test_seq_pkey PRIMARY KEY (id)
+);
diff --git a/test/unit/Adapter/AdapterInterfaceFactoryTest.php b/test/unit/Adapter/AdapterInterfaceFactoryTest.php
new file mode 100644
index 0000000..309cd68
--- /dev/null
+++ b/test/unit/Adapter/AdapterInterfaceFactoryTest.php
@@ -0,0 +1,116 @@
+getDependencies();
+ $config['services']['config'] = $dbConfig;
+
+ return new ServiceManager($config);
+ }
+
+ #[Override]
+ protected function setUp(): void
+ {
+ if (! extension_loaded('pdo_sqlite')) {
+ $this->markTestSkipped('Adapter factory tests require pdo_sqlite');
+ }
+
+ $this->factory = new AdapterInterfaceFactory();
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testV3FactoryReturnsDefaultAdapter(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $services = $this->createServiceManager([
+ 'db' => [
+ 'driver' => 'Pdo_Pgsql',
+ 'database' => ':memory:',
+ ],
+ ]);
+
+ $this->factory->__invoke($services, Adapter::class);
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testV3FactoryReturnsDefaultAdapterWithDefaultProfiler(): void
+ {
+ $services = $this->createServiceManager([
+ 'db' => [
+ 'driver' => 'Pdo_Pgsql',
+ 'database' => ':memory:',
+ 'profiler' => true,
+ ],
+ ]);
+
+ $adapter = $this->factory->__invoke($services, Adapter::class);
+ self::assertInstanceOf(ProfilerInterface::class, $adapter->getProfiler());
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testV3FactoryReturnsDefaultAdapterWithProfilerClassname(): void
+ {
+ $services = $this->createServiceManager([
+ 'db' => [
+ 'driver' => 'Pdo_Pgsql',
+ 'database' => ':memory:',
+ 'profiler' => Profiler::class,
+ ],
+ ]);
+
+ $adapter = $this->factory->__invoke($services, Adapter::class);
+ self::assertInstanceOf(ProfilerInterface::class, $adapter->getProfiler());
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testV3FactoryReturnsDefaultAdapterWithProfilerInstance(): void
+ {
+ $services = $this->createServiceManager([
+ 'db' => [
+ 'driver' => 'Pdo_Pgsql',
+ 'database' => ':memory:',
+ 'profiler' => $this->getMockBuilder(ProfilerInterface::class)->getMock(),
+ ],
+ ]);
+
+ $adapter = $this->factory->__invoke($services, Adapter::class);
+ self::assertInstanceOf(ProfilerInterface::class, $adapter->getProfiler());
+ }
+}
diff --git a/test/unit/Adapter/AdapterTest.php b/test/unit/Adapter/AdapterTest.php
new file mode 100644
index 0000000..0ee756b
--- /dev/null
+++ b/test/unit/Adapter/AdapterTest.php
@@ -0,0 +1,252 @@
+adapter->setProfiler(new Profiler\Profiler());
+ self::assertSame($this->adapter, $ret);
+ }
+
+ #[TestDox('unit test: Test getProfiler() will store profiler')]
+ public function testGetProfiler(): void
+ {
+ $this->adapter->setProfiler($profiler = new Profiler\Profiler());
+ self::assertSame($profiler, $this->adapter->getProfiler());
+
+ $adapter = new Adapter(
+ $this->mockDriver,
+ $this->mockPlatform,
+ $this->getMockBuilder(ResultSetInterface::class)->getMock(),
+ $profiler
+ );
+ self::assertInstanceOf(Profiler\Profiler::class, $adapter->getProfiler());
+ }
+
+ #[TestDox('unit test: Test getDriver() will return driver object')]
+ public function testGetDriver(): void
+ {
+ self::assertSame($this->mockDriver, $this->adapter->getDriver());
+ }
+
+ #[TestDox('unit test: Test getPlatform() returns platform object')]
+ public function testGetPlatform(): void
+ {
+ self::assertSame($this->mockPlatform, $this->adapter->getPlatform());
+ }
+
+ #[TestDox('unit test: Test getPlatform() returns platform object')]
+ public function testGetQueryResultSetPrototype(): void
+ {
+ self::assertInstanceOf(ResultSetInterface::class, $this->adapter->getQueryResultSetPrototype());
+ }
+
+ #[TestDox('unit test: Test getCurrentSchema() returns current schema from connection object')]
+ public function testGetCurrentSchema(): void
+ {
+ $this->mockConnection->expects($this->any())->method('getCurrentSchema')->willReturn('FooSchema');
+ self::assertEquals('FooSchema', $this->adapter->getCurrentSchema());
+ }
+
+ /**
+ * @throws \Exception
+ */
+ #[TestDox('unit test: Test query() in prepare mode produces a statement object')]
+ public function testQueryWhenPreparedProducesStatement(): void
+ {
+ $s = $this->adapter->query('SELECT foo');
+ self::assertSame($this->mockStatement, $s);
+ }
+
+ /**
+ * @throws Exception
+ * @throws \Exception
+ */
+ #[Group('#210')]
+ public function testProducedResultSetPrototypeIsDifferentForEachQuery(): void
+ {
+ $statement = $this->createMock(StatementInterface::class);
+ $result = $this->createMock(ResultInterface::class);
+
+ $this->mockDriver->method('createStatement')
+ ->willReturn($statement);
+ $this->mockStatement->method('execute')
+ ->willReturn($result);
+ $result->method('isQueryResult')
+ ->willReturn(true);
+
+ self::assertNotSame(
+ $this->adapter->query('SELECT foo', []),
+ $this->adapter->query('SELECT foo', [])
+ );
+ }
+
+ /**
+ * @throws \Exception
+ */
+ #[TestDox('unit test: Test query() in prepare mode, with array of parameters, produces a result object')]
+ public function testQueryWhenPreparedWithParameterArrayProducesResult(): void
+ {
+ $parray = ['bar' => 'foo'];
+ $sql = 'SELECT foo, :bar';
+ $statement = $this->getMockBuilder(StatementInterface::class)->getMock();
+ $result = $this->getMockBuilder(ResultInterface::class)->getMock();
+ $this->mockDriver->expects($this->any())->method('createStatement')
+ ->with($sql)->willReturn($statement);
+ $this->mockStatement->expects($this->any())->method('execute')->willReturn($result);
+
+ $r = $this->adapter->query($sql, $parray);
+ self::assertSame($result, $r);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ #[TestDox('unit test: Test query() in prepare mode, with ParameterContainer, produces a result object')]
+ public function testQueryWhenPreparedWithParameterContainerProducesResult(): void
+ {
+ $sql = 'SELECT foo';
+ $parameterContainer = $this->getMockBuilder(ParameterContainer::class)->getMock();
+ $result = $this->getMockBuilder(ResultInterface::class)->getMock();
+ $this->mockDriver->expects($this->any())->method('createStatement')
+ ->with($sql)->willReturn($this->mockStatement);
+ $this->mockStatement->expects($this->any())->method('execute')->willReturn($result);
+ $result->expects($this->any())->method('isQueryResult')->willReturn(true);
+
+ $r = $this->adapter->query($sql, $parameterContainer);
+ self::assertInstanceOf(ResultSet::class, $r);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ #[TestDox('unit test: Test query() in execute mode produces a driver result object')]
+ public function testQueryWhenExecutedProducesAResult(): void
+ {
+ $sql = 'SELECT foo';
+ $result = $this->getMockBuilder(ResultInterface::class)->getMock();
+ $this->mockConnection->expects($this->any())->method('execute')->with($sql)->willReturn($result);
+
+ $r = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE);
+ self::assertSame($result, $r);
+ }
+
+ /**
+ * @throws \Exception
+ */
+ #[TestDox('unit test: Test query() in execute mode produces a resultset object')]
+ public function testQueryWhenExecutedProducesAResultSetObjectWhenResultIsQuery(): void
+ {
+ $sql = 'SELECT foo';
+
+ $result = $this->getMockBuilder(ResultInterface::class)->getMock();
+ $this->mockConnection->expects($this->any())->method('execute')->with($sql)->willReturn($result);
+ $result->expects($this->any())->method('isQueryResult')->willReturn(true);
+
+ $r = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE);
+ self::assertInstanceOf(ResultSet::class, $r);
+
+ $r = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE, new TemporaryResultSet());
+ self::assertInstanceOf(TemporaryResultSet::class, $r);
+ }
+
+ #[TestDox('unit test: Test createStatement() produces a statement object')]
+ public function testCreateStatement(): void
+ {
+ self::assertSame($this->mockStatement, $this->adapter->createStatement());
+ }
+
+ #[TestDox('unit test: Test __get() magic method')]
+ public function testMagicGet(): void
+ {
+ self::assertSame($this->mockDriver, $this->adapter->driver);
+ /** @psalm-suppress UndefinedMagicPropertyFetch */
+ self::assertSame($this->mockDriver, $this->adapter->DrivER);
+ /** @psalm-suppress UndefinedMagicPropertyFetch */
+ self::assertSame($this->mockPlatform, $this->adapter->PlatForm);
+ self::assertSame($this->mockPlatform, $this->adapter->platform);
+
+ $this->expectException('InvalidArgumentException');
+ $this->expectExceptionMessage('Invalid magic');
+ $this->adapter->foo;
+ }
+
+ /**
+ * @throws Exception
+ */
+ #[Override]
+ protected function setUp(): void
+ {
+ $this->mockConnection = $this->createMock(ConnectionInterface::class);
+ $this->mockPlatform = new PgsqlPlatform();
+ $this->mockStatement = $this->getMockBuilder(Statement::class)->getMock();
+ $this->mockDriver = $this->getMockBuilder(Pdo::class)
+ ->setConstructorArgs([
+ $this->mockConnection,
+ $this->mockStatement,
+ ])
+ ->getMock();
+
+ $this->mockResultSet = $this->getMockBuilder(ResultSetInterface::class)->getMock();
+
+ $this->mockDriver->method('getDatabasePlatformName')->willReturn('Pgsql');
+ $this->mockDriver->method('checkEnvironment')->willReturn(true);
+ $this->mockDriver->method('getConnection')->willReturn($this->mockConnection);
+ //$this->mockDriver->method('createStatement')->willReturn($this->mockStatement);
+
+ $this->adapter = new Adapter(
+ $this->mockDriver,
+ $this->mockPlatform,
+ $this->mockResultSet
+ );
+ }
+}
diff --git a/test/unit/Adapter/ConfigProviderTest.php b/test/unit/Adapter/ConfigProviderTest.php
new file mode 100644
index 0000000..4132144
--- /dev/null
+++ b/test/unit/Adapter/ConfigProviderTest.php
@@ -0,0 +1,52 @@
+> */
+ private array $config = [
+ 'aliases' => [
+ PlatformInterface::class => Platform\Postgresql::class,
+ ProfilerInterface::class => Profiler::class,
+ ],
+ 'factories' => [
+ AdapterInterface::class => AdapterServiceFactory::class,
+ DriverInterface::class => Driver\Pdo\DriverFactory::class,
+ Platform\Postgresql::class => InvokableFactory::class,
+ Profiler::class => InvokableFactory::class,
+ ],
+ ];
+
+ public function testProvidesExpectedConfiguration(): ConfigProvider
+ {
+ $provider = new ConfigProvider();
+ self::assertEquals($this->config, $provider->getDependencies());
+
+ return $provider;
+ }
+
+ #[Depends('testProvidesExpectedConfiguration')]
+ public function testInvocationProvidesDependencyConfiguration(ConfigProvider $provider): void
+ {
+ self::assertEquals(['dependencies' => $provider->getDependencies()], $provider());
+ }
+}