PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 304 · Первое подключение PostgreSQL → полезные результаты
PDO_PGSQL: подключение, собственная заявка и ограниченный отчёт
Полный вводный контракт PDO_PGSQL/DSN/Pdo\Pgsql и три самостоятельных проекта PostgreSQL с реальными файлами результата.
PostgreSQL для приложения: каталог, заявка и ведомость
Небольшому складу нужны три вещи: увидеть доступные товары, принять заявку с собственным номером и отдать ведомость за выбранные дни. PostgreSQL хранит строки, а PHP выбирает данные и делает понятный файл для человека. PDO_PGSQL связывает эти две стороны через клиентскую библиотеку libpq. Начнём с двух товаров, затем добавим запись и управляемое чтение результата.
| Что хочет получить человек | Самостоятельный проект | Полезный файл |
|---|---|---|
| Доступные кабель и ленту из нужной базы | Паспорт каталога | connection.json |
| Номер принятой заявки, её текст и точную сумму | Квитанция заявки | request.csv |
| Ведомость 8–9 ноября: 3 строки, 10 единиц | Ограниченный отчёт | report.csv |
- products: sku=CAB, title=Кабель, qty=7; sku=TAP, title=Лента, qty=4
- SQL выбирает sku,title,qty; PHP передаёт category=Монтаж отдельно
- PostgreSQL возвращает две строки → PDOStatement читает их в PHP
- Программа создаёт connection.json; таблица остаётся источником, JSON — снимком выбранных данных
Таблица — набор строк с заданными столбцами. Запрос SELECT просит только перечисленные поля; результат запроса не равен всей базе. Переменная $db хранит подключение, $statement — команду и её результат. Стрелка -> вызывает метод этого объекта, скобки передают аргументы, знак = сохраняет результат справа в переменную слева. Основы переменных и SQL/PDO понадобятся для расширения примера; текущую операцию можно понять по следующему короткому шагу.
$statement = $db->query('SELECT sku, title, qty FROM products ORDER BY id LIMIT 2');
$row = $statement->fetch(PDO::FETCH_ASSOC);
if (is_array($row)) {
echo $row['title'], ': ', $row['qty'];
}
$statement->closeCursor();
// Для первой строки модели: Кабель: 7Здесь подключение $db и таблица products уже подготовлены. query отправляет SQL, fetch читает одну строку, FETCH_ASSOC даёт ключи с именами столбцов. is_array защищает обращение к строке, если выборка пуста. echo выводит название, двоеточие и количество. closeCursor завершает работу с текущим результатом. В трёх полных проектах ниже подготовка данных включена в ZIP, каждое обращение идёт к собственной случайно названной таблице и после наблюдений убирается.
Это драйвер PDO, а не набор функций pg_connect/pg_query: общий объект PDO и расширение pgsql имеют разные интерфейсы. Наличие одного расширения не доказывает загрузку другого. Проверенный профиль новых проектов — PHP 8.5.11, PDO_PGSQL той же версии, libpq 16.15 и PostgreSQL 18.6. Это фиксированная конфигурация выполнения, а не утверждение о последних доступных версиях.
Подготовить PHP, libpq и собственную учебную базу
Для запуска нужны четыре слоя: PHP, поддержка PDO_PGSQL в нём, библиотека libpq и доступный сервер PostgreSQL. libpq может быть другой версии, чем сервер: это клиентский компонент PHP. Начиная с PHP 8.4 драйвер требует libpq не ниже 10.0. Для lazy fetching в этих проектах нужен PHP 8.5; обычные операции PDO_PGSQL существовали раньше.
| Слой | Что проверить | Что это ещё не доказывает |
|---|---|---|
| PHP и выбранный php.ini | php --ini; php --ri pdo_pgsql | Загруженный модуль не означает доступный сервер. CLI и веб-SAPI могут читать разные ini. |
| Драйвер PDO | PDO::getAvailableDrivers() содержит pgsql | Это имя драйвера, не имя базы и не подтверждение пароля. |
| libpq | getAttribute(PDO::ATTR_CLIENT_VERSION) у соединения | Клиентская версия не равна ATTR_SERVER_VERSION. |
| База и роль | current_database(), current_user и нужный SELECT | Вход не даёт разрешения читать произвольные таблицы или создавать чужие объекты. |
Включить расширение в своём PHP
В Windows возьмите согласованный комплект PHP для своей архитектуры и режима TS/NTS; включите его php_pdo_pgsql.dll и используйте совместимые DLL libpq/зависимостей из поставки. Не подменяйте случайной libpq.dll файл другой программы. extension_dir указывает на каталог расширений именно этого PHP. Для проекта достаточно PDO_PGSQL; отдельное extension=pgsql не требуется.
extension_dir="ext"
extension=pdo_pgsqlВ Linux установите PDO_PGSQL для выбранной версии PHP из доверенного пакета своего дистрибутива либо соберите его вместе с PHP и заголовками libpq. Официальный ключ configure принимает каталог PostgreSQL или путь к pg_config. Следующий фрагмент относится к сборке исходников PHP и отдельному prefix; он не является командой для уже готового Windows PHP.
./configure --prefix="$LAB_PHP" --enable-cli --enable-pdo \
--with-pdo-pgsql=/usr/bin/pg_config
make -j2
make installУкажите существующий pg_config своей установки и собственный каталог LAB_PHP. Сборке также нужны обычные инструменты и зависимости выбранной конфигурации PHP. Если расширение собрано динамически, загрузите его в собственном ini после PDO; статически включённый модуль повторно не подключают. Исходник 8.5.11 отдельно проверяет наличие функции PQresultMemorySize в libpq: константа размера результата зависит от этой возможности, а не только от номера PostgreSQL-сервера.
Создать отдельный локальный стенд
Ниже предполагается, что официальные PostgreSQL bin уже установлены, а порт 55180 свободен. Новый каталог данных принадлежит только этому стенду. initdb создаёт кластер — собственный набор баз сервера; это не подключение к существующей рабочей базе. Команды выполняет владелец каталога, в Unix не root. initdb спрашивает пароль администратора интерактивно; не записывайте настоящий пароль в текст урока.
# PowerShell: замените только расположение официальных bin и своей папки.
$PgBin = 'C:\PostgreSQL\18\bin'
$LabRoot = Join-Path $PWD 'pgsql180-lab'
New-Item -ItemType Directory -Path $LabRoot -ErrorAction Stop
$LabData = Join-Path $LabRoot 'data'
& "$PgBin\initdb.exe" -D $LabData --username=postgres --pwprompt --encoding=UTF8 --auth=scram-sha-256
& "$PgBin\pg_ctl.exe" -D $LabData -l (Join-Path $LabRoot 'server.log') -o '-h 127.0.0.1 -p 55180' -w start
& "$PgBin\psql.exe" -h 127.0.0.1 -p 55180 -U postgres -d postgres -WВ sh те же программы вызываются без .exe; здесь PGBIN задаёт каталог установленного PostgreSQL, а LAB_ROOT — новую собственную папку. Для каждого варианта проверьте завершение команды прежде чем переходить к следующей.
PGBIN=/usr/lib/postgresql/18/bin
LAB_ROOT="$PWD/pgsql180-lab"
mkdir -m 700 "$LAB_ROOT"
"$PGBIN/initdb" -D "$LAB_ROOT/data" --username=postgres --pwprompt --encoding=UTF8 --auth=scram-sha-256
"$PGBIN/pg_ctl" -D "$LAB_ROOT/data" -l "$LAB_ROOT/server.log" -o '-h 127.0.0.1 -p 55180' -w start
"$PGBIN/psql" -h 127.0.0.1 -p 55180 -U postgres -d postgres -WВ открытом psql создайте роль приложения без прав администратора. Команда \password спрашивает новый пароль этой роли; это команда psql, а не SQL. CREATE DATABASE выполняется вне BEGIN. Владелец своей базы сможет создавать учебные таблицы в её public; рабочему приложению обычно дают более узкие права, чем учебному создателю схемы.
CREATE ROLE phpmanual180 LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE;
\password phpmanual180
CREATE DATABASE phpmanual180 OWNER phpmanual180 ENCODING 'UTF8' TEMPLATE template0;
\qВ распакованной папке проекта создайте приватный lab-config.json. Имя роли/базы и порт соответствуют отдельному стенду; password замените своим значением. Этот файл не включён в скачиваемый архив и не должен быть доступен через публичный веб-каталог.
{
"schema": "aen-pdo-pgsql180-private-lab",
"private_lab": true,
"host": "127.0.0.1",
"port": 55180,
"database": "phpmanual180",
"authentication": "scram",
"sslmode": "disable",
"user": "phpmanual180",
"password": "ЗАМЕНИТЕ_СВОИМ_ПАРОЛЕМ"
}# PowerShell, из папки распакованного примера:
$env:PHPMAN_PGSQL_LAB = 'pdo-pgsql180'
$env:PHPMAN_PGSQL_CONFIG = (Resolve-Path .\lab-config.json).Path
php example.php# sh, из папки распакованного примера:
chmod 600 lab-config.json
export PHPMAN_PGSQL_LAB=pdo-pgsql180
export PHPMAN_PGSQL_CONFIG="$PWD/lab-config.json"
php example.phpВ Windows ограничьте чтение файла своей учётной записью через права доступа. README внутри ZIP повторяет контракт профиля. lab.php принимает только собственный локальный стенд, создаёт случайные имена таблиц и удаляет только свой реестр. Внешнее приложение получает обычный DSN и свои права, а не отключает эту защиту в учебном helper. sslmode=disable выбран исключительно для изолированного loopback-стенда; для удалённой БД настройте проверяемый TLS. После занятий остановите только свой сервер: pg_ctl -D «свой каталог data» -m fast -w stop; это отменяет ещё открытые транзакции, поэтому сначала завершите проекты.
Установка PDO_PGSQL · Проверки libpq при сборке PHP 8.5.11 · initdb · pg_ctl · CREATE ROLE · CREATE DATABASE
DSN: адрес, база, роль, socket и TLS
DSN описывает, какой драйвер вызвать и куда подключиться. В PDO_PGSQL начало pgsql: выбирает драйвер; последующие пары ключ=значение описывают PostgreSQL-соединение. Параметры можно разделять пробелом или точкой с запятой. Логин и пароль удобнее передавать отдельными аргументами из приватной конфигурации, чтобы их не оказалось в напечатанной строке маршрута.
$dsn = 'pgsql:host=127.0.0.1;port=55180;dbname=phpmanual180;sslmode=disable';
$db = PDO::connect($dsn, $config['user'], $config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
$statement = $db->query('SELECT current_database()');
echo $statement->fetchColumn(); // phpmanual180
$statement->closeCursor();PDO::connect с PHP 8.4 создаёт подкласс драйвера: здесь Pdo\Pgsql. Первый аргумент задаёт маршрут, второй и третий — имя роли и секрет, четвёртый массив — настройки PDO. Двоеточие после pgsql относится к DSN; оно не связано с :category внутри SQL. Настройка ATTR_TIMEOUT в этом конструкторе ограничивает подключение, а ERRMODE_EXCEPTION превращает сообщённый отказ PDO в исключение. Вход может завершиться PDOException даже независимо от выбранного режима ошибок.
| Элемент официального DSN | Назначение и граница |
|---|---|
| pgsql: | Обязательный префикс PostgreSQL PDO. Строка URI postgresql:// не заменяет его сама по себе. |
| host | Имя/IP сервера; Unix-путь каталога socket, например /tmp, выбирает Unix domain socket. Если host не задан, действуют правила libpq/окружения; для воспроизводимого маршрута укажите его явно. |
| port | Порт сервера. 5432 — обычный PostgreSQL default, 55180 — выбранный порт отдельного учебного стенда. Для socket он участвует в имени файла. |
| dbname | Имя выбранной базы, а не имя таблицы. В одном сервере может быть несколько баз. |
| user | Роль для входа. С PHP 8.4 значение DSN имеет приоритет над username конструктора; до8.4 было наоборот. |
| password | Пароль роли. С PHP 8.4 значение DSN имеет приоритет над password конструктора; до8.4 аргумент конструктора побеждал. Не размещайте настоящий секрет в URL, журнале или downloadableфайле. |
| sslmode | Политика TLS: disable, allow, prefer, require, verify-ca, verify-full. Наличие параметра не создаёт сертификат или SSL-поддержку сервера. |
PDO заменяет все точки с запятой в DSN пробелами перед libpq. Поэтому точка с запятой внутри компонента DSN, включая dbname или password, не поддерживается: кавычки не отменяют эту замену. Не конкатенируйте DSN из произвольного ввода. Пробелы, обратные слэши и кавычки в дополнительных libpq-значениях требуют правил conninfo. Отдельные аргументы username/password проходят специальное экранирование драйвера; не дублируйте их одновременно в DSN без понятной политики приоритета.
TCP, socket и проверяемый TLS
pgsql:host=127.0.0.1;port=55180;dbname=phpmanual180;sslmode=disable
pgsql:host=/tmp;port=5432;dbname=warehouse
pgsql:host=db.example.test;port=5432;dbname=warehouse;sslmode=verify-full;sslrootcert=/etc/myapp/root.crtПервый профиль реально используется проектами этой главы. Во втором /tmp — каталог Unix socket, соответствующий серверу; это не путь к файлу базы. Третий — контекстный профиль удалённого сервера с доверенным корневым сертификатом. Пути/имя заменяют своими, а сокетный и TLS-варианты здесь не выдаются за выполненные Windows-подключения.
| sslmode | Что выбирает клиент |
|---|---|
| disable | Соединение без TLS; применено только в локальном стенде. |
| allow | Сначала без TLS, при необходимости пробует TLS. Не гарантирует шифрование. |
| prefer | Сначала TLS, возможен переход без TLS; типичное libpq-умолчание при SSL-поддержке. |
| require | Требует шифрование, но не задаёт полноценную проверку имени сервера. Совместимое поведение при найденном rootCA не заменяет явный verify-ca/full. |
| verify-ca | Требует TLS и доверенную цепочку сертификата. |
| verify-full | Дополнительно сверяет имя сервера с сертификатом; подходящий профиль для проверяемого удалённого адресата. |
Метод аутентификации SCRAM проверяет учётные данные и не подменяет шифрование канала. Настройки pg_hba.conf, права роли и TLS — разные уровни. У socket TLS-параметры не дают сетевого TLS-соединения. Конкретные возможности зависят от сборки libpq; за пределами описанных режимов следуйте документации установленного клиента.
Дополнительные параметры libpq и бюджеты
| Настройка | Как применять |
|---|---|
| application_name | Подписать своё приложение для серверной диагностики. Значение задаётся доверенной конфигурацией, не является логином. |
| client_encoding | Выбрать клиентскую кодировку, например UTF8. Это не тип столбца и не преобразование неверно закодированного PHP-файла. |
| options | Передать серверные параметры вида -c name=value при входе. Это конфигурация приложения, не произвольная строка пользователя. |
| service и окружение PGSERVICEFILE | Имя профиля libpq; PGSERVICEFILE задаёт файл через окружение, а не является полем PDO DSN. Наличие имени не означает доступность файла у другого PHP-SAPI. |
| sslrootcert, sslcert, sslkey | Файлы доверия и при необходимости клиентского сертификата; private key должен иметь ограниченный доступ. |
| connect_timeout / PDO::ATTR_TIMEOUT | libpq измеряет ожидание подключения в секундах. В PHP8.5.11 драйвер дописывает connect_timeout из ATTR_TIMEOUT конструктора, default30; одно только connect_timeout в DSN может быть переопределено этой дописанной настройкой. |
| statement_timeout / lock_timeout PostgreSQL | Серверные пределы длительности команды и ожидания блокировки. Это не общий PDO::ATTR_TIMEOUT и не время всего PHP/HTTP-запроса. Учитывайте единицы и транзакционное состояние после отказа. |
Шесть именованных полей выше составляют baseline страницы PHP DSN. Дополнительные ключи — возможности libpq, а не обещание поддержки любой настройки любой версией. При нескольких адресах connect_timeout применяется к попытке подключения к каждому host; это не универсальный deadline всей задачи. Драйверские атрибуты SQLSRV/DBLIB/ODBC в PostgreSQL не переносят.
PHP: полный DSN-контракт · libpq: ключи соединения · libpq: TLS и проверка сертификата · PHP8.5.11: сборка conninfo
Pdo\Pgsql: возможности класса, атрибуты и миграция имён
Pdo\Pgsql — подкласс PDO, доступный с PHP 8.4. Он сохраняет общие методы prepare, query, beginTransaction и добавляет операции PostgreSQL. Объект создаётся через new Pdo\Pgsql(...) или PDO::connect(...); обычный new PDO(...) остаётся базовым PDO. Это различие важно при переходе от старых pgsql-методов к современным именам.
$db = PDO::connect($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
if (!$db instanceof Pdo\Pgsql) {
throw new RuntimeException('Этот проект использует именно PDO_PGSQL.');
}
$pid = $db->getPid();
$column = $db->escapeIdentifier('title'); // "title"instanceof проверяет класс объекта. getPid() не принимает аргументов и возвращает int — PID процесса на сервере базы. escapeIdentifier(string $input): string возвращает процитированное имя столбца/таблицы/функции с сохранением регистра и специальных символов. Цитирование не даёт полномочий на произвольное имя, поэтому в проекте квитанции ему предшествует allowlist. Отдельные части составного public.products цитируют отдельно; строка public.products целиком означала бы одно имя с точкой.
Собственные константы и относящиеся к драйверу атрибуты
| Имя | Область и значение | Граница |
|---|---|---|
| Pdo\Pgsql::ATTR_DISABLE_PREPARES | bool-настройка соединения или опция prepare; исходное значение false. true передаёт SQL и параметры одной операцией без создания отдельного именованного prepared statement. | Работает отдельно от PDO::ATTR_EMULATE_PREPARES; при эмуляции другой механизм. Настройка объяснена ниже, факт скорости не обещается. |
| Pdo\Pgsql::ATTR_RESULT_MEMORY_SIZE | getAttribute у PDOStatement: число байт PGresult; документированный результат до выполнения — null. | Константа зависит от наличия PQresultMemorySize при сборке (libpq12+). В исходнике8.5.11 доexecute дополнительно устанавливается HY000-диагностика. В проектах измерение только после execute/успешногоfetch. |
| PDO::ATTR_PREFETCH | С PHP8.5 значение0 включает lazy fetching; любое ненулевое значение оставляет buffered. Устанавливается на соединении или через options prepare. | Это выбор режима, а не обещание пакета ровно Nстрок. По умолчанию результат buffered. Не путать с PDO::FETCH_LAZY — формой строки PDORow. |
| PDO::ATTR_EMULATE_PREPARES | false использует параметры сервера, true — подстановку, выполняемую PDO. Оба режима принимают bindValue/execute. | Настройка процесса подготовки не превращает имя таблицы в значение параметра. |
| PDO::ATTR_TIMEOUT | Опция конструктора задаёт connect_timeout в секундах; default30 в исходнике8.5.11. | Не является ограничением SELECT; для SQL существуют серверные statement_timeout/lock_timeout. |
| PDO::ATTR_DRIVER_NAME / CLIENT_VERSION / SERVER_VERSION | pgsql, версия загруженного libpq, версия сервера. | Наследование остальных PDO-констант не гарантирует поддержку каждого getter/setter конкретным драйвером. |
Драйверские константы определены только при загруженном расширении. Если общая библиотека работает с несколькими драйверами, сначала определите ATTR_DRIVER_NAME. Не отправляйте PGSQL-атрибут в PDO_SQLSRV или PDO_ODBC по совпадению целочисленного значения. ERRMODE_EXCEPTION и FETCH_ASSOC в проектах заданы явно, а runtime сохраняет фактическую конфигурацию.
| Историческая константа класса и старое имя PDO | Статус |
|---|---|
| TRANSACTION_IDLE / PDO::PGSQL_TRANSACTION_IDLE | Не имеет действия в PDO; обе формы устарели с PHP8.5. Не задаёт режим изоляции и не является рабочим способом получить состояние транзакции. |
| TRANSACTION_ACTIVE / PDO::PGSQL_TRANSACTION_ACTIVE | Не имеет действия в PDO; обе формы устарели с PHP8.5. Не задаёт режим изоляции и не является рабочим способом получить состояние транзакции. |
| TRANSACTION_INTRANS / PDO::PGSQL_TRANSACTION_INTRANS | Не имеет действия в PDO; обе формы устарели с PHP8.5. Не задаёт режим изоляции и не является рабочим способом получить состояние транзакции. |
| TRANSACTION_INERROR / PDO::PGSQL_TRANSACTION_INERROR | Не имеет действия в PDO; обе формы устарели с PHP8.5. Не задаёт режим изоляции и не является рабочим способом получить состояние транзакции. |
| TRANSACTION_UNKNOWN / PDO::PGSQL_TRANSACTION_UNKNOWN | Не имеет действия в PDO; обе формы устарели с PHP8.5. Не задаёт режим изоляции и не является рабочим способом получить состояние транзакции. |
В первой колонке краткие имена принадлежат Pdo\Pgsql. Старый PDO::PGSQL_ATTR_DISABLE_PREPARES тоже устарел с8.5: используйте Pdo\Pgsql::ATTR_DISABLE_PREPARES. Для пяти TRANSACTION_* смена префикса не оживляет отсутствующую возможность. Границу управляемой транзакции проверяют через PDO::inTransaction(), завершают PDO::commit() или PDO::rollBack(); подробности серверной изоляции относятся к SQL PostgreSQL, а не к этим историческим числам.
Карта методов: от этой главы к COPY, вложениям и событиям
| Метод и полный маршрут к каноническому описанию | Сигнатура | Практическая задача |
|---|---|---|
Pdo\Pgsql::escapeIdentifier() | (string $input): string | Цитировать разрешённое имя для квитанции. Выбор поля заявки |
Pdo\Pgsql::getPid() | (): int | Сопоставить PID backend с SQL или отличить два соединения. Паспорт каталога |
Pdo\Pgsql::copyFromArray() | (string $tableName, array|Traversable $rows, string $separator="\t", string $nullAs="\\\\N", ?string $fields=null): bool | Передать строки COPY text; Traversable добавлен в8.5. Загрузка поставки |
Pdo\Pgsql::copyFromFile() | (string $tableName, string $filename, string $separator="\t", string $nullAs="\\\\N", ?string $fields=null): bool | Передать локальный файл PHP по COPY. Атомарный импорт файла |
Pdo\Pgsql::copyToArray() | (string $tableName, string $separator="\t", string $nullAs="\\\\N", ?string $fields=null): array|false | Получить небольшой массив COPY-строк. Сверка выгрузки |
Pdo\Pgsql::copyToFile() | (string $tableName, string $filename, string $separator="\t", string $nullAs="\\\\N", ?string $fields=null): bool | Записать COPY-выгрузку в локальный файл PHP. Выбранный снимок данных |
Pdo\Pgsql::lobCreate() | (): string|false | Создать отдельный large object; строка результата — OID. Файл с отдельным LO |
Pdo\Pgsql::lobOpen() | (string $oid, string $mode="rb"): resource|false | Получить поток LO внутри транзакции. Порционное чтение вложения |
Pdo\Pgsql::lobUnlink() | (string $oid): bool | Явно удалить принадлежащий приложению LO. Согласованная замена вложения |
Pdo\Pgsql::getNotify() | (int $fetchMode=PDO::FETCH_DEFAULT, int $timeoutMilliseconds=0): array|false | Прочитать уведомление; timeout измеряется миллисекундами. Сигнал о подтверждённом остатке |
Pdo\Pgsql::setNoticeCallback() | (?callable $callback): void | Получать NOTICE/WARNING; null снимает свой обработчик. Отличить notice от notification |
Сигнатуры карты обозначают параметры, значения по умолчанию и тип возврата; полные правила COPY text, экранирования, транзакций и ресурсов раскрываются в связанных проектах и сохранённых карточках. false в функциях COPY/LO может означать отказ, а getNotify(false) — отсутствие сообщения; режим ERRMODE, тип операции и SQLSTATE учитывают отдельно. Новые методы escapeIdentifier и setNoticeCallback не имеют старого pgsql-псевдонима.
У PDO_PGSQL есть ресурс потока от lobOpen. Кроме того, столбец bytea возвращается как stream, хотя он не является отдельным OID large object. Для чтения применяются функции потоков, для привязки — PARAM_LOB; нельзя заменить stream числом OID или автоматически считать DELETE строки удалением отдельного LO. Глава вложений показывает эти два пути на самостоятельных файлах.
Это девять исторических имён. Существующие карточки и прежние минипримеры остаются справочником; новые самостоятельные проекты вызывают методы подкласса. При миграции сначала создают правильный объект через PDO::connect или new Pdo\Pgsql, затем меняют имя метода. Простая замена pgsqlGetPid на getPid у прежнего new PDO не меняет класс объекта.
| Унаследованная группа PDO | Её роль и предел |
|---|---|
| __construct, connect, getAvailableDrivers, getAttribute, setAttribute | Создание и настройки соединения. Общий справочник PDO сохраняет все общие сигнатуры. |
| prepare, query, exec, quote | SQL-команда и её значения; PDO::prepare() возвращает statement, exec — счётчик затронутых строк либо отказ. quote цитирует значение, escapeIdentifier — имя. |
| beginTransaction, inTransaction, commit, rollBack | Граница целой операции. Полученная строка RETURNING сама по себе не является commit. |
| lastInsertId | Имя последовательности может быть нужно для CURRVAL; без имени источник8.5.11 использует LASTVAL. Для этой заявки явное RETURNING связывает результат с конкретным INSERT. |
| errorCode, errorInfo | Диагностика соединения; отдельный statement имеет свои errorCode/errorInfo. Сохраняйте ошибку около отказавшего вызова и не печатайте полный сырой текст посетителю. |
PHP: класс Pdo\Pgsql · PHP8.5: устаревшие имена · 8.5.11: константы/сигнатуры
Параметры значений, имена и парсер PostgreSQL
Параметризация отделяет изменяющиеся значения от структуры команды. Название Кабель O'Reilly и сумма 1250.40 приходят из заявки; список столбцов и правила отбора задаёт приложение. Апостроф в названии не должен закрыть SQL-строку. Передайте значение через bindValue или execute вместо самостоятельного добавления кавычек.
$statement = $db->prepare('SELECT id, title FROM requests WHERE id = :id');
$statement->bindValue(':id', 1801, PDO::PARAM_INT);
$statement->execute();
$row = $statement->fetch(PDO::FETCH_ASSOC);
$statement->closeCursor();Именованный :id обозначает одно значение. Позиционный ? работает аналогично, но в одной команде нельзя смешивать оба стиля. Один маркер не раскрывается автоматически в список IN и не является частью SQL-имени. bindValue задаёт значение сейчас; bindParam связывает переменную для момента execute. Если тип PostgreSQL неоднозначен, задайте его в SQL через CAST(:amount AS numeric(12,2)) или CAST(:id AS integer): PHP PARAM_INT не является декларацией всех перегруженных серверных функций.
| Способ | Что делает PDO_PGSQL8.5.11 | Когда выбирать |
|---|---|---|
| ATTR_EMULATE_PREPARES=false, ATTR_DISABLE_PREPARES=false | Именованная подготовленная команда на сервере; native prepare фактически откладывается до первого execute. | Повторяемый запрос с параметрами. Успех PHPprepare не гарантирует успешный серверныйexecute. |
| ATTR_EMULATE_PREPARES=false, ATTR_DISABLE_PREPARES=true | SQL и параметры отправляются вместе через параметризованный libpq-вызов без отдельной именованной подготовки. | Однократный параметризованный запрос. Может убрать отдельный обмен подготовки; выигрыш зависит от работы/сети и здесь не измеряется. |
| ATTR_EMULATE_PREPARES=true | PDO подставляет корректно оформленные значения в SQL перед отправкой. | Осознанный режим совместимости. Он отличается от native и execute-only; не выдавайте переключение за универсальный способ исправить любой SQL. |
$once = $db->prepare('SELECT id, amount FROM requests WHERE id = :id', [
PDO::ATTR_EMULATE_PREPARES => false,
Pdo\Pgsql::ATTR_DISABLE_PREPARES => true,
]);
$once->execute(['id' => 1801]);
$row = $once->fetch(PDO::FETCH_ASSOC);
$once->closeCursor();Оба native-режима сохраняют отдельные значения. Слово disable относится к отдельной подготовленной команде, а не к защите от SQL-инъекции. В проекте заявки одинаковый SELECT выполняется обоими способами после единственного INSERT. Сложные заявления о plan cache/пуле соединений требуют отдельного профиля: этот небольшой проект их не измеряет.
Как парсер узнаёт маркеры и текст PostgreSQL
| Фрагмент SQL | Как читать |
|---|---|
| 'O''Reilly' | Одинарные кавычки задают SQL-литерал; удвоение представляет одну внутреннюю кавычку. Для внешнего значения всё равно используйте параметр. |
| "OrderTitle" | Двойные кавычки задают SQL-идентификатор с сохранением регистра. В этом контексте содержимое не является маркером значения. |
| E'line\nnext' | Escape-строка PostgreSQL с C-подобными последовательностями. PHP-строка, содержащая SQL, имеет собственный слой экранирования. |
| $tag$текст ? :name$tag$ | Dollar-quoted строка; вопрос и двоеточие внутри — обычный текст. С PHP8.4 не нужно писать ?? внутри неё; такое экранирование там deprecated. |
| -- комментарий; /* комментарий */ | Специальный парсер распознаёт однострочный и C-подобный комментарии. Вложенные C-комментарии в описанном PDO-парсере не поддержаны, хотя серверная грамматика PostgreSQL шире. |
| data ?? :key | ?? вне строк превращается в SQL-оператор ?. :key остаётся параметром; это нужно, например, для проверки ключа jsonb. Один ? в этой позиции выглядел бы как позиционный маркер. |
$statement = $db->prepare(<<<'SQL'
SELECT CAST(:document AS jsonb) ?? :key AS has_key,
$message$Есть вопрос?$message$ AS caption
SQL);
$statement->execute(['document' => '{"sku":"CAB"}', 'key' => 'sku']);
$row = $statement->fetch(PDO::FETCH_ASSOC);
$statement->closeCursor();
// has_key=true; caption='Есть вопрос?'.Здесь nowdoc с меткой SQL сохраняет текст без интерполяции PHP. $message$ — уже синтаксис PostgreSQL внутри этого текста. Пример небольшой и контекстный: он объясняет границу парсера, а не добавляет ещё один самостоятельный проект. Не добавляйте ?? механически во все места: внутри dollar quotes это не оператор.
В вопросе о типе параметра PostgreSQL задача была связана с выбором перегрузки функции, а не с безопасным ручным добавлением кавычек. Здесь решение — явный серверный CAST в нужном месте и современный проверенный путь передачи значения. Утверждения старого ответа о всех текущих версиях драйвера не используются как норма.
PHP8.5.11: выбор режима подготовки · PHP8.5.11: PostgreSQL-парсер · PostgreSQL: лексика SQL · RETURNING
Ограничить отчёт и различить две буферизации
Одна и та же ведомость проходит несколько мест памяти: сервер выполняет запрос, libpq получает PGresult, PHP создаёт строки/массивы, программа формирует файл. Замена fetchAll на while(fetch) устраняет большой PHP-массив только там, где приложение действительно не накапливает строки. Она сама по себе не доказывает, что libpq перестал буферизовать весь результат.
- SQL: диапазон дней и LIMIT ограничивают сам результат
- Buffered: libpq хранит результат до первогоfetch; PHP может читать его по одной строке
- Lazy PHP8.5: libpq выдаёт отдельные строки результата по мереfetch
- PHP: fputcsv пишет полученную строку; fetchAll вместо этого соберёт PHP-массив
$statement = $db->prepare($sql, [PDO::ATTR_PREFETCH => 0]);
$statement->execute($parameters);
while (($row = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
$currentBytes = defined('Pdo\\Pgsql::ATTR_RESULT_MEMORY_SIZE')
? $statement->getAttribute(Pdo\Pgsql::ATTR_RESULT_MEMORY_SIZE) : null;
// Записать разрешённые поля этой строки; не выполнять здесь второй SELECT.
}
$statement->closeCursor();
$statement = null;ATTR_PREFETCH=0 появился для PDO_PGSQL в8.5. Ненулевое значение выбирает buffered; оно не означает «получи ровно столько строк». Опция prepare ограничивает изменение одним statement. Если ставить её через $db->setAttribute, режим станет настройкой следующих statement этого соединения. Прокручиваемый CURSOR_SCROLL в исходнике использует другой путь и отключает single-row; его нельзя считать синонимом lazy.
| Наблюдение | Верное прочтение |
|---|---|
| fetch(PDO::FETCH_ASSOC) | Форма одной PHP-строки; в нашем профиле integer→int, numeric→string, SQLNULL→null. Точные типы сверяются в проектах, не переносятся от другого PDO-драйвера. |
| fetch(false) | Конец текущего результата при отсутствии сообщённой ошибки. В lazy-режиме ошибка может прийти после уже полученных строк; не считайте недочитанный файл подтверждённым отчётом. |
| ATTR_RESULT_MEMORY_SIZE | Байты памяти PGresult данного statement, не память PHP-процесса/сервера. В lazy он относится к текущему результату, а не сумме всех ранее прочитанных строк. |
| Доexecute | Manual указывает null; исходник8.5.11 также устанавливает диагностическое состояние. Для рабочего измерения сначала выполните запрос. |
| После нового запроса на том же соединении | Непрочитанные строки активного lazy-result могут быть молча отброшены. Даже маленький служебный SELECT нарушает последовательность. |
| rowCount | Не переносимый способ посчитать SELECT и не обязательный итог потока до его чтения. Для отчёта считаются реально обработанные строки; для общего количества используют отдельный COUNT после завершения результата. |
Проверенный Windows DLL не объявляет константу ATTR_RESULT_MEMORY_SIZE, хотя загруженный клиент — libpq16.15. Это фактическая граница этой сборки, а не опровержение документированного атрибута. Проверяйте defined до обращения: значение null в наших наблюдениях означает «не измерено». Номер сервера18.6 не может добавить константу, отсутствующую в PHP-модуле.
Закрывайте statement в finally, даже если запись CSV завершилась ошибкой. В обычном экспорте удобно сначала писать в свой временный файл, затем публиковать его только после успешного окончания чтения. В маленьком проекте файлы остаются локальными артефактами процесса: публикацию результатов выполняют только после успешных проверок. Он не предлагает переслать клиенту заголовок «готово» до получения всех строк.
В обсуждении памяти отчёта на PostgreSQL/PDO автор уже убрал fetchAll и хотел понять оставшуюся буферизацию. Ответы относятся к старым PHP. Нынешняя глава различает SQL-ограничение, PGresult и PHP-массив и показывает поддержанный8.5 режим, а не переносит старый совет о CURSOR_SCROLL как единственный путь.
Два последовательных SELECT могут увидеть изменения другого приложения между ними. Учебные данные во время сравнения никто не меняет. Если настоящий отчёт требует одного согласованного снимка, выберите транзакционную политику PostgreSQL и выполните её явно; две одинаковые строки SQL сами по себе не создают такой снимок. Самостоятельная ведомость проверяет данные и освобождение результата, но не является тестом конкурентной изоляции или benchmark.
PHP: lazy fetching и память результата · libpq: single-row режим · PHP8.5.11: fetch и диагностика statement
Подтвердить маршрут к своему каталогу
Перед выгрузкой склада нужно убедиться, что приложение читает выбранную базу и нужную группу товаров. connection.json содержит две доступные позиции, название учебного профиля и проверенные свойства маршрута. Версии клиента и сервера показаны отдельно в результате; реквизиты входа туда не попадают.
- В таблице: CAB/Монтаж/7; CLP/Монтаж/0; TAP/Монтаж/4; BOX/Тара/2
- category=Монтаж и qty>0 → CAB:7, TAP:4
- PDO_PGSQL → libpq → конкретная база PostgreSQL
- connection.json: выбранные товары и совпадение двух способов получить PID
Минимальный вызов
$sql = 'SELECT sku, title, qty FROM products
WHERE category = :category AND qty > 0 ORDER BY id LIMIT 2';
$statement = $db->prepare($sql);
$statement->execute(['category' => 'Монтаж']);
$items = $statement->fetchAll(PDO::FETCH_ASSOC);
$statement->closeCursor();
// Для приведённых ниже данных: CAB/Кабель/7 и TAP/Лента/4.В маленьком фрагменте $db уже подключён к своей базе, а products — заранее созданная таблица. SELECT перечисляет три нужных столбца, WHERE оставляет группу и положительный остаток, ORDER BY задаёт воспроизводимый порядок, LIMIT ограничивает число строк. :category — место для одного значения; execute передаёт его отдельно. FETCH_ASSOC даёт ключи sku/title/qty. Полный проект создаёт свою таблицу и сверяет реальный результат, поэтому чужой products ему не нужен.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Каталог | CAB: Кабель — 7; TAP: Лента — 4 |
| Маршрут | pgsql → phpmanual180 → UTF8 |
| Серверное соединение | getPid совпадает с pg_backend_pid |
| Файл | connection.json |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=p180Config();$db=p180Connect($config);$runtime=p180Runtime($db);
function saveJson180(string $name,array $value): void {
$text=json_encode($value,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/'.$name,$text)!==strlen($text)) {
throw new RuntimeException('Не удалось записать собственный JSON.');
}
}
function saveCsv180(string $name,array $rows): void {
$file=fopen(__DIR__.'/'.$name,'wb');
if ($file===false) {throw new RuntimeException('Не удалось открыть CSV.');}
try {foreach($rows as $row) {
if (fputcsv($file,$row,',','"','',"\n")===false) {
throw new RuntimeException('Не удалось записать CSV.');
}
}} finally {fclose($file);}
}
$name=p180OwnTable($db,$owned,'passport','(id integer NOT NULL PRIMARY KEY,
sku varchar(12) NOT NULL,title text NOT NULL,category text NOT NULL,qty integer NOT NULL)');
$table=p180Qualified($name);
foreach ([[1,'CAB','Кабель','Монтаж',7],[2,'CLP','Клемма','Монтаж',0],
[3,'TAP','Лента','Монтаж',4],[4,'BOX','Короб','Тара',2]] as $item) {
p180Command($db,'INSERT INTO '.$table.' (id,sku,title,category,qty) VALUES (?,?,?,?,?)',$item);
}
$identity=p180Read($db,"SELECT current_database() AS database_name,
current_setting('client_encoding') AS encoding,pg_backend_pid() AS backend_pid")[0];
$statement=null;
try {
$statement=$db->prepare('SELECT sku,title,qty FROM '.$table.
' WHERE category=:category AND qty>0 ORDER BY id LIMIT 2');
$statement->bindValue(':category','Монтаж',PDO::PARAM_STR);
$statement->execute();
$items=$statement->fetchAll(PDO::FETCH_ASSOC);
} finally {p180Release($statement);}
$pid=$db->getPid();
$passport=['profile'=>'warehouse-demo','driver'=>$db->getAttribute(PDO::ATTR_DRIVER_NAME),
'connection_class'=>get_class($db),'database'=>$identity['database_name'],
'encoding'=>$identity['encoding'],'backend_pid_matches'=>$pid===$identity['backend_pid'],'items'=>$items];
saveJson180('connection.json',$passport);
$checks=[
'pgsql_driver'=>$passport['driver']==='pgsql',
'pgsql_subclass'=>$db instanceof Pdo\Pgsql,
'own_database'=>$identity['database_name']===$config['database'],
'utf8_connection'=>$identity['encoding']==='UTF8',
'backend_identity'=>is_int($pid) && $pid>0 && $pid===$identity['backend_pid'],
'bounded_selected_catalog'=>$items===[['sku'=>'CAB','title'=>'Кабель','qty'=>7],['sku'=>'TAP','title'=>'Лента','qty'=>4]],
'integer_quantity'=>is_int($items[0]['qty']),
'released_result'=>$statement===null,
'json_roundtrip'=>json_decode(file_get_contents(__DIR__.'/connection.json'),true,512,JSON_THROW_ON_ERROR)===$passport,
];
$observations=['connection'=>$passport,'backend_pid'=>$pid,
'client_version'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),
'server_version'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION),
'selection'=>['category'=>'Монтаж','positive_quantity'=>true,'limit'=>2,'order'=>'id']];
$rows=[['Каталог','CAB: Кабель — 7; TAP: Лента — 4'],['Маршрут','pgsql → phpmanual180 → UTF8'],
['Серверное соединение','getPid совпадает с pg_backend_pid'],['Файл','connection.json']];
} finally {
if($db instanceof PDO){try{$removed=p180Cleanup($db,$owned);}finally{p180Close($db);}}
}
p180Report('pdo180-connection-passport','Подтвердить маршрут к своему каталогу',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Каталог — CAB: Кабель — 7; TAP: Лента — 4
Маршрут — pgsql → phpmanual180 → UTF8
Серверное соединение — getPid совпадает с pg_backend_pid
Файл — connection.json
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать паспорт каталога JSON
Границы решения
getPid возвращает PID серверного backend, не идентификатор PHP-процесса и не постоянный ключ клиента. В JSON сохранено само сравнение; фактическое число меняется между запусками и находится в наблюдениях. Успешный вход не гарантирует права на любую таблицу. Здесь проверен TCP локального стенда; Unix socket и TLS разобраны как конфигурационные варианты, а не как дополнительные выполненные подключения.
Попробуйте изменить условие
Покажите только кабели с остатком не меньше 5. Какие части SQL меняются, а какие остаются параметрами?
Разбор
Сохраните явный список колонок и ORDER BY. В условие добавьте sku=:sku и qty>=:minimum, передайте CAB как строку, 5 как PARAM_INT. Ожидается одна позиция CAB/Кабель/7. Строку SQL не собирают из значения категории или названия товара.
Принять заявку и вернуть её собственный номер
Оператор принял заявку на кабель и должен дать клиенту номер именно этой записи. PostgreSQL возвращает его непосредственно из INSERT через RETURNING. После commit приложение перечитывает заявку и сохраняет request.csv с суммой 1250.40. Второй режим отправки параметров сравнивается только на чтении, поэтому демонстрация не создаёт повторную заявку.
- Вход PHP: CAB, Кабель O'Reilly, сумма "1250.40", note=null
- Параметры → INSERT в свою таблицу → RETURNING id=1801
- closeCursor → commit → SELECT по собственному id
- Поле title из allowlist → escapeIdentifier; значение id → bindValue
- Оба режима SELECT читают ту же заявку → request.csv
Минимальный вызов
$statement = $db->prepare('INSERT INTO requests (title, amount)
VALUES (:title, CAST(:amount AS numeric(12,2))) RETURNING id');
$statement->execute(['title' => "Кабель O'Reilly", 'amount' => '1250.40']);
$id = $statement->fetchColumn();
$statement->closeCursor();
// id относится к этому INSERT, а не к максимальному номеру таблицы.В минимальном фрагменте requests уже имеет генерируемый id. RETURNING — часть SQL после INSERT; fetchColumn читает первое поле возвращённой строки. CAST задаёт серверный тип десятичного значения, строка 1250.40 не превращается в PHP float. Полный проект отдельно показывает beginTransaction/commit, явный PARAM_NULL и проверку квитанции. Полученная до commit строка ещё не доказывает фиксацию записи.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Заявка | 1801 · CAB · Кабель O'Reilly |
| Сумма | 1250.40 — точная строка NUMERIC |
| Примечание | SQL NULL |
| Два чтения | native prepare = disable-prepares |
| Файл | request.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=p180Config();$db=p180Connect($config);$runtime=p180Runtime($db);
function saveJson180(string $name,array $value): void {
$text=json_encode($value,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/'.$name,$text)!==strlen($text)) {
throw new RuntimeException('Не удалось записать собственный JSON.');
}
}
function saveCsv180(string $name,array $rows): void {
$file=fopen(__DIR__.'/'.$name,'wb');
if ($file===false) {throw new RuntimeException('Не удалось открыть CSV.');}
try {foreach($rows as $row) {
if (fputcsv($file,$row,',','"','',"\n")===false) {
throw new RuntimeException('Не удалось записать CSV.');
}
}} finally {fclose($file);}
}
$name=p180OwnTable($db,$owned,'request','(id integer GENERATED ALWAYS AS IDENTITY (START WITH 1801) PRIMARY KEY,
sku varchar(12) NOT NULL,title text NOT NULL,amount numeric(12,2) NOT NULL,note text NULL)');
$table=p180Qualified($name);
$input=['sku'=>'CAB','title'=>"Кабель O'Reilly",'amount'=>'1250.40','note'=>null];
$statement=null;$native=null;$once=null;
try {
$db->beginTransaction();
$statement=$db->prepare('INSERT INTO '.$table.' (sku,title,amount,note)
VALUES (:sku,:title,CAST(:amount AS numeric(12,2)),:note) RETURNING id,sku,title,amount,note');
$statement->bindValue(':sku',$input['sku'],PDO::PARAM_STR);
$statement->bindValue(':title',$input['title'],PDO::PARAM_STR);
$statement->bindValue(':amount',$input['amount'],PDO::PARAM_STR);
$statement->bindValue(':note',$input['note'],PDO::PARAM_NULL);
$statement->execute();
$receipt=$statement->fetch(PDO::FETCH_ASSOC);
if (!is_array($receipt)) {throw new RuntimeException('Заявка не вернула квитанцию.');}
$returningEof=$statement->fetch(PDO::FETCH_ASSOC)===false;
p180Release($statement);
$db->commit();
$field='title';
$allowed=['title','sku'];
if (!in_array($field,$allowed,true)) {throw new InvalidArgumentException('Поле отчёта не разрешено.');}
$quoted=$db->escapeIdentifier($field);
$sql='SELECT id,'.$quoted.' AS selected_value,amount,note FROM '.$table.' WHERE id=:id';
$native=$db->prepare($sql,[PDO::ATTR_EMULATE_PREPARES=>false,Pdo\Pgsql::ATTR_DISABLE_PREPARES=>false]);
$native->bindValue(':id',$receipt['id'],PDO::PARAM_INT);$native->execute();
$nativeRow=$native->fetch(PDO::FETCH_ASSOC);
p180Release($native);
$once=$db->prepare($sql,[PDO::ATTR_EMULATE_PREPARES=>false,Pdo\Pgsql::ATTR_DISABLE_PREPARES=>true]);
$once->bindValue(':id',$receipt['id'],PDO::PARAM_INT);$once->execute();
$onceRow=$once->fetch(PDO::FETCH_ASSOC);
} finally {
p180Release($statement);p180Release($native);p180Release($once);
if ($db->inTransaction()) {$db->rollBack();}
}
$count=p180Read($db,'SELECT COUNT(*) AS total FROM '.$table)[0]['total'];
saveCsv180('request.csv',[['request_id','sku','title','amount','note_state'],
[$receipt['id'],$receipt['sku'],$receipt['title'],$receipt['amount'],$receipt['note']===null?'NULL':'TEXT']]);
$checks=[
'generated_own_id'=>$receipt['id']===1801,
'exact_returning'=>$receipt===['id'=>1801,'sku'=>'CAB','title'=>"Кабель O'Reilly",'amount'=>'1250.40','note'=>null],
'returning_single_row'=>$returningEof,
'committed_receipt'=>!$db->inTransaction() && $nativeRow===['id'=>1801,'selected_value'=>"Кабель O'Reilly",'amount'=>'1250.40','note'=>null],
'one_insert_only'=>(int)$count===1,
'native_and_execute_only_equal'=>$nativeRow===$onceRow,
'identifier_quoted'=>$quoted==='"title"',
'amount_is_exact_text'=>is_string($receipt['amount']) && $receipt['amount']==='1250.40',
'null_preserved'=>$receipt['note']===null,
'all_results_released'=>$statement===null && $native===null && $once===null,
];
$observations=['input'=>$input,'returning'=>$receipt,'read_native'=>$nativeRow,'read_execute_only'=>$onceRow,
'field_allowlist'=>$allowed,'chosen_field'=>$field,'quoted_field'=>$quoted,
'insert_count'=>(int)$count,'returning_types'=>array_map('get_debug_type',$receipt)];
$rows=[['Заявка','1801 · CAB · Кабель O\'Reilly'],['Сумма','1250.40 — точная строка NUMERIC'],
['Примечание','SQL NULL'],['Два чтения','native prepare = disable-prepares'],['Файл','request.csv']];
} finally {
if($db instanceof PDO){try{$removed=p180Cleanup($db,$owned);}finally{p180Close($db);}}
}
p180Report('pdo180-parameter-receipt','Принять заявку и вернуть её собственный номер',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Заявка — 1801 · CAB · Кабель O'Reilly
Сумма — 1250.40 — точная строка NUMERIC
Примечание — SQL NULL
Два чтения — native prepare = disable-prepares
Файл — request.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать квитанцию заявки CSV
Границы решения
Маркер параметра представляет значение, а не название поля или выражение ORDER BY. escapeIdentifier оформляет одно имя, но не разрешает доступ к нему: список title/sku проверяется до цитирования. Identity может иметь пропуски после rollback; номер не является счётчиком успешных заявок. Для защиты от повторной отправки нужна отдельная политика бизнес-ключа. ATTR_DISABLE_PREPARES не отключает параметризацию и не обещает измеренное ускорение.
Попробуйте изменить условие
Добавьте разрешённый выбор sku вместо title. Нужно ли заново выполнять INSERT и менять сумму на float?
Разбор
Нет. Измените только разрешённое имя поля до escapeIdentifier и повторите SELECT по уже полученному id. selected_value станет CAB; сумма останется строкой 1250.40. Во внешнем приложении allowlist задают кодом, а запрос посетителя лишь выбирает один из его элементов.
Выгрузить ограниченную ведомость двумя способами чтения
Бухгалтеру нужна ведомость выбранных дней, а не вся таблица продаж. SQL исключает чужой период и нулевые строки, задаёт порядок и предел. Три строки report.csv одинаковы при обычном чтении и при новом режиме PDO_PGSQL 8.5. Отдельная проверка показывает возможность измерить память PGresult: в проверенной Windows-сборке атрибут отсутствует, поэтому программа сохраняет null, а не выдуманное число.
- 6 исходных продаж → диапазон [8 ноября, 10 ноября), qty>0
- ORDER BY sale_day,id и LIMIT 3 → CAB/3, CLP/2, TAP/5
- Buffered: один результат → маленький массив для сопоставления
- Lazy: fetch → строка CSV; наличие атрибута памяти проверяется отдельно
- Дочитать и освободить результат → следующий SELECT → report.csv
Минимальный вызов
$statement = $db->prepare('SELECT sale_day, sku, qty, total
FROM sales WHERE sale_day >= :from_day AND sale_day < :until_day
ORDER BY sale_day, id LIMIT 3', [PDO::ATTR_PREFETCH => 0]);
$statement->execute(['from_day' => '2026-11-08', 'until_day' => '2026-11-10']);
while (($row = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
// Здесь обрабатывают одну строку, например записывают её в CSV.
}
$statement->closeCursor();
$statement = null;PDO::ATTR_PREFETCH=>0 выбирает lazy fetching для этого statement с PHP 8.5. while повторяет тело для каждой полученной строки; строгое !==false отличает конец результата от нулевых значений полей. Ограничение диапазона и LIMIT выполняет PostgreSQL до выгрузки. Полный проект сначала получает небольшой buffered-эталон, затем записывает CSV построчно. Для демонстрации сравнения он сохраняет ровно три lazy-строки, а не изображает безграничную потоковую обработку.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Период | 8–9 ноября 2026 |
| Ведомость | CAB × 3 = 420.00; CLP × 2 = 36.50; TAP × 5 = 250.00 |
| Количество | 10 единиц в 3 строках |
| Чтение | buffered и lazy дали одинаковые строки |
| Память PGresult | Атрибут недоступен в проверенной сборке |
| Файл | report.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=p180Config();$db=p180Connect($config);$runtime=p180Runtime($db);
function saveJson180(string $name,array $value): void {
$text=json_encode($value,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/'.$name,$text)!==strlen($text)) {
throw new RuntimeException('Не удалось записать собственный JSON.');
}
}
function saveCsv180(string $name,array $rows): void {
$file=fopen(__DIR__.'/'.$name,'wb');
if ($file===false) {throw new RuntimeException('Не удалось открыть CSV.');}
try {foreach($rows as $row) {
if (fputcsv($file,$row,',','"','',"\n")===false) {
throw new RuntimeException('Не удалось записать CSV.');
}
}} finally {fclose($file);}
}
$name=p180OwnTable($db,$owned,'report','(id integer NOT NULL PRIMARY KEY,sale_day date NOT NULL,
sku varchar(12) NOT NULL,title text NOT NULL,qty integer NOT NULL,total numeric(12,2) NOT NULL)');
$table=p180Qualified($name);
foreach ([[1,'2026-11-07','OLD','Ранняя продажа',1,'10.00'],
[2,'2026-11-08','CAB','Кабель',3,'420.00'],[3,'2026-11-08','ZERO','Нулевая строка',0,'0.00'],
[4,'2026-11-09','CLP','Клемма',2,'36.50'],[5,'2026-11-09','TAP','Лента',5,'250.00'],
[6,'2026-11-10','LATE','Следующий период',1,'20.00']] as $sale) {
p180Command($db,'INSERT INTO '.$table.' (id,sale_day,sku,title,qty,total) VALUES (?,?,?,?,?,?)',$sale);
}
$sql='SELECT sale_day,sku,title,qty,total FROM '.$table.
' WHERE sale_day>=CAST(:from_day AS date) AND sale_day<CAST(:until_day AS date) AND qty>0 ORDER BY sale_day,id LIMIT :maximum';
$bind=static function(PDOStatement $q):void {
$q->bindValue(':from_day','2026-11-08',PDO::PARAM_STR);
$q->bindValue(':until_day','2026-11-10',PDO::PARAM_STR);
$q->bindValue(':maximum',3,PDO::PARAM_INT);
};
$buffered=null;$lazy=null;$file=null;
$memorySupported=defined('Pdo\\Pgsql::ATTR_RESULT_MEMORY_SIZE');
$bufferedBytes=null;$memory=[];$lazyRows=[];
try {
$buffered=$db->prepare($sql,[PDO::ATTR_PREFETCH=>1]);$bind($buffered);$buffered->execute();
if ($memorySupported) {$bufferedBytes=$buffered->getAttribute(Pdo\Pgsql::ATTR_RESULT_MEMORY_SIZE);}
$bufferedRows=$buffered->fetchAll(PDO::FETCH_ASSOC);
p180Release($buffered);
$lazy=$db->prepare($sql,[PDO::ATTR_PREFETCH=>0]);$bind($lazy);$lazy->execute();
$file=fopen(__DIR__.'/report.csv','wb');
if ($file===false) {throw new RuntimeException('CSV не открыт.');}
if (fputcsv($file,['sale_day','sku','title','qty','total'],',','"','',"\n")===false) {
throw new RuntimeException('Заголовок CSV не записан.');
}
while (($row=$lazy->fetch(PDO::FETCH_ASSOC))!==false) {
$memory[]=$memorySupported?$lazy->getAttribute(Pdo\Pgsql::ATTR_RESULT_MEMORY_SIZE):null;
if (fputcsv($file,[$row['sale_day'],$row['sku'],$row['title'],$row['qty'],$row['total']],',','"','',"\n")===false) {
throw new RuntimeException('Строка CSV не записана.');
}
// Только три строки ограниченного учебного отчёта сохраняются для сопоставления.
$lazyRows[]=$row;
}
$eofState=$lazy->errorCode();
} finally {
if (is_resource($file)) {fclose($file);}$file=null;
p180Release($buffered);p180Release($lazy);
}
$healthy=p180Read($db,'SELECT 180 AS ready')[0]['ready'];
$expected=[['sale_day'=>'2026-11-08','sku'=>'CAB','title'=>'Кабель','qty'=>3,'total'=>'420.00'],
['sale_day'=>'2026-11-09','sku'=>'CLP','title'=>'Клемма','qty'=>2,'total'=>'36.50'],
['sale_day'=>'2026-11-09','sku'=>'TAP','title'=>'Лента','qty'=>5,'total'=>'250.00']];
$checks=[
'sql_selected_report'=>$bufferedRows===$expected,
'buffered_lazy_same'=>$lazyRows===$bufferedRows,
'bounded_three_rows'=>count($lazyRows)===3,
'quantity_total'=>array_sum(array_column($lazyRows,'qty'))===10,
'numeric_exact_text'=>array_column($lazyRows,'total')===['420.00','36.50','250.00'],
'memory_capability_recorded'=>is_bool($memorySupported),
'memory_values_honest'=>$memorySupported
? is_int($bufferedBytes) && $bufferedBytes>0 && count($memory)===3 && count(array_filter($memory,static fn($n)=>is_int($n) && $n>0))===3
: $bufferedBytes===null && $memory===[null,null,null],
'clean_eof'=>$eofState==='00000',
'lazy_result_finished'=>$lazy===null && $buffered===null,
'next_query_healthy'=>$healthy===180,
'csv_written'=>is_file(__DIR__.'/report.csv') && filesize(__DIR__.'/report.csv')>0,
];
$observations=['buffered_rows'=>$bufferedRows,'lazy_rows'=>$lazyRows,
'result_memory_supported'=>$memorySupported,'result_memory_bytes'=>['buffered'=>$bufferedBytes,'per_lazy_row'=>$memory],
'range'=>['from_inclusive'=>'2026-11-08','until_exclusive'=>'2026-11-10','maximum'=>3],
'eof_state'=>$eofState,'next_query'=>$healthy];
$rows=[['Период','8–9 ноября 2026'],['Ведомость','CAB × 3 = 420.00; CLP × 2 = 36.50; TAP × 5 = 250.00'],
['Количество','10 единиц в 3 строках'],['Чтение','buffered и lazy дали одинаковые строки'],
['Память PGresult',$memorySupported?'Фактические байты — в наблюдениях':'Атрибут недоступен в проверенной сборке'],['Файл','report.csv']];
} finally {
if($db instanceof PDO){try{$removed=p180Cleanup($db,$owned);}finally{p180Close($db);}}
}
p180Report('pdo180-bounded-report','Выгрузить ограниченную ведомость двумя способами чтения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Период — 8–9 ноября 2026
Ведомость — CAB × 3 = 420.00; CLP × 2 = 36.50; TAP × 5 = 250.00
Количество — 10 единиц в 3 строках
Чтение — buffered и lazy дали одинаковые строки
Память PGresult — Атрибут недоступен в проверенной сборке
Файл — report.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать ведомость CSV
Границы решения
Lazy-режим допускает только один активный statement на соединении. Новый запрос может молча отбросить непрочитанные строки предыдущего. Поэтому даже служебный SELECT не выполняют внутри этого цикла на том же PDO. ATTR_RESULT_MEMORY_SIZE относится к PGresult конкретного statement, когда его поддерживает сборка. В поставленном Windows DLL этот атрибут не объявлен даже при libpq16.15; defined проверяет факт до обращения. null здесь означает отсутствие измерения, не нулевую память. Три строки не являются нагрузочным тестом.
Попробуйте изменить условие
Сделайте ведомость только за 9 ноября и оставьте максимум две позиции. Сохранится ли порядок при одинаковой дате?
Разбор
Поставьте from_day=2026-11-09, until_day=2026-11-10, maximum=2. Ожидаются CLP/2/36.50 и TAP/5/250.00. Второй ключ id делает порядок однозначным. Для следующей страницы выберите явную стратегию пагинации, не начинайте новый SELECT до завершения текущего lazy-result.
Ошибки, завершение результата и подтверждение заявки
Ошибка окружения, пустой ответ и отклонённая заявка требуют разных действий. Не печатайте посетителю DSN, пароль, полный server message или стек со служебными путями. Сохраните диагностические поля рядом с отказом в закрытом журнале; публичная квитанция содержит только сведения, которые нужны её получателю.
| Ситуация | Различение и действие |
|---|---|
| could not find driver | Проверить именно тот PHP/ini/SAPI, который выполняет приложение; наличие psql или расширения pgsql не загружает PDO_PGSQL. |
| Вход не выполнен | Проверить host/порт, выбранную базу, роль/SCRAM и SSL-профиль. Текст ошибки и SQLSTATE не универсальны для всех причин; не подменять диагностику одним bool. |
| Подключение есть, таблица недоступна | Проверить схему, имя, права и search_path. Приложение в проектах явно указывает public и своё имя. Вход и разрешение на строку — разные проверки. |
| SELECT не выбрал строки | Пустой массив допустим для фильтра. Не обращаться к полямfalse. Отличать успешное окончание от исключения на execute/fetch. |
| Заявка нарушила constraint | ERRMODE_EXCEPTION даёт PDOException; в явной транзакции PostgreSQL дальнейшая работа обычно требует rollback. Нельзя продолжить серию команд так, будто ошибочный INSERT просто пропущен. |
| RETURNING дал номер, затемcommitнеуспешен | Квитанция ещё не подтверждает фиксацию. Для retry нужен бизнес-ключ/идемпотентность; MAX(id) и повторныйINSERT проблему не решают. |
| Появилось NOTICE | Серверное информационное сообщение не равно SQL-отказу и не равно асинхронному NOTIFY. Следующая глава показывает разные обработчики. |
| Строки lazy-result исчезли | Проверить второй активный statement на том же PDO. Сначала закончить чтение/освобождение, затем начинать следующую команду. |
| Нужен бинарный файл | bytea stream и OID large object различаются по хранению, удалению и транзакционному lifetime. Сначала выбрать модель, затем копировать байты. |
$db->beginTransaction();
try {
// Команды одной заявки; результаты каждой заканчиваются до следующей.
$db->commit();
} catch (Throwable $error) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $error; // На внешней границе приложения — закрытый журнал и безопасный ответ.
}Этот фрагмент задаёт границу обработки, а не заменяет проверку входных данных. Для транзакций смотрите основы commit/rollback, для диагностических массивов — PDO::errorInfo(). PDOStatement удерживает связь с соединением; сначала завершайте свои statement и потоки, затем убирайте ссылки на PDO. Постоянное соединение не отменяет очистку состояния сессии и не гарантирует отдельный процесс PostgreSQL на каждую HTTP-заявку.
В проекте заявки identity создаёт зависимую последовательность для id. Она принадлежит таблице и удаляется вместе с ней; это не самостоятельная таблица каталога и не отдельное выделение large object. Итоговая проверка стенда учитывает отсутствие этой зависимости после DROP. При аварии процесса вне finally возможны остатки: проверяйте только свои имена и журнал отдельной базы, не выполняйте массовый DROP чужих объектов.
Дальше: пакет поставки, вложение и сигнал об изменении
Теперь подключение приводит к выбранным товарам, параметризация — к своей заявке, а режим чтения — к точному файлу ведомости. Следующий шаг — COPY-поставки и сверка снимка: там формат записи и граница транзакции важнее одного успешного bool. Вложения и события PostgreSQL продолжают этот путь через bytea, отдельные large objects и сообщения после commit. Общие карточки PDO и справочник native pgsql остаются отдельными маршрутами; одинаковая база не делает их API взаимозаменяемыми.
Источники и обсуждения
- PHP Manual: PDO_PGSQL
- PHP Manual: PDO_PGSQL DSN
- PHP Manual: Pdo\Pgsql
- PHP Manual: PDO::connect
- PHP Manual: escapeIdentifier
- PHP Manual: getPid
- PHP8.5: устаревшие имена
- PHP8.5.11: соединение и атрибуты
- PHP8.5.11: результаты
- PHP8.5.11: SQL-парсер
- PostgreSQL: параметры libpq
- PostgreSQL: TLS
- PostgreSQL: RETURNING
- PostgreSQL: single-row
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


