Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 291 · После подключения и подготовленного запроса

PDO_CUBRID: вложения, состав комплекта и порядок работ

Файл с точными байтами, комплект с повторяющимися деталями и последовательность шагов: сохранение, чтение и ограничения PDO_CUBRID.

Выбрать форму данных для сервисного заказа

У сервисного заказа могут быть вложение с показаниями прибора, набор меток, состав комплекта и порядок действий мастера. Это разные данные. Для вложения требуется сохранить каждый байт; для меток — убрать повтор; для комплекта — сохранить количество одинаковых деталей; для действий — сохранить порядок. Выбор типа начинается с этого требования.

Один заказ — три самостоятельных результата
  1. Файл измерений → BLOB → attachment.bin с теми же байтами
  2. Метки и детали → SET / MULTISET → kit.csv
  3. Шаги мастера → SEQUENCE → checklist.csv с номерами

Приложению нужны определённые строки: товары на экране, остаток после заявки или описание столбцов для импорта. PDO даёт объект соединения и объекты подготовленных запросов. PDO_CUBRID связывает этот интерфейс с CUBRID; особенности SQL, типов и транзакций остаются важны.

Проверяемая среда совместимости: PHP 7.4.33, PDO core, PDO_CUBRID v11.2.0.0002 и CUBRID 11.3.5. Это отдельный стенд для исторического драйвера, а не рекомендация менять PHP существующего сайта. Начало PDO объясняет общий интерфейс, а начало CUBRID — таблицу и SQL.

Сначала пройдите подключение, подготовленный запрос и транзакцию. Здесь $db — уже созданный объект PDO, $statement — подготовленный запрос. Знак -> обращается к методу объекта; ? — место одного значения, которое передаётся отдельно от текста SQL. Полные ZIP создают собственные таблицы в отдельной учебной базе и удаляют их после получения результата. Короткие записи показывают сам механизм внутри этих проектов.

PHP-массив и коллекция CUBRID не одно и то же. Массив живёт в PHP-процессе; значение SET, MULTISET или SEQUENCE хранится в столбце базы. Драйвер переводит данные через CCI. Возможности этого перевода зависят от версии расширения, поэтому одинаковое имя PDO::PARAM_* не обещает одинакового поведения у всех баз.

BLOB, CLOB и поток: сохранить байты и освободить ресурсы

Пусть прибор прислал файл, в котором встречаются нулевой байт и байт FF. Это не строка для перекодировки и не HTML. BLOB хранит двоичные данные. CLOB предназначен для текста; кодировка и длина в символах имеют смысл отдельно от длины в байтах. Маленькую заметку заказа удобно хранить в VARCHAR: выбор LOB не определяется универсальным порогом «больше четырёх килобайт».

СостояниеСмысл в приложенииКак различать
NULLФайл или текст ещё не предоставленЗначение === null / SQL IS NULL
Непустой LOBЕсть содержимоеЧтение потока и фактическое число байт
Пустой LOBПользователь передал файл длиной 0 байтЕсть объект/поток, но чтение даёт 0 байт
Файл проходит через запрос
  1. fopen(..., rb) открывает источник
  2. bindParam связывает переменную потока с ?
  3. execute читает источник и передаёт LOB
  4. SELECT возвращает поток → fread → файл получателя
$input = fopen('measurements.bin', 'rb');
$statement = $db->prepare('INSERT INTO attachments (id, payload) VALUES (1, ?)');
$statement->bindParam(1, $input, PDO::PARAM_LOB);
$statement->execute();
fclose($input);

Число 1 в bindParam — первое место ? в SQL, а не id записи. $input — переменная с открытым потоком, не сами байты файла. bindParam связывает её по ссылке; читать содержимое драйвер будет при execute. Поэтому источник нельзя закрывать до execute. rb означает чтение двоичных данных. Проверки fopen, результата execute и закрытие при исключении показаны в полном проекте ниже.

В закреплённом PHP7-исходнике PDO_CUBRID PARAM_LOB выбирает тип BLOB. Ветвь передачи имени файла тоже есть: строка с этим типом трактуется как имя источника, который драйвер открывает, а не как готовое содержимое. Не подставляйте туда произвольный путь из HTTP-запроса и не принимайте бинарную строку за путь. Для текстового CLOB ниже отдельно проверяется SQL-преобразование и чтение результата; произвольная пятая строка "clob" не включает отсутствующую ветвь обработки параметра.

