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

Дерево руководства
Глава 305 · После первой параметризованной выборки PDO_PGSQL
PDO_PGSQL: загрузить поставку, отменить ошибочный файл и сверить выгрузку
Три законченных процесса COPY: точная поставка из массива/Generator, отмена ошибочного файла и сверка выбранного экспорта.
Поставка, квитанция и файл для партнёра
Магазин получил поставку из четырёх строк. Нужно загрузить её без ручного INSERT для каждой записи, отказаться от всего файла при неверном остатке и передать партнёру выбранные товары. На выходе этой главы будут три открываемых результата: supplier.tsv с точными значениями, import-receipt.csv с изменениями остатков и selected.tsv с проверенным набором для партнёра.
PDO позволяет PHP отправить запрос в PostgreSQL и получить нужные строки. Драйвер PDO_PGSQL использует клиентскую библиотеку libpq. Одна строка таблицы, файл в PHP и отдельный объект базы — разные сущности; ниже показано, как сохранить их связь.
Проекты запускаются собственным PHP 8.5.11 с PDO_PGSQL в отдельной учебной базе PostgreSQL. Начало SQL и PDO объясняет таблицы и запросы, а подключение PDO_PGSQL — подготовку окружения и параметры. Сначала рассмотрите маленький вызов, затем скачайте проект с созданием данных и уборкой.
- Поставщик: SKU, название, остаток, заметка
- Кодировщик COPY text: понятные значения → строки протокола
- PostgreSQL: типы колонок, CHECK и общая транзакция
- SQL выбирает нужные данные → COPY возвращает записи
- HTML показывает товары; TSV/CSV можно открыть и передать дальше
COPY — команда массового обмена строками с PostgreSQL. Четыре метода PDO_PGSQL дают ей вход из PHP-массива/итератора или файла и выход в массив или файл. Они не создают структуру таблицы и не угадывают смысл колонок. Для одного изменения карточки привычный prepare и execute обычно проще; для одинаковых записей поставки удобно один раз задать договор передачи.
| Что передаём | В нашем примере | Чем это не является |
|---|---|---|
| PHP-массив значений | [1, "SUP-41", "Кабель", 3, null] | Ещё не готовая запись COPY |
| Строка COPY text | 1[TAB]SUP-41[TAB]Кабель[TAB]3[TAB]\N[LF] | Не SQL INSERT и не строка CSV с кавычками |
| Строка таблицы | ordinal=1, sku=SUP-41, qty=3, note=SQL NULL | Не объект PHP и не содержимое файла |
| Полезный артефакт | supplier.tsv или квитанция import-receipt.csv | У каждого файла явно свой формат |
Сначала разберём одну запись. В PHP знак $ начинает имя переменной; = сохраняет значение, [] создаёт массив, запятая разделяет его элементы. Строка в двойных кавычках понимает \t как табуляцию и \n как перенос. \\ даёт один обратный слеш. У COPY есть свой второй уровень правил: поэтому для произвольных названий ниже используется отдельный кодировщик.
$line = "1\tSUP-41\tКабель\t3\t\\N\n";
// В bytes записи: пять полей, четыре табуляции и один завершающий LF.
$accepted = $db->copyFromArray('supplier_items', [$line],
fields: 'ordinal,sku,title,qty,note');Если supplier_items уже создана с указанными колонками, после успешного вызова в ней появится товар SUP-41 с количеством3 и отсутствующей заметкой. $accepted содержит true, а сам товар проверяется отдельным SELECT. Стрелка -> обращается к методу соединения $db; круглые скобки содержат аргументы, fields: называет необязательный аргумент, точка с запятой завершает инструкцию. Полный проект ниже самостоятельно создаёт таблицу, выполняет чтение и показывает результат.
Предварительно нужны массивы, обход коллекции и обработка отказа. Каждая новая операция COPY разобрана здесь до полного исходника. Существующие канонические справочные карточки PDO_PGSQL остаются на прежних адресах; эта глава развивает их в рабочие процессы.
Значения PHP и записи COPY text
Договор файла должен отвечать на три вопроса: где заканчивается колонка, где заканчивается запись и как отличить отсутствие значения от пустого текста. В проектах это COPY text: разделитель TAB, конец записи LF, маркер SQL NULL — обратный слеш и N. Заголовка нет. Формат сохраняется одинаковым в кодировщике, методе импорта и проверке выгрузки.
| Значение приложения | Что находится внутри одного поля COPY text | После чтения SQL |
|---|---|---|
| PHP null | \N | SQL NULL |
| Пустая строка | Ноль символов между разделителями | Пустая строка |
| Буквальные два символа \N | \\N | Текст \N |
| Название с табуляцией | Бирка\tсиняя | Табуляция внутри названия |
| Две строки заметки | Строка 1\nСтрока 2 | Перенос внутри одного значения |
| Папка\архив | Папка\\архив | Один обратный слеш |
Кодировщик проекта сохраняет NULL отдельно, затем заменяет управляющие символы и обратный слеш. Апостроф в O'Reilly здесь не завершает SQL-литерал: данные отправляются как COPY-записи. Это не повод передавать непроверенные имена таблиц: синтаксис команды и содержимое записи идут разными путями. В UTF-8 обычные русские символы остаются текстом; NUL отклоняется до COPY.
function copyField(string|int|null $value): string {
if ($value === null) {return '\\N';}
return strtr((string)$value, [
"\\" => "\\\\", "\t" => "\\t", "\n" => "\\n", "\r" => "\\r",
]);
}
$line = implode("\t", array_map('copyField',
[1, 'SUP-41', "Кабель O'Reilly", 3, null])) . "\n";Двоеточие после списка аргументов задаёт возвращаемый тип string; знак | разрешает входную строку, целое число или null. array_map применяет функцию к каждому полю, implode соединяет результаты табуляцией. Точка соединяет две PHP-строки. В самостоятельном проекте кодировщик дополнительно обрабатывает управляющие символы и проверяет NUL. Это узкий кодировщик наших типов, а не универсальная сериализация массивов, дат, bytea или произвольных объектов.
Расширение .tsv обозначает здесь файл обмена с правилами COPY text. Чтобы получить обычный CSV, нельзя просто заменить TAB запятой: CSV иначе обрабатывает кавычки и переносы. Для входного CSV сначала разбирают записи CSV-парсером, проверяют число и назначение колонок, затем кодируют значения в выбранный COPY-формат. Обсуждение реального отказа импорта из-за переноса строки показывает именно эту путаницу; ответы с разбиением CSV через explode не используются в наших проектах.
Не заменяйте PHP null строкой «NULL» или «None». Вопрос о NULL при COPY из Python служит примером потребности сохранить смысл значения; правила другого клиентского API сюда не переносятся. На нашей стороне копируемый маркер и PHP-синтаксис заданы явно, а результат проверяется SELECT.
У nullAs есть дополнительный уровень экранирования: значение аргумента по умолчанию состоит из трёх байтов — два обратных слеша и N. Обёртка вставляет его в SQL escape-литерал, а маркер внутри записи состоит уже из одного слеша и N. В подготовке для PHP8.5.11 переданный вручную аргумент с одним обратным слешем ошибочно задал маркер N. Отдельная проверка стандартного аргумента сохранила четыре различных значения: SQL NULL, пустой текст, буквальное \N и N. В наших вызовах nullAs опущен, а fields передан по имени.
copyFromArray: массив и последовательный источник
Когда приложение уже получило товары как значения PHP, Pdo\Pgsql::copyFromArray передаёт подготовленные строковые записи в таблицу. Первый полезный вариант — маленькая поставка целиком в массиве. Второй — последовательный источник записей: Generator отдаёт следующую строку по запросу метода, не требуя отдельного массива готовых COPY-строк.
public function Pdo\Pgsql::copyFromArray(
string $tableName,
array|Traversable $rows,
string $separator = "\t",
string $nullAs = "\\\\N",
?string $fields = null
): boolСовременный метод доступен с PHP8.4; Traversable в rows добавлен в PHP8.5. rows — набор строк с разделёнными полями, не массив ассоциативных записей. true обозначает успех команды, false — отказ в режиме без исключения. С ERRMODE_EXCEPTION SQL-отказ попадает в PDOException; неверный PHP-тип аргумента может дать TypeError раньше SQL. Вернувшийся true не заменяет commit явной транзакции и не сообщает число принятых строк.
| Аргумент | Смысл и договор проекта |
|---|---|
| tableName: string | Уже существующая таблица. В проектах — проверенное имя своей таблицы, отдельно заключённое в кавычки вместе со схемой public. Это не значение SQL-параметра. |
| separator: string = "\t" | Один однобайтовый разделитель. Проекты оставляют TAB. Это не переключатель FORMAT CSV. |
| nullAs: string = "\\\\N" | Синтаксис PHP по умолчанию содержит экранирование для конструкции команды; поле данных содержит маркер \N. Проекты не меняют default и не берут маркер из запроса пользователя. |
| fields: ?string = null | Фиксированный SQL-список колонок через запятую, в порядке полей записи. null оставляет стандартный набор колонок COPY; явный список устойчивее к добавлению служебной колонки. |
function nextStockRows(): Generator {
yield "3\tSUP-43\tПапка\\\\архив\t2\t\\\\N\n";
yield "4\tSUP-44\tИнструкция\t1\t\\N\n";
}
$db->copyFromArray('supplier_items', nextStockRows(),
fields: 'ordinal,sku,title,qty,note');yield выдаёт одну строку и приостанавливает функцию до следующего шага. В первой записи выше заметка — буквальное \N, во второй — SQL NULL. На настоящих данных удобнее вызывать copyLine180 для каждого набора значений, как в проекте поставки. Там два вызова COPY объединены одной транзакцией и проверены обычным SELECT. Generator не превращает уже загруженный исходный массив в источник с нулевым потреблением памяти.
Второе применение — подготовить строки приёмной таблицы после проверки входного API, а затем выполнить тот же UPDATE JOIN, что в файловом импорте. Меняется только источник строк; проверка SKU и граница commit сохраняются. Не выполняйте запросы к этому же соединению внутри Generator: COPY занимает его протокол. Источник должен завершаться предсказуемо; обработку пользовательских ошибок преобразования выполняйте до передачи или в явно разработанном процессе восстановления.
В закреплённом исходнике8.5.11 обработчик принимает iterable, тогда как современная stub/arginfo ещё описывает array; опубликованный Manual уже указывает array|Traversable. Подготовка данной сборки фактически приняла и массив, и Generator. Это наблюдение исполнения, а не догадка по одному файлу метаданных. Переход от PDO::pgsqlCopyFromArray разобран ниже.
Принять четыре товара без потери табуляции, NULL и обратного слеша
Поставщик передаёт названия и остатки. В названии может быть апостроф, в заметке — перенос строки, а отсутствие заметки отличается от намеренно пустой заметки. Проект сохраняет эти различия и выдаёт supplier.tsv для повторной загрузки. В HTML видны сами товары; обозначения [TAB] и [LF] показывают невидимые символы, а JSON хранит исходные значения.
- [1, SUP-41, Кабель, 3, null] — значения приложения
- copyLine180: значения → одна строка COPY text
- copyFromArray: 2 записи массива; затем Generator выдаёт ещё 2
- SELECT ORDER BY ordinal: исходные значения восстановлены
- copyToArray → порядок ordinal → supplier.tsv
Минимальный вызов
$lines = ["1\tSUP-41\tКабель\t3\t\\N\n"];
$db->copyFromArray('supplier_items', $lines,
fields: 'ordinal,sku,title,qty,note');В массиве $lines одна строка протокола: пять полей соединены табуляцией, последний маркер обозначает SQL NULL. Стрелка -> вызывает метод готового соединения; fields: задаёт именованный аргумент со списком колонок в том же порядке. supplier_items здесь — заранее созданная таблица. Полный проект создаёт собственное случайное имя. Для обычных значений удобнее кодировщик, чем ручной подсчёт обратных слешей.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Поставка | SUP-41: Кабель O'Reilly — 3; SUP-42: Бирка [TAB] синяя — 0 |
| Продолжение | SUP-43: Папка\архив — 2; SUP-44: Инструкция [LF] склад — 1 |
| Заметки | SUP-41: SQL NULL; SUP-42: пустая строка; SUP-43: буквальное \N |
| Вход | 2 строки массива + 2 строки Generator |
| Файл | supplier.tsv: 4 записи COPY text, UTF-8, без заголовка |
Самостоятельный проект, исходник и архив
<?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);
// Кодировщик именно COPY text с табуляцией; это не CSV-кодировщик.
function copyCell180(string|int|null $value): string {
if ($value===null) {return '\\N';}
$text=(string)$value;
if (str_contains($text,"\0")) {throw new InvalidArgumentException('NUL не допускается в PostgreSQL text.');}
return strtr($text,["\\"=>"\\\\","\t"=>"\\t","\n"=>"\\n","\r"=>"\\r","\x08"=>"\\b","\x0c"=>"\\f","\x0b"=>"\\v"]);
}
function copyLine180(array $record): string {
return implode("\t",array_map('copyCell180',$record))."\n";
}
function saveBytes180(string $name,string $bytes): void {
if(file_put_contents(__DIR__.'/'.$name,$bytes)!==strlen($bytes)) {
throw new RuntimeException('Файл записан не полностью.');
}
}
// Только малый, уже отобранный SQL набор с целым уникальным ordinal в первом поле.
function orderedCopy180(array $lines,int $limit): array {
if(count($lines)>$limit){throw new RuntimeException('Превышен предел выбранного набора.');}
$indexed=[];
foreach($lines as $line){
if(!is_string($line)||!str_ends_with($line,"\n")){throw new RuntimeException('Ожидалась завершённая COPY-запись.');}
$first=explode("\t",$line,2)[0];
if(!ctype_digit($first)||isset($indexed[(int)$first])){throw new RuntimeException('Неверный ordinal.');}
$indexed[(int)$first]=$line;
}
ksort($indexed,SORT_NUMERIC);
return array_values($indexed);
}
$name=p180OwnTable($db,$owned,'supplier',
'(ordinal integer PRIMARY KEY,sku text NOT NULL UNIQUE,title text NOT NULL,qty integer NOT NULL CHECK(qty>=0),note text NULL)');
$target=p180Qualified($name);
$records=[
[1,'SUP-41',"Кабель O'Reilly",3,null],
[2,'SUP-42',"Бирка\tсиняя",0,''],
[3,'SUP-43',"Папка\\архив",2,'\\N'],
[4,'SUP-44',"Инструкция\nсклад",1,"Строка 1\nСтрока 2"],
];
$first=array_map('copyLine180',array_slice($records,0,2));
$later=(function()use($records): Generator {
foreach(array_slice($records,2) as $record){yield copyLine180($record);}
})();
$db->beginTransaction();
try {
$arrayAccepted=$db->copyFromArray($target,$first,fields:'ordinal,sku,title,qty,note');
$generatorAccepted=$db->copyFromArray($target,$later,fields:'ordinal,sku,title,qty,note');
if(!$arrayAccepted||!$generatorAccepted){throw new RuntimeException('Поставка не принята.');}
$db->commit();
} catch(Throwable $e){p180Rollback($db);throw $e;}
$actual=p180Read($db,'SELECT ordinal,sku,title,qty,note FROM '.$target.' ORDER BY ordinal');
$values=array_map(static fn(array $r):array=>[(int)$r['ordinal'],$r['sku'],$r['title'],(int)$r['qty'],$r['note']],$actual);
$export=$db->copyToArray($target,fields:'ordinal,sku,title,qty,note');
if($export===false){throw new RuntimeException('Не удалось сверить поставку.');}
$ordered=orderedCopy180($export,4);
$expected=implode('',array_map('copyLine180',$records));
saveBytes180('supplier.tsv',implode('',$ordered));
$checks=[
'array_accepted'=>$arrayAccepted===true,
'generator_accepted'=>$generatorAccepted===true,
'all_values_roundtrip'=>$values===$records,
'null_empty_literal_distinct'=>$values[0][4]===null&&$values[1][4]===''&&$values[2][4]==='\\N',
'export_exact'=>file_get_contents(__DIR__.'/supplier.tsv')===$expected,
];
$rows=[
['Поставка','SUP-41: Кабель O\'Reilly — 3; SUP-42: Бирка [TAB] синяя — 0'],
['Продолжение','SUP-43: Папка\\архив — 2; SUP-44: Инструкция [LF] склад — 1'],
['Заметки','SUP-41: SQL NULL; SUP-42: пустая строка; SUP-43: буквальное \\N'],
['Вход','2 строки массива + 2 строки Generator'],
['Файл','supplier.tsv: 4 записи COPY text, UTF-8, без заголовка'],
];
$observations=['catalog'=>$actual,'copy_records'=>$ordered,'format'=>['delimiter'=>'TAB','null_marker'=>'\\N','newline'=>'LF','header'=>false],
'artifact'=>['bytes'=>filesize(__DIR__.'/supplier.tsv'),'sha256'=>hash_file('sha256',__DIR__.'/supplier.tsv')]];
} finally {
if($db instanceof PDO){try{$removed=p180Cleanup($db,$owned);}finally{p180Close($db);}}
}
p180Report('pdo180-copy-supplier','Принять четыре товара без потери табуляции, NULL и обратного слеша',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Поставка — SUP-41: Кабель O'Reilly — 3; SUP-42: Бирка [TAB] синяя — 0
Продолжение — SUP-43: Папка\архив — 2; SUP-44: Инструкция [LF] склад — 1
Заметки — SUP-41: SQL NULL; SUP-42: пустая строка; SUP-43: буквальное \N
Вход — 2 строки массива + 2 строки Generator
Файл — supplier.tsv: 4 записи COPY text, UTF-8, без заголовка
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать поставку supplier.tsv
Границы решения
Generator поддерживается начиная с PHP8.5. Его тело выдаёт готовые строки и не выполняет SQL на том же соединении во время COPY. В этом учебном наборе исходные четыре записи уже находятся в памяти: пример показывает интерфейс Traversable, а не экономию памяти. TSV не является универсальным CSV: кавычки и запятые не включают режим CSV. Ключи массива не становятся именами SQL-колонок.
Попробуйте изменить условие
Добавьте товар SUP-45 с заметкой, состоящей из двух символов: обратный слеш и N. Как не превратить её в SQL NULL?
Разбор
Передайте обычную PHP-строку в copyCell180: он удвоит обратный слеш. Только PHP null получает неэкранированный маркер. Добавьте запись и ожидаемые значения к проверке, увеличьте предел выбранного набора до5. Сама строка «NULL» также остаётся текстом.
copyFromFile: входной файл и общая транзакция
Когда поставка уже записана в локальный файл правильного формата, Pdo\Pgsql::copyFromFile читает его и передаёт записи по соединению. PHP открывает файл на своей стороне: серверу PostgreSQL не требуется видеть этот путь. Это удобно для принятого файла поставщика и для повторной загрузки ранее сохранённого supplier.tsv.
public function Pdo\Pgsql::copyFromFile(
string $tableName,
string $filename,
string $separator = "\t",
string $nullAs = "\\\\N",
?string $fields = null
): boolМетод существует с PHP8.4. filename — путь читаемого PHP-файла с записями; tableName, separator, nullAs и fields имеют договор предыдущей таблицы. Возвращается true или false. Невозможность открыть входной файл обрабатывается через ERRMODE соединения; при ERRMODE_EXCEPTION это PDOException. Ошибки количества полей, типов, уникальности или CHECK также требуют обработки. Чтение файла само по себе не делает весь последующий бизнес-процесс транзакцией.
$db->beginTransaction();
try {
$loaded = $db->copyFromFile('supplier_items', __DIR__.'/supplier.tsv',
fields: 'ordinal,sku,title,qty,note');
if (!$loaded) {throw new RuntimeException('Файл не загружен.');}
$db->commit();
} catch (Throwable $e) {
if ($db->inTransaction()) {$db->rollBack();}
throw $e;
}Этот вариант загружает файл из первого проекта в заранее созданную пустую таблицу той же формы. COPY добавляет строки: повторная загрузка тех же ключей не является автоматическим обновлением. Для замены остатков нужен иной договор — приёмная таблица и сопоставление по SKU, показанные в втором проекте. Так различаются перенос архива и применение новых данных к существующему каталогу.
Импорт из неизвестного пути не становится безопасным благодаря PDO. Приложение выбирает разрешённый файл, проверяет размер и согласованный формат. Имена колонок не берутся из первой строки файла без проверки. После SQL-ошибки внутри явной транзакции сначала выполняется rollback; новая попытка использует исправленные данные. Локальный исходник остаётся на диске, и его сохранность полезна для разбора отказа. Старое имя — PDO::pgsqlCopyFromFile.
Применить файл остатков целиком или оставить прежний каталог
Файл поставщика должен изменить остатки сразу у нескольких товаров. Если одна строка содержит отрицательное количество, нельзя оставить уже обработанную часть каталога. Вторая собственная таблица служит приёмной зоной: сначала COPY и проверка SKU, затем одно обновление каталога. Квитанция показывает конкретные остатки до и после подтверждённой операции.
- Каталог: SUP-41=3, SUP-42=0
- Первая транзакция: временно99 → COPY(8,-1) → CHECK-ошибка
- ROLLBACK: снова3/0, приёмная таблица пуста
- Вторая транзакция: COPY(8,4) → проверить SKU → UPDATE JOIN → COMMIT
- CSV-квитанция: 3→8 и0→4
Минимальный вызов
$db->beginTransaction();
try {
$db->copyFromFile('incoming_stock', __DIR__.'/incoming.tsv',
fields: 'sku,qty');
$db->commit();
} catch (Throwable $e) {
if ($db->inTransaction()) {$db->rollBack();}
throw $e;
}beginTransaction начинает общую операцию; try содержит шаги, catch получает исключение, rollBack отменяет SQL-изменения. __DIR__ — каталог PHP-скрипта, а не каталог сервера PostgreSQL. fields задаёт две колонки файла. В полном проекте commit наступает только после проверки приёмной таблицы и обновления каталога; finally отменяет намеренно ошибочную попытку.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Плохой файл | SUP-42: -1 отклонён; CHECK, SQLSTATE 23514 |
| После rollback | SUP-41 Кабель: 3; SUP-42 Хомут: 0; входная таблица пуста |
| Исправленный файл | SUP-41 Кабель: 3 → 8; SUP-42 Хомут: 0 → 4 |
| Квитанция | import-receipt.csv: оба SKU со статусом APPLIED |
Самостоятельный проект, исходник и архив
<?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);
$catalogName=p180OwnTable($db,$owned,'catalog','(sku text PRIMARY KEY,title text NOT NULL,qty integer NOT NULL CHECK(qty>=0))');
$stageName=p180OwnTable($db,$owned,'incoming','(sku text PRIMARY KEY,qty integer NOT NULL CHECK(qty>=0))');
$catalog=p180Qualified($catalogName);$stage=p180Qualified($stageName);
p180Command($db,'INSERT INTO '.$catalog.' (sku,title,qty) VALUES (?,?,?),(?,?,?)',
['SUP-41','Кабель',3,'SUP-42','Хомут',0]);
$before=p180Read($db,'SELECT sku,title,qty FROM '.$catalog.' ORDER BY sku');
$failure=null;
$db->beginTransaction();
try {
// Это изменение тоже должно отмениться, если следующая загрузка не пройдёт.
p180Command($db,'UPDATE '.$catalog.' SET qty=? WHERE sku=?',[99,'SUP-41']);
$db->copyFromFile($stage,__DIR__.'/incoming-bad.tsv',fields:'sku,qty');
throw new RuntimeException('Ошибочный файл неожиданно принят.');
} catch(PDOException $e) {
$failure=['state'=>(string)($e->errorInfo[0]??$e->getCode()),'message_present'=>$e->getMessage()!==''];
} finally {p180Rollback($db);}
$afterFailure=p180Read($db,'SELECT sku,title,qty FROM '.$catalog.' ORDER BY sku');
$stageAfterFailure=(int)p180Read($db,'SELECT COUNT(*) AS n FROM '.$stage)[0]['n'];
$db->beginTransaction();
try {
$imported=$db->copyFromFile($stage,__DIR__.'/incoming-good.tsv',fields:'sku,qty');
if(!$imported){throw new RuntimeException('Исправленный файл не загружен.');}
$unmatched=(int)p180Read($db,'SELECT COUNT(*) AS n FROM '.$stage.' s LEFT JOIN '.$catalog.' c ON c.sku=s.sku WHERE c.sku IS NULL')[0]['n'];
if($unmatched!==0){throw new RuntimeException('В файле есть неизвестные SKU.');}
$changed=p180Command($db,'UPDATE '.$catalog.' AS c SET qty=s.qty FROM '.$stage.' AS s WHERE c.sku=s.sku');
if($changed!==2){throw new RuntimeException('Ожидалось обновление двух товаров.');}
$db->commit();
} catch(Throwable $e){p180Rollback($db);throw $e;}
$after=p180Read($db,'SELECT sku,title,qty FROM '.$catalog.' ORDER BY sku');
$receipt=[['sku','before_qty','after_qty','status']];
foreach($after as $i=>$r){$receipt[]=[$r['sku'],(int)$before[$i]['qty'],(int)$r['qty'],'APPLIED'];}
$file=fopen(__DIR__.'/import-receipt.csv','wb');
if($file===false){throw new RuntimeException('Нет файла квитанции.');}
try{foreach($receipt as $r){if(fputcsv($file,$r,';','"','',"\n")===false){throw new RuntimeException('Ошибка записи квитанции.');}}}
finally{fclose($file);}
$checks=[
'bad_file_rejected'=>$failure!==null&&$failure['state']==='23514'&&$failure['message_present'],
'earlier_change_rolled_back'=>$afterFailure===$before,
'no_partial_staging'=>$stageAfterFailure===0,
'corrected_file_accepted'=>$imported===true,
'catalog_updated'=>(int)$after[0]['qty']===8&&(int)$after[1]['qty']===4,
'transaction_finished'=>!$db->inTransaction(),
];
$rows=[
['Плохой файл','SUP-42: -1 отклонён; CHECK, SQLSTATE 23514'],
['После rollback','SUP-41 Кабель: 3; SUP-42 Хомут: 0; входная таблица пуста'],
['Исправленный файл','SUP-41 Кабель: 3 → 8; SUP-42 Хомут: 0 → 4'],
['Квитанция','import-receipt.csv: оба SKU со статусом APPLIED'],
];
$observations=['before'=>$before,'after_failed_import'=>$afterFailure,'after_commit'=>$after,'failure'=>$failure,
'staging_rows_after_rollback'=>$stageAfterFailure,'receipt'=>$receipt];
} finally {
if($db instanceof PDO){try{$removed=p180Cleanup($db,$owned);}finally{p180Close($db);}}
}
p180Report('pdo180-copy-file-atomic','Применить файл остатков целиком или оставить прежний каталог',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Плохой файл — SUP-42: -1 отклонён; CHECK, SQLSTATE 23514
После rollback — SUP-41 Кабель: 3; SUP-42 Хомут: 0; входная таблица пуста
Исправленный файл — SUP-41 Кабель: 3 → 8; SUP-42 Хомут: 0 → 4
Квитанция — import-receipt.csv: оба SKU со статусом APPLIED
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать квитанцию import-receipt.csv
Границы решения
Файлы входа намеренно малы и принадлежат проекту. В рабочем импорте перед началом ограничивают размер, проверяют формат и выбирают разрешённый путь. SQL-транзакция не удаляет локальный файл и не отменяет запись CSV. Квитанция создаётся после commit: ошибка диска требует восстановления квитанции, а не слепого повторения импорта. Проверка неизвестных SKU не заменяет предметное разрешение на изменение каталога.
Попробуйте изменить условие
Как принять файл, где присутствует новый неизвестный SUP-99, не обновив известные товары наполовину?
Разбор
В этом договоре неизвестный SKU — отказ всего пакета: существующая проверка unmatched выбросит исключение до UPDATE и выполнится rollback. Для разрешённого создания новых товаров заранее измените правило импорта и обязательные поля; один остаток недостаточен для новой карточки товара.
copyToArray: сравнить небольшой набор записей
Pdo\Pgsql::copyToArray полезен, когда небольшой выбранный набор требуется сопоставить с входными записями: например, убедиться, что поставка сохранила NULL и специальные символы, или сравнить массив записей с записанным файлом. Результат здесь ближе к строкам файла, чем к результату fetch(PDO::FETCH_ASSOC).
public function Pdo\Pgsql::copyToArray(
string $tableName,
string $separator = "\t",
string $nullAs = "\\\\N",
?string $fields = null
): array|falseС PHP8.4. tableName — исходная таблица; fields выбирает экспортируемые колонки и их последовательность. separator и nullAs задают формат записей, как при импорте. Возвращается индексированный массив строк с завершающим переносом; при отказе — false либо PDOException согласно ERRMODE. Для пустой таблицы нормальный результат — пустой массив, поэтому сравнивайте с false строго.
$lines = $db->copyToArray('selected_stock', fields: 'ordinal,sku,qty');
if ($lines === false) {throw new RuntimeException('Нет выгрузки.');}
foreach ($lines as $line) {
echo htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}Если выбранная таблица содержит ordinal1, SUP-41 и qty8, одна запись имеет вид 1[TAB]SUP-41[TAB]8[LF]. В HTML значение экранировано отдельно от формата базы. Для вывода отдельных ячеек проще обычный параметризованный SELECT; COPY-строка не разбирается автоматически в PHP-поля.
Первое применение в проверке поставки сравнивает обратную выгрузку с собственным кодировщиком. Второе в сверке экспорта проверяет те же записи через файл. Оба набора заранее малы. copyToArray размещает весь возвращаемый массив в памяти; LIMIT и WHERE не являются его аргументами. Выберите нужные данные SQL заранее или используйте другой маршрут чтения. Отдельно задавайте порядок строк — fields отвечает только за колонки. Старое имя — PDO::pgsqlCopyToArray.
copyToFile: получить файл таблицы
Pdo\Pgsql::copyToFile создаёт файл из записей таблицы, когда само содержимое не нужно сначала получать большим PHP-массивом. В одном применении это переносимая выгрузка собственных остатков; в другом — файл небольшого проверяемого снимка для партнёра. Смысл таблицы, отбор и порядок задаются отдельно от записи файла.
public function Pdo\Pgsql::copyToFile(
string $tableName,
string $filename,
string $separator = "\t",
string $nullAs = "\\\\N",
?string $fields = null
): boolС PHP8.4. filename — выходной путь на стороне PHP; остальные аргументы определяют таблицу, разделитель, NULL и список колонок. true означает завершение вызова, false — отказ без исключения. Ошибка открытия или записи файла подчиняется ERRMODE; с ERRMODE_EXCEPTION выбрасывается PDOException. Нужны права PHP на запись. Драйвер8.5.11 открывает файл в режиме wb: существующее содержимое может быть заменено, а при отказе может остаться неполный файл.
$ok = $db->copyToFile('supplier_items', __DIR__.'/supplier-raw.tsv',
fields: 'ordinal,sku,title,qty,note');
if (!$ok) {throw new RuntimeException('Архив не создан.');}
echo filesize(__DIR__.'/supplier-raw.tsv');Первое применение — сохранить собственную поставку в формате, который затем принимает copyFromFile. Сумма байтов помогает диагностике, но полезность файла определяется его товарами и договором колонок. Второе — снимок выбранных остатков: результат сравнивается с copyToArray, а пользователь получает файл с явным порядком. В обоих случаях используйте отдельное имя назначения; не направляйте экспорт на единственную копию исходного файла.
Запись файла не откатывается SQL-командой rollback. Для публикации рабочего экспорта обычно сначала пишут отдельный временный файл, проверяют результат и лишь затем заменяют доступный пользователю файл. Такое файловое переключение требует собственного договора; три проекта здесь используют отдельные учебные каталоги и не объявляют межсистемную атомарность. Старое имя — PDO::pgsqlCopyToFile.
Выгрузить выбранный остаток и сверить два пути COPY
Партнёру нужен остаток только своего поставщика. Метод copyToFile не имеет аргументов WHERE и ORDER BY. Поэтому SQL сначала формирует небольшую собственную таблицу снимка. Два пути COPY читают уже выбранные данные; итоговый selected.tsv получает явный порядок по ordinal, и его можно побайтно сверить с путём через массив.
- Исходный каталог: 4 товара двух поставщиков
- SQL WHERE supplier=S1 AND qty>0 + ORDER BY sku LIMIT2
- Собственный снимок: ordinal1=Кабель8, ordinal2=Лента5
- copyToArray и copyToFile: две формы передачи тех же записей
- Сортировка двух записей по ordinal → selected.tsv; каталог уже может измениться
Минимальный вызов
$records = $db->copyToArray('selected_stock', fields: 'ordinal,sku,qty');
$written = $db->copyToFile('selected_stock', __DIR__.'/selected-raw.tsv',
fields: 'ordinal,sku,qty');$records содержит строки COPY text, а не ассоциативные записи fetch. $written — признак записи файла, не число строк и не его содержимое. Оба вызова читают заранее подготовленную selected_stock. fields явно выбирает порядок колонок, но не сортирует строки. В полном проекте SQL отбирает два товара, ordinal закрепляет требуемый порядок, а нормализация применяется только к этому маленькому набору.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Условие | Поставщик S1, qty > 0, не более2 товаров по SKU |
| Снимок | 1: SUP-41 Кабель — 8; 2: SUP-44 Лента — 5 |
| Не вошли | SUP-42: нулевой остаток; SUP-43: другой поставщик |
| После изменения каталога | SUP-41 в каталоге10, в выгрузке выбранного снимка8 |
| Сверка | copyToArray и copyToFile содержат те же2 записи; selected.tsv упорядочен по ordinal |
Самостоятельный проект, исходник и архив
<?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);
// Кодировщик именно COPY text с табуляцией; это не CSV-кодировщик.
function copyCell180(string|int|null $value): string {
if ($value===null) {return '\\N';}
$text=(string)$value;
if (str_contains($text,"\0")) {throw new InvalidArgumentException('NUL не допускается в PostgreSQL text.');}
return strtr($text,["\\"=>"\\\\","\t"=>"\\t","\n"=>"\\n","\r"=>"\\r","\x08"=>"\\b","\x0c"=>"\\f","\x0b"=>"\\v"]);
}
function copyLine180(array $record): string {
return implode("\t",array_map('copyCell180',$record))."\n";
}
function saveBytes180(string $name,string $bytes): void {
if(file_put_contents(__DIR__.'/'.$name,$bytes)!==strlen($bytes)) {
throw new RuntimeException('Файл записан не полностью.');
}
}
// Только малый, уже отобранный SQL набор с целым уникальным ordinal в первом поле.
function orderedCopy180(array $lines,int $limit): array {
if(count($lines)>$limit){throw new RuntimeException('Превышен предел выбранного набора.');}
$indexed=[];
foreach($lines as $line){
if(!is_string($line)||!str_ends_with($line,"\n")){throw new RuntimeException('Ожидалась завершённая COPY-запись.');}
$first=explode("\t",$line,2)[0];
if(!ctype_digit($first)||isset($indexed[(int)$first])){throw new RuntimeException('Неверный ordinal.');}
$indexed[(int)$first]=$line;
}
ksort($indexed,SORT_NUMERIC);
return array_values($indexed);
}
$sourceName=p180OwnTable($db,$owned,'stock_source','(sku text PRIMARY KEY,supplier text NOT NULL,title text NOT NULL,qty integer NOT NULL)');
$snapshotName=p180OwnTable($db,$owned,'stock_snapshot','(ordinal integer PRIMARY KEY,sku text NOT NULL,title text NOT NULL,qty integer NOT NULL)');
$source=p180Qualified($sourceName);$snapshot=p180Qualified($snapshotName);
foreach([['SUP-41','S1','Кабель',8],['SUP-42','S1','Хомут',0],['SUP-43','S2','Верёвка',9],['SUP-44','S1','Лента',5]] as $r){
p180Command($db,'INSERT INTO '.$source.' (sku,supplier,title,qty) VALUES (?,?,?,?)',$r);
}
$empty=$db->copyToArray($snapshot,fields:'ordinal,sku,title,qty');
p180Command($db,'INSERT INTO '.$snapshot.' (ordinal,sku,title,qty) SELECT CAST(row_number() OVER (ORDER BY sku) AS integer),sku,title,qty FROM '.$source.' WHERE supplier=? AND qty>0 ORDER BY sku LIMIT 2',['S1']);
// Меняется исходный каталог, а выбранный снимок уже зафиксировал8.
p180Command($db,'UPDATE '.$source.' SET qty=? WHERE sku=?',[10,'SUP-41']);
$selected=p180Read($db,'SELECT ordinal,sku,title,qty FROM '.$snapshot.' ORDER BY ordinal');
$fromArray=$db->copyToArray($snapshot,fields:'ordinal,sku,title,qty');
if($fromArray===false){throw new RuntimeException('COPY в массив не завершён.');}
$toFile=$db->copyToFile($snapshot,__DIR__.'/copy-raw.tsv',fields:'ordinal,sku,title,qty');
if(!$toFile){throw new RuntimeException('COPY в файл не завершён.');}
$raw=file(__DIR__.'/copy-raw.tsv');
if($raw===false){throw new RuntimeException('Нет экспортированного файла.');}
$a=orderedCopy180($fromArray,2);$b=orderedCopy180($raw,2);
saveBytes180('selected.tsv',implode('',$b));
$fresh=p180Read($db,'SELECT qty FROM '.$source.' WHERE sku=?',['SUP-41'])[0]['qty'];
$checks=[
'empty_array'=>$empty===[],
'selected_only'=>count($selected)===2&&$selected[0]['sku']==='SUP-41'&&$selected[1]['sku']==='SUP-44',
'snapshot_values'=>(int)$selected[0]['qty']===8&&(int)$selected[1]['qty']===5&&(int)$fresh===10,
'array_file_agree'=>$a===$b&&$toFile===true,
'artifact_matches'=>file_get_contents(__DIR__.'/selected.tsv')===implode('',$a),
];
$rows=[
['Условие','Поставщик S1, qty > 0, не более2 товаров по SKU'],
['Снимок','1: SUP-41 Кабель — 8; 2: SUP-44 Лента — 5'],
['Не вошли','SUP-42: нулевой остаток; SUP-43: другой поставщик'],
['После изменения каталога','SUP-41 в каталоге10, в выгрузке выбранного снимка8'],
['Сверка','copyToArray и copyToFile содержат те же2 записи; selected.tsv упорядочен по ordinal'],
];
$observations=['selected'=>$selected,'fresh_source_qty'=>(int)$fresh,'array_records'=>$a,'file_records'=>$b,
'order_contract'=>'SQL selection + explicit ordinal; bounded normalization after COPY',
'artifact'=>['bytes'=>filesize(__DIR__.'/selected.tsv'),'sha256'=>hash_file('sha256',__DIR__.'/selected.tsv')]];
} finally {
if($db instanceof PDO){try{$removed=p180Cleanup($db,$owned);}finally{p180Close($db);}}
}
p180Report('pdo180-copy-reconciliation','Выгрузить выбранный остаток и сверить два пути COPY',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Условие — Поставщик S1, qty > 0, не более2 товаров по SKU
Снимок — 1: SUP-41 Кабель — 8; 2: SUP-44 Лента — 5
Не вошли — SUP-42: нулевой остаток; SUP-43: другой поставщик
После изменения каталога — SUP-41 в каталоге10, в выгрузке выбранного снимка8
Сверка — copyToArray и copyToFile содержат те же2 записи; selected.tsv упорядочен по ordinal
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать выбранный остаток selected.tsv
Границы решения
COPY всей таблицы не обещает порядок вставки или первичного ключа. Здесь сравниваются нормализованные записи, а исходный copy-raw.tsv не объявлен отсортированным. Таблица снимка — сохранённый результат одной выборки, не долгоживущая гарантия неизменности рабочего каталога. Для большой выгрузки не переносите эту сортировку целиком в PHP: нужен отдельный, проверенный маршрут упорядоченного серверного COPY или последовательное чтение SELECT ORDER BY.
Попробуйте изменить условие
Партнёр просит три позиции, включая товары с нулевым остатком. Какие две части договора меняются?
Разбор
Уберите условие qty>0, измените SQL LIMIT на3 и предел orderedCopy180 на3; ожидаемый файл теперь включает SUP-42. Отбор остаётся в SQL. Нельзя просто увеличить PHP-предел: он проверяет размер уже выбранного набора, а не добавляет отсутствующие товары.
Отбор, порядок, имена и отказ передачи
У COPY-методов нет параметров для WHERE, ORDER BY, FORMAT CSV, HEADER или произвольной команды. PostgreSQL как сервер умеет больше, но это не расширяет автоматически контракт конкретного PHP-метода. В этой главе tableName всегда остаётся проверенным именем таблицы, fields — фиксированным списком колонок.
- WHERE supplier=S1 AND qty>0: какие товары нужны
- ORDER BY sku LIMIT2: какие два товара входят в снимок
- ordinal: номер выбранного товара, сохранённый в снимке
- COPY: перенос записей снимка без обещания порядка
- Нормализация двух строк по ordinal: точный selected.tsv
В PHP8.5.11 обёртка собирает текст COPY из tableName, fields и параметров формата. Она не вызывает escapeIdentifier для этих аргументов и не связывает их как значения prepare. Поэтому имена берутся из реестра своих объектов, кавычки применяются к имени схемы и таблицы отдельно, а формат не зависит от URL или заголовка внешнего файла. Параметры и идентификаторы PDO_PGSQL объясняются в соседней главе.
Выборка без ORDER BY не гарантирует порядок. Важно различать физическую очередность записей и предметный номер: первичный ключ ordinal не заставляет COPY сортировать вывод. Проект сортирует только две уже выбранные строки; он не читает весь рабочий каталог ради фильтрации в PHP. Сравнение сырых файлов без договорённости о порядке могло бы показать разные хеши у одинаковых наборов.
| Отказ или ограничение | Действие приложения |
|---|---|
| Неверное число полей или CHECK | Сохранить безопасную диагностику, отменить явную транзакцию, исправить вход |
| Ошибка открытия файла | Проверить разрешённый путь и права PHP; не путать с доступом сервера к его файлам |
| Ошибка записи после части вывода | Не публиковать частичный файл; SQL rollback не удалит его |
| Неправильный PHP-тип | Исправить контракт до COPY; TypeError не является отказом PostgreSQL |
| Повторный файл с теми же ключами | Заранее выбрать правило дубликатов; COPY не является UPSERT |
| Рост объёма | Ограничить вход, планировать staging и место на диске; измерять конкретный маршрут |
ERRMODE_SILENT оставляет код ошибки для явной проверки, ERRMODE_WARNING дополнительно выдаёт предупреждение, ERRMODE_EXCEPTION передаёт PDOException. Здесь выбран последний режим, но ожидаемый отказ всё равно перехватывается локально и показывается понятным результатом. Полный текст SQL-ошибки может включать значения и внутренние имена, поэтому публичный отчёт сохраняет SQLSTATE и предметное объяснение, а не весь текст исключения.
COPY объединяет передачу одинаковых записей, но эта глава не измеряет скорость и не обещает превосходство для любого объёма. Важнее сохранённый смысл данных и проверенная граница завершения. Если нужна выдача отдельного двоичного вложения или уведомление после commit, следующий шаг — вложения и события PDO_PGSQL.
Четыре старых имени PDO и переход на Pdo\Pgsql
В существующем PHP-проекте могут встретиться методы PDO с префиксом pgsql. Они доступны с PHP5.3.3 и объявлены устаревшими с PHP8.5. Современные имена принадлежат Pdo\Pgsql, появившемуся в PHP8.4. При сопровождении сначала определяют версию PHP и способ создания соединения, затем заменяют имя вызова; формат данных, права и транзакцию сохраняют. Старые имена не являются новым механизмом обмена.
#[\Deprecated]
public function PDO::pgsqlCopyFromArray(
string $tableName, array|Traversable $rows,
string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null
): bool
#[\Deprecated]
public function PDO::pgsqlCopyFromFile(
string $tableName, string $filename,
string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null
): bool
#[\Deprecated]
public function PDO::pgsqlCopyToArray(
string $tableName, string $separator = "\t",
string $nullAs = "\\\\N", ?string $fields = null
): array|false
#[\Deprecated]
public function PDO::pgsqlCopyToFile(
string $tableName, string $filename,
string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null
): boolЭто справочная запись сигнатур, а не программа для выполнения. #[Deprecated] обозначает устаревание. Для старого pgsqlCopyFromArray тип Traversable также добавлен только в8.5; до этого rows был массивом строк. У file-методов filename остаётся PHP-путём, у toArray результат остаётся массивом записей, у остальных — bool. Все общие аргументы, правила NULL, отказов и ERRMODE совпадают с соответствующим современным методом выше.
| Старое имя → современный вызов | Первое применение | Второе применение |
|---|---|---|
| PDO::pgsqlCopyFromArray → Pdo\Pgsql::copyFromArray | Принять небольшую поставку массивом | Подготовить последовательный источник Generator в PHP8.5, сохранив кодировщик |
| PDO::pgsqlCopyFromFile → Pdo\Pgsql::copyFromFile | Применить файл остатков через staging | Загрузить сохранённый supplier.tsv в пустую таблицу той же формы |
| PDO::pgsqlCopyToArray → Pdo\Pgsql::copyToArray | Проверить обратное преобразование NULL и текста | Сопоставить выбранные записи с файловым экспортом |
| PDO::pgsqlCopyToFile → Pdo\Pgsql::copyToFile | Передать проверенный снимок партнёру | Сохранить поставку для последующей загрузки через copyFromFile |
// Было в существующем коде:
$oldPdo->pgsqlCopyFromFile('incoming_stock', $approvedFile,
fields: 'sku,qty');
// После перевода создания соединения на Pdo\Pgsql:
$pgsql->copyFromFile('incoming_stock', $approvedFile,
fields: 'sku,qty');Пара строк показывает замену имени, выполнять оба импорта подряд не нужно. Второй пример сопровождения — заменить pgsqlCopyToFile на copyToFile для прежнего файла выбранной таблицы, сохранив проверку результата и порядок колонок. Новые проекты используют современные методы; старые вызовы здесь не объявлены отдельно исполненными. Канонические карточки и прежние учебные определения остаются доступными по ссылкам.
Упражнение: в старом импорте разработчик заменил имя метода, а separator изменил с табуляции на запятую, оставив файл прежним. Почему перенос версии не решит отказ? Разбор: это два разных изменения. Для миграции API сохраняют договор формата; изменение разделителя требует заново сформировать файл и проверить его поля. Исправьте separator либо преобразуйте записи отдельным шагом, затем проверьте те же товары через SELECT.
Источники и обсуждения
- PHP Manual: Pdo\Pgsql::copyFromArray
- PHP Manual: Pdo\Pgsql::copyFromFile
- PHP Manual: Pdo\Pgsql::copyToArray
- PHP Manual: Pdo\Pgsql::copyToFile
- PHP Manual: устаревший PDO::pgsqlCopyFromArray
- PHP Manual: устаревший PDO::pgsqlCopyFromFile
- PHP Manual: устаревший PDO::pgsqlCopyToArray
- PHP Manual: устаревший PDO::pgsqlCopyToFile
- PHP8.5.11: реализация COPY-методов
- PostgreSQL18: COPY
- PostgreSQL18: явный порядок результата
- PostgreSQL18: транзакции
- PHP Manual: обработка ошибок PDO
- PHP Manual: генераторы
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


