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

Дерево руководства
Глава 68 · После первого Firebird SELECT и явных транзакций
Firebird: постоянное соединение между HTTP-запросами и границы его состояния
Два вызова, два HTTP-запроса, отмена резерва и перезапуск worker: что сохраняет ibase_pconnect и что приложение обязано очистить.
Зачем сохранять соединение и чему оно не принадлежит
Каталог получает много коротких обращений: показать товар, обновить остаток, отменить резерв. Если создание соединения дорого, живущий PHP-процесс может использовать его повторно. Но соединение не принадлежит посетителю: прежде чем добавить pconnect, нужно понять, что сохраняется между запросами, что необходимо очистить и что исчезает вместе с процессом.
- Посетитель → HTTP-запрос №1 → worker P
- pconnect(настройки) → соединение A → SELECT Cable
- Ответ отправлен; переменные запроса больше не продолжаются
- HTTP-запрос №2 попал в P → pconnect(те же настройки) → A
- Другая транзакция/новый SELECT → свежий ответ
- Запрос попал в worker Q → у Q собственное соединение B
Можно представить рабочее место оператора: подготовленный канал связи остаётся на его рабочем месте, но сведения о предыдущем клиенте нельзя оставить для следующего. Это только модель времени жизни; она не означает общий канал для всех процессов или сохранение PHP-переменных. Подключение — канал, транзакция — группа изменений, HTTP-запрос — одно обращение клиента.
| Не путать | Что это означает |
|---|---|
| Соединение Firebird | Ресурс доступа к одной базе и его серверное состояние. |
| Транзакция | Граница согласованного чтения/изменений до commit или rollback. |
| PHP worker | Процесс, внутри которого драйвер хранит повторно используемые подключения. |
| PHP $_SESSION | Данные сессии сайта; живой resource драйвера через неё не переносится. |
Перед этим маршрутом полезны первое подключение и SELECT и транзакции с наблюдаемым результатом. Примеры используют небольшой синтетический каталог, а не выгрузку всей рабочей базы.
Как запустить реальные HTTP-примеры
Каждый ZIP запускается одной командой PHP CLI из README. Внешний example.php запускает scenario.php: тот создаёт собственную учебную базу, отправляет запросы настоящему HTTP-процессу на 127.0.0.1, сохраняет ответы, останавливает HTTP-процесс и сам завершается. Только затем отдельный процесс проверяет и удаляет свою базу. Persistent-вызовы находятся именно в HTTP-обработчике; короткий одиночный CLI-запуск не выдаётся за межзапросное повторное использование.
| Файл | Зачем он нужен |
|---|---|
| example.php | Дождаться завершения сценария, выполнить отдельную очистку и записать result.html/result.json. |
| scenario.php | Создать свой каталог, выполнить выбранные HTTP-действия и сравнить полученные данные. |
| http-router.php | Получить pconnect, проверить учебную базу по токену, прочитать товар или отменить резерв. |
| http-lab.php | Ограниченно запускать дочерние процессы и сохранять доказательства HTTP-ответов и завершения. |
| http-cleanup.php | В новом процессе проверить точный путь и владельца, затем удалить только собственную базу. |
| lab.php, seed.sql, seed.fbk.base64.txt | Подготовить синтетический каталог LAB_ITEMS: Cable=3, Clamp=0; ограничить операции собственной учебной областью. |
| README.txt | Версии, установка отдельного окружения и команда запуска. |
Почему процессы разделены: в Windows принудительная остановка worker не равна штатному завершению PHP и освобождению всех ресурсов драйвера. В проверенной embedded-лаборатории перед удалением завершается весь процесс сценария, а удаление подтверждается отдельно. Это техническая организация запуска примера: рабочую базу сайта не создают и не удаляют на каждый HTTP-запрос. Приватный scenario-handoff.json и журналы остаются в локальном каталоге; для просмотра результата нужны result.html и result.json.
PID и attachment ID не имеют заранее фиксированного числового значения. В таблицах ниже показывается смысл сравнения, а JSON сохраняет фактические номера, HTTP-коды и SHA256 тел ответов. Эти результаты подтверждают конкретное поведение и очистку на стенде; они не являются замером ускорения сервера. HTTP-обработчик допускает только четыре учебных действия и случайный заголовок своего запуска; это не публичный API магазина.
Два участка страницы: один канал к базе, два нужных товара
На странице каталога один блок показывает Cable, другой — Clamp. Оба получают настройки подключения из одного места. Второй вызов pconnect с теми же параметрами не означает обязательное создание второго соединения: проверим идентификатор соединения на стороне самой базы и получим только нужные товары.
- HTTP-запрос → PHP worker
- Первый pconnect с набором параметров → подключение A
- SELECT ID=1 → Cable, остаток 3
- Второй pconnect с тем же набором → подключение A
- SELECT ID=2 → Clamp, остаток 0
- Конец запроса ≠ конец процесса
Минимальный вызов и его смысл
$db = ibase_pconnect($database, $user, $password, 'UTF8', 0, 3);
if ($db === false) {
$error = ibase_errmsg();
throw new RuntimeException('Не удалось подключиться');
}pconnect — вызов функции; в скобках передаются путь базы, пользователь, пароль, кодировка соединения, число буферов и SQL-диалект. Возвращённый ресурс сохраняется в $db и передаётся запросам. Это служебный идентификатор PHP, а не строка пароля или готовый список товаров. Сравнение === false отделяет отказ подключения от результата.
Что делает HTTP-обработчик
$first = ibase_pconnect($database, $user, $password, 'UTF8', 0, 3);
$second = ibase_pconnect($database, $user, $password, 'UTF8', 0, 3);
// Через каждый ресурс выполнить SELECT CURRENT_CONNECTION FROM RDB$DATABASE.
// Затем SELECT ID, TITLE, QTY FROM LAB_ITEMS WHERE ID = 1 или ID = 2.Этот фрагмент выделяет изучаемый шаг. Полный http-router.php в ZIP содержит проверку результата, ограничение собственной базы, завершение транзакций и обработку ошибок.
Полный сценарий: создание каталога, HTTP-действия и сравнение результата
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;$error=null;
$rows=$checks=$observations=$runtime=[];
$transferToken=getenv('PHPMAN_FIREBIRD_SCENARIO_TOKEN');
try {
$config=f158Config();
if(!is_string($transferToken)||!preg_match('/^[a-f0-9]{32}$/D',$transferToken))throw new RuntimeException('Запускайте example.php, а не scenario.php напрямую.');
$service=f158Service();$runtime=f158Runtime($service);
require __DIR__.'/http-lab.php';
$database=f158CreateDatabase($service,$owned,'reuse');
$session=f158HttpSession($database,['same-request']);$reply=$session['responses'][0];$seen=$reply['observed'];
$same=$reply['attachment_id']===$seen['second_attachment_id'];
$rows=[['HTTP-запросов',$session['http_requests']],['Два вызова используют одно подключение',$same],
['Товар основного блока',$seen['item']['title']],['Его остаток',$seen['item']['qty']],
['Товар второго блока',$seen['second_item']['title']],['Его остаток',$seen['second_item']['qty']],
['HTTP-процесс остановлен',$session['stopped']]];
$checks=['one_real_http_request'=>$session['http_requests']===1,'same_attachment'=>$same,
'first_item'=>$seen['item']===['id'=>1,'title'=>'Cable','qty'=>3],
'second_item'=>$seen['second_item']===['id'=>2,'title'=>'Clamp','qty'=>0],
'worker_identity'=>$reply['pid']===$session['worker_pid'],'http_200'=>$session['proof']['statuses']===[200],
'worker_stopped'=>$session['stopped'],'no_inherited_context'=>$seen['context']===null];
$observations=['http_session'=>$session,'same_attachment'=>$same,'two_calls_not_two_connections'=>true,
'single_request_is_not_cross_request_proof'=>true];
}catch(Throwable $caught){$error=['class'=>get_class($caught),'message'=>$caught->getMessage()];}
finally{
try{
if(is_resource($db))f158Close($db);
if(is_resource($service))f158Detach($service);
}catch(Throwable $cleanupError){$error=$error??['class'=>get_class($cleanupError),'message'=>$cleanupError->getMessage()];}
}
$handoff=['schema'=>'aen-firebird158-private-handoff-v1','ok'=>$error===null,'pid'=>getmypid(),
'transfer_token'=>$transferToken,'state'=>f158State(),'error'=>$error,
'result'=>['rows'=>$rows,'checks'=>$checks,'observations'=>$observations,'runtime'=>$runtime,'owned'=>$owned]];
file_put_contents(__DIR__.'/scenario-handoff.json',json_encode($handoff,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n",LOCK_EX);
// The process exits before the outer controller starts database cleanup.
exit($error===null?0:1);
Прикладные шаги этого сценария сохранены в scenario.php. Запускается внешний example.php ниже: он дожидается штатного завершения сценария перед отдельным удалением базы.
Видимый результат полного запуска
| Наблюдение | Результат |
|---|---|
| HTTP-запросов | 1 |
| Два вызова используют одно подключение | да |
| Товар основного блока | Cable |
| Его остаток | 3 |
| Товар второго блока | Clamp |
| Его остаток | 0 |
| HTTP-процесс остановлен | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';require __DIR__.'/http-lab.php';
// No service/database connection is opened by this outer process.
$result=f158RunScenario();
$owned=$result['owned'];$rows=$result['rows'];$checks=$result['checks'];
$observations=$result['observations'];$runtime=$result['runtime'];
f158HttpDrop($owned[0]['name']);
$removed=f158Cleanup($owned);
f158Report('fb158-pconnect-reuse','Два участка страницы: один канал к базе, два нужных товара',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
HTTP-запросов — 1
Два вызова используют одно подключение — да
Товар основного блока — Cable
Его остаток — 3
Товар второго блока — Clamp
Его остаток — 0
HTTP-процесс остановлен — да
Самостоятельный контроллер CLI с приложенными HTTP-файлами. Создаёт собственную синтетическую базу и настоящий локальный HTTP worker; после запросов останавливает worker, удаляет свою базу и сохраняет HTML/JSON-результат.
Фактический HTML-результат · Фактические данные JSON.
Ошибки, границы и применение
CURRENT_CONNECTION — идентификатор подключения внутри Firebird, а не номер PHP resource. Два вызова обычного ibase_connect с одинаковыми аргументами в одном запросе тоже могут использовать одно соединение. Поэтому этот первый шаг показывает повторное использование внутри запроса; отличие pconnect между разными запросами проверяется следующим примером. Из того, что соединение существует, не следует, что любой SELECT выполнится успешно.
В историческом вопросе второй вызов подключения приводил к недействительному первому ресурсу при переходе на PHP 7. Исходный вопрос Stack Overflow. Это повод фиксировать версию драйвера и проверять оба реальных чтения, а не выводить из числа PHP-переменных число соединений. Старый дефект не объявляется обязательным поведением interbase 3.0.1.
Проверьте понимание на новой задаче
Два компонента страницы должны независимо читать товары. Нужно ли каждому обязательно открывать отдельное физическое соединение?
Вариант решения
Нет. Передавайте согласованное подключение либо используйте одинаковые настройки; контролируйте время жизни результатов и транзакций. Если нужна независимая транзакция, объявите её явно: число PHP-переменных не задаёт число транзакций.
Обновить карточку товара вторым HTTP-запросом без нового подключения
Посетитель открыл карточку Cable и нажал «Обновить остаток». Это два HTTP-запроса, каждый выполняет свой SELECT. Проверим, что один обслуживающий процесс PHP использует уже созданное соединение, хотя PHP-переменные предыдущего запроса больше не являются состоянием страницы. Сохранённое соединение не превращает старый остаток в кэш: данные надо запросить снова.
- Запрос №1 → worker P → pconnect → подключение A → SELECT → Cable: 3
- PHP заканчивает запрос №1; процесс P остаётся
- Запрос №2 → тот же P → тот же набор параметров pconnect
- Подключение A → новый SELECT → Cable: 3
- Контроллер сравнивает PID и CURRENT_CONNECTION обоих ответов
Минимальный вызов и его смысл
$db = ibase_pconnect($database, $user, $password, 'UTF8', 0, 3);
// На каждом запросе снова читаем требуемые данные.
$result = ibase_query($db, 'SELECT TITLE, QTY FROM LAB_ITEMS WHERE ID = 1');HTTP-запрос — отдельное обращение клиента к обработчику. Worker — процесс PHP, который способен выполнить несколько обращений последовательно. pid сообщает номер этого процесса, CURRENT_CONNECTION — номер соединения с базой. Нужны обе величины: одна и та же строка товара сама по себе не доказывает повторного использования соединения.
Что делает HTTP-обработчик
// Эти сведения добавляются к JSON только в локальной учебной диагностике.
$process = getmypid();
$result = ibase_query($db, 'SELECT CURRENT_CONNECTION AS ID FROM RDB$DATABASE');
$row = ibase_fetch_assoc($result);
$connection = $row['ID'];
ibase_free_result($result);
$result = null;Этот фрагмент выделяет изучаемый шаг. Полный http-router.php в ZIP содержит проверку результата, ограничение собственной базы, завершение транзакций и обработку ошибок.
Полный сценарий: создание каталога, HTTP-действия и сравнение результата
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;$error=null;
$rows=$checks=$observations=$runtime=[];
$transferToken=getenv('PHPMAN_FIREBIRD_SCENARIO_TOKEN');
try {
$config=f158Config();
if(!is_string($transferToken)||!preg_match('/^[a-f0-9]{32}$/D',$transferToken))throw new RuntimeException('Запускайте example.php, а не scenario.php напрямую.');
$service=f158Service();$runtime=f158Runtime($service);
require __DIR__.'/http-lab.php';
$database=f158CreateDatabase($service,$owned,'requests');
$session=f158HttpSession($database,['read-item','read-item']);[$first,$second]=$session['responses'];
$sameWorker=$first['pid']===$second['pid'];$sameConnection=$first['attachment_id']===$second['attachment_id'];
$rows=[['Отдельных HTTP-запросов',$session['http_requests']],['Запросы обслужил один PHP-процесс',$sameWorker],
['Соединение Firebird использовано повторно',$sameConnection],['Первый ответ: товар',$first['observed']['item']['title']],
['Первый ответ: остаток',$first['observed']['item']['qty']],['Второй ответ: остаток',$second['observed']['item']['qty']],
['HTTP-процесс остановлен',$session['stopped']]];
$checks=['two_real_http_requests'=>$session['http_requests']===2,'same_worker'=>$sameWorker,'same_attachment'=>$sameConnection,
'first_response'=>$first['observed']['item']===['id'=>1,'title'=>'Cable','qty'=>3],
'second_response'=>$second['observed']['item']===['id'=>1,'title'=>'Cable','qty'=>3],
'both_http_200'=>$session['proof']['statuses']===[200,200],'worker_stopped'=>$session['stopped'],
'separate_response_bodies'=>count($session['proof']['sha256s'])===2];
$observations=['http_session'=>$session,'same_worker'=>$sameWorker,'same_attachment'=>$sameConnection,
'each_response_queries_current_data'=>true,'resource_not_stored_in_browser_session'=>true];
}catch(Throwable $caught){$error=['class'=>get_class($caught),'message'=>$caught->getMessage()];}
finally{
try{
if(is_resource($db))f158Close($db);
if(is_resource($service))f158Detach($service);
}catch(Throwable $cleanupError){$error=$error??['class'=>get_class($cleanupError),'message'=>$cleanupError->getMessage()];}
}
$handoff=['schema'=>'aen-firebird158-private-handoff-v1','ok'=>$error===null,'pid'=>getmypid(),
'transfer_token'=>$transferToken,'state'=>f158State(),'error'=>$error,
'result'=>['rows'=>$rows,'checks'=>$checks,'observations'=>$observations,'runtime'=>$runtime,'owned'=>$owned]];
file_put_contents(__DIR__.'/scenario-handoff.json',json_encode($handoff,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n",LOCK_EX);
// The process exits before the outer controller starts database cleanup.
exit($error===null?0:1);
Прикладные шаги этого сценария сохранены в scenario.php. Запускается внешний example.php ниже: он дожидается штатного завершения сценария перед отдельным удалением базы.
Видимый результат полного запуска
| Наблюдение | Результат |
|---|---|
| Отдельных HTTP-запросов | 2 |
| Запросы обслужил один PHP-процесс | да |
| Соединение Firebird использовано повторно | да |
| Первый ответ: товар | Cable |
| Первый ответ: остаток | 3 |
| Второй ответ: остаток | 3 |
| HTTP-процесс остановлен | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';require __DIR__.'/http-lab.php';
// No service/database connection is opened by this outer process.
$result=f158RunScenario();
$owned=$result['owned'];$rows=$result['rows'];$checks=$result['checks'];
$observations=$result['observations'];$runtime=$result['runtime'];
f158HttpDrop($owned[0]['name']);
$removed=f158Cleanup($owned);
f158Report('fb158-http-reuse','Обновить карточку товара вторым HTTP-запросом без нового подключения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Отдельных HTTP-запросов — 2
Запросы обслужил один PHP-процесс — да
Соединение Firebird использовано повторно — да
Первый ответ: товар — Cable
Первый ответ: остаток — 3
Второй ответ: остаток — 3
HTTP-процесс остановлен — да
Самостоятельный контроллер CLI с приложенными HTTP-файлами. Создаёт собственную синтетическую базу и настоящий локальный HTTP worker; после запросов останавливает worker, удаляет свою базу и сохраняет HTML/JSON-результат.
Фактический HTML-результат · Фактические данные JSON.
Ошибки, границы и применение
В примере HTTP-запросы направлены одному специально запущенному процессу, поэтому сравнение воспроизводимо. На сайте с несколькими workers второй запрос того же посетителя может обслужить другой процесс. Persistent-соединение не закрепляется за браузером и не хранится в $_SESSION. Короткие независимые CLI-запуски завершают свои процессы: один такой запуск не подтверждает межзапросную работу. Лабораторный cli-server здесь проверен фактически; его не предлагают использовать как публичный промышленный сервер.
Автор хотел сохранить соединение Firebird в PHP-сессии и повторно получать его на других страницах. Исходный вопрос Stack Overflow. Вместо переноса resource через $_SESSION показаны повторные HTTP-запросы живому worker и серверный идентификатор соединения. Предположения о чужих высоконагруженных системах из обсуждения здесь не используются.
Проверьте понимание на новой задаче
Почему нельзя сохранить $db в $_SESSION и затем продолжить чтение результата на следующей странице?
Вариант решения
Сессия сайта хранит данные пользователя, но не переносит живой ресурс драйвера в другой процесс. При следующем запросе заново получите допустимое подключение, начните нужную транзакцию и выполните SELECT. Для продолжения списка храните курсор пагинации или ключ, а не PHP resource.
Отменить резерв и не передать контекст клиента следующему запросу
В каталоге есть 3 единицы Cable. Покупатель временно резервирует 2, но отменяет заявку до подтверждения. Вернём остаток через rollback и отдельно удалим служебную метку CUSTOMER-A из состояния соединения. Следующий запрос должен увидеть 3 единицы и отсутствие чужой метки, даже если физическое соединение осталось прежним.
- pconnect → подключение A, Cable: 3
- Явная транзакция → UPDATE QTY=QTY-2 → внутри видно 1
- USER_SESSION.CUSTOMER = CUSTOMER-A относится к соединению A
- Клиент отменяет → rollback → Cable снова 3
- Контекст всё ещё CUSTOMER-A → установить NULL
- Следующий HTTP-запрос на A → Cable: 3, CUSTOMER: null
Минимальный вызов и его смысл
$tx = ibase_trans(IBASE_WRITE | IBASE_CONCURRENCY | IBASE_NOWAIT, $db);
if ($tx === false) { throw new RuntimeException('Транзакция не началась'); }
ibase_query($tx, 'UPDATE LAB_ITEMS SET QTY=QTY-2 WHERE ID=1 AND QTY>=2');
ibase_rollback($tx);
$tx = null;Явная транзакция $tx объединяет изменения до commit или rollback; запрос получает именно $tx. Условие QTY>=2 проверяет возможность резерва в самой базе, а affected_rows подтверждает изменение одной строки. Вертикальная черта | объединяет флаги: запись, снимок согласованного состояния и немедленный отказ при конфликте блокировки. Они не означают фиксацию изменения.
Что делает HTTP-обработчик
// Метка привязана к соединению, не к браузеру и не к транзакции.
ibase_query($tx, "SELECT RDB\$SET_CONTEXT('USER_SESSION','CUSTOMER','CUSTOMER-A') FROM RDB\$DATABASE");
// После rollback другая транзакция того же подключения удаляет метку:
ibase_query($cleanupTx, "SELECT RDB\$SET_CONTEXT('USER_SESSION','CUSTOMER',NULL) FROM RDB\$DATABASE");Этот фрагмент выделяет изучаемый шаг. Полный http-router.php в ZIP содержит проверку результата, ограничение собственной базы, завершение транзакций и обработку ошибок.
Полный сценарий: создание каталога, HTTP-действия и сравнение результата
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;$error=null;
$rows=$checks=$observations=$runtime=[];
$transferToken=getenv('PHPMAN_FIREBIRD_SCENARIO_TOKEN');
try {
$config=f158Config();
if(!is_string($transferToken)||!preg_match('/^[a-f0-9]{32}$/D',$transferToken))throw new RuntimeException('Запускайте example.php, а не scenario.php напрямую.');
$service=f158Service();$runtime=f158Runtime($service);
require __DIR__.'/http-lab.php';
$database=f158CreateDatabase($service,$owned,'cancel');
$session=f158HttpSession($database,['cancel-reservation','check-clean']);[$cancel,$next]=$session['responses'];
$seen=$cancel['observed'];$clean=$next['observed'];
$same=$cancel['attachment_id']===$next['attachment_id'];
$rows=[['Остаток перед резервом',$seen['qty_before']],['Остаток внутри неподтверждённого резерва',$seen['qty_during']],
['Остаток после rollback',$seen['qty_after']],['Контекст соединения после rollback',$seen['context_before_clear']],
['Контекст после явной очистки',$seen['context_after_clear']],['Следующий запрос: остаток',$clean['item']['qty']],
['Следующий запрос: контекст',$clean['context']],['Соединение осталось тем же',$same],['HTTP-процесс остановлен',$session['stopped']]];
$checks=['two_real_http_requests'=>$session['http_requests']===2,'expected_reservation_cancelled'=>$seen['cancelled'],
'reserve_was_visible_inside_transaction'=>$seen['qty_before']===3&&$seen['qty_during']===1,
'rollback_restored_quantity'=>$seen['qty_after']===3,'session_context_not_transactional'=>$seen['context_before_clear']==='CUSTOMER-A',
'context_cleared'=>$seen['context_after_clear']===null,'next_request_clean'=>$clean['context']===null&&$clean['item']['qty']===3,
'same_attachment'=>$same,'both_http_200'=>$session['proof']['statuses']===[200,200],'worker_stopped'=>$session['stopped']];
$observations=['http_session'=>$session,'explicit_rollback'=>true,'explicit_context_cleanup'=>true,
'same_attachment'=>$same,'connection_context_is_not_browser_identity'=>true];
}catch(Throwable $caught){$error=['class'=>get_class($caught),'message'=>$caught->getMessage()];}
finally{
try{
if(is_resource($db))f158Close($db);
if(is_resource($service))f158Detach($service);
}catch(Throwable $cleanupError){$error=$error??['class'=>get_class($cleanupError),'message'=>$cleanupError->getMessage()];}
}
$handoff=['schema'=>'aen-firebird158-private-handoff-v1','ok'=>$error===null,'pid'=>getmypid(),
'transfer_token'=>$transferToken,'state'=>f158State(),'error'=>$error,
'result'=>['rows'=>$rows,'checks'=>$checks,'observations'=>$observations,'runtime'=>$runtime,'owned'=>$owned]];
file_put_contents(__DIR__.'/scenario-handoff.json',json_encode($handoff,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n",LOCK_EX);
// The process exits before the outer controller starts database cleanup.
exit($error===null?0:1);
Прикладные шаги этого сценария сохранены в scenario.php. Запускается внешний example.php ниже: он дожидается штатного завершения сценария перед отдельным удалением базы.
Видимый результат полного запуска
| Наблюдение | Результат |
|---|---|
| Остаток перед резервом | 3 |
| Остаток внутри неподтверждённого резерва | 1 |
| Остаток после rollback | 3 |
| Контекст соединения после rollback | CUSTOMER-A |
| Контекст после явной очистки | null |
| Следующий запрос: остаток | 3 |
| Следующий запрос: контекст | null |
| Соединение осталось тем же | да |
| HTTP-процесс остановлен | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';require __DIR__.'/http-lab.php';
// No service/database connection is opened by this outer process.
$result=f158RunScenario();
$owned=$result['owned'];$rows=$result['rows'];$checks=$result['checks'];
$observations=$result['observations'];$runtime=$result['runtime'];
f158HttpDrop($owned[0]['name']);
$removed=f158Cleanup($owned);
f158Report('fb158-session-cleanup','Отменить резерв и не передать контекст клиента следующему запросу',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Остаток перед резервом — 3
Остаток внутри неподтверждённого резерва — 1
Остаток после rollback — 3
Контекст соединения после rollback — CUSTOMER-A
Контекст после явной очистки — null
Следующий запрос: остаток — 3
Следующий запрос: контекст — null
Соединение осталось тем же — да
HTTP-процесс остановлен — да
Самостоятельный контроллер CLI с приложенными HTTP-файлами. Создаёт собственную синтетическую базу и настоящий локальный HTTP worker; после запросов останавливает worker, удаляет свою базу и сохраняет HTML/JSON-результат.
Фактический HTML-результат · Фактические данные JSON.
Ошибки, границы и применение
USER_SESSION — пространство переменных подключения Firebird. Оно не равно PHP $_SESSION; откат транзакции не обязан удалять эти значения. Полный HTTP-обработчик использует finally для rollback и удаления метки, в том числе при исключении. Если очистка не завершилась, ответ не выдаётся за успешный. Драйвер 3.0.1 при завершении ресурса запроса подтверждает оставшуюся неявную транзакцию, а остальные отменяет; полагаться на это вместо явного решения о покупке нельзя. Нельзя растягивать бизнес-транзакцию на несколько HTTP-запросов.
Проверьте понимание на новой задаче
После отмены заявки остаток восстановился, но следующая заявка получила метку предыдущего клиента. Почему одного rollback недостаточно?
Вариант решения
Остаток — транзакционные данные таблицы, а USER_SESSION — состояние подключения. Явно завершите транзакцию, удалите установленные приложением значения контекста в finally и заново определяйте пользователя каждого HTTP-запроса. Не используйте сохранённую метку как доказательство авторизации.
Перезапустить обработчик: данные остаются, постоянное соединение создаётся заново
После обновления PHP или перезапуска обработчика соединение к базе не переносится в новый процесс. Каталог при этом остаётся в файле базы. Покажем границу явно: первый HTTP-процесс читает Cable и завершается, второй процесс с теми же настройками снова читает Cable, но получает другой идентификатор соединения.
- Worker P1 → pconnect → соединение A → Cable: 3
- Остановить P1 → его persistent-подключение больше не хранится
- Файл учебной базы остаётся
- Worker P2 → pconnect с теми же настройками → новое соединение B
- Cable: 3 получен новым SELECT; A не равно B
- Остановить P2 → только теперь удалить учебную базу
Минимальный вызов и его смысл
$db = ibase_pconnect($database, $user, $password, 'UTF8', 0, 3);
// Этот вызов ищет соединение в текущем процессе PHP.
// Запуск другого процесса не переносит сюда старый resource.«Постоянное» означает возможность пережить отдельный запрос в живущем процессе, а не вечное соединение. В полном примере контроллер запускает два процесса последовательно и сохраняет их реальные PID, ответы и идентификаторы Firebird. Число PID не является вечным уникальным именем: ОС может использовать номер снова, поэтому проверка опирается также на подтверждённую остановку и новый attachment.
Что делает HTTP-обработчик
// Одинаковый код выполняется в каждом из двух процессов отдельно.
$result = ibase_query($db, 'SELECT CURRENT_CONNECTION AS ID FROM RDB$DATABASE');
$attachment = ibase_fetch_assoc($result)['ID'];
ibase_free_result($result);
$result = null;Этот фрагмент выделяет изучаемый шаг. Полный http-router.php в ZIP содержит проверку результата, ограничение собственной базы, завершение транзакций и обработку ошибок.
Полный сценарий: создание каталога, HTTP-действия и сравнение результата
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;$error=null;
$rows=$checks=$observations=$runtime=[];
$transferToken=getenv('PHPMAN_FIREBIRD_SCENARIO_TOKEN');
try {
$config=f158Config();
if(!is_string($transferToken)||!preg_match('/^[a-f0-9]{32}$/D',$transferToken))throw new RuntimeException('Запускайте example.php, а не scenario.php напрямую.');
$service=f158Service();$runtime=f158Runtime($service);
require __DIR__.'/http-lab.php';
$database=f158CreateDatabase($service,$owned,'workers');
$firstSession=f158HttpSession($database,['read-item']);
$secondSession=f158HttpSession($database,['read-item']);
$first=$firstSession['responses'][0];$second=$secondSession['responses'][0];
$changed=$first['attachment_id']!==$second['attachment_id'];
$sameData=$first['observed']['item']===$second['observed']['item'];
$rows=[['Последовательно запущено HTTP-процессов',2],['Первый процесс остановлен до второго',$firstSession['stopped']],
['Новое подключение Firebird получило другой ID',$changed],['Данные товара сохранились',$sameData],
['Товар после перезапуска',$second['observed']['item']['title']],['Остаток после перезапуска',$second['observed']['item']['qty']],
['Второй HTTP-процесс остановлен',$secondSession['stopped']]];
$checks=['first_worker_stopped'=>$firstSession['stopped'],'second_worker_stopped'=>$secondSession['stopped'],
'one_request_each'=>$firstSession['http_requests']===1&&$secondSession['http_requests']===1,
'new_attachment'=>$changed,'persistent_data_survived'=>$sameData&&$second['observed']['item']===['id'=>1,'title'=>'Cable','qty'=>3],
'fresh_connection_context'=>$second['observed']['context']===null,
'both_http_200'=>$firstSession['proof']['statuses']===[200]&&$secondSession['proof']['statuses']===[200]];
$observations=['http_sessions'=>[$firstSession,$secondSession],'new_attachment'=>$changed,
'processes_were_sequential'=>true,'pid_numbers_are_observations_not_permanent_identity'=>true,
'database_file_kept_until_both_workers_stopped'=>true];
}catch(Throwable $caught){$error=['class'=>get_class($caught),'message'=>$caught->getMessage()];}
finally{
try{
if(is_resource($db))f158Close($db);
if(is_resource($service))f158Detach($service);
}catch(Throwable $cleanupError){$error=$error??['class'=>get_class($cleanupError),'message'=>$cleanupError->getMessage()];}
}
$handoff=['schema'=>'aen-firebird158-private-handoff-v1','ok'=>$error===null,'pid'=>getmypid(),
'transfer_token'=>$transferToken,'state'=>f158State(),'error'=>$error,
'result'=>['rows'=>$rows,'checks'=>$checks,'observations'=>$observations,'runtime'=>$runtime,'owned'=>$owned]];
file_put_contents(__DIR__.'/scenario-handoff.json',json_encode($handoff,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n",LOCK_EX);
// The process exits before the outer controller starts database cleanup.
exit($error===null?0:1);
Прикладные шаги этого сценария сохранены в scenario.php. Запускается внешний example.php ниже: он дожидается штатного завершения сценария перед отдельным удалением базы.
Видимый результат полного запуска
| Наблюдение | Результат |
|---|---|
| Последовательно запущено HTTP-процессов | 2 |
| Первый процесс остановлен до второго | да |
| Новое подключение Firebird получило другой ID | да |
| Данные товара сохранились | да |
| Товар после перезапуска | Cable |
| Остаток после перезапуска | 3 |
| Второй HTTP-процесс остановлен | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';require __DIR__.'/http-lab.php';
// No service/database connection is opened by this outer process.
$result=f158RunScenario();
$owned=$result['owned'];$rows=$result['rows'];$checks=$result['checks'];
$observations=$result['observations'];$runtime=$result['runtime'];
f158HttpDrop($owned[0]['name']);
$removed=f158Cleanup($owned);
f158Report('fb158-worker-boundary','Перезапустить обработчик: данные остаются, постоянное соединение создаётся заново',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Последовательно запущено HTTP-процессов — 2
Первый процесс остановлен до второго — да
Новое подключение Firebird получило другой ID — да
Данные товара сохранились — да
Товар после перезапуска — Cable
Остаток после перезапуска — 3
Второй HTTP-процесс остановлен — да
Самостоятельный контроллер CLI с приложенными HTTP-файлами. Создаёт собственную синтетическую базу и настоящий локальный HTTP worker; после запросов останавливает worker, удаляет свою базу и сохраняет HTML/JSON-результат.
Фактический HTML-результат · Фактические данные JSON.
Ошибки, границы и применение
Несколько работников PHP обычно держат отдельные наборы постоянных подключений; посетители не получают персональный канал по номеру своей сессии. Чем больше процессов и различных наборов параметров, тем больше потенциально открытых соединений. Здесь не измеряется скорость: выгоду надо оценивать на своей нагрузке, отдельно от времени SQL и передачи данных. Временно недоступную базу pconnect не делает доступной; false и диагностику всё равно проверяют.
Разработчик сравнивал удаление переменной через unset с закрытием соединения через ibase_close. Исходный вопрос Stack Overflow. Разделяем PHP-ссылку, ресурс текущего запроса, физическое соединение и завершение транзакции. Для persistent-варианта окончание процесса показано отдельно; неизвестный эффект очистки не подменяется советом выбрать «более быстрый unset».
Проверьте понимание на новой задаче
После перезапуска worker ID подключения изменился, но данные совпадают. Это означает потерю persistent-механизма?
Вариант решения
Нет. Содержимое таблиц сохраняет сама база, а соединение принадлежит времени жизни процесса. Новый worker создаёт свой канал. Для устойчивого состояния приложения используйте таблицы и подтверждённые транзакции, а не ожидание, что прежний PHP-процесс будет жить всегда.
Карточка функции и следующий шаг
ibase_pconnect: все параметры, результаты и ограничения
Соединение остаётся готовым к следующим запросам, но изменения в базе ещё нужно обнаруживать. В главе о событиях Firebird рассмотрим сигнал о подтверждённом изменении и новый SELECT после него. Persistent-соединение само по себе не подписывает приложение на обновления и не превращает событие в надёжную очередь.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
ibase_pconnectПереиспользовать подключение Firebird в пределах процесса PHP, понимая границы запроса и транзакции.
Исторический PHP 5 / PHP 7 < 7.4; FirebirdSQL 3.0.1 отдельно · interbase
ibase_pconnect(string $database = "", string $username = "", string $password = "", string $charset = "", int $buffers = 0, int $dialect = 0, string $role = "", int $sync = 0): resource|falseЕсли один PHP-процесс обслуживает много запросов, повторное создание соединения может стать заметной частью работы. ibase_pconnect позволяет этому процессу сохранить подключение для последующих обращений с теми же настройками. Функция возвращает ресурс для SQL; готовые строки и состояние пользователя она не возвращает. Сначала оцените цену подключения: ускорение не гарантировано, а число занятых соединений может вырасти.
ibase_pconnect(
string $database = "", string $username = "", string $password = "",
string $charset = "", int $buffers = 0, int $dialect = 0,
string $role = "", int $sync = 0
): resource|falseЭто поясняющая запись разбираемых аргументов, не объявление пользовательской функции. Параметры можно опускать; пустые строковые настройки database/user/password/charset драйвер дополняет значениями ini, когда они заданы. PHP Manual показывает их неопределённые значения знаком ?, а sync оставляет без описания. Ниже приведено поведение закреплённого драйвера 3.0.1; оно отделено от исторической страницы.
| Параметр | Смысл, значение по умолчанию и границы |
|---|---|
| database | Путь/алиас базы со стороны сервера; для TCP адрес вида host/port:path или host:path. Без явного значения используется ibase.default_db, если задан. Историческая страница также перечисляет NetBEUI и IPX/SPX: это не обещание поддержки этих старых транспортов современной сборкой Firebird. Локальный embedded-путь в уроке не доказывает наличие сетевого сервера. |
| username, password | Учётная запись базы и пароль; возможны ibase.default_user / ibase.default_password. Это не пользователь сайта. Берите секреты из окружения и не включайте их в HTML/логи. |
| charset | Кодировка соединения для передачи и преобразования текста; это не команда изменения кодировки существующей базы. При пропуске учитывается ibase.default_charset. Пример задаёт UTF8 явно. Несогласованная кодировка может проявиться при запросе/чтении строк. |
| buffers | Число серверных буферов, запрашиваемое при подключении. 0 или отсутствие оставляет выбор серверу. Не подбирайте значение наугад как универсальное ускорение. |
| dialect | SQL-диалект запросов соединения; 0 оставляет выбор текущего поддерживаемого диалекта клиентской библиотеки. В примере явно 3. В официальной исторической странице параметр относится к InterBase 6 и новее. |
| role | SQL-роль при подключении, если роль предоставлена пользователю; исторически доступна с InterBase 5. Это не автоматически выданные права. |
| sync | В C-коде 3.0.1 восьмой аргумент учитывается: ненулевое значение добавляет isc_dpb_force_write; сравнение с синхронной константой выбирает синхронный/асинхронный режим. Это настройка записи базы с последствиями для надёжности, не переключатель persistent. В учебных вызовах он опущен, поэтому настройка не изменяется. |
| Возврат | При успехе ресурс соединения, при отказе false; на отказе немедленно сохраните ibase_errcode/ibase_errmsg и предупреждение PHP. Ошибочные PHP-типы могут вызвать исключение языка, а успешное подключение не исключает последующий отказ SQL. |
Тот же каталог в следующем запросе
$db = ibase_pconnect($database, $user, $password, 'UTF8', 0, 3);
if ($db === false) {
$code = ibase_errcode();
$message = ibase_errmsg();
throw new RuntimeException('Каталог временно недоступен');
}
$result = ibase_query($db, 'SELECT TITLE, QTY FROM LAB_ITEMS WHERE ID = 1');Сначала соединение, затем ограниченный SELECT; полная программа проверяет и результат запроса, освобождает выборку и завершает транзакцию. Два действительных HTTP-запроса показывают, что переиспользуется канал, а каждый ответ содержит заново прочитанные данные.
Отменённый заказ на повторно используемом соединении
$tx = ibase_trans(IBASE_WRITE | IBASE_CONCURRENCY | IBASE_NOWAIT, $db);
if ($tx === false) { throw new RuntimeException('Нет транзакции'); }
try {
// Проверить заказ и выполнить параметризованные изменения через $tx.
// При успехе: ibase_commit($tx); $tx = null;
} finally {
if (is_resource($tx)) { ibase_rollback($tx); $tx = null; }
}Это фрагмент структуры владения транзакцией; полный сценарий резерва действительно уменьшает остаток с 3 до 1, отменяет изменение и отдельно очищает контекст соединения. Не используйте завершение запроса или ibase_close как решение, подтверждать ли заказ.
Что считается одинаковым подключением
В 3.0.1 ключ вычисляется из строк database, user, password, charset, role и числовых buffers, dialect, sync после подстановки ini. Это ключ внутри процесса; отдельного аргумента «номер пула» нет. Согласуйте настройки в одном месте: другое написание пути или другая кодировка могут образовать иной ключ. Не считайте набор аргументов привязкой к посетителю и не пытайтесь управлять соединениями подменой пароля.
| Настройка / жизненный цикл | Практическое следствие |
|---|---|
| ibase.allow_persistent=1 | Разрешает создавать постоянные подключения; системная настройка PHP. В новом процессе при 0 вызов работает без создания persistent-подключения. |
| ibase.max_persistent=-1 | Документация определяет максимальное число persistent-соединений процесса; -1 означает отсутствие лимита, а при превышении предусмотрен обычный режим нового подключения. Однако в C-коде именно 3.0.1 расстановка скобок присваивает внутренней переменной результат сравнения с -1: при любом значении не -1 условие использует порог 1, а не заданное число. Это ограничение реализации, обнаруженное чтением исходника; отдельный runtime-прогон лимитов в этой главе не выполнялся. Повторное использование подходящего существующего соединения проверяется раньше. |
| ibase.max_links=-1 | Общий лимит обычных и постоянных соединений одного процесса; при исчерпании отказ даёт false и диагностику. Размер всего приложения зависит также от числа процессов. |
| Конец PHP-запроса | Ресурсы запроса завершаются; persistent-attachment сохраняется. В 3.0.1 оставшаяся default-транзакция подтверждается, прочие транзакции отменяются, обработчики событий освобождаются. Приложение должно само явно завершать работу раньше. |
| ibase_close / unset | Удаление PHP-переменной не равно закрытию физического канала. Для pconnect обычный close не является обещанием отключить persistent-attachment; не закрывайте один и тот же ресурс через несколько его псевдонимов. |
| Завершение процесса | Его persistent-соединения больше не переиспользуются следующим процессом. Старое или разорванное соединение при поиске проверяется драйвером; ошибка нового подключения всё равно возможна. |
Параметры INI_SYSTEM меняются в конфигурации запуска/сервера, а не гарантированно через ini_set в странице. Настройки по умолчанию и режимы сверяйте с официальной конфигурацией interbase. Не включайте вывод phpinfo на публичной странице только ради этой проверки.
Для PHP-FPM/Apache каждый worker имеет собственную область соединений: два запроса одного посетителя могут попасть в разные процессы. Сессия браузера, транзакция Firebird и persistent-attachment — три разные сущности. Поэтому нельзя продолжать одну выборку или незавершённый заказ по старому resource на другой странице. Официальный общий раздел persistent connections описывает эти границы; наш локальный HTTP-профиль cli-server отдельно проверен реальным запуском.
Исторический встроенный interbase помечен PHP 5 / PHP 7 ниже 7.4. Здесь используется отдельное поддерживаемое расширение FirebirdSQL 3.0.1 под PHP 8.2.35. Работа конкретного стенда не удостоверяет все старые версии PHP или все способы развертывания. Связанные операции: ibase_connect, явные commit/rollback, диагностика ошибки.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


