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

Дерево руководства
Глава 307 · Первый локальный каталог → связанные данные
PDO_SQLITE: локальный каталог, доступ и связанный импорт
Подготовка PDO_SQLITE, все формы DSN и возможности Pdo\Sqlite через три самостоятельные задачи с файлом базы, CSV и JSON.
Локальная база: каталог, публикация и поставка
Небольшому каталогу мастерской нужно помнить остатки между запусками программы: кабель — 3 штуки, изолента — 5, кейс — 0. Массив PHP существует, пока работает программа. SQLite сохраняет таблицы в файле, а SQL выбирает нужные строки. В этой главе получится CSV товаров в наличии, JSON для публикации без права записи и квитанция связанной поставки.
| Слой | Что хранится или выполняется в примере |
|---|---|
| Файл catalog.sqlite | База с таблицей products; остаётся после закрытия обычного подключения. |
| Таблица products | Поля id, sku, title, qty, note. Строка CAB-7 / Кабель / 3 — один товар. |
| Pdo\Sqlite в переменной $db | Открытое подключение, через которое PHP передаёт SQL библиотеке SQLite. |
| PDOStatement в $statement | Один подготовленный запрос и его текущий результат. |
| PHP-массив $items | Уже прочитанные выбранные строки, которые можно записать в CSV. |
- Файл: CAB-7 → 3, TAP-2 → 5, CASE-1 → 0
- SQLite выполняет WHERE qty > 0 и ORDER BY id LIMIT 2
- PDOStatement отдаёт поля sku, title, qty
- PHP записывает CAB-7 и TAP-2 в catalog.csv
Это схема ответственности, а не устройство байтов файла. Здесь нет отдельного сервера и SQL-логина: PHP загружает PDO_SQLITE, тот вызывает SQLite в том же процессе. SQLite3 — другое PHP-расширение со своим API; наличие класса SQLite3 не доказывает наличие драйвера PDO_SQLITE. Основы таблиц, SQL и PDO пригодятся, но первый результат можно получить ещё без создания таблицы.
$db = new Pdo\Sqlite('sqlite::memory:');
$statement = $db->query("SELECT 'Кабель' AS title, 3 AS qty");
$item = $statement->fetch(PDO::FETCH_ASSOC);
echo $item['title'] . ': ' . $item['qty'] . "\n";
$statement->closeCursor();
$statement = null;
$db = null;
// Кабель: 3Знак $ начинает имя переменной; = сохраняет значение справа под этим именем. new Pdo\Sqlite создаёт подключение; обратная косая черта отделяет имя класса Sqlite от пространства имён Pdo. Круглые скобки содержат аргумент — строку подключения. Запись ->query вызывает метод этого подключения, а ; завершает команду PHP. SELECT создаёт одну строку результата, AS задаёт названия её полей. FETCH_ASSOC выбирает массив с такими же ключами. Квадратные скобки читают title/qty, точка соединяет текст, echo выводит его. В конце закрывается результат и убираются обе ссылки на объекты; null здесь означает отсутствие значения, а не пустой текст.
В этом минимальном вызове память служит только короткой пробой. Первый самостоятельный проект заменяет её собственным файлом, создаёт таблицу и подтверждает сохранение новым подключением. Второй ограничивает доступ отчёта, третий связывает товары с поставщиками. Это последовательное развитие одного каталога, а не требование сразу освоить все атрибуты PDO.
Подготовить PHP и папку приложения
Для этих проектов нужен PHP с PDO и PDO_SQLITE. В стандартной сборке PHP поддержка PDO_SQLITE включается по умолчанию, но пакет дистрибутива или отдельный php.ini может её не загружать. Команды ниже выполняются тем же исполняемым PHP, которым вы запускаете проект. Для сайта отдельно проверьте конфигурацию web-процесса: CLI и сервер могут читать разные ini.
php --ini
php --ri pdo_sqlite
php -r "var_export(PDO::getAvailableDrivers());"В списке драйверов должен присутствовать sqlite. Версия расширения PHP и версия библиотеки SQLite — разные сведения. После открытия базы SELECT sqlite_version() сообщает библиотеку, реально исполняющую SQL. Это полезнее догадки по установленной отдельной программе sqlite3. Современные проекты ниже используют PHP 8.5: класс Pdo\Sqlite появился в 8.4, новые атрибуты транзакций и statement — в 8.5.
| Среда | Подготовка и граница |
|---|---|
| Windows, официальный комплект PHP | В своём php.ini задайте extension_dir и extension=pdo_sqlite. Сопутствующая libsqlite3.dll должна быть доступна процессу через PATH; используйте библиотеку того же комплекта/архитектуры. Не копируйте DLL в системный каталог Windows. |
| Linux, пакет PHP | Установите PDO_SQLITE для выбранной версии PHP средствами дистрибутива. Имя пакета часто содержит sqlite3, хотя он может поставлять оба разных расширения; решающая проверка — PDO::getAvailableDrivers и php --ri. |
| Своя сборка PHP | Нужны компилятор и заголовки libsqlite. С PHP 7.4 используется внешняя библиотека минимум 3.5.0; с PHP 8.5 минимум 3.7.17. Для новых EXPLAIN-возможностей требуется более новая библиотека. |
| Отключение при сборке | Manual документирует --without-pdo-sqlite[=DIR]. Старые инструкции о встроенной копии SQLite и необязательном DIR относятся к прежним сборкам. |
# В распакованном официальном исходнике PHP; libsqlite development headers уже установлены.
./configure --prefix="$HOME/php-sqlite" --disable-all --enable-cli --enable-pdo --with-pdo-sqlite
make -j2
make install
"$HOME/php-sqlite/bin/php" --ri pdo_sqliteЭто пример отдельной установки в домашний каталог, не команда обновления работающего сайта. Для Windows выбранный ini может выглядеть так:
extension_dir="C:/tools/php/ext"
extension=pdo_sqliteЗамените путь собственным каталогом PHP. Разрешения файла и папки относятся к пользователю, под которым выполняется PHP. Для записи SQLite может потребоваться создавать журнальные файлы рядом с базой; одного права записи на существующий catalog.sqlite недостаточно. Не размещайте постоянную базу в папке, из которой web-сервер раздаёт файлы посетителям, и не исправляйте отказ выдачей доступа всем пользователям.
Самостоятельные архивы уже содержат example.php, lab.php и README.txt. Распакуйте архив в отдельную доступную для записи папку, откройте там терминал и задайте только учебный маркер:
# PowerShell
$env:PHPMAN_SQLITE_LAB = 'pdo-sqlite181'
php example.php
# sh/bash
PHPMAN_SQLITE_LAB=pdo-sqlite181 php example.phpМаркер подтверждает запуск учебного кода с собственными временными файлами. Это не настройка самого SQLite и не пароль. Каждый проект сначала создаёт отдельный каталог/базу, затем сохраняет result.html, result.json и полезный CSV/JSON; после завершения освобождает statement/подключения и убирает только свои SQL-объекты и файлы базы. Рабочее приложение должно сохранять постоянную базу, а не копировать эту учебную уборку после каждого запроса.
В вопросе о подключении PHP к SQLite на Linux пустая страница скрывала разные причины: отсутствие драйвера и невозможность открыть файл. Здесь они проверяются раздельно: сначала загруженное расширение, затем разрешённый путь и права. Отсутствие серверного процесса SQLite не означает отсутствие зависимостей у PHP.
Выбрать файл, память или временную базу
DSN — строка, по которой PDO выбирает драйвер и базу. У SQLite префикс sqlite: отделён двоеточием от имени файла. Для постоянного каталога используйте абсолютный путь, собранный из конфигурации приложения. Обычный SQLite не создаёт пользователей и не проверяет переданные конструктору username/password; доступ определяется возможностью процесса открыть файл и выбранными флагами.
| Запись DSN | Где живут данные | Когда использовать |
|---|---|---|
| sqlite:/srv/shop/data/catalog.sqlite | Обычный файл; в Windows, например, sqlite:C:/shop/data/catalog.sqlite. | Каталог, который открывается повторно. Папка должна существовать. |
| sqlite::memory: | Частная база в памяти этого подключения. | Короткое преобразование/проверка. Второе такое PDO получает другую базу. |
| sqlite: | Временная база; удаляется после закрытия подключения. | Промежуточная SQL-задача без постоянного имени файла. Не обещайте, что все страницы всегда остаются только в RAM. |
| sqlite:file:/srv/shop/data/catalog.sqlite?mode=ro | URI-имя файла, поддерживается PDO_SQLITE с PHP 8.1. | Фиксированный разрешённый файл с режимом открытия; не строка от посетителя. |
$path = __DIR__ . '/data/catalog.sqlite'; // Папка data уже существует.
$db = PDO::connect('sqlite:' . $path, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_TIMEOUT => 1,
Pdo\Sqlite::ATTR_OPEN_FLAGS => Pdo\Sqlite::OPEN_READWRITE | Pdo\Sqlite::OPEN_CREATE,
]);
// В PHP 8.4+ PDO::connect выберет Pdo\Sqlite для sqlite:.
// После завершения всех statement: $db = null;Здесь __DIR__ — папка текущего PHP-файла; точка присоединяет постоянное имя подпапки, а не входной параметр HTTP. PDO::connect появился в 8.4 и выбирает специализированный класс. new Pdo\Sqlite(...) прямо задаёт его; new PDO(...) остаётся базовым вариантом для общего PDO-кода. Ошибка открытия вызывает PDOException независимо от выбранного режима обработки SQL-ошибок. В массиве options знак => связывает атрибут со значением, а | объединяет совместимые битовые флаги READWRITE и CREATE.
По умолчанию драйвер открывает с READWRITE | CREATE. OPEN_READWRITE без CREATE требует существующий файл; OPEN_READONLY запрещает запись через это подключение. Флаги задают при создании PDO, а не как способ превратить уже открытое соединение в read-only. В проекте публикации запрет проверяется реальным UPDATE и последующим чтением исходного количества.
URI передаётся SQLite без преобразования PHP. mode=ro означает чтение, mode=rw — запись без создания, mode=rwc — запись с созданием; mode=memory имеет отдельную семантику памяти. Для особых символов пути применяются правила URI: ? начинает параметры, # — фрагмент, поэтому такие символы в имени кодируют. Неизвестные параметры могут быть проигнорированы: опечатка не обязана дать ошибку.
При установленном open_basedir PDO_SQLITE не принимает file:-URI. Используйте обычный абсолютный путь, разрешённый этой настройкой, и OPEN_READONLY в options. Нельзя проверять безопасность по последним символам DSN: строка sqlite:file:/srv/data/private.php?x=.sqlite всё равно называет private.php. Внешнему пользователю предоставляют выбор из собственных логических имён каталога, а не возможность прислать DSN.
SQLite file:-URI не следует путать с общим PDO DSN uri:, который читает DSN из другого файла и объявлен устаревшим в PHP 8.5. В этой главе ни один проект не использует общий uri:. Для одного локального приложения нормальный абсолютный путь обычно проще; URI оправдан, когда его опции действительно нужны и выбранная политика PHP позволяет их применить.
PHP: все формы SQLite DSN · SQLite: URI и параметры открытия · PDO::connect() · Оба способа открыть публикацию
Pdo\Sqlite: класс, флаги и карта атрибутов
Pdo\Sqlite наследует PDO: prepare, query, транзакции и обработка результатов остаются знакомыми. Специализированный класс добавляет функции SQL на PHP, сортировку, доступ к BLOB, авторизатор и загрузку расширения. Класс доступен с PHP 8.4; перечисленные ниже дополнения 8.5 не возникают автоматически в старом PHP от замены файла базы.
| Константа Pdo\Sqlite | Назначение и область |
|---|---|
| OPEN_READONLY | Флаг открытия только для чтения; требует существующий файл. |
| OPEN_READWRITE | Флаг открытия с записью; сам по себе не создаёт отсутствующую базу. |
| OPEN_CREATE | Добавляется к READWRITE, чтобы разрешить создание. |
| ATTR_OPEN_FLAGS | Атрибут constructor options: набор флагов открытия. |
| ATTR_EXTENDED_RESULT_CODES | Настройка соединения: включить подробный SQLite-код ошибки вместо только основной категории. |
| ATTR_READONLY_STATEMENT | Читаемый атрибут statement: запрос не меняет файл напрямую. Это не проверка всех побочных действий пользовательской функции. |
| DETERMINISTIC | Флаг createFunction: автор обещает одинаковый ответ при одинаковых аргументах. |
| ATTR_BUSY_STATEMENT (8.5) | Читаемый bool-атрибут statement: выполнен и имеет незавершённый результат. Не означает, что другой процесс удерживает write-lock. |
| ATTR_EXPLAIN_STATEMENT (8.5) | Режим statement для EXPLAIN; изменение требует сборку с SQLite 3.43.0+, иначе ValueError. |
| EXPLAIN_MODE_PREPARED (8.5) | Обычный режим выполнения подготовленного statement. |
| EXPLAIN_MODE_EXPLAIN (8.5) | Выдать инструкции виртуальной машины SQLite вместо выполнения прикладного запроса. |
| EXPLAIN_MODE_EXPLAIN_QUERY_PLAN (8.5) | Выдать план доступа к данным; его текст не является стабильным форматом бизнес-отчёта. |
| ATTR_TRANSACTION_MODE (8.5) | Режим последующих beginTransaction на соединении; не переключение уже начавшейся транзакции. |
| TRANSACTION_MODE_DEFERRED (8.5) | Режим по умолчанию: захват необходимой блокировки откладывается до обращения к базе. |
| TRANSACTION_MODE_IMMEDIATE (8.5) | Заявить write-транзакцию при BEGIN; возможный конфликт проявится раньше. |
| TRANSACTION_MODE_EXCLUSIVE (8.5) | Исключительный режим; отношение к читателям зависит от journal mode, в WAL отличается от обычного rollback journal. |
| OK (8.5) | Ответ авторизатора: разрешить операцию. |
| DENY (8.5) | Ответ авторизатора: отклонить операцию. |
| IGNORE (8.5) | Ответ авторизатора с зависящим от действия смыслом; при чтении поля можно подставить SQL NULL. Не равнозначен общему «пропусти весь запрос». |
Это карта выбора, а не предложение выставить все значения разом. В публикации используется OPEN_READONLY, в импорте — EXTENDED_RESULT_CODES. Конкуренция и результаты разбирает режимы BEGIN, BUSY/READONLY и EXPLAIN на самостоятельных задачах. Расширение приложения раскрывает DETERMINISTIC и решения авторизатора с конкретными полями.
Атрибуты statement читают/меняют на PDOStatement, настройки соединения — на PDO либо в options. Наличие константы не делает её универсальным атрибутом каждого драйвера. В PHP 8.5.11 константы EXPLAIN_MODE_* условны по библиотеке при сборке; проверяйте поддержку выбранного PHP, а не только версию отдельной утилиты sqlite3.
$statement = $db->prepare('SELECT title FROM products WHERE id = :id');
$readonly = $statement->getAttribute(Pdo\Sqlite::ATTR_READONLY_STATEMENT);
$statement->bindValue(':id', 1, PDO::PARAM_INT);
$statement->execute();
$title = $statement->fetchColumn();
$statement->closeCursor();
$statement = null;
// Для обычного SELECT $readonly === true; $title — название или false, если строки нет.READONLY_STATEMENT описывает подготовленный SQL, а не права на файл. UPDATE с условием, которое не нашло ни строки, остаётся записывающим запросом. SELECT с прикладным callback может иметь косвенный эффект; поэтому такой bool не служит песочницей для чужого SQL. Открытие файла только для чтения, разрешённые операции приложения и авторизатор решают разные задачи.
| Современный метод | Задача и переход к контракту |
|---|---|
Pdo\Sqlite::createFunction() | Зарегистрировать скалярную функцию SQL; name/callback/число аргументов/flags → bool. |
Pdo\Sqlite::createAggregate() | Свернуть строки step/finalize с состоянием → bool регистрации. |
Pdo\Sqlite::createCollation() | Зарегистрировать сравнение двух строк для сортировки → bool. |
Pdo\Sqlite::openBlob() | Открыть table/column/rowid/[dbname]/flags как поток resource либо false. |
Pdo\Sqlite::loadExtension() | Загрузить доверенное нативное расширение по имени, результат void; доступность зависит от сборки/настроек. |
Pdo\Sqlite::setAuthorizer() | Установить callback либо null для снятия; результат void, новый метод 8.5. |
Исторические PDO::sqliteCreateFunction, sqliteCreateAggregate и sqliteCreateCollation сохранены как отдельные карточки для старого кода. С PHP 8.5 эти три алиаса устарели: новый код использует методы Pdo\Sqlite. Возвращаемый bool регистрации не является итогом SQL-запроса, а void не означает, что ошибка невозможна. Полные параметры, исключения и практические вызовы находятся по ссылкам и в главе расширений; прямую загрузку DLL из пользовательского ввода эти проекты не предлагают.
В PHP 8.5 также устарели семь имён констант на базовом PDO: SQLITE_DETERMINISTIC, SQLITE_ATTR_OPEN_FLAGS, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE, SQLITE_ATTR_READONLY_STATEMENT и SQLITE_ATTR_EXTENDED_RESULT_CODES. Например, PDO::SQLITE_OPEN_READONLY заменяется на Pdo\Sqlite::OPEN_READONLY: смысл флага сохраняется, меняется владелец и убирается префикс SQLITE_. Это отдельная миграция имён, а не изменение формата файла базы. Новые константы 8.5 перечислены в таблице уже под современным классом.
Из общих атрибутов здесь явно устанавливаются ERRMODE_EXCEPTION, DEFAULT_FETCH_MODE=FETCH_ASSOC, STRINGIFY_FETCHES=false. ATTR_TIMEOUT задаёт ожидание блокировки в секундах, а не ограничение длительности любого вычисления SQL. Реализация 8.5.11 использует 60 секунд по умолчанию; проекты задают 1 секунду явно. CLIENT_VERSION и SERVER_VERSION для SQLite относятся к библиотеке внутри процесса: слово server в имени общего PDO-атрибута не означает наличие сервера.
PHP: Pdo\Sqlite · PHP 8.5.11: объявления и условия компиляции · Регистрация семи устаревших алиасов · SQLite: граница readonly statement · PHP: устаревший sqliteCreateFunction
Передать товар и проверить связи между таблицами
Товар из PHP должен остаться данными, даже когда в названии есть апостроф. Текст SQL задаёт действие и имена полей; параметр передаёт значение отдельно. Подготовка возвращает PDOStatement, bindValue связывает конкретное значение с маркером, execute выполняет действие. Позиционные ? в bindValue нумеруются с 1; массив execute начинается обычным ключом 0. Не смешивайте оба стиля маркеров в одном SQL.
$statement = $db->prepare('INSERT INTO products (sku, title, qty, note) VALUES (:sku, :title, :qty, :note)');
$statement->bindValue(':sku', 'CAB-7', PDO::PARAM_STR);
$statement->bindValue(':title', "Кабель O'Reilly", PDO::PARAM_STR);
$statement->bindValue(':qty', 3, PDO::PARAM_INT);
$statement->bindValue(':note', null, PDO::PARAM_NULL);
$statement->execute();
$affected = $statement->rowCount();
$statement->closeCursor();
$statement = null;
// В подготовленной таблице добавлен один товар; $affected === 1.Пример предполагает свободный SKU в существующей products с auto-generated INTEGER PRIMARY KEY либо полем id, не требующим ручного ввода; самостоятельный проект явно передаёт id. Маркер :title не окружают SQL-кавычками, а апостроф в значении не заменяют вручную. Тип PARAM_INT задаёт целое значение остатка и лимита. PARAM_NULL передаёт отсутствие значения; строка NULL и пустая строка — другие значения. Для таблиц/столбцов параметр не подходит: ORDER BY :field не выбирает имя колонки. Имена и допустимые направления сортировки выбирают из явного списка приложения.
| Механизм | Что он гарантирует и чего не делает |
|---|---|
| Подготовленный запрос | Отделяет значения от SQL. Не проверяет, что SKU допустим по правилам магазина. |
| TEXT / INTEGER и привязка | Определяют предпочтительное хранение и передачу. Обычная SQLite-таблица использует affinity, а не строгую модель всех других СУБД. |
| NOT NULL / UNIQUE / CHECK | Закрепляют конкретные ограничения: номер обязателен, SKU уникален, qty неотрицателен. |
| FOREIGN KEY | Связывает supplier_id с suppliers.id при включённой проверке на данном подключении. |
| Транзакция | Объединяет изменения принятой партии; решение откатить всю партию после одного отказа задаёт приложение. |
| STRICT-таблица | Начиная с SQLite 3.37.0 ужесточает допустимые типы/преобразования. Это отдельный выбор схемы, не скрытое свойство любой INTEGER-колонки. |
SQLite различает классы хранения NULL, INTEGER, REAL, TEXT и BLOB. Поэтому объявление DECIMAL само по себе не даёт точной десятичной арифметики, а значение true обычно хранится как 1. Для денег можно выбрать целые минимальные единицы или отдельно обоснованный точный формат. В каталоге количество — целое; проект проверяет фактический PHP-тип при чтении. Для BLOB PDO_SQLITE принимает с PARAM_LOB и строку, и поток. В реализации 8.5.11 входной поток может целиком копироваться в память: наличие resource не является обещанием обработки без полного буфера. Pdo\Sqlite::openBlob() и глава расширений показывают другой способ работать с частью большого значения.
Специализированный парсер PDO понимает одинарные/двойные кавычки и обратные кавычки с удвоением, идентификаторы в квадратных скобках, комментарии -- и невложенные /* … */. Маркер внутри строки или комментария не становится параметром. Для нового SQL используйте одинарные кавычки для строк и двойные — для идентификаторов; историческая терпимость SQLite к некоторым формам не нужна для приложения.
В этом драйвере prepare работает через нативную подготовку SQLite. Не переносите сюда SQLSRV DIRECT_QUERY, параметры TDS, PostgreSQL disable prepares или ожидание нескольких rowset. Один prepared statement представляет одну команду: реализация 8.5.11 не исполняет оставшийся после неё SQL как серию. Для нескольких связанных команд применяйте отдельные prepare/execute внутри явной транзакции. exec имеет другой контракт; передавать в него склеенный текст посетителя нельзя.
В обсуждении PDO, beginTransaction и внешнего ключа SQLite ограничение не срабатывало после попытки включить foreign_keys внутри транзакции. Настройку ставят заранее и читают назад; значение по умолчанию может зависеть от сборки. Связанный импорт показывает и отказ, и отсутствие обеих ранее добавленных записей после rollback.
SQLite: типы и affinity · STRICT-таблицы · Внешние ключи · PHP 8.5.11: SQL-парсер · Привязка и чтение значения
Сохранить каталог и прочитать его новым подключением
Для небольшого склада нужен каталог, который переживает закрытие подключения. Проект записывает три позиции, фиксирует их и открывает тот же файл заново. catalog.csv содержит две позиции в наличии; кейс с нулевым остатком остаётся в базе, но не попадает в выгрузку.
- Вход: CAB-7 → 3, TAP-2 → 5, CASE-1 → 0; заметки null / пустая строка / Под заказ
- INSERT трёх строк → commit → закрыть все statement и первое PDO
- Новое PDO открывает тот же файл → SELECT видит те же три строки
- qty > 0, ORDER BY id, LIMIT 2 → две позиции → catalog.csv
Минимальный вызов
$statement = $db->prepare(
'SELECT sku, title, qty FROM products WHERE qty > :minimum ORDER BY id LIMIT :limit'
);
$statement->bindValue(':minimum', 0, PDO::PARAM_INT);
$statement->bindValue(':limit', 2, PDO::PARAM_INT);
$statement->execute();
$items = $statement->fetchAll(PDO::FETCH_ASSOC);
$statement->closeCursor();
// Две строки: CAB-7 / Кабель O'Reilly / 3 и TAP-2 / Изолента / 5.Здесь $db уже открыл файл, а products обозначает подготовленную таблицу. SELECT перечисляет нужные поля; WHERE сравнивает остаток с отдельным числовым параметром. ORDER BY id задаёт порядок до LIMIT, а LIMIT ограничивает выборку в SQLite. bindValue связывает имя :limit с целым 2; execute выполняет команду. fetchAll допустим для этих двух строк, FETCH_ASSOC даёт ключи sku/title/qty. Полный файл ниже сам создаёт таблицу и освобождает результат перед закрытием PDO.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Сохранено | 3 товара в собственном файле |
| Новое подключение | Те же 3 товара |
| В наличии | CAB-7: Кабель O'Reilly — 3; TAP-2: Изолента — 5 |
| Примечания | NULL и пустая строка различаются |
| Файл | catalog.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$database=null;$owned=$removed=$rows=$checks=$observations=[];$ownedFiles=$removedFiles=[];
try {
$database=p181Database($ownedFiles,'project');$db=p181Connect($database);$runtime=p181Runtime($db);
function saveCsv181(string $name,array $rows): void {
$file=fopen(__DIR__.'/'.$name,'wb');
if ($file===false) {throw new RuntimeException('Не удалось открыть свой CSV.');}
try {foreach ($rows as $row) {
if (fputcsv($file,$row,',','"','',"\n")===false) {
throw new RuntimeException('Не удалось записать CSV.');
}
}} finally {fclose($file);}
}
function saveJson181(string $name,array $value): void {
$text=json_encode($value,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/'.$name,$text)!==strlen($text)) {
throw new RuntimeException('Не удалось записать свой JSON.');
}
}
$name=p181OwnTable($db,$owned,'catalog','(id INTEGER NOT NULL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,title TEXT NOT NULL,qty INTEGER NOT NULL CHECK(qty>=0),note TEXT NULL)');
$table=p181Qualified($name);
$input=[[1,'CAB-7',"Кабель O'Reilly",3,null],[2,'TAP-2','Изолента',5,''],[3,'CASE-1','Кейс',0,'Под заказ']];
$insert=null;
$inserted=[];
try {
$insert=$db->prepare('INSERT INTO '.$table.' (id,sku,title,qty,note) VALUES (?,?,?,?,?)');
$db->beginTransaction();
foreach ($input as $values) {
foreach ($values as $index=>$value) {
$type=$value===null ? PDO::PARAM_NULL : (is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
$insert->bindValue($index+1,$value,$type);
}
$insert->execute();
$inserted[]=$insert->rowCount();
$insert->closeCursor();
}
p181Release($insert);
$db->commit();
} catch (Throwable $error) {
p181Release($insert);
if ($db->inTransaction()) {$db->rollBack();}
throw $error;
} finally {p181Release($insert);}
$before=p181Read($db,'SELECT id,sku,title,qty,note FROM '.$table.' ORDER BY id');
p181Close($db);
$firstClosed=$db===null;
$db=p181Connect($database);
$after=p181Read($db,'SELECT id,sku,title,qty,note FROM '.$table.' ORDER BY id');
$statement=null;
try {
$statement=$db->prepare('SELECT sku,title,qty FROM '.$table.' WHERE qty>:minimum ORDER BY id LIMIT :limit');
$statement->bindValue(':minimum',0,PDO::PARAM_INT);
$statement->bindValue(':limit',2,PDO::PARAM_INT);
$statement->execute();
$items=$statement->fetchAll(PDO::FETCH_ASSOC);
$eof=$statement->fetch(PDO::FETCH_ASSOC);
} finally {p181Release($statement);}
$csv=[['sku','title','qty']];
foreach ($items as $item) {$csv[]=[$item['sku'],$item['title'],$item['qty']];}
saveCsv181('catalog.csv',$csv);
$checks=[
'sqlite_driver'=>$db->getAttribute(PDO::ATTR_DRIVER_NAME)==='sqlite',
'sqlite_subclass'=>$db instanceof Pdo\Sqlite,
'three_inserted'=>$inserted===[1,1,1],
'first_connection_closed'=>$firstClosed,
'reopened_data_equal'=>$after===$before && count($after)===3,
'utf8_apostrophe'=>$after[0]['title']==="Кабель O'Reilly",
'null_and_empty'=>$after[0]['note']===null && $after[1]['note']==='',
'integer_quantity'=>is_int($after[0]['qty']) && $after[0]['qty']===3,
'bounded_catalog'=>$items===[['sku'=>'CAB-7','title'=>"Кабель O'Reilly",'qty'=>3],['sku'=>'TAP-2','title'=>'Изолента','qty'=>5]],
'eof_false'=>$eof===false,
'statement_released'=>$statement===null && $insert===null,
];
$observations=['input'=>$input,'before_close'=>$before,'after_reopen'=>$after,'selected'=>$items,
'storage'=>'own SQLite file','selection'=>['minimum_exclusive'=>0,'limit'=>2,'order'=>'id'],
'integer_php_type'=>get_debug_type($after[0]['qty']),'eof'=>$eof];
$rows=[['Сохранено','3 товара в собственном файле'],['Новое подключение','Те же 3 товара'],
['В наличии',"CAB-7: Кабель O'Reilly — 3; TAP-2: Изолента — 5"],['Примечания','NULL и пустая строка различаются'],['Файл','catalog.csv']];
} finally {
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-file-catalog','Сохранить каталог и прочитать его новым подключением',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Сохранено — 3 товара в собственном файле
Новое подключение — Те же 3 товара
В наличии — CAB-7: Кабель O'Reilly — 3; TAP-2: Изолента — 5
Примечания — NULL и пустая строка различаются
Файл — catalog.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать каталог CSV
Границы решения
Новый объект PDO здесь означает новое подключение к тому же файлу; переменная $items не служит хранилищем базы. С sqlite::memory: повторное подключение создало бы другую пустую базу. В конце учебный проект удаляет только свой файл и таблицу, сохраняя CSV/HTML/JSON: проверяется сохранность между подключениями, а не оставляется постоянная рабочая база. В приложении файл каталога хранится вне публичной web-папки и удаляется только отдельной операцией владельца. CSV — выбранные данные, не полная резервная копия SQLite.
Попробуйте изменить условие
Увеличьте остаток CASE-1 до 2. Должен ли он появиться в текущем CSV, если LIMIT остаётся равным 2?
Разбор
Нет: после фильтра будут три строки, но ORDER BY id оставляет CAB-7 и TAP-2 первыми, LIMIT 2 отбирает их. Для выгрузки всех трёх измените лимит на 3. Не удаляйте ORDER BY ради случайного попадания нужного товара.
Открыть каталог для публикации без права записи
Отчёту о наличии достаточно читать готовый каталог. Открытие только для чтения превращает случайный UPDATE в явный отказ. Проект показывает два способа выбрать этот режим для одного файла и сохраняет одинаковые товары в JSON вместе с результатом проверки запрета записи.
- Владелец подготовил файл: Кабель: 3, Изолента: 5, Кейс: 0
- Ветка flags: OPEN_READONLY → выбранные строки; UPDATE → отказ
- Ветка URI: file:…?mode=ro → те же строки; UPDATE → отказ
- Владелец перечитал CAB-7: 3 → readonly-report.json
Минимальный вызов
$reader = new Pdo\Sqlite('sqlite:' . $path, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
Pdo\Sqlite::ATTR_OPEN_FLAGS => Pdo\Sqlite::OPEN_READONLY,
]);
$statement = $reader->query('SELECT title, qty FROM products WHERE qty > 0 ORDER BY id LIMIT 2');
$items = $statement->fetchAll(PDO::FETCH_ASSOC);
$statement->closeCursor();
$statement = null;
$reader = null;
// Для модели: Кабель / 3; Изолента / 5.Переменная $path содержит разрешённый абсолютный путь существующей базы, products — её таблица. Четвёртый аргумент конструктора — массив настроек key => value: OPEN_READONLY выбирает доступ при открытии. Это не setAttribute уже открытого writable-подключения. null в двух средних аргументах означает, что обычному SQLite не передаются имя пользователя и пароль. В полном проекте второй читатель использует фиксированный URI mode=ro; путь не берётся из URL посетителя.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Публикация | Кабель: 3; Изолента: 5 |
| Флаги открытия | Чтение разрешено, UPDATE отклонён |
| URI mode=ro | Тот же каталог, UPDATE отклонён |
| Исходные данные | Остаток CAB-7 остался 3 |
| Файл | readonly-report.json |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$database=null;$owned=$removed=$rows=$checks=$observations=[];$ownedFiles=$removedFiles=[];
$byFlags=$byUri=null;
try {
$database=p181Database($ownedFiles,'project');$db=p181Connect($database);$runtime=p181Runtime($db);
function saveCsv181(string $name,array $rows): void {
$file=fopen(__DIR__.'/'.$name,'wb');
if ($file===false) {throw new RuntimeException('Не удалось открыть свой CSV.');}
try {foreach ($rows as $row) {
if (fputcsv($file,$row,',','"','',"\n")===false) {
throw new RuntimeException('Не удалось записать CSV.');
}
}} finally {fclose($file);}
}
function saveJson181(string $name,array $value): void {
$text=json_encode($value,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/'.$name,$text)!==strlen($text)) {
throw new RuntimeException('Не удалось записать свой JSON.');
}
}
$name=p181OwnTable($db,$owned,'publication','(id INTEGER NOT NULL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,title TEXT NOT NULL,qty INTEGER NOT NULL CHECK(qty>=0))');
$table=p181Qualified($name);
foreach ([[1,'CAB-7','Кабель',3],[2,'TAP-2','Изолента',5],[3,'CASE-1','Кейс',0]] as $item) {
p181Command($db,'INSERT INTO '.$table.' (id,sku,title,qty) VALUES (?,?,?,?)',$item);
}
$before=p181Read($db,'SELECT id,sku,title,qty FROM '.$table.' ORDER BY id');
$sql='SELECT sku,title,qty FROM '.$table.' WHERE qty>0 ORDER BY id LIMIT 2';
try {
$byFlags=p181Connect($database,[Pdo\Sqlite::ATTR_OPEN_FLAGS=>Pdo\Sqlite::OPEN_READONLY]);
$flagItems=p181Read($byFlags,$sql);
$flagAttempt=p181Capture(fn()=>p181Command($byFlags,'UPDATE '.$table.' SET qty=99 WHERE id=1'));
p181Close($byFlags);
$byUri=p181Connect($database,[],true);
$uriItems=p181Read($byUri,$sql);
$uriAttempt=p181Capture(fn()=>p181Command($byUri,'UPDATE '.$table.' SET qty=99 WHERE id=1'));
p181Close($byUri);
} finally {p181Close($byFlags);p181Close($byUri);}
$after=p181Read($db,'SELECT id,sku,title,qty FROM '.$table.' ORDER BY id');
$expected=[['sku'=>'CAB-7','title'=>'Кабель','qty'=>3],['sku'=>'TAP-2','title'=>'Изолента','qty'=>5]];
$flagRejected=$flagAttempt['failed'] && $flagAttempt['state']==='HY000' && $flagAttempt['code']===8;
$uriRejected=$uriAttempt['failed'] && $uriAttempt['state']==='HY000' && $uriAttempt['code']===8;
$report=['profile'=>'catalog-publication','items'=>$flagItems,
'flags_readonly'=>$flagRejected,'uri_readonly'=>$uriRejected,
'updates_rejected'=>(int)$flagRejected+(int)$uriRejected,'source_unchanged'=>$before===$after];
saveJson181('readonly-report.json',$report);
$checks=[
'flags_catalog'=>$flagItems===$expected,
'uri_catalog'=>$uriItems===$expected,
'same_publication'=>$flagItems===$uriItems,
'flags_update_denied'=>$flagRejected,
'uri_update_denied'=>$uriRejected,
'source_unchanged'=>$before===$after && $after[0]['qty']===3,
'readers_closed'=>$byFlags===null && $byUri===null,
'json_roundtrip'=>json_decode(file_get_contents(__DIR__.'/readonly-report.json'),true,512,JSON_THROW_ON_ERROR)===$report,
];
$observations=['by_flags'=>['rows'=>$flagItems,'update'=>$flagAttempt],
'by_uri'=>['rows'=>$uriItems,'update'=>$uriAttempt],'source_after'=>$after,
'report'=>$report,'access_boundary'=>'read-only connections, not an operating-system ACL test'];
$rows=[['Публикация','Кабель: 3; Изолента: 5'],['Флаги открытия','Чтение разрешено, UPDATE отклонён'],
['URI mode=ro','Тот же каталог, UPDATE отклонён'],['Исходные данные','Остаток CAB-7 остался 3'],['Файл','readonly-report.json']];
} finally {
p181Close($byFlags);p181Close($byUri);
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-readonly-publication','Открыть каталог для публикации без права записи',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Публикация — Кабель: 3; Изолента: 5
Флаги открытия — Чтение разрешено, UPDATE отклонён
URI mode=ro — Тот же каталог, UPDATE отклонён
Исходные данные — Остаток CAB-7 остался 3
Файл — readonly-report.json
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать отчёт доступа JSON
Границы решения
Запрет проверен на этих двух подключениях, а не на правах Windows/Linux и не на отдельном HTTP-пользователе. Другой процесс с правом записи может изменить тот же файл. Наличие read-only PDO не превращает произвольный SQL или callback в безопасную консоль. URI file: в PDO_SQLITE поддерживается с PHP 8.1; при установленном open_basedir этот путь недоступен. Для ограниченного PHP-окружения используйте обычный разрешённый путь и constructor flags. Сначала закройте читателей, затем меняйте или удаляйте свой файл.
Попробуйте изменить условие
Почему для готового отчёта опасно оставить OPEN_READWRITE | OPEN_CREATE и ошибиться в имени файла?
Разбор
При разрешённом каталоге может появиться новая пустая база вместо понятного отказа «файл не найден». OPEN_READONLY требует существующую базу и запрещает запись через это подключение. Это помогает обнаружить ошибку пути, но каталог всё равно должен быть выбран конфигурацией и защищён правами ОС.
Принять поставку целиком и откатить ошибочную партию
В поставке товар ссылается на поставщика. Если одной записи не хватает родительского поставщика, нельзя оставлять незаметно половину партии. Проект принимает первую поставку, затем отменяет вторую вместе с уже добавленными поставщиком и товаром. import-receipt.csv показывает итог каждой партии, а HTML — действующий каталог.
- suppliers: 1 → Север, 2 → Юг; items: CAB-7 → 1, TAP-2 → 2
- Партия A: обе таблицы заполнены → commit → две связанные позиции
- Партия B: Запад/3 и CASE-1/3 добавлены; BAD-9/999 нарушает FOREIGN KEY
- rollback всей B → Запад и CASE-1 отсутствуют → квитанция и прежний каталог
Минимальный вызов
$db->exec('PRAGMA foreign_keys = ON'); // До beginTransaction.
$db->beginTransaction();
try {
$statement = $db->prepare('INSERT INTO items (sku, title, supplier_id, qty) VALUES (?, ?, ?, ?)');
$statement->execute(['BAD-9', 'Неизвестная позиция', 999, 1]);
$statement->closeCursor();
$db->commit();
} catch (PDOException $error) {
$statement = null;
if ($db->inTransaction()) {
$db->rollBack();
}
echo "Партия не принята\n";
}
// При REFERENCES suppliers(id), NOT NULL и отсутствии id=999: Партия не принята.Маленький фрагмент предполагает две существующие таблицы и ERRMODE_EXCEPTION. supplier_id хранит номер, REFERENCES требует соответствующий id в suppliers, а NOT NULL запрещает отсутствие номера. PRAGMA foreign_keys — настройка именно текущего подключения: её ставят до BEGIN и отдельно читают фактическое значение. beginTransaction отмечает начало партии, commit подтверждает все её изменения, rollBack отменяет их. catch перехватывает SQL-отказ; полный проект также перечитывает обе таблицы и сохраняет квитанцию.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Партия A | Приняты 2 поставщика и 2 товара |
| Партия B | Поставщик 999 отсутствует; вся партия отменена |
| После отката | Нет ни поставщика Запад, ни товара CASE-1 |
| Каталог | Кабель — Север: 3; Изолента — Юг: 5 |
| Файл | import-receipt.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$database=null;$owned=$removed=$rows=$checks=$observations=[];$ownedFiles=$removedFiles=[];
try {
$database=p181Database($ownedFiles,'project');$db=p181Connect($database);$runtime=p181Runtime($db);
function saveCsv181(string $name,array $rows): void {
$file=fopen(__DIR__.'/'.$name,'wb');
if ($file===false) {throw new RuntimeException('Не удалось открыть свой CSV.');}
try {foreach ($rows as $row) {
if (fputcsv($file,$row,',','"','',"\n")===false) {
throw new RuntimeException('Не удалось записать CSV.');
}
}} finally {fclose($file);}
}
function saveJson181(string $name,array $value): void {
$text=json_encode($value,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/'.$name,$text)!==strlen($text)) {
throw new RuntimeException('Не удалось записать свой JSON.');
}
}
$db->exec('PRAGMA foreign_keys=ON');
$foreignKeys=p181Read($db,'PRAGMA foreign_keys')[0]['foreign_keys'];
$db->setAttribute(Pdo\Sqlite::ATTR_EXTENDED_RESULT_CODES,true);
$suppliersName=p181OwnTable($db,$owned,'suppliers','(id INTEGER NOT NULL PRIMARY KEY,name TEXT NOT NULL)');
$suppliers=p181Qualified($suppliersName);
$itemsName=p181OwnTable($db,$owned,'items','(id INTEGER NOT NULL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,title TEXT NOT NULL,supplier_id INTEGER NOT NULL,
qty INTEGER NOT NULL CHECK(qty>=0),FOREIGN KEY(supplier_id) REFERENCES '.$suppliers.'(id))');
$itemsTable=p181Qualified($itemsName);
$supplierInput=[[1,'Север'],[2,'Юг']];
$itemInput=[[1,'CAB-7','Кабель',1,3],[2,'TAP-2','Изолента',2,5]];
$db->beginTransaction();
try {
foreach ($supplierInput as $supplier) {
p181Command($db,'INSERT INTO '.$suppliers.' (id,name) VALUES (?,?)',$supplier);
}
foreach ($itemInput as $item) {
p181Command($db,'INSERT INTO '.$itemsTable.' (id,sku,title,supplier_id,qty) VALUES (?,?,?,?,?)',$item);
}
$db->commit();
} catch (Throwable $error) {
if ($db->inTransaction()) {$db->rollBack();}
throw $error;
}
$select='SELECT i.sku,i.title,s.name AS supplier,i.qty FROM '.$itemsTable.' AS i JOIN '.$suppliers.
' AS s ON s.id=i.supplier_id ORDER BY i.id LIMIT 10';
$accepted=p181Read($db,$select);
$db->beginTransaction();
try {
p181Command($db,'INSERT INTO '.$suppliers.' (id,name) VALUES (?,?)',[3,'Запад']);
p181Command($db,'INSERT INTO '.$itemsTable.' (id,sku,title,supplier_id,qty) VALUES (?,?,?,?,?)',[3,'CASE-1','Кейс',3,2]);
$rejected=p181Capture(fn()=>p181Command($db,'INSERT INTO '.$itemsTable.
' (id,sku,title,supplier_id,qty) VALUES (?,?,?,?,?)',[4,'BAD-9','Неизвестная позиция',999,1]));
$beforeRollback=p181Read($db,'SELECT id,sku FROM '.$itemsTable.' ORDER BY id');
} finally {
if ($db->inTransaction()) {$db->rollBack();}
}
$after=p181Read($db,$select);
$supplierCount=p181Read($db,'SELECT COUNT(*) AS total FROM '.$suppliers)[0]['total'];
$itemCount=p181Read($db,'SELECT COUNT(*) AS total FROM '.$itemsTable)[0]['total'];
$missingSupplier=p181Read($db,'SELECT id FROM '.$suppliers.' WHERE id=3');
$missingItems=p181Read($db,'SELECT id FROM '.$itemsTable.' WHERE id IN (3,4) ORDER BY id');
$foreignKeyCheck=p181Read($db,'PRAGMA foreign_key_check');
$receipt=[['batch','status','new_suppliers','new_items','reason'],
['A','committed',2,2,'ok'],['B','rolled_back',0,0,'unknown_supplier']];
saveCsv181('import-receipt.csv',$receipt);
$expected=[['sku'=>'CAB-7','title'=>'Кабель','supplier'=>'Север','qty'=>3],
['sku'=>'TAP-2','title'=>'Изолента','supplier'=>'Юг','qty'=>5]];
$checks=[
'foreign_keys_enabled'=>$foreignKeys===1,
'accepted_catalog'=>$accepted===$expected,
'foreign_key_refusal'=>$rejected['failed'] && $rejected['state']==='HY000' && $rejected['code']===787,
'first_bad_batch_row_existed'=>$beforeRollback===[['id'=>1,'sku'=>'CAB-7'],['id'=>2,'sku'=>'TAP-2'],['id'=>3,'sku'=>'CASE-1']],
'whole_batch_rolled_back'=>$missingSupplier===[] && $missingItems===[],
'accepted_data_unchanged'=>$after===$accepted,
'two_suppliers_two_items'=>$supplierCount===2 && $itemCount===2,
'no_foreign_key_violations'=>$foreignKeyCheck===[],
'transaction_finished'=>!$db->inTransaction(),
];
$observations=['supplier_input'=>$supplierInput,'item_input'=>$itemInput,'accepted'=>$accepted,
'refused_supplier_id'=>999,'failure'=>$rejected,'before_rollback'=>$beforeRollback,
'final_catalog'=>$after,'final_counts'=>['suppliers'=>$supplierCount,'items'=>$itemCount],
'foreign_keys'=>$foreignKeys,'foreign_key_check'=>$foreignKeyCheck,'receipt'=>$receipt];
$rows=[['Партия A','Приняты 2 поставщика и 2 товара'],['Партия B','Поставщик 999 отсутствует; вся партия отменена'],
['После отката','Нет ни поставщика Запад, ни товара CASE-1'],
['Каталог','Кабель — Север: 3; Изолента — Юг: 5'],['Файл','import-receipt.csv']];
} finally {
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-related-import','Принять поставку целиком и откатить ошибочную партию',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Партия A — Приняты 2 поставщика и 2 товара
Партия B — Поставщик 999 отсутствует; вся партия отменена
После отката — Нет ни поставщика Запад, ни товара CASE-1
Каталог — Кабель — Север: 3; Изолента — Юг: 5
Файл — import-receipt.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать квитанцию импорта CSV
Границы решения
Настройка FOREIGN KEY не переносится автоматически между независимыми подключениями и не включается командой внутри уже начатой транзакции. Приложение явно задаёт политику, не полагаясь на default сборки. Обычный immediate constraint запрещает ошибочную команду; приложение само решает отменить всю партию. Расширенный SQLite-код отдельно от SQLSTATE уточняет причину, поэтому нельзя узнавать отказ по фразе локализованного сообщения. Пустой PRAGMA foreign_key_check означает отсутствие найденных нарушений в этой базе, а не общий аудит всех данных и бизнес-правил.
Попробуйте изменить условие
Поставщик 3 теперь известен заранее. Как принять CASE-1, сохранив правило «вся партия или ничего»?
Разбор
Проверьте входную поставку до BEGIN, вставьте или сопоставьте реального поставщика, затем связывайте товары с его id. Если BAD-9 всё ещё указывает на 999, вся партия остаётся отклонённой. Нельзя отключать foreign_keys ради успешного импорта; исправьте данные либо явно отделите независимые партии и выдайте отдельные квитанции.
Различить отказ, пустой результат и завершение партии
Отказ открытия, пустая выборка и непринятая поставка — разные события. В проектах ERRMODE_EXCEPTION превращает SQL-отказ в PDOException; публичный результат хранит состояние и код, не полный служебный путь или stack trace. При обычном завершении fetch возвращает false, пустой fetchAll — []; перед обращением к полям надо отличить отсутствие строки от найденного товара.
| Наблюдение | Что проверять |
|---|---|
| could not find driver | Текущий PHP, SAPI, ini и загруженный PDO_SQLITE; отдельный sqlite3.exe не заменяет расширение. |
| Файл не открывается | Абсолютный путь, существование каталога, флаги и права процесса; READONLY не создаёт отсутствующий файл. |
| Неожиданная пустая база | Не создал ли READWRITE | CREATE другой файл из-за неверного пути? Проверьте конфигурацию до миграции схемы. |
| UPDATE в публикации отклонён | Это ожидаемая политика read-only; исходное количество перечитывается через соединение владельца. |
| database is locked / busy | Проверить чужую write-транзакцию и собственный незавершённый statement; не увеличивать тайм-аут вместо завершения результата. |
| FOREIGN KEY отказ | Сохранить SQLSTATE и SQLite-код рядом с отказом; полный rollback партии выполняет приложение. Основной и расширенный код могут давать разные SQLSTATE. |
| commit не завершён | Не выдавать успешную квитанцию только по rowCount предыдущего INSERT. Сначала commit, затем итог и контролируемое чтение. |
| Нужно скопировать действующую базу | CSV сохраняет выбранные данные, а копирование только основного файла при активном WAL может потерять изменения. Используйте согласованный способ резервирования SQLite. |
$db->exec('PRAGMA foreign_keys = ON');
$db->beginTransaction();
try {
// Здесь выполняются команды одной поставки и закрываются их результаты.
$db->commit();
} catch (Throwable $error) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $error; // Внешний слой приложения сохранит закрытую диагностику.
}Этот каркас объясняет границу транзакции, но не заменяет обработку входных данных. Не удерживайте BEGIN во время ожидания решения пользователя или скачивания внешнего файла. Сначала получите/проверьте пакет, затем выполняйте короткую серию SQL. SQLite может обслуживать несколько читателей, но конкурирующие записи требуют явной политики; следующая глава показывает отказ бронирования, WAL-снимок и освобождение statement.
PDOStatement удерживает соединение. Присвоить $db=null недостаточно, если другой statement, поток BLOB или callback продолжает его использовать. Сначала завершите результаты и собственные транзакции, закройте дополнительные подключения, затем уберите последние ссылки. Учебный helper проверяет и удаляет только зарегистрированные имена и свой случайный каталог; его уборка не является командой очистки всех SQLite-баз компьютера.
Попробуйте изменить задачу: добавить к публикации только товары поставщика «Север». Решение — JOIN items с suppliers и параметр для имени поставщика, затем прежние ORDER BY/LIMIT и read-only подключение. Параметр меняет значение фильтра; имена таблиц остаются выбранными программой. Если поставщиков несколько, не храните их названия в строке с запятыми: отдельная связующая таблица сохраняет проверяемые отношения. Основы транзакций и PDOStatement::bindValue() помогают развить этот пример.
PHP 8.5.11: открытие, ограничения пути и ошибки · SQLite: WAL и файлы журнала
Дальше: конкуренция, план запроса и правила приложения
Теперь файл хранит каталог, публикация получает только нужные поля, а ошибочная поставка не оставляет половину данных. Конкуренция и результаты SQLite добавляет второе подключение и проверяемый срок ожидания. Функции, BLOB и расширения приложения продолжает работу с расчётами, сортировкой, вложением и доверенной нативной функцией. Канонические карточки PDO сохраняют полный контракт каждого отдельного вызова; вводная PDO остаётся маршрутом к общим понятиям.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


