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()); + } +}