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

Дерево руководства
Глава 309 · После локальной базы SQLite и транзакций
PDO_SQLITE: правила отчёта, вложения и точная арифметика
PHP callbacks в SQL, разрешённая выгрузка, восстановление двоичного файла и реальная загрузка decimal-расширения.
Выбрать способ дополнения SQLite
Локальной программе нужны свои правила отчёта: объединить разные метки доставки, скрыть служебную колонку, приложить двоичный файл и точно сложить десятичные суммы. PDO_SQLITE позволяет дополнить SQL функциями PHP и нативной библиотекой, но эти способы имеют разные границы.
- LATE/late → PHP callback → единая метка и отчёт
- Чтение private_note → authorizer → NULL в выгрузке
- Документ → BLOB фиксированного размера → восстановленный файл
- Десятичные строки → decimal DLL → точный итог 0.03
SQLite выполняет SQL внутри процесса PHP и сохраняет данные в файле. Отдельный сервер базы для этих проектов не нужен. Таблица задаёт поля, строка хранит одну запись, а запрос выбирает нужную часть данных. Начало SQL и PDO объясняет эти понятия на маленьких примерах.
Каждый архив создаёт собственный файл базы и учебные данные. Сначала изучите маленький вызов и его результат, затем откройте самостоятельный проект. Перед учебной уборкой программа сохраняет полезную выгрузку; в рабочем приложении постоянную базу, конечно, не удаляют после запроса. Подготовка PHP и пути разобраны в главе о локальной базе.
Callback — функция, которую вызывает библиотека в нужный момент. Здесь SQLite передаёт ей SQL-значения и использует возвращённый результат; это не запуск отдельного процесса. Функции обратного вызова, транзакции и потоки файлов помогают разобрать состав полного проекта; текущие методы объясняются ниже до использования.
Одна строка, группа и порядок сравнения
Для одной строки подходит скалярная функция, для группы — агрегат, для сравнения двух строк — правило collation. Регистрация действует на текущем соединении; новое соединение надо настроить отдельно. Имена выбирает приложение. Префикс app_ помогает не переопределить случайно встроенный SQL-инструмент.
| Вызов | Что передаётся | Что получается |
|---|---|---|
| createFunction(name, callback, numArgs=-1, flags=0): bool | SQL-имя, PHP callable, число SQL-аргументов; -1 разрешает переменное число | true/false при регистрации; callback возвращает SQL-совместимый скаляр или NULL |
| createAggregate(name, step, finalize, numArgs=-1): bool | step(context,rowNumber,...values) возвращает следующий context; finalize получает последнее состояние | Один SQL-результат на группу; массив может быть внутренним context, но не конечным SQL-значением |
| createCollation(name, callback): bool | callback(string a,string b): int сравнивает пару строк | Отрицательное/0/положительное число задаёт порядок; не-int в PHP 8.4+ вызывает TypeError |
- SELECT app_sla(' LATE ')
- Аргумент SQL → локальный $value = ' LATE '
- trim и strtolower → return 'late'
- SQL получает late → fetchColumn возвращает late
$db->createFunction('app_sla', fn ($value) => strtolower(trim($value)), 1);
$label = $db->query("SELECT app_sla(' LATE ')")->fetchColumn();После выполнения $label содержит late. fn объявляет короткую PHP-функцию; её выражение становится возвращаемым значением. Одна регистрация не вызывает callback: это делает последующий SQL. Проверяйте результат регистрации. Флаг DETERMINISTIC является обещанием одинакового результата при одинаковых аргументах; часы, случайность и внешнее состояние такому обещанию не соответствуют.
У агрегата первый context пуст; возвращайте из step новое состояние. В нашем отчёте это список минут, а finalize сортирует его и выбирает середину. Число обработанных ненулевых значений считаем самостоятельно: NULL тоже может входить в поток строк, и полагаться на служебный счётчик для вычисления размера массива не требуется. Пустому набору явно возвращаем NULL. Количество значений ограничено, чтобы пример не превращал большой запрос в неограниченное накопление PHP-массивов.
В обсуждении медианы по группам показана практическая потребность: один итог для каждой группы. Старое утверждение об отсутствии медианы нельзя переносить на все современные сборки: percentile extension входит в amalgamation с SQLite 3.51, но включается опцией сборки. При наличии подходящего встроенного инструмента начинайте с него. Здесь собственный aggregate показывает правила ограничения группы и обработки неизвестных измерений.
Callback может вызываться многократно; не размещайте в нём отправку писем, изменение заказа или обращение к сети. Ошибки PHP внутри него требуют обработки в вызывающем коде. В реализации PHP 8.5.11 целочисленные аргументы/результаты callback проходят через 32-битный SQLite API; обычное чтение INTEGER устроено иначе. Поэтому проект использует минуты 0..1440, а точные большие величины ниже идут через десятичные строки. Правило collation должно быть последовательным и стабильным; изменение индексируемого правила требует продуманной перестройки индекса.
Полные карточки: createFunction, createAggregate и createCollation.
Собрать отчёт по доставке с собственными правилами
Две службы доставки присылают разные записи статуса: LATE, late, OK и пустое значение. Для небольшого отчёта нужны единые метки, медиана времени по каждой группе и задержки в начале списка. SQL выбирает и группирует строки, а PHP добавляет три явно определённых правила.
- Строки A: LATE/35, late/45 → общий статус late
- step: [] → [35] → [35,45]
- finalize: (35+45)/2 → 40 минут
- COLLATE: late раньше on_time → CSV из 4 групп
Минимальный вызов
$db->createFunction('app_sla', fn ($value) => strtolower(trim($value)), 1);
$status = $db->query("SELECT app_sla(' LATE ')")->fetchColumn();
echo $status; // latecreateFunction связывает SQL-имя app_sla с PHP-функцией. Число 1 задаёт один SQL-аргумент. SQLite передаёт строку LATE с пробелами в $value; PHP возвращает late, и fetchColumn получает результат SQL. В полном проекте отдельно оговорены NULL и неизвестные статусы.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Групп отчёта | 4 |
| Медиана задержек A, мин | 40 |
| Медиана задержек B, мин | 80 |
| Пустая группа | null |
| Порядок | late → on_time → unknown |
| Файл | service-metrics.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);
$table=p181OwnTable($db,$owned,'deliveries','(id INTEGER PRIMARY KEY,service TEXT NOT NULL,minutes INTEGER,status TEXT)');$q=p181Qualified($table);
$input=[[1,'A',35,' LATE '],[2,'A',45,'late'],[3,'A',25,'OK'],[4,'B',70,'late'],[5,'B',90,'LATE'],[6,'B',null,null]];
$insert=$db->prepare("INSERT INTO $q (id,service,minutes,status) VALUES(?,?,?,?)");
foreach($input as $v){$insert->bindValue(1,$v[0],PDO::PARAM_INT);$insert->bindValue(2,$v[1]);$insert->bindValue(3,$v[2],$v[2]===null?PDO::PARAM_NULL:PDO::PARAM_INT);$insert->bindValue(4,$v[3],$v[3]===null?PDO::PARAM_NULL:PDO::PARAM_STR);$insert->execute();}p181Release($insert);
$function=$db->createFunction('app_sla',static function(?string $raw):string {
return match(strtolower(trim($raw??''))){'late'=>'late','ok'=>'on_time',default=>'unknown'};
},1,Pdo\Sqlite::DETERMINISTIC);
$aggregate=$db->createAggregate('app_median',static function($context,int $row,$minutes):array {
$values=$context??[];
if($minutes!==null){if(!is_int($minutes)||$minutes<0||$minutes>1440){throw new DomainException('Нужны целые минуты 0..1440.');}$values[]=$minutes;}
if(count($values)>100){throw new LengthException('Учебная группа ограничена 100 измерениями.');}
return $values;
},static function($context,int $rows):?float {
$values=$context??[];$n=count($values);if($n===0){return null;}sort($values,SORT_NUMERIC);$middle=intdiv($n,2);
return $n%2?(float)$values[$middle]:($values[$middle-1]+$values[$middle])/2;
},1);
$rank=['late'=>0,'on_time'=>1,'unknown'=>2];
$collation=$db->createCollation('app_sla_order',static function(string $a,string $b)use($rank):int {
return (($rank[$a]??3)<=>($rank[$b]??3))?:(strcmp($a,$b)<=>0);
});
$report=p181Read($db,"SELECT service,app_sla(status) AS status,app_median(minutes) AS median_minutes,COUNT(minutes) AS samples FROM $q GROUP BY service,app_sla(status) ORDER BY status COLLATE app_sla_order,service LIMIT 10");
$empty=p181Read($db,"SELECT app_median(minutes) AS median_minutes FROM $q WHERE id<0")[0]['median_minutes'];
$out=fopen(__DIR__.'/service-metrics.csv','wb');fputcsv($out,['service','status','median_minutes','samples'],',','"','',"\n");
foreach($report as $r){fputcsv($out,array_values($r),',','"','',"\n");}fclose($out);
$checks=['three_registrations'=>$function&&$aggregate&&$collation,'four_groups'=>count($report)===4,
'median_even_A'=>(float)$report[0]['median_minutes']===40.0,'median_even_B'=>(float)$report[1]['median_minutes']===80.0,
'median_single'=>(float)$report[2]['median_minutes']===25.0,'null_group'=>$report[3]['median_minutes']===null&&(int)$report[3]['samples']===0,
'empty_is_null'=>$empty===null,'priority_order'=>array_column($report,'status')===['late','late','on_time','unknown'],
'normalized_labels'=>p181Read($db,"SELECT app_sla(' OK ') AS status")[0]['status']==='on_time'];
$rows=[['Групп отчёта',count($report)],['Медиана задержек A, мин',(int)$report[0]['median_minutes']],['Медиана задержек B, мин',(int)$report[1]['median_minutes']],['Пустая группа',$empty],['Порядок','late → on_time → unknown'],['Файл','service-metrics.csv']];
$observations=['report'=>$report,'input'=>$input,'empty_group'=>$empty,'group_limit'=>100];
} finally {
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-service-metrics','Собрать отчёт по доставке с собственными правилами',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Групп отчёта — 4
Медиана задержек A, мин — 40
Медиана задержек B, мин — 80
Пустая группа — null
Порядок — late → on_time → unknown
Файл — service-metrics.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать отчёт служб доставки
Границы решения
Медиана — середина отсортированных значений; здесь NULL пропускается, пустая группа даёт NULL. Массив агрегата ограничен 100 измерениями и требует памяти на группу. Встроенные SQL-возможности обычно предпочтительнее, когда уже решают задачу; доступность median() зависит от сборки SQLite. Правило статусов не заменяет сортировку человеческого текста по Unicode/локали.
Попробуйте изменить условие
В группе A добавьте задержку 100 минут. Чему должна равняться медиана и почему не среднему?
Разбор
После сортировки [35,45,100] середина равна 45. Среднее было бы 60; эти показатели отвечают на разные вопросы. Проверьте отчёт SQL, а не выгружайте всю рабочую таблицу для пересортировки в PHP.
Разрешить конкретные действия при подготовке SQL
Авторизатор — контроль действий SQL на конкретном соединении. Представьте список разрешённых операций перед выполнением заявки: прочитать имя можно, читать заметку надо как NULL, менять данные нельзя. Это аналогия к подготовке SQL; callback не проверяет каждую строку по бизнес-правам пользователя.
$db->setAuthorizer(function (int $action, ?string $arg1, ?string $arg2,
?string $database, ?string $triggerOrView): int {
return $action === 23 ? Pdo\Sqlite::DENY : Pdo\Sqlite::OK;
});Метод доступен с PHP 8.5 и возвращает void. $action описывает действие; смысл arg1/arg2 зависит от него. Например, для READ это таблица и столбец. $database — main/temp или имя присоединённой базы; последний аргумент указывает внутренний trigger/view либо NULL. Один callback заменяет прежний; setAuthorizer(null) снимает его. Приведённый короткий запрет UPDATE не является полным списком разрешений — такой список показан в самостоятельном проекте.
| Ответ / код | Смысл для проекта |
|---|---|
| Pdo\Sqlite::OK | Разрешить действие |
| Pdo\Sqlite::DENY | Отклонить всю SQL-команду |
| Pdo\Sqlite::IGNORE | Для READ заменить чтение колонки SQL NULL |
| SQLITE_READ = 20 | arg1 — таблица, arg2 — колонка |
| SQLITE_SELECT = 21 | Подготовка SELECT |
| SQLITE_UPDATE = 23 | Изменение указанной колонки |
Коды действий взяты из SQLite Authorizer Action Codes. IGNORE имеет разный смысл для разных действий: например, при DELETE он не равен универсальному запрету удаления. В возвращаемом значении callback нужен int из OK/DENY/IGNORE; неправильный тип вызывает TypeError, неподдерживаемое целое — ValueError. Имена объектов сравнивайте без учёта регистра.
Правило применяется при prepare и возможной переподготовке SQLite, в том числе во время execute после изменения схемы. Не меняйте это же соединение внутри callback. Ограничение действий не задаёт лимит времени, памяти, строк или авторизацию пользователя; для фиксированного отчёта по-прежнему нужны WHERE, нужные столбцы и LIMIT. Карточка setAuthorizer сохраняет короткие варианты отказа и подмены.
Выгрузить разрешённые поля и отклонить изменение
Администратор формирует фиксированную выгрузку контактов. Имена нужны в отчёте, служебные заметки — нет. Авторизатор соединения подменяет чтение закрытого поля NULL и отклоняет попытку UPDATE; затем владелец проверяет, что исходные данные сохранены.
- SELECT id,name,private_note → подготовка SQL
- READ private_note → IGNORE → NULL в отчёте
- UPDATE name → DENY → отказ команды
- Снять правило → проверить исходные строки → CSV
Минимальный вызов
$db->setAuthorizer(fn ($action) => $action === 23
? Pdo\Sqlite::DENY : Pdo\Sqlite::OK);
// 23 — действие SQLITE_UPDATE; конкретный UPDATE теперь запрещён.
$db->setAuthorizer(null); // снять правилоsetAuthorizer устанавливает callback на текущее соединение и не возвращает значение. $action — код операции SQLite. DENY отклоняет команду; null снимает правило. Этот короткий фрагмент показывает только запрет UPDATE, а полный проект разрешает ровно нужные действия и колонки.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Строк выгрузки | 2 |
| Закрытые поля | NULL |
| UPDATE запрещён | да |
| Код SQLite | 23 |
| Исходные данные сохранены | да |
| Файл | approved-export.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);
$table=p181OwnTable($db,$owned,'contacts','(id INTEGER PRIMARY KEY,name TEXT NOT NULL,private_note TEXT NOT NULL)');$q=p181Qualified($table);
p181Command($db,"INSERT INTO $q VALUES(?,?,?)",[1,'Анна','internal-A']);p181Command($db,"INSERT INTO $q VALUES(?,?,?)",[2,'Борис','internal-B']);
$calls=[];
$db->setAuthorizer(static function(int $action,?string $a,?string $b,?string $database,?string $trigger)use($table,&$calls):int {
$calls[]=['action'=>$action,'column'=>$b];
if($action===21){return Pdo\Sqlite::OK;} // SQLITE_SELECT
if($action===20&&strcasecmp($a??'',$table)===0&&strcasecmp($database??'','main')===0){
if(strcasecmp($b??'','private_note')===0){return Pdo\Sqlite::IGNORE;}
if(in_array(strtolower($b??''),['id','name'],true)){return Pdo\Sqlite::OK;}
}
return Pdo\Sqlite::DENY;
});
try{
$selected=p181Read($db,"SELECT id,name,private_note FROM $q WHERE id<=? ORDER BY id LIMIT 2",[2]);
$denied=p181Capture(fn()=>p181Command($db,"UPDATE $q SET name=? WHERE id=1",['Changed']));
}finally{$db->setAuthorizer(null);}
$original=p181Read($db,"SELECT id,name,private_note FROM $q ORDER BY id LIMIT 2");
$out=fopen(__DIR__.'/approved-export.csv','wb');fputcsv($out,['id','name','private_note'],',','"','',"\n");
foreach($selected as $r){fputcsv($out,array_values($r),',','"','',"\n");}fclose($out);
$checks=['two_rows'=>count($selected)===2,'names_preserved'=>array_column($selected,'name')===['Анна','Борис'],
'hidden_as_null'=>array_column($selected,'private_note')===[null,null],
'write_denied'=>$denied['failed']&&$denied['code']===23,
'original_unchanged'=>array_column($original,'name')===['Анна','Борис']&&array_column($original,'private_note')===['internal-A','internal-B'],
'read_actions'=>in_array(20,array_column($calls,'action'),true),'write_action'=>in_array(23,array_column($calls,'action'),true)];
$rows=[['Строк выгрузки',count($selected)],['Закрытые поля','NULL'],['UPDATE запрещён',$denied['failed']],['Код SQLite',$denied['code']],['Исходные данные сохранены',$checks['original_unchanged']],['Файл','approved-export.csv']];
$observations=['selected'=>$selected,'denied'=>$denied,'authorizer_calls'=>$calls,'owner_verification'=>true];
} finally {
if($db instanceof Pdo\Sqlite){$db->setAuthorizer(null);}
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-authorized-export','Выгрузить разрешённые поля и отклонить изменение',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Строк выгрузки — 2
Закрытые поля — NULL
UPDATE запрещён — да
Код SQLite — 23
Исходные данные сохранены — да
Файл — approved-export.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать разрешённую выгрузку
Границы решения
Авторизатор срабатывает при подготовке и возможной переподготовке, а не для каждой строки. Это не авторизация пользователя и не песочница произвольного SQL. Для заранее известной выгрузки обычно проще вообще не выбирать закрытый столбец; сценарий нужен, когда требуется дополнительно ограничить возможности SQL-инструмента. Callback не должен менять то же соединение.
Попробуйте изменить условие
Что нужно изменить, если выгрузка должна содержать только контакты текущего клиента?
Разбор
Установите tenant_id через проверенную авторизацию приложения и добавьте WHERE tenant_id=:tenant. Авторизатор видит действия и столбцы, но не определяет автоматически владельца каждой строки.
BLOB: байты записи и поток фиксированного размера
BLOB хранит последовательность байтов, в том числе NUL и данные вне UTF-8. В обычном INSERT можно передать строку/поток с PDO::PARAM_LOB. Если требуется менять или читать часть уже выделенного поля, openBlob даёт поток к конкретной записи.
| Параметр openBlob | Как выбрать |
|---|---|
| $table / $column | Реальная таблица и поле; имена задаёт приложение |
| $rowid | Целочисленный rowid записи; INTEGER PRIMARY KEY обычно является его именем |
| $dbname = "main" | Логическое имя базы, не путь .sqlite |
| $flags = OPEN_READONLY | Для записи используйте OPEN_READWRITE; OPEN_CREATE не создаёт отсутствующую строку |
Успех возвращает PHP-ресурс. Ошибка открытия в PHP 8.5.11 выдаёт предупреждение и false; это отличается от исключения SQL при PDO::ERRMODE_EXCEPTION. Проверяйте ресурс перед fread/fwrite, проверяйте объём записи и закрывайте fclose в finally. Предупреждение потока само по себе не становится PDOException.
- INSERT zeroblob(4) → четыре нулевых байта
- openBlob(rowid=1) → дескриптор существующего поля
- fwrite NOTE → четыре сохранённых байта
- fclose → commit → новый поток чтения
Размер задаётся до открытия; запись за границу не удлиняет поле. WITHOUT ROWID не подходит. Для записи SQLite также ограничивает некоторые индексируемые, PRIMARY KEY/UNIQUE и дочерние FK-колонки при включённой проверке связей. UPDATE/DELETE строки может аннулировать открытый handle. Подробный договор дополняют SQLite incremental BLOB API и реализация PHP 8.5.11: официальная PHP-страница openBlob пока содержит незаполненные разделы.
Не выбирайте содержимое всех документов для списка названий. Сначала получите нужные метаданные, затем откройте один BLOB после проверки доступа. В учебном примере маленькое содержимое собирается целиком для сравнения SHA; для большой выгрузки передавайте порции в файл/ответ, измеряя память именно своего приложения. Карточка openBlob содержит минимальные чтение и запись.
Записать вложение порциями и восстановить точные байты
К локальной записи нужно приложить двоичный документ. Внутри есть русское название, нулевой байт и FF, поэтому обрабатывать содержимое как обычную строку для HTML нельзя. Выделим точный размер BLOB, запишем части и восстановим файл новым подключением.
Минимальный вызов
$db->exec("INSERT INTO documents VALUES(1, 'Памятка', zeroblob(4))");
$stream = $db->openBlob('documents', 'body', 1, 'main', Pdo\Sqlite::OPEN_READWRITE);
fwrite($stream, 'NOTE');
fclose($stream);В этом фрагменте уже существует таблица documents(id,title,body). zeroblob(4) выделяет четыре байта. openBlob открывает поле body строки rowid=1 в базе main; main — имя подключения SQLite, а не путь файла. После записи поле содержит четыре байта NOTE. Полный проект ниже создаёт таблицу и управляет транзакцией.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Документ | Инструкция |
| Байт | 37 |
| Порций записи | 5 |
| Байты совпали | да |
| NUL и FF сохранены | да |
| Файл | attachment.bin |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$database=null;$owned=$removed=$rows=$checks=$observations=[];$ownedFiles=$removedFiles=[];
$stream=null;
try {
$database=p181Database($ownedFiles,'project');$db=p181Connect($database);$runtime=p181Runtime($db);
$table=p181OwnTable($db,$owned,'documents','(id INTEGER PRIMARY KEY,title TEXT NOT NULL,body BLOB NOT NULL)');$q=p181Qualified($table);
$source="AEN-DOC181\0Инструкция\n\xFFEND\n";$bytes=strlen($source);
$db->beginTransaction();$insert=$db->prepare("INSERT INTO $q (id,title,body) VALUES(1,?,zeroblob(?))");
$insert->bindValue(1,'Инструкция');$insert->bindValue(2,$bytes,PDO::PARAM_INT);$insert->execute();p181Release($insert);
$stream=$db->openBlob($table,'body',1,'main',Pdo\Sqlite::OPEN_READWRITE);
if($stream===false){throw new RuntimeException('Не открыт поток BLOB.');}
$chunks=0;try{for($offset=0;$offset<$bytes;$offset+=8){$piece=substr($source,$offset,8);if(fwrite($stream,$piece)!==strlen($piece)){throw new RuntimeException('Неполная запись.');}$chunks++;}}finally{fclose($stream);$stream=null;}
$db->commit();p181Close($db);$db=p181Connect($database);
$stream=$db->openBlob($table,'body',1);if($stream===false){throw new RuntimeException('Не открыт поток чтения.');}
$restored='';try{while(!feof($stream)){$piece=fread($stream,8);if($piece===false){throw new RuntimeException('Ошибка чтения.');}$restored.=$piece;}}finally{fclose($stream);$stream=null;}
file_put_contents(__DIR__.'/attachment.bin',$restored);
$record=p181Read($db,"SELECT title,length(body) AS bytes,typeof(body) AS storage FROM $q WHERE id=1")[0];
$checks=['title_preserved'=>$record['title']==='Инструкция','storage_blob'=>$record['storage']==='blob',
'same_bytes'=>$restored===$source,'right_length'=>strlen($restored)===$bytes&&(int)$record['bytes']===$bytes,
'multiple_chunks'=>$chunks>1,'hash_equal'=>hash('sha256',$restored)===hash('sha256',$source),
'nul_preserved'=>str_contains($restored,"\0"),'ff_preserved'=>str_contains($restored,"\xFF"),'transaction_closed'=>!$db->inTransaction()];
$rows=[['Документ',$record['title']],['Байт',strlen($restored)],['Порций записи',$chunks],['Байты совпали',$checks['same_bytes']],['NUL и FF сохранены',$checks['nul_preserved']&&$checks['ff_preserved']],['Файл','attachment.bin']];
$observations=['sha256'=>hash('sha256',$restored),'chunks'=>$chunks,'record'=>$record,'hex'=>bin2hex($restored)];
} finally {
if(is_resource($stream)){fclose($stream);$stream=null;}
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-incremental-attachment','Записать вложение порциями и восстановить точные байты',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Документ — Инструкция
Байт — 37
Порций записи — 5
Байты совпали — да
NUL и FF сохранены — да
Файл — attachment.bin
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать восстановленный документ
Границы решения
Поток не увеличивает BLOB: размер задают заранее. Число 8 выбрано для видимых учебных порций, а не как рекомендация для производительности. WITHOUT ROWID и некоторые индексируемые/связанные столбцы для записи не подходят. UPDATE или DELETE той же строки может сделать открытый handle недействительным. Закрывайте поток до commit и изменения строки.
Попробуйте изменить условие
Новая версия документа длиннее старой. Можно ли просто дописать байты в прежний поток?
Разбор
Нет. Закройте старый поток, обновите поле до zeroblob нового размера внутри транзакции и откройте новый поток. При небольшом файле проще привязать целое значение через PDO::PARAM_LOB.
Загрузить совместимое нативное расширение
loadExtension подключает нативную библиотеку SQLite в процесс PHP. Это другая операция, чем подключить PHP-модуль pdo_sqlite в php.ini или файл через require. После успешной загрузки функции библиотеки доступны SQL этого соединения.
$db->loadExtension($trustedDecimalLibrary);
// Успешный метод возвращает void; теперь доступна decimal_sum(amount).Аргумент — существующий путь к совместимой DLL/.so/.dylib. Пустое имя даёт ValueError; недоступный либо несовместимый файл — PDOException. Метод может отсутствовать при сборке PDO_SQLITE_OMIT_LOAD_EXTENSION. В PHP 8.5.11 есть ограничение многопоточных web-SAPI; CLI-сценарий ниже использует NTS PHP. Реализация временно разрешает загрузку через C API и затем выключает её, не включая SQL-функцию load_extension(). Не пытайтесь выполнять C-команды или команды консоли .load как SQL PDO.
Для воспроизведения Windows-сборки нужны три официальных архива: SQLite source 3.53.4, заголовки amalgamation 3.53.4 и LLVM-MinGW UCRT x86_64 20261104. Распакуйте их в отдельную папку. В source лежит ext/misc/decimal.c, в amalgamation — sqlite3.h и sqlite3ext.h. SQLite распространяет эти исходники как public domain; лицензии toolchain сохраняются в его архиве.
# PowerShell, из папки с распакованными архивами:
& .\llvm-mingw-20261104-ucrt-x86_64\bin\x86_64-w64-mingw32-gcc.exe `
-O2 -shared -I .\sqlite-amalgamation-3530400 `
.\sqlite-src-3530400\ext\misc\decimal.c -o .\decimal.dll
$env:PHPMAN_SQLITE_LAB='pdo-sqlite181'
$env:PHPMAN_SQLITE_EXTENSION=(Resolve-Path .\decimal.dll).Path
php example.phpКоманда компиляции создаёт decimal.dll архитектуры x64; PHP также должен быть x64. Использован LLVM-MinGW/Clang 23.1.5. На Linux типовая команда имеет вид cc -O2 -fPIC -shared -I ./sqlite-amalgamation-3530400 ./sqlite-src-3530400/ext/misc/decimal.c -o decimal.so; этот вариант здесь приведён как инструкция для другой ОС, а не как выполненный Windows-тест. Затем укажите абсолютный путь через PHPMAN_SQLITE_EXTENSION. Документация сборки расширений SQLite объясняет entry point и зависимости.
В вопросе о сумме DECIMAL ожидаемый ноль превратился в маленький остаток. Само имя DECIMAL в SQLite не устанавливает фиксированную десятичную арифметику. Наш проект сохраняет исходные строки в TEXT, использует decimal_sum и отдаёт результат строкой, сохраняя цифры до последнего шага. Официальное описание decimal.c показывает отдельные функции decimal_add/sub/mul/cmp/sum и отличие от binary64. Ошибочный ввод надо проверять до расчёта; библиотека не определяет валюту, масштаб или правила округления вашего приложения.
Получить точный итог через доверенное decimal-расширение
В отчёте встречаются очень большие суммы и небольшая корректировка. Нужно получить ровно 0.03, не потеряв копейки при преобразовании в двоичный float. Сохраним входные десятичные строки как TEXT и загрузим официальное decimal-расширение SQLite.
- Доверенный decimal.c → совместимая DLL/.so
- loadExtension → функции текущего соединения
- TEXT: 10000000000000000.01 + 0.02 − 10000000000000000.00
- decimal_sum → строка 0.03 → CSV
Минимальный вызов
$db->loadExtension($trustedDecimalLibrary);
$total = $db->query("SELECT decimal_add('0.10', '0.20')")->fetchColumn();
echo $total; // 0.30$trustedDecimalLibrary — заранее установленный путь из конфигурации развёртывания. Метод не возвращает значение, а регистрирует функции библиотеки в этом соединении. decimal_add складывает десятичные строки; в полном проекте decimal_sum получает столбец из трёх строк. PHP не переводит их в float.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Строк расчёта | 3 |
| Хранение | TEXT |
| Точный итог | 0.03 |
| Сравнение с 0.03 | 0 |
| PHP float использован | нет |
| Файл | exact-total.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);
$extension=getenv('PHPMAN_SQLITE_EXTENSION');$trusted=$extension?realpath($extension):false;
if($trusted===false||!is_file($trusted)){throw new RuntimeException('Укажите установленную доверенную decimal-библиотеку из главы.');}
$db->loadExtension($trusted);
$table=p181OwnTable($db,$owned,'amounts','(id INTEGER PRIMARY KEY,amount TEXT NOT NULL)');$q=p181Qualified($table);
$amounts=['10000000000000000.01','0.02','-10000000000000000.00'];
$insert=$db->prepare("INSERT INTO $q(id,amount) VALUES(?,?)");
foreach($amounts as $i=>$value){$insert->bindValue(1,$i+1,PDO::PARAM_INT);$insert->bindValue(2,$value,PDO::PARAM_STR);$insert->execute();}p181Release($insert);
$sourceRows=p181Read($db,"SELECT id,amount,typeof(amount) AS storage FROM $q ORDER BY id");
$total=p181Read($db,"SELECT decimal_sum(amount) AS total FROM $q")[0]['total'];
$same=p181Read($db,"SELECT decimal_cmp(?,?) AS comparison",[$total,'0.03'])[0]['comparison'];
$out=fopen(__DIR__.'/exact-total.csv','wb');fputcsv($out,['line','amount'],',','"','',"\n");
foreach($sourceRows as $r){fputcsv($out,[$r['id'],$r['amount']],',','"','',"\n");}fputcsv($out,['TOTAL',$total],',','"','',"\n");fclose($out);
$checks=['text_inputs'=>array_column($sourceRows,'amount')===$amounts,'stored_as_text'=>array_column($sourceRows,'storage')===['text','text','text'],
'result_is_text'=>is_string($total),'exact_total'=>$total==='0.03','decimal_comparison'=>$same===0,
'all_lines'=>count($sourceRows)===3,'extension_available'=>method_exists($db,'loadExtension')];
$rows=[['Строк расчёта',count($sourceRows)],['Хранение','TEXT'],['Точный итог',$total],['Сравнение с 0.03',$same],['PHP float использован',false],['Файл','exact-total.csv']];
$observations=['lines'=>$sourceRows,'exact_total'=>$total,'extension_sha256'=>hash_file('sha256',$trusted),'input_contract'=>'trusted decimal strings with dot separator'];
} finally {
p181Close($db);
if($database!==null){[$removed,$removedFiles]=p181Cleanup($database,$owned,$ownedFiles);}
}
p181Report('pdo181-decimal-extension','Получить точный итог через доверенное decimal-расширение',$rows,$checks,$observations,$runtime,$owned,$removed,$ownedFiles,$removedFiles);
Ожидаемый результат
Строк расчёта — 3
Хранение — TEXT
Точный итог — 0.03
Сравнение с 0.03 — 0
PHP float использован — нет
Файл — exact-total.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать точный расчёт
Границы решения
Имя типа DECIMAL в обычной SQLite не включает такую арифметику автоматически. Возможность загрузки зависит от сборки PHP, архитектуры и зависимостей библиотеки. Расширение — нативный код; путь задаёт развёртывание, не посетитель. Для обычных денежных величин с известным масштабом часто достаточно целых копеек с проверкой диапазона; здесь показана отдельная потребность точных десятичных строк.
Попробуйте изменить условие
Можно ли заменить TEXT на REAL и затем вызвать decimal_sum?
Разбор
Если точность уже потеряна при сохранении REAL, расширение не восстановит исходные десятичные цифры. Сохраняйте проверенную исходную строку или целое значение в согласованных единицах с самого начала.
Сохранить смысл при переходе на современные методы
Современные методы createFunction/createAggregate/createCollation доступны у Pdo\Sqlite с PHP 8.4. Имена PDO::sqliteCreateFunction, sqliteCreateAggregate и sqliteCreateCollation устарели с PHP 8.5, но их старые карточки сохранены для миграции. Замените создание базового PDO на подходящий драйверный объект либо PDO::connect и короткий метод; проверьте тип callback и его поведение на данных приложения.
| Старое имя | Современная замена |
|---|---|
| PDO::sqliteCreateFunction | Pdo\Sqlite::createFunction |
| PDO::sqliteCreateAggregate | Pdo\Sqlite::createAggregate |
| PDO::sqliteCreateCollation | Pdo\Sqlite::createCollation |
Сигнатуры, версии и минимальные примеры остаются в справочнике PDO. Регистрация callbacks не сохраняется внутри .sqlite как PHP-код: каждое новое соединение настраивается заново.
Выбрать минимальный достаточный механизм
Начните с нужных строк и столбцов SQL. Если встроенного выражения достаточно, используйте его. PHP callback добавляет конкретное правило, authorizer ограничивает подготовку действий, BLOB-поток работает с частью поля, а нативное расширение приносит отдельную реализацию. Файловое приложение связывает это с постоянными данными; два подключения и срок результата объясняет конкуренцию и завершение операций.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


