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

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

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

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

Глава 294 · После подключения и параметров PDO

PDO_FIREBIRD: заказы, видимость изменений и отчёт без записи

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

Заказ, остаток и согласованный отчёт

В заявке две связанные записи: сколько деталей осталось на складе и сколько принято в заказ. Пользователю нужен согласованный результат. Транзакция объединяет изменения в одну работу: либо они подтверждаются вместе, либо отменяются вместе. Отдельная задача — определить, какие изменения других пользователей видит отчёт.

Одна заявка, две таблицы
  1. stock: остаток 5 → зарезервировать 2
  2. orders: сохранить заявку на 2
  3. COMMIT: остаток 3 и заявка существуют вместе
  4. ROLLBACK: незавершённая заявка не меняет склад

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

Примеры рассчитаны на отдельную учебную базу. Проверяемая связка: PHP 8.5.11 с PDO_FIREBIRD и Firebird 5.0.4. Pdo\Firebird появился в PHP 8.4; старый new PDO с префиксом firebird: остаётся доступным. Введение в PDO объясняет соединение и подготовленный запрос.

Начните с подключения и первого запроса. Здесь $db — объект соединения, $db->beginTransaction() — вызов его метода без аргументов, а точка с запятой завершает PHP-команду. Метод исполняется именно на этом соединении: другое PDO-соединение имеет собственную транзакцию. Общие основы транзакций объясняют атомарность подробнее.

База хранит версии строк; это модель MVCC. Удобная аналогия — несколько подписанных редакций складского листа. Читатель получает доступ к редакциям по правилам своей транзакции. Это не физическая копия всей базы при каждом beginTransaction: аналогия объясняет видимость, а не устройство файлов и памяти.

Начать, подтвердить или отменить работу

Три действия нужно различать. beginTransaction начинает явную транзакцию. commit подтверждает её изменения и завершает её. rollBack отменяет ещё не подтверждённые изменения и завершает её. После commit нельзя вернуть прежнее состояние вызовом rollBack: нужна новая компенсирующая операция с бизнес-правилами.

$db->beginTransaction();
// Здесь выполняются изменения одной задачи.
$db->rollBack();

Это минимальная форма отмены. Комментарий после // ничего не выполняет; без изменений в середине отменять нечего. В следующем проекте между границами стоят реальные UPDATE и INSERT. try/catch позволяет отменить транзакцию при исключении; finally освобождает ресурсы независимо от результата. Не перехватывайте ошибку только ради сообщения «успешно».

ВопросМеханизмПример
Сохранить несколько изменений вместеТранзакция и проверка результата каждого действияОстаток и заказ
Какие чужие изменения видныУровень изоляцииОтчёт видит 10 или новые 20
Разрешены ли изменения в этой транзакцииREAD ONLY / READ WRITEЗащитить отчёт от UPDATE
Что пользователю вообще разрешеноSQL-права пользователя и ролиРоль читателя имеет только SELECT

PDO может выполнять отдельные команды в autocommit. Это не означает отсутствие внутренних транзакций Firebird. Явный beginTransaction нужен, когда несколько действий составляют одну задачу. Не смешивайте произвольный SET TRANSACTION, ручной COMMIT через exec и методы PDO без понимания состояния драйвера: PDO должен правильно учитывать начало и конец. В этих программах прикладные границы задаются методами PDO, параметры — до начала.

Pdo\Firebird: класс, атрибуты и границы транзакции

Pdo\Firebird — специализированный подкласс PDO начиная с PHP 8.4. Его экземпляр представляет соединение Firebird, а не таблицу и не результат SELECT. Он наследует общий интерфейс PDO: prepare/query/exec, атрибуты, управление транзакцией и диагностику. Запрос возвращает PDOStatement; конкретные строки читаются уже из него. Справочник PDO содержит общие методы; здесь разобраны особенности драйвера.

$db = new Pdo\Firebird($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

new создаёт объект, $dsn описывает подключение, $user и $password — реквизиты, массив в четвёртом аргументе задаёт опции. Запись с => связывает ключ атрибута со значением. Двоеточия :: обращаются к константе класса, а -> — к методу экземпляра. На PHP до 8.4 используют new PDO с firebird:; наличие драйвера проверяют отдельно. Не подставляйте путь к базе из пользовательского запроса. Подробности DSN и ошибок есть в первой главе.

Константа Pdo\FirebirdЗначение для приложенияКогда выбирать
ATTR_DATE_FORMATФормат получаемого DATE; форматная строка семейства strftimeЭкспорт календарной даты
ATTR_TIME_FORMATФормат получаемого TIMEВремя приёма заявки
ATTR_TIMESTAMP_FORMATФормат получаемого TIMESTAMPВременная отметка события
TRANSACTION_ISOLATION_LEVELАтрибут выбора одного из трёх режимов нижеПеред beginTransaction
READ_COMMITTEDЧтение подтверждённых данных; граница зависит от варианта режима FirebirdЭкран, которому нужны свежие подтверждённые данные
REPEATABLE_READСоответствует Firebird SNAPSHOTСогласованные связанные чтения одного отчёта
SERIALIZABLEСоответствует SNAPSHOT TABLE STABILITYОсобый сценарий со строгими ограничениями конкурентного доступа
WRITABLE_TRANSACTIONБулев атрибут: true — READ WRITE, false — READ ONLY; исходный режим записи trueЗащита транзакции отчёта от записи

Три формата и их реальные CSV разобраны в главе результатов. Это формат вывода значений драйвером, а не изменение типа столбца и не правило ввода пользовательской даты. Имена исторических PDO::FB_* и современного класса нельзя механически переносить между версиями; используйте константы доступной версии и проверяйте загрузку расширения.

Официальная страница класса называет READ_COMMITTED режимом по умолчанию. В закреплённом исходнике PHP 8.5.11 для явных транзакций поле isolation_level инициализируется REPEATABLE_READ; autocommit использует отдельную ветвь READ_COMMITTED. Поэтому здесь все бизнес-примеры задают нужную изоляцию явно и проверяют её наблюдаемое действие двумя соединениями. Значение по умолчанию из описания не подменяет проверку конкретного драйвера.

SERIALIZABLE не означает «самый быстрый и всегда правильный режим». Его более строгие правила могут существенно влиять на параллельную работу. Выбор начинается с инварианта задачи; не держите блокировки во время сетевых вызовов или ожидания пользователя. Примеры ниже не измеряют производительность и не объявляют все режимы одинаковыми.

Метод getApiVersion() возвращает номер API из заголовка, использованного при сборке, без аргументов. Это не версия работающего сервера. Общие getAttribute/setAttribute могут поддерживать разные наборы атрибутов у разных драйверов; не обещайте произвольный атрибут лишь потому, что он есть в базовом классе PDO.

Сохранить заказ и остаток одной транзакцией

Сервис принимает заявку на две детали. Нужно одновременно уменьшить остаток и записать заказ. Если второе действие не закончено, первое тоже отменяется: иначе склад и журнал заявок расходятся.

Путь данных и полученный результат
  1. Остаток 5 → UPDATE с qty>=2 → внутри транзакции 3
  2. INSERT заказа 101 → COMMIT → остаток 3 и заказ сохранены
  3. Недостаток или ошибка → ROLLBACK → обе таблицы согласованы

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

$db->beginTransaction();
$statement = $db->prepare('UPDATE stock SET qty=qty-? WHERE id=? AND qty>=?');
$statement->execute([2, 1, 2]);
$db->rollBack(); // учебная отмена: остаток вернётся

beginTransaction начинает общую границу изменений. Три ? — три отдельно переданных значения: вычесть 2, найти id=1, проверить минимум 2. rollBack отменяет изменение этой транзакции. Сам SELECT перед UPDATE не резервирует товар: условие наличия должно оставаться в UPDATE. В полном проекте rowCount проверяет изменённую строку, затем добавляется заказ и выполняется commit.

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

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

try {
    $config=p176Config();$db=p176Connect();$runtime=p176Runtime($db);

    $stockName=p176OwnName($db,$owned,'TABLE','stock');$stock=p176Qualified($stockName);
    p176Ddl($db,'CREATE TABLE '.$stock.' (id INTEGER PRIMARY KEY, qty INTEGER NOT NULL CHECK(qty>=0))');
    $logName=p176OwnName($db,$owned,'TABLE','orders');$log=p176Qualified($logName);
    p176Ddl($db,'CREATE TABLE '.$log.' (id INTEGER PRIMARY KEY, item_id INTEGER NOT NULL REFERENCES '.$stock.'(id), qty INTEGER NOT NULL)');
    p176Command($db,'INSERT INTO '.$stock.' (id,qty) VALUES (1,5)');
    $db->setAttribute(Pdo\Firebird::TRANSACTION_ISOLATION_LEVEL,Pdo\Firebird::READ_COMMITTED);
    $reserve=static function(int $order,int $qty,bool $simulateFailure=false) use ($db,$stock,$log):string {
        if($qty<=0) {throw new InvalidArgumentException('Количество должно быть положительным.');}
        p176Begin($db);
        try {
            $changed=p176Command($db,'UPDATE '.$stock.' SET qty=qty-? WHERE id=? AND qty>=?',[$qty,1,$qty]);
            if($changed!==1) {p176Rollback($db);return 'недостаточно';}
            p176Command($db,'INSERT INTO '.$log.' (id,item_id,qty) VALUES (?,?,?)',[$order,1,$qty]);
            if($simulateFailure) {throw new RuntimeException('Учебная отмена перед подтверждением');}
            p176Commit($db);return 'сохранено';
        } catch(Throwable $e) {p176Rollback($db);throw $e;}
    };
    $saved=$reserve(101,2);
    $shortage=$reserve(102,7);
    $rolledBack=false;
    try {$reserve(103,1,true);}catch(RuntimeException $e) {$rolledBack=$e->getMessage()==='Учебная отмена перед подтверждением';}
    $remaining=p176Read($db,'SELECT qty AS "qty" FROM '.$stock.' WHERE id=1')[0]['qty'];
    $orders=p176Read($db,'SELECT id AS "id",qty AS "qty" FROM '.$log.' ORDER BY id');
    $checks=['reservation_committed'=>$saved==='сохранено','shortage_refused'=>$shortage==='недостаточно',
        'failure_rolled_back'=>$rolledBack,'stock_consistent'=>$remaining===3,'one_order'=>$orders===[['id'=>101,'qty'=>2]],'transaction_finished'=>!$db->inTransaction()];
    $observations=['orders'=>$orders,'remaining'=>$remaining,'shortage'=>$shortage,'cancelled_order_present'=>false];
    $rows=[['Заказ 101','2 единицы сохранены'],['Заказ 102','7 единиц: недостаточно'],['Заказ 103','отменён вместе с уменьшением остатка'],['Осталось',3],['Сохранённых заказов',1]];
    file_put_contents(__DIR__.'/reservation.csv',"order_id,quantity\n101,2\n");

} finally {

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

p176Report('pdo176-reservation','Сохранить заказ и остаток одной транзакцией',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Заказ 101 — 2 единицы сохранены
Заказ 102 — 7 единиц: недостаточно
Заказ 103 — отменён вместе с уменьшением остатка
Осталось — 3
Сохранённых заказов — 1

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

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

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

Это атомарность базы, а не откат отправленного письма или HTTP-запроса. Внешние действия выполняют после успешного commit либо через outbox. Повтор операции требует уникального ключа заявки; слепой повтор при потерянном ответе может создать дубль. Конкурентный отказ SQL обрабатывают отдельно и не превращают любой сбой в «нет товара». Таблицы и связи создаются до прикладной транзакции.

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

Попросите 3 единицы вместо 7 во второй заявке. Какой остаток получится и сколько заказов сохранится?

Разбор

После первого заказа остаётся 3. Второй успешно заберёт все 3: остаток 0, заказов 2. Третья попытка уже не пройдёт условие наличия. Измените также ожидаемый CSV и проверки; отказ и принудительная отмена — разные ветви.

Увидеть изменение другой сессии: READ COMMITTED и SNAPSHOT

Диспетчер открыл отчёт, а кладовщик изменил остаток. Должен ли следующий запрос отчёта увидеть новое число? Это решает изоляция. Один PHP-процесс ниже управляет двумя настоящими соединениями и точным порядком шагов; случайных задержек нет.

Путь данных и полученный результат
  1. Читатель: SELECT → 10
  2. Писатель: UPDATE 20 → COMMIT
  3. Читатель снова: READ_COMMITTED → 20, SNAPSHOT → 10
  4. Читатель завершил транзакцию → новое чтение → 20

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

$db->setAttribute(Pdo\Firebird::TRANSACTION_ISOLATION_LEVEL, Pdo\Firebird::REPEATABLE_READ);
$db->beginTransaction();
// Здесь выполняются связанные SELECT одного отчёта.
$db->commit();

setAttribute задаёт параметр соединения для следующей явной транзакции. REPEATABLE_READ у этого драйвера соответствует Firebird SNAPSHOT. До beginTransaction режим уже выбран. commit закрывает снимок; новая транзакция получает новую границу видимости. В полном сценарии SELECT расположен до и после commit другого соединения.

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

НаблюдениеРезультат
READ_COMMITTED в той же транзакции10 → 20
REPEATABLE_READ в той же транзакции10 → 10
После завершения и нового чтения20 в обоих режимах
Самостоятельный проект, исходник и архив
Увидеть изменение другой сессии: READ COMMITTED и SNAPSHOTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p176Config();$db=p176Connect();$runtime=p176Runtime($db);

    $name=p176OwnName($db,$owned,'TABLE','visibility');$table=p176Qualified($name);
    p176Ddl($db,'CREATE TABLE '.$table.' (id INTEGER PRIMARY KEY, qty INTEGER NOT NULL)');
    p176Command($db,'INSERT INTO '.$table.' (id,qty) VALUES (1,10)');
    $writer=p176Connect();$samples=[];
    try {
        foreach(['READ_COMMITTED'=>Pdo\Firebird::READ_COMMITTED,'REPEATABLE_READ'=>Pdo\Firebird::REPEATABLE_READ] as $label=>$mode) {
            p176Command($writer,'UPDATE '.$table.' SET qty=10 WHERE id=1');
            $db->setAttribute(Pdo\Firebird::TRANSACTION_ISOLATION_LEVEL,$mode);p176Begin($db);
            try {
                $before=p176Read($db,'SELECT qty AS "qty" FROM '.$table.' WHERE id=1')[0]['qty'];
                p176Begin($writer);p176Command($writer,'UPDATE '.$table.' SET qty=20 WHERE id=1');p176Commit($writer);
                $after=p176Read($db,'SELECT qty AS "qty" FROM '.$table.' WHERE id=1')[0]['qty'];
                p176Commit($db);
            } catch(Throwable $e) {p176Rollback($db);p176Rollback($writer);throw $e;}
            $fresh=p176Read($db,'SELECT qty AS "qty" FROM '.$table.' WHERE id=1')[0]['qty'];
            $samples[]=['mode'=>$label,'before'=>$before,'same_transaction'=>$after,'new_read'=>$fresh];
        }
    } finally {p176Close($writer);}
    $checks=['read_committed_refresh'=>$samples[0]===['mode'=>'READ_COMMITTED','before'=>10,'same_transaction'=>20,'new_read'=>20],
        'snapshot_stable'=>$samples[1]===['mode'=>'REPEATABLE_READ','before'=>10,'same_transaction'=>10,'new_read'=>20],
        'transaction_finished'=>!$db->inTransaction()];
    $observations=['sequence'=>'reader SELECT 10; writer UPDATE 20 + COMMIT; reader SELECT; reader COMMIT; new SELECT','samples'=>$samples];
    $rows=[['READ_COMMITTED в той же транзакции','10 → 20'],['REPEATABLE_READ в той же транзакции','10 → 10'],['После завершения и нового чтения','20 в обоих режимах']];
    file_put_contents(__DIR__.'/visibility.csv',"mode,before,same_transaction,new_read\nREAD_COMMITTED,10,20,20\nREPEATABLE_READ,10,10,20\n");

} finally {

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

p176Report('pdo176-visibility','Увидеть изменение другой сессии: READ COMMITTED и SNAPSHOT',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
READ_COMMITTED в той же транзакции — 10 → 20
REPEATABLE_READ в той же транзакции — 10 → 10
После завершения и нового чтения — 20 в обоих режимах

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

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

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

Не держите снимок открытым, пока человек заполняет форму: длительные транзакции мешают освобождению старых версий. В Firebird 4/5 настройка ReadConsistency влияет на варианты READ COMMITTED; этот проект сравнивает разные завершённые SQL-запросы, а не изменение строки посреди одного SELECT. Это тест видимости, не нагрузочный тест и не измерение скорости.

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

Перенесите commit читателя перед вторым SELECT. Будет ли режим SNAPSHOT всё ещё показывать 10?

Разбор

Нет: прежний снимок закончился. Новый запрос начинает новую транзакционную область и видит уже подтверждённые 20. Кеш на стороне PHP или интерфейса остаётся отдельной причиной устаревшего отображения.

Собрать отчёт в SQL и запретить случайную запись

Отчёту нужны суммы по категориям, а не все складские позиции. GROUP BY и SUM вычисляют две итоговые строки на сервере. Режим READ ONLY дополнительно защищает эту транзакцию от случайного UPDATE в коде отчёта.

Путь данных и полученный результат
  1. Строки cable: 4 и 6; tools: 2
  2. SQL GROUP BY + SUM → cable:10, tools:2
  3. READ ONLY → случайный UPDATE отклонён
  4. Закрыть транзакцию → сохранить report.csv

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

$db->setAttribute(Pdo\Firebird::WRITABLE_TRANSACTION, false);
$db->beginTransaction();
$statement = $db->query('SELECT category, SUM(qty) FROM stock GROUP BY category');
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
$statement->closeCursor();
$db->commit();

false здесь означает режим транзакции READ ONLY. Это не отключение подключения и не выбор изоляции. GROUP BY объединяет строки по category; SUM суммирует qty внутри группы. fetchAll уместен для заранее ограниченного количества категорий, а не для загрузки всей таблицы. Полный проект отдельно задаёт SNAPSHOT, проверяет отказ UPDATE и возвращает режим записи после завершения.

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

НаблюдениеРезультат
Кабель: сумма остатка10
Инструмент: сумма остатка2
UPDATE внутри READ ONLYотклонён
Исходный остаток первой позиции4
Самостоятельный проект, исходник и архив
Собрать отчёт в SQL и запретить случайную записьСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=p176Config();$db=p176Connect();$runtime=p176Runtime($db);

    $name=p176OwnName($db,$owned,'TABLE','report');$table=p176Qualified($name);
    p176Ddl($db,'CREATE TABLE '.$table.' (id INTEGER PRIMARY KEY, category VARCHAR(20), qty INTEGER NOT NULL)');
    foreach([[1,'cable',4],[2,'cable',6],[3,'tools',2]] as $row) {p176Command($db,'INSERT INTO '.$table.' (id,category,qty) VALUES (?,?,?)',$row);}
    $db->setAttribute(Pdo\Firebird::TRANSACTION_ISOLATION_LEVEL,Pdo\Firebird::REPEATABLE_READ);
    $db->setAttribute(Pdo\Firebird::WRITABLE_TRANSACTION,false);p176Begin($db);
    try {
        $summary=p176Read($db,'SELECT category AS "category",SUM(qty) AS "quantity" FROM '.$table.' GROUP BY category ORDER BY category');
        $denied=p176Capture(static fn()=>p176Command($db,'UPDATE '.$table.' SET qty=0 WHERE id=1'));
        p176Rollback($db);
    } finally {p176Rollback($db);$db->setAttribute(Pdo\Firebird::WRITABLE_TRANSACTION,true);}
    $after=p176Read($db,'SELECT qty AS "qty" FROM '.$table.' WHERE id=1')[0]['qty'];
    $checks=['aggregated_in_sql'=>$summary===[['category'=>'cable','quantity'=>10],['category'=>'tools','quantity'=>2]],
        'write_rejected'=>$denied['exception'] && $denied['sqlstate']!=='00000','unchanged'=>$after===4,
        'transaction_finished'=>!$db->inTransaction(),'writable_restored'=>$db->getAttribute(Pdo\Firebird::WRITABLE_TRANSACTION)===true];
    unset($denied['value']);$observations=['summary'=>$summary,'rejected_update'=>$denied,'quantity_after'=>$after];
    $rows=[['Кабель: сумма остатка',10],['Инструмент: сумма остатка',2],['UPDATE внутри READ ONLY','отклонён'],['Исходный остаток первой позиции',4]];
    file_put_contents(__DIR__.'/report.csv',"category,quantity\ncable,10\ntools,2\n");

} finally {

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

p176Report('pdo176-read-only-report','Собрать отчёт в SQL и запретить случайную запись',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Кабель: сумма остатка — 10
Инструмент: сумма остатка — 2
UPDATE внутри READ ONLY — отклонён
Исходный остаток первой позиции — 4

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

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

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

READ ONLY не заменяет права отдельного пользователя: после переключения режима привилегированное соединение снова может писать. Не меняйте атрибут посреди активной транзакции. Особые временные таблицы имеют отдельные правила; пример работает с обычной таблицей. Количество категорий тоже нужно ограничивать, если оно растёт до размеров исходного каталога.

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

Добавьте фильтр, чтобы показать только cable. Где разместить WHERE: до GROUP BY или после?

Разбор

WHERE category=? размещают перед GROUP BY и передают cable параметром. WHERE фильтрует исходные строки; HAVING проверяет уже агрегированные группы, например SUM(qty)>5. Не выгружайте все позиции в PHP ради этого отбора.

Встроить транзакцию в приложение

В реальном обработчике сначала проверьте входные данные и права. Затем откройте короткую транзакцию, выполните только нужные запросы, проверьте результат и подтвердите изменения. При исключении отмените незавершённую работу и отделите безопасное сообщение пользователю от закрытого технического журнала. Полный текст ошибки соединения может содержать внутренний путь — не показывайте его в HTML.

Вопрос «Транзакция не откатывается» относится к старой связке PHP/Firebird. Он объясняет, зачем доказывать откат повторным чтением, а не только печатью true от метода. Новый проект проверяет и остаток, и отсутствие отменённого заказа; старый совет про атрибуты не переносится без проверки.

Обсуждение READ ONLY показывает смешение запрета записи с видимостью. Это независимые настройки: read-only отчёт всё равно нуждается в выбранной изоляции. Случай временной таблицы и PDO напоминает также о времени жизни временных данных и состоянии драйвера. Проекты этой главы используют обычные собственные таблицы; отдельный контракт GTT нельзя выводить из их поведения.

Следующий шаг: параметры и роль чтения, даты, точные числа и вложения, транзакции в приложении. Для большой таблицы отбирайте WHERE, сортируйте ORDER BY, ограничивайте строки и агрегируйте в SQL. SELECT * не является ошибкой синтаксиса, но выгрузка ненужных столбцов и сотен тысяч строк ради небольшого экрана расходует передачу, память и время. Транзакция сама по себе эту ошибку проектирования не исправляет.

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

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

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

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

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