Полученный LOB-поток связан с запросом. Сначала прочитайте и закройте его, затем закройте курсор/освободите PDOStatement. В исходнике этой версии поток предназначен для чтения, обработчика seek нет. Не обещайте fseek/rewind как у обычного дискового файла. Чтобы прочитать снова, выполните отдельный SELECT или сохраните данные в контролируемый временный файл.

Чтение fread порциями ограничивает размер PHP-буфера в нашем экспортёре. Оно не доказывает, что драйвер, CCI или сервер вообще нигде не буферизуют объект. В каталоге выбирайте id, имя, размер и MIME; сами LOB выбирайте только для запрошенного вложения. Это та же причина избегать ненужного SELECT *: не передавать содержимое всех файлов ради списка заголовков.

В веб-обработчике после проверки права на конкретный заказ можно отправить Content-Type и затем fpassthru($stream), до любых пробелов или HTML. Это описание HTTP-встраивания, а не заявление о запуске веб-сервера в этом пакете: здесь реально проверяется CLI-экспорт с сопоставлением байтов. Для сравнения с native LOB2 см. сохранение LOB и чтение LOB.

Сохранить файл прибора и выгрузить те же байты

Сервис принимает маленькую запись прибора с заголовком MTR1 и бинарными значениями. После хранения нужен файл attachment.bin, совпадающий с исходником байт в байт, и читаемая текстовая инструкция. HEX ниже позволяет увидеть даже нулевой байт и FF: это содержимое полезного результата, а не только сообщение «файл создан».

Путь данных и полученный результат
  1. measurements.hex → 9 двоичных байт с заголовком MTR1
  2. PARAM_LOB → payload; текст → CAST(? AS CLOB)
  3. SELECT только id=1 → два потока
  4. fread по 3 байта → attachment.bin → сравнение байтов и SHA-256

Минимальный вызов

$input = fopen('input.bin', 'rb');
$statement->bindParam(1, $input, PDO::PARAM_LOB);
$statement->execute();
fclose($input);

execute выполняется, пока источник открыт. В полном примере SQL-столбцы payload BLOB и body CLOB независимы. Текст передан PARAM_STR в CAST(? AS CLOB); полученные потоки читаются до closeCursor. Цикл fwrite учитывает частичную запись.

Что получится после запуска

НаблюдениеРезультат
Сохранено и выгружено байт9
Непустых порций по 3 байта3
HEX выгруженного файла4d5452310000ff10fe
UTF-8 текст прочитан без измененияда
Пустой BLOB отличается от SQL NULLда
Самостоятельный проект, исходник и архив
Сохранить файл прибора и выгрузить те же байтыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

    $name=p175OwnName($db,$owned,'TABLE','attachment');$table=p175Qualified($name);
    p175Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,payload BLOB,body CLOB)');
    $bytes=hex2bin(trim(file_get_contents(__DIR__.'/measurements.hex')));
    if ($bytes===false || file_put_contents(__DIR__.'/input.bin',$bytes)!==strlen($bytes)) {throw new RuntimeException('Источник измерений не подготовлен.');}
    $input=fopen(__DIR__.'/input.bin','rb');
    if (!is_resource($input)) {throw new RuntimeException('Файл измерений не открыт.');}
    $insert=$db->prepare('INSERT INTO '.$table.' (id,payload,body) VALUES (1,?,CAST(? AS CLOB))');
    $instruction="Инструкция O'Reilly\nКабель: 3 шт.\n";
    try {
        if (!$insert->bindParam(1,$input,PDO::PARAM_LOB) || !$insert->bindValue(2,$instruction,PDO::PARAM_STR) || !$insert->execute()) {throw new RuntimeException('Вложение не сохранено.');}
    } finally {fclose($input);p175Release($insert);}
    file_put_contents(__DIR__.'/empty.bin','');$empty=fopen(__DIR__.'/empty.bin','rb');
    $insert=$db->prepare('INSERT INTO '.$table.' (id,payload,body) VALUES (2,?,NULL)');
    try {$insert->bindParam(1,$empty,PDO::PARAM_LOB);if (!$insert->execute()) {throw new RuntimeException('Пустое вложение не сохранено.');}}
    finally {fclose($empty);p175Release($insert);}
    p175Command($db,'INSERT INTO '.$table.' (id,payload,body) VALUES (3,NULL,NULL)');

    // Потоки живут до освобождения этого результата; не используем helper p175Read.
    $select=p175Request($db,'SELECT payload,body FROM '.$table.' WHERE id=?',[1]);
    $record=$select->fetch(PDO::FETCH_ASSOC);$output=null;$chunkCount=0;$written=0;$body='';
    if (!is_array($record) || !is_resource($record['payload']) || !is_resource($record['body'])) {throw new RuntimeException('Ожидались два LOB-потока.');}
    try {
        $output=fopen(__DIR__.'/attachment.bin','wb');
        if (!is_resource($output)) {throw new RuntimeException('Экспортный файл не открыт.');}
        while(true) {
            $chunk=fread($record['payload'],3);
            if ($chunk===false) {throw new RuntimeException('BLOB не прочитан.');}
            if ($chunk==='') {break;}
            $offset=0;
            while($offset<strlen($chunk)) {
                $n=fwrite($output,substr($chunk,$offset));
                if ($n===false || $n===0) {throw new RuntimeException('Файл записан не полностью.');}
                $offset+=$n;$written+=$n;
            }
            $chunkCount++;
        }
        while(true) {
            $chunk=fread($record['body'],7);
            if ($chunk===false) {throw new RuntimeException('CLOB не прочитан.');}
            if ($chunk==='') {break;}
            $body.=$chunk;
            if (strlen($body)>1024) {throw new RuntimeException('Учебная инструкция слишком велика.');}
        }
    } finally {
        if (is_resource($output)) {fclose($output);}
        fclose($record['payload']);fclose($record['body']);p175Release($select);
    }
    $select=p175Request($db,'SELECT id,payload,body FROM '.$table.' WHERE id IN (2,3) ORDER BY id');
    try {
        $emptyRow=$select->fetch(PDO::FETCH_ASSOC);$emptyResource=is_resource($emptyRow['payload']);
        $emptyBytes=$emptyResource?stream_get_contents($emptyRow['payload']):false;
        if ($emptyResource) {fclose($emptyRow['payload']);}
        $nullRow=$select->fetch(PDO::FETCH_ASSOC);
        $nullPreserved=$nullRow['payload']===null && $nullRow['body']===null;
    } finally {p175Release($select);}
    $export=file_get_contents(__DIR__.'/attachment.bin');
    $checks['binary_roundtrip']=$export===$bytes;
    $checks['nul_and_ff']=strpos($export,"\0")!==false && strpos($export,"\xff")!==false;
    $checks['chunked_export']=$chunkCount===3 && $written===9;
    $checks['clob_utf8_roundtrip']=$body===$instruction;
    $checks['empty_lob_is_resource']=$emptyResource && $emptyBytes==='';
    $checks['sql_null_distinct']=$nullPreserved && $emptyRow['body']===null;
    $checks['file_hash']=hash_file('sha256',__DIR__.'/attachment.bin')===hash('sha256',$bytes);
    $rows=[['Сохранено и выгружено байт',$written],['Непустых порций по 3 байта',$chunkCount],['HEX выгруженного файла',bin2hex($export)],
        ['UTF-8 текст прочитан без изменения',$body===$instruction],['Пустой BLOB отличается от SQL NULL',$emptyResource && $emptyBytes==='' && $nullPreserved]];
    $observations['attachment']=['bytes'=>$written,'sha256'=>hash('sha256',$export),'clob_bytes'=>strlen($body),
        'blob_fetch_type'=>'resource','clob_fetch_type'=>'resource','stream_closed_before_statement'=>true];

} finally {

    if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}

p175Report('pdoc175-attachment','Сохранить файл прибора и выгрузить те же байты',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Сохранено и выгружено байт — 9
Непустых порций по 3 байта — 3
HEX выгруженного файла — 4d5452310000ff10fe
UTF-8 текст прочитан без изменения — да
Пустой BLOB отличается от SQL NULL — да

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.

Открыть результат выполнения · Посмотреть данные JSON · Скачать файл прибора

Границы решения

Проверен небольшой файл и текст в отдельной базе. Это не тест гигабайтного объекта и не HTTP-доставка. Ограничение PHP-буфера экспортёра не измеряет скрытые буферы CCI. NULL означает отсутствие, пустой BLOB — существующее вложение нулевой длины. Текст инструкции для сравнения ограничен 1024 байтами; большой текст можно также сразу писать порциями в файл.

Попробуйте изменить условие

Замените последний байт FE на 7F. Что должно измениться в результате, а что останется тем же?

Разбор

Длина останется 9 байт, непустых порций останется 3. Последняя пара HEX и SHA-256 изменятся; текстовый CLOB останется прежним. Сравнивать только длину недостаточно: она не замечает подмену одного байта.

Метки, детали и шаги: SET, MULTISET и SEQUENCE

Представьте коробку сервисного комплекта. На крышке метки «монтаж» и «выезд» — повторять одну метку бессмысленно. Внутри два одинаковых хомута — повтор важен. Инструкция «отключить → проверить → включить» зависит от порядка. Три вида коллекций отвечают на эти три требования.

Тип CUBRIDЧто сохраняетсяПрактический смысл
SETУникальные значения; порядок не является контрактомНабор меток заказа
MULTISETПовторяющиеся значения; порядок не является контрактомКомплект с двумя одинаковыми деталями
SEQUENCEПорядок и повторяющиеся значенияШаги работы или маршрут

Скобки PHP [ ... ] создают массив. SQL-запись SET OF VARCHAR(32) объявляет тип столбца, а MULTISET OF INT — коллекцию целых чисел. Это не список отдельных строк SQL-таблицы. SEQUENCE в этом разделе — тип коллекции; он не выдаёт автоматически номера заказов и не заменяет SERIAL/AUTO_INCREMENT.

$labels = ['монтаж', 'выезд', 'монтаж'];
$parts = [101, 101, 205];
$steps = ['Отключить питание', 'Проверить отсутствие напряжения', 'Включить питание'];

После записи в SET первая коллекция должна содержать две разные метки; в MULTISET должны остаться три детали, включая два значения 101; в SEQUENCE должны остаться три шага в заданном порядке. Не сравнивайте SET и MULTISET с исходным PHP-массивом по индексам. Для SET сравнивайте отсортированные значения, для MULTISET — частоты, для SEQUENCE — саму последовательность.

Для запроса «какие заявки содержат деталь 101» и особенно для связей с каталогом деталей часто удобнее дочерняя таблица order_parts(order_id, part_id, quantity) с внешними ключами. Коллекция значений не даёт каждой детали все преимущества отдельной строки: независимый ключ, собственные атрибуты и обычный JOIN. Если порядок требует отдельного редактирования и конкурентной проверки, подойдёт order_steps(order_id, position, text, version). Ключи и связи схемы помогают выбрать структуру.

bindParam и тип коллекции: официальный контракт и PHP7-порт

У общего PDO вызов bindParam($parameter, &$variable, $type, $maxLength, $driverOptions) привязывает переменную. Первое место ? нумеруется с 1. Параметр $type — подсказка драйверу о типе; $maxLength и $driverOptions поддерживаются по-разному. Передача массива одним параметром не является общим правилом PDO и не раскрывает автоматически список для SQL IN (...).

Официальная страница PDO_CUBRID описывает исторический способ: массив с PDO::PARAM_NULL и пятая строка с именем типа, например int. Для закреплённого PHP7-порта это нужно читать вместе с исходником: комментарий driver_params присутствует, но значение пятого аргумента в обработчике не читается; При PHP-массиве обработчик меняет внешний тип на CCI SET, поэтому PARAM_NULL здесь не означает SQL NULL: такой вызов в проверке действительно сохранил SET. У обычного NULL смысл другой. PARAM_STR способен преобразовать массив в строку ещё в общем слое PDO, до обработчика CUBRID; этот вариант дал отказ. Пятый int в проверке не мешал сохранению чисел, однако это не доказательство чтения пятого аргумента: преобразование произошло по типу столбца и пути CCI. Проекты ниже показывают реальную запись и проверку результата, а не делают вывод о работе опции по одному успешному execute.

Имена типов из официального исторического спискаНазначение в том контракте
CHAR, STRING, NCHAR, VARNCHARСимвольные значения и национальные символьные типы
BIT, VARBITБитовые значения
NUMERIC, NUMBERТочные десятичные числа
INT, SHORT, BIGINTЦелые числа разной разрядности
MONETARY, FLOAT, DOUBLEДеньги и числа с плавающей точкой
DATE, TIME, DATETIME, TIMESTAMPДата и/или время

Это полный перечень 18 имён из официального описания, а не 18 проверенных режимов пятого аргумента в нашем бинарнике. В выбранной реализации отправляемые скаляры PARAM_INT и PARAM_STR, массивы и поток PARAM_LOB проходят разные ветви. PARAM_BOOL здесь не объявляем поддерживаемым отдельным режимом: для логического флага в целочисленном столбце используйте явно 0/1 и PARAM_INT. Проверка бизнес-значения — отдельная обязанность: тип параметра не проверяет остаток, право пользователя или разумный размер коллекции.

Если сервер не принимает нужную коллекцию или драйвер не сохраняет её семантику, не собирайте SQL склеиванием пользовательских строк. Используйте поддерживаемую подготовленную форму конструктора с отдельными параметрами либо нормализованную дочернюю таблицу. Проверка этой версии показала: массив с PARAM_NULL при записи в MULTISET теряет повторы, а в SEQUENCE — повторы и исходный порядок. Поэтому ниже применяется CAST({?,?,?} AS MULTISET) или CAST({?,?,?,?} AS SEQUENCE) с отдельными скалярными параметрами. Пустой PHP-массив этой ветви не вызываем: в исходнике отсутствует безопасная инициализация элементного типа при нуле элементов; пустую коллекцию задают отдельной SQL-формой, NULL — отдельным значением. Размер списка ограничивайте до отправки; чтение всего каталога и сбор миллионов элементов в один массив проблему не решает.

Сохранить метки и комплект с двумя одинаковыми деталями

К заявке относятся метки «выезд» и «монтаж». Мастер берёт два хомута и один кабель. Метки нужно убрать из повторов, а хомуты — сохранить в количестве двух. CSV ниже получается из реально прочитанных значений базы, сгруппированных SQL-запросом.

Путь данных и полученный результат
  1. монтаж, выезд, монтаж → SET → две метки
  2. Хомут, Хомут, кабель → MULTISET → три детали
  3. TABLE(parts) + GROUP BY → кабель: 1, хомут: 2
  4. Нужные строки → kit.csv

Минимальный вызов

$labels = ['монтаж', 'выезд', 'монтаж'];
$statement = $db->prepare('UPDATE kits SET tags=? WHERE id=1');
$statement->bindParam(1, $labels, PDO::PARAM_NULL);
$statement->execute();

PARAM_NULL в специальной ветви массива PDO_CUBRID сохраняет SET; это не переносимый рецепт PDO для других драйверов. Для MULTISET подготовленная запись CAST({?,?,?} AS MULTISET) сохраняет все три переданных значения. TABLE(t.parts) даёт строки элементов, COUNT и GROUP BY сразу считают нужные количества в SQL.

Что получится после запуска

НаблюдениеРезультат
Уникальных меток2
Всего деталей в комплекте3
Хомутов2
Уникальные номера101, 205
Прямой fetch коллекции возвращает строкуда
Самостоятельный проект, исходник и архив
Сохранить метки и комплект с двумя одинаковыми деталямиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

    $name=p175OwnName($db,$owned,'TABLE','kit');$table=p175Qualified($name);
    p175Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,tags SET(VARCHAR(40)),parts MULTISET(VARCHAR(40)),numbers SET(INT))');
    p175Command($db,'INSERT INTO '.$table.' (id) VALUES (1)');
    $labels=['монтаж','выезд','монтаж'];$labelsBefore=$labels;
    $statement=$db->prepare('UPDATE '.$table.' SET tags=? WHERE id=1');
    try {$statement->bindParam(1,$labels,PDO::PARAM_NULL);if (!$statement->execute()) {throw new RuntimeException('Метки не сохранены.');}}
    finally {p175Release($statement);}
    $numbers=[205,101,205];
    $statement=$db->prepare('UPDATE '.$table.' SET numbers=? WHERE id=1');
    // В этой версии тип столбца/CCI преобразует элементы, пятый аргумент не читается.
    try {$statement->bindParam(1,$numbers,PDO::PARAM_NULL,0,'int');if (!$statement->execute()) {throw new RuntimeException('Номера не сохранены.');}}
    finally {p175Release($statement);}
    $parts=['Хомут','Хомут',"Кабель O'Reilly"];
    p175Begin($db);
    p175Command($db,'UPDATE '.$table.' SET parts=CAST({?,?,?} AS MULTISET) WHERE id=1',$parts);
    p175Commit($db);
    // TABLE превращает элементы коллекции в SQL-строки, PHP не разбирает строку { ... }.
    $tags=p175Read($db,'SELECT x.element FROM '.$table.' t,TABLE(t.tags) AS x(element) WHERE t.id=1 ORDER BY x.element');
    $counts=p175Read($db,'SELECT x.element,COUNT(*) AS occurrences FROM '.$table.' t,TABLE(t.parts) AS x(element) WHERE t.id=1 GROUP BY x.element ORDER BY x.element');
    $ids=p175Read($db,'SELECT x.element FROM '.$table.' t,TABLE(t.numbers) AS x(element) WHERE t.id=1 ORDER BY x.element');
    $raw=p175Read($db,'SELECT tags,parts,numbers FROM '.$table.' WHERE id=1');
    $tagValues=array_column($tags,'element');$numberValues=array_map('intval',array_column($ids,'element'));
    $partCounts=[];foreach($counts as $row) {$partCounts[$row['element']]=(int)$row['occurrences'];}
    $csv="section;value;quantity\n";
    $add=static function(string $section,string $value,int $qty) use (&$csv):void {$csv.=$section.';"'.str_replace('"','""',$value).'";'.$qty."\n";};
    foreach($tagValues as $value) {$add('tag',$value,1);}
    foreach($partCounts as $value=>$qty) {$add('part',$value,$qty);}
    foreach($numberValues as $value) {$add('number',(string)$value,1);}
    if (file_put_contents(__DIR__.'/kit.csv',$csv)!==strlen($csv)) {throw new RuntimeException('CSV комплекта не сохранён.');}
    $checks['set_unique']=$tagValues===['выезд','монтаж'];
    $checks['multiset_duplicates']=$partCounts===["Кабель O'Reilly"=>1,'Хомут'=>2];
    $checks['integer_column_conversion']=$numberValues===[101,205];
    $checks['text_input_retained']=$labels===$labelsBefore;
    $checks['collection_fetch_is_text']=is_string($raw[0]['tags']) && is_string($raw[0]['parts']);
    $checks['committed_readback']=!$db->inTransaction() && $db->getAttribute(PDO::ATTR_AUTOCOMMIT)===true;
    $checks['useful_csv']=count($tags)===2 && array_sum($partCounts)===3 && strlen($csv)>0;
    $rows=[['Уникальных меток',count($tagValues)],['Всего деталей в комплекте',array_sum($partCounts)],
        ['Хомутов',$partCounts['Хомут']],['Уникальные номера',implode(', ',$numberValues)],['Прямой fetch коллекции возвращает строку',is_string($raw[0]['tags'])]];
    $observations['kit']=['tags'=>$tagValues,'parts'=>$partCounts,'numbers'=>$numberValues,'raw_collection_type'=>gettype($raw[0]['tags']),
        'fifth_option_not_consumed_in_pinned_source'=>true,'php_array_elements_after_numeric_binding'=>$numbers];

} finally {

    if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}

p175Report('pdoc175-kit-membership','Сохранить метки и комплект с двумя одинаковыми деталями',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Уникальных меток — 2
Всего деталей в комплекте — 3
Хомутов — 2
Уникальные номера — 101, 205
Прямой fetch коллекции возвращает строку — да

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.

Открыть результат выполнения · Посмотреть данные JSON · Скачать состав комплекта

Границы решения

Пятый int сохранён в одном вызове как проверяемый исторический пример: в закреплённом PHP7-обработчике он не читается, поэтому успешный INSERT не подтверждает работу этой опции. По умолчанию fetch вернул коллекции строками; разбирать их explode по запятой нельзя, если значение само содержит разделитель. Для повторов не передавайте массив целиком: SET-путь потеряет количество. Для связи с отдельным каталогом деталей обычно предпочтительнее дочерняя таблица с количеством и внешним ключом.

Попробуйте изменить условие

Добавьте ещё один хомут. Сколько будет уникальных наименований деталей и сколько физических деталей?

Разбор

Наименований останется два; физических деталей станет четыре, хомутов — три. Увеличьте число ? в подготовленном конструкторе до четырёх и передайте четыре значения; имена SQL-типов выбирает программа, не пользователь.

Сохранить порядок работ и отменить ошибочную редакцию

Мастер открывает согласованный порядок обслуживания. Редактор ошибочно поставил включение питания первым шагом. Проект показывает черновое изменение внутри транзакции, обнаружение нарушенного условия и откат и списка, и номера редакции. После выполнения checklist.csv содержит исходные четыре шага.

Путь данных и полученный результат
  1. Четыре строки PHP → CAST({?,?,?,?} AS SEQUENCE)
  2. WHERE id=1 AND version_no=1 → условная новая редакция
  3. Первая операция неправильная → rollBack
  4. Повторный SELECT → четыре исходных шага, версия 1 → checklist.csv

Минимальный вызов

$statement = $db->prepare('UPDATE jobs SET steps=CAST({?,?,?,?} AS SEQUENCE) WHERE id=?');
$statement->execute([$step1, $step2, $step3, $step4, $jobId]);

Четыре ? внутри фигурных скобок получают отдельные строки в известном порядке. CAST задаёт SQL-тип SEQUENCE. Последний ? — идентификатор конкретной заявки. SQL-текст с типом и количеством мест задаёт программа; значения с апострофами передаются отдельно. При чтении TABLE раскрывает элементы, ROWNUM даёт позиции, ORDER BY ordinal оформляет выдачу этого одного списка.

Что получится после запуска

НаблюдениеРезультат
Шагов в утверждённом списке4
Первый шагОтключить питание
Второй шагПроверить кабель O'Reilly
Ошибочная редакция отмененада
Версия после отката1
Самостоятельный проект, исходник и архив
Сохранить порядок работ и отменить ошибочную редакциюСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);

    $name=p175OwnName($db,$owned,'TABLE','sequence');$table=p175Qualified($name);
    p175Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,version_no INT NOT NULL,steps SEQUENCE(VARCHAR(120)))');
    $steps=['Отключить питание',"Проверить кабель O'Reilly",'Подключить нагрузку','Проверить кабель ещё раз'];
    p175Command($db,'INSERT INTO '.$table.' (id,version_no,steps) VALUES (1,1,CAST({?,?,?,?} AS SEQUENCE))',$steps);
    $readSteps=static function(PDO $db,string $table):array {
        return p175Read($db,'SELECT ROWNUM AS ordinal,x.element FROM '.$table.' t,TABLE(t.steps) AS x(element) WHERE t.id=1 ORDER BY ordinal');
    };
    $before=$readSteps($db,$table);
    $invalid=['Включить питание','Пропустить проверку'];$expectedVersion=1;$rejected=false;
    p175Begin($db);
    try {
        $changed=p175Command($db,'UPDATE '.$table.' SET steps=CAST({?,?} AS SEQUENCE),version_no=version_no+1 WHERE id=? AND version_no=?',[$invalid[0],$invalid[1],1,$expectedVersion]);
        if ($changed!==1) {throw new RuntimeException('Версия была изменена другим редактором.');}
        $pending=$readSteps($db,$table);
        // Требование проекта: первая операция отключает питание, порядок согласуется до commit.
        if (($pending[0]['element']??null)!=='Отключить питание') {$rejected=true;p175Rollback($db);}
        else {p175Commit($db);}
    } catch(Throwable $failure) {p175Rollback($db);throw $failure;}
    $after=$readSteps($db,$table);$version=p175Read($db,'SELECT version_no FROM '.$table.' WHERE id=1');
    $csv="position;step\n";foreach($after as $row) {$csv.=(int)$row['ordinal'].';"'.str_replace('"','""',$row['element']).'"'."\n";}
    if (file_put_contents(__DIR__.'/checklist.csv',$csv)!==strlen($csv)) {throw new RuntimeException('Порядок работ не сохранён в CSV.');}
    $checks['order_matches_input']=array_column($before,'element')===$steps;
    $checks['ordinal_is_explicit']=array_map('intval',array_column($after,'ordinal'))===[1,2,3,4];
    $checks['rejected_revision']=$rejected && array_column($pending,'element')===$invalid;
    $checks['rollback_restored_sequence']=$after===$before;
    $checks['rollback_restored_version']=(int)$version[0]['version_no']===1;
    $checks['apostrophe_utf8']=$after[1]['element']==="Проверить кабель O'Reilly";
    $checks['transaction_finished']=!$db->inTransaction() && $db->getAttribute(PDO::ATTR_AUTOCOMMIT)===true;
    $rows=[['Шагов в утверждённом списке',count($after)],['Первый шаг',$after[0]['element']],['Второй шаг',$after[1]['element']],
        ['Ошибочная редакция отменена',$rejected && $after===$before],['Версия после отката',(int)$version[0]['version_no']]];
    $observations['sequence']=['before'=>$before,'pending'=>$pending,'after'=>$after,'version'=>(int)$version[0]['version_no'],'conditional_update_rows'=>$changed];

} finally {

    if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}

p175Report('pdoc175-work-sequence','Сохранить порядок работ и отменить ошибочную редакцию',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Шагов в утверждённом списке — 4
Первый шаг — Отключить питание
Второй шаг — Проверить кабель O'Reilly
Ошибочная редакция отменена — да
Версия после отката — 1

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.

Открыть результат выполнения · Посмотреть данные JSON · Скачать порядок работ

Границы решения

Это учебное правило редактора, не инструкция по безопасному выполнению электротехнических работ. Порядок физического обслуживания утверждают отдельно. Проверка после UPDATE демонстрирует откат; простое правило первой строки выгодно проверять ещё до SQL. Условие version_no предотвращает незаметную перезапись чужой версии, но не заменяет права доступа. Для больших списков, сложной сортировки и индивидуального изменения шагов рассмотрите дочернюю таблицу с отдельным столбцом position.

Попробуйте изменить условие

Пусть другой редактор уже сохранил version_no=2, а форма передаёт expectedVersion=1. Можно ли сообщать пользователю, что запись сохранена?

Разбор

Нет. UPDATE с WHERE version_no=1 изменит 0 строк. Нужно откатить текущую операцию, сообщить о конфликте и предложить перечитать актуальный список. Нельзя повторять запись автоматически, игнорируя условие версии.

Выбрать модель хранения и развить проект

Для пользовательского вложения начните с ограничения размера, проверки загрузки и права на запись; затем выполните сохранение, проверьте результат и обеспечьте освобождение потока. Для коллекции сначала определите, важны ли повтор и порядок. Для сохранения нескольких связанных изменений используйте транзакцию с конкретным условием отката.

Обсуждение: выбирать PARAM_LOB или PARAM_STR для текста показывает частую путаницу между SQL-типом, объёмом и способом передачи. Ответ про другой драйвер не переносится автоматически на PDO_CUBRID: в этой главе отдельно проверяются поток, двоичные байты и текст.

Обсуждение: bindParam принимает неожиданный тип ставит вопрос, где заканчивается подсказка PDO и начинается преобразование драйвера. Здесь авторское решение строится на конкретном PHP7-исходнике и сохранённом результате, а не на предположении об одинаковом поведении баз.

Продолжение: каталог, резервирование и ожидание блокировки; метаданные, ключи и импорт; полный справочник PDO. Для сравнения с native cubrid_* смотрите коллекции CUBRID.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.