diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9e0a135 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + name: PG ${{ matrix.pg }} / PHP ${{ matrix.php }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + # Every supported PostgreSQL against the distro PHP... + pg: [11, 12, 13, 14, 15, 16, 17, 18] + php: ["8.3"] + # ...plus the other supported PHP versions against the newest PG. + include: + - { pg: 18, php: "8.1" } + - { pg: 18, php: "8.2" } + - { pg: 18, php: "8.4" } + + steps: + - uses: actions/checkout@v4 + + - name: Add the PGDG apt repository + run: | + sudo install -d /usr/share/postgresql-common/pgdg + sudo curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc + echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + + - name: Add the ondrej/php PPA (non-distro PHP versions) + if: matrix.php != '8.3' + run: sudo add-apt-repository -y ppa:ondrej/php + + - name: Install PostgreSQL ${{ matrix.pg }} and PHP ${{ matrix.php }} + run: | + sudo apt-get update + sudo apt-get install -y \ + postgresql-${{ matrix.pg }} postgresql-server-dev-${{ matrix.pg }} \ + php${{ matrix.php }}-dev libphp${{ matrix.php }}-embed php${{ matrix.php }}-xml + php-config${{ matrix.php }} --php-sapis | grep -q embed + + - name: Build and install PL/php + run: | + make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} + sudo make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} install + + - name: Build and install jsonb_plphp + run: | + make -C jsonb_plphp \ + PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} + sudo make -C jsonb_plphp \ + PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} install + + - name: Start the cluster + run: | + # the runner image disables automatic cluster creation + sudo pg_createcluster ${{ matrix.pg }} main 2>/dev/null || true + sudo pg_ctlcluster ${{ matrix.pg }} main start + pg_lsclusters + + - name: Run the regression tests + run: | + port=$(pg_lsclusters | awk '$1 == "${{ matrix.pg }}" && $2 == "main" { print $3 }') + # let the postgres user reach and write the workspace + sudo chmod a+x /home/runner /home/runner/work /home/runner/work/PL-php + chmod -R a+rwX . + sudo -u postgres env PGPORT=$port \ + make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck + + - name: Run the jsonb_plphp regression tests + run: | + port=$(pg_lsclusters | awk '$1 == "${{ matrix.pg }}" && $2 == "main" { print $3 }') + sudo -u postgres env PGPORT=$port \ + make -C jsonb_plphp \ + PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck + + - name: Show regression diffs on failure + if: failure() + run: | + cat regression.diffs 2>/dev/null || true + cat jsonb_plphp/regression.diffs 2>/dev/null || true diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc7d5c..6c72670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,24 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). ### Added +- **Continuous integration** — a GitHub Actions matrix building and running + the full regression suite (core + `jsonb_plphp`) on PostgreSQL 11-18 and + PHP 8.1/8.2/8.3/8.4 for every pull request. - **VARIADIC parameters.** A variadic function's collected arguments arrive as a single PHP array, matching every other PL (`VARIADIC "any"` remains unsupported and is rejected with a clear error). Previously any VARIADIC declaration failed. +### Fixed + +- **PHP 8.1, 8.2, and 8.4 compatibility.** PL/php now builds and passes the + full suite on PHP 8.1 through 8.4: a version guard for the + `php_module_startup` signature change (8.2), explicit `fgetcsv` arguments + in the cookbook (8.4 deprecation), and a clear compile error below 8.1. + The Makefiles now prefer the `libphp` matching `PHP_CONFIG`'s version when + several are installed (see INSTALL for a packaging caveat about shared + SONAMEs). + ## [2.2.0] — 2026-07-05 ### Added diff --git a/INSTALL b/INSTALL index 7848e4f..06565ab 100644 --- a/INSTALL +++ b/INSTALL @@ -41,6 +41,12 @@ from PATH. Override them if needed: make PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config PHP_CONFIG=/usr/bin/php-config sudo make PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config install +Note: if several libphpX.Y-embed versions are installed side by side, some +packagings (e.g. deb.sury.org) give them all the same SONAME (libphp.so), so +the backend loads whatever /usr/lib/libphp.so points at -- regardless of the +version linked at build time. Keep a single embed package installed, or +point that symlink at the intended version. + This installs the shared library plphp.so plus the extension control and SQL files into the directories reported by pg_config. diff --git a/Makefile b/Makefile index 7c414b8..17c2122 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,13 @@ DATA = plphp--2.2.sql plphp--2.0--2.1.sql plphp--2.1--2.2.sql PHP_CONFIG ?= php-config PHP_INCLUDES := $(shell $(PHP_CONFIG) --includes) PHP_LIBDIR := $(shell $(PHP_CONFIG) --prefix)/lib -PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) +# Prefer the library matching PHP_CONFIG's version (e.g. libphp8.3.so); with +# several PHP versions installed, a bare "libphp*.so" glob would pick the +# unversioned symlink of whichever version owns it. +PHP_VERSION := $(shell $(PHP_CONFIG) --version | cut -d. -f1-2) +PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard \ + $(PHP_LIBDIR)/libphp$(PHP_VERSION).so /usr/lib/libphp$(PHP_VERSION).so /usr/lib/*/libphp$(PHP_VERSION).so \ + $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) PG_CPPFLAGS = $(PHP_INCLUDES) # Link against the PHP embed library only; its own transitive dependencies diff --git a/README.md b/README.md index 4ed4a16..6e29c2c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Write PostgreSQL functions, triggers, and procedures in PHP.** [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-11_to_18-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/) -[![PHP](https://img.shields.io/badge/PHP-8.x-777BB4?logo=php&logoColor=white)](https://www.php.net/) +[![PHP](https://img.shields.io/badge/PHP-8.1--8.4-777BB4?logo=php&logoColor=white)](https://www.php.net/) [![Tests](https://img.shields.io/badge/tests-17_passing-brightgreen)](sql/) [![License](https://img.shields.io/badge/license-permissive-blue)](#license) @@ -99,7 +99,7 @@ $$; - **PostgreSQL 11 or newer** (tested on 11–18; 18 recommended), with the server development files that provide `pg_config`. -- **PHP 8.x** built with the **embed SAPI** and **without** ZTS (thread safety). +- **PHP 8.1 through 8.4** built with the **embed SAPI** and **without** ZTS (thread safety). On Debian/Ubuntu, install `php8.x-dev` and `libphp8.x-embed`. ## Installation diff --git a/doc/cookbook.md b/doc/cookbook.md index 3f472e0..07e94e9 100644 --- a/doc/cookbook.md +++ b/doc/cookbook.md @@ -216,7 +216,7 @@ LANGUAGE plphp STRICT AS $$ $fh = fopen($args[0], "r"); if ($fh === false) pg_raise('error', "cannot open {$args[0]}"); - while (($rec = fgetcsv($fh)) !== false) + while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false) return_next($rec); fclose($fh); return; diff --git a/doc/plphp.md b/doc/plphp.md index 7a5d51f..a6acb90 100644 --- a/doc/plphp.md +++ b/doc/plphp.md @@ -4,8 +4,8 @@ PL/php lets you write PostgreSQL functions and triggers in PHP. This document describes the programming interface. For build and install instructions see [`INSTALL`](../INSTALL); for a feature summary see [`README`](../README.md). -Tested on **PostgreSQL 11 through 18** with **PHP 8.3** (embed SAPI, -non-thread-safe). +Tested on **PostgreSQL 11 through 18** with **PHP 8.1 through 8.4** (embed +SAPI, non-thread-safe). - [Enabling the language](#enabling-the-language) - [Writing functions](#writing-functions) diff --git a/expected/cookbook.out b/expected/cookbook.out index 421597d..b43f0b8 100644 --- a/expected/cookbook.out +++ b/expected/cookbook.out @@ -216,7 +216,7 @@ LANGUAGE plphp STRICT AS $$ $fh = fopen($args[0], "r"); if ($fh === false) pg_raise('error', "cannot open {$args[0]}"); - while (($rec = fgetcsv($fh)) !== false) + while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false) return_next($rec); fclose($fh); return; diff --git a/jsonb_plphp/Makefile b/jsonb_plphp/Makefile index d783b78..4192c0a 100644 --- a/jsonb_plphp/Makefile +++ b/jsonb_plphp/Makefile @@ -16,7 +16,13 @@ DATA = jsonb_plphp--1.0.sql PHP_CONFIG ?= php-config PHP_INCLUDES := $(shell $(PHP_CONFIG) --includes) PHP_LIBDIR := $(shell $(PHP_CONFIG) --prefix)/lib -PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) +# Prefer the library matching PHP_CONFIG's version (e.g. libphp8.3.so); with +# several PHP versions installed, a bare "libphp*.so" glob would pick the +# unversioned symlink of whichever version owns it. +PHP_VERSION := $(shell $(PHP_CONFIG) --version | cut -d. -f1-2) +PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard \ + $(PHP_LIBDIR)/libphp$(PHP_VERSION).so /usr/lib/libphp$(PHP_VERSION).so /usr/lib/*/libphp$(PHP_VERSION).so \ + $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) PG_CPPFLAGS = $(PHP_INCLUDES) SHLIB_LINK = -L$(PHP_LIBDIR) -l$(PHP_LIBNAME) $(shell $(PHP_CONFIG) --ldflags) diff --git a/plphp.c b/plphp.c index 010d04c..c74eb30 100644 --- a/plphp.c +++ b/plphp.c @@ -76,6 +76,10 @@ /* PHP stuff */ #include "php.h" +#if PHP_VERSION_ID < 80100 +#error "PL/php requires PHP 8.1 or newer (zend_error_cb signature)" +#endif + #include "php_variables.h" #include "php_globals.h" #include "zend_hash.h" @@ -413,9 +417,11 @@ plphp_init_all(void) plphp_init(); /* - * Any other initialization that must be done each time a new - * backend starts -- currently none. + * Loaded PHP extensions can swap zend_error_cb behind our back at any + * point; put ours back before running anything. It is a plain pointer + * assignment, so doing it on every call is free. */ + zend_error_cb = plphp_error_cb; } /* @@ -450,8 +456,14 @@ plphp_init(void) plphp_sapi_module.phpinfo_as_text = 1; sapi_startup(&plphp_sapi_module); +#if PHP_VERSION_ID >= 80200 if (php_module_startup(&plphp_sapi_module, NULL) == FAILURE) elog(ERROR, "php_module_startup call failed"); +#else + /* PHP 8.1 still took an additional-modules count */ + if (php_module_startup(&plphp_sapi_module, NULL, 0) == FAILURE) + elog(ERROR, "php_module_startup call failed"); +#endif /* php_module_startup changed it, so put it back */ zend_error_cb = plphp_error_cb; @@ -476,6 +488,22 @@ plphp_init(void) INI_HARDCODED("max_execution_time", "0"); INI_HARDCODED("max_input_time", "-1"); + /* + * The host's php.ini must not decide which errors reach our + * zend_error_cb, or how they are routed: force full reporting + * and disable opcache/JIT, which add nothing for our one-shot + * eval'd functions and can interfere with error delivery. + * (GitHub's runner images, for example, ship a PHP configured + * with opcache+JIT enabled for CLI, which broke both notices + * and error messages.) + */ + INI_HARDCODED("error_reporting", "32767"); /* E_ALL */ + INI_HARDCODED("display_errors", "0"); + INI_HARDCODED("log_errors", "0"); + INI_HARDCODED("opcache.enable", "0"); + INI_HARDCODED("opcache.enable_cli", "0"); + INI_HARDCODED("xdebug.mode", "off"); + /* * Set memory limit to ridiculously high value. This helps the * server not to crash, because the PHP allocator has the really @@ -504,6 +532,13 @@ plphp_init(void) PG(during_request_startup) = true; + /* + * Extensions loaded from the host's php.ini (xdebug, for one) + * may have hooked zend_error_cb during module or request + * startup; ours must be the one in place. + */ + zend_error_cb = plphp_error_cb; + /* * Register our SPI functions and the procedure cache now that the * request (and its interned-string tables) is fully initialized. diff --git a/sql/cookbook.sql b/sql/cookbook.sql index f13ced7..899126d 100644 --- a/sql/cookbook.sql +++ b/sql/cookbook.sql @@ -156,7 +156,7 @@ LANGUAGE plphp STRICT AS $$ $fh = fopen($args[0], "r"); if ($fh === false) pg_raise('error', "cannot open {$args[0]}"); - while (($rec = fgetcsv($fh)) !== false) + while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false) return_next($rec); fclose($fh); return;