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

Дерево руководства
Глава 100 · От начала работы с CUBRID к прикладному отчёту
CUBRID: подготовить окружение и приложение, затем получить отчёт склада
Полная основа официальных разделов установки и настройки: контракт API, проверенный JSON-план и реальная складская сводка с CSV.
От требований приложения к отчёту склада
Представьте задачу: утром получить две строки о стоимости электрики и крепежа на складе. SQL для такого отчёта короткий, но его не выполнит PHP, в котором нет подходящего драйвера. А установленный драйвер ещё не знает, какую базу выбрать и какие товары нужны. В этой главе мы последовательно отделим эти вопросы и получим три полезных файла: readiness.json с готовностью приложения, export-plan.json с проверенным заданием и stock-by-category.csv с настоящей сводкой.
Чтобы приложение сформировало полезный отчёт, нужно подготовить подходящий PHP, выбрать функции для задачи и определить, какой участок кода закрывает открытые ресурсы. Эти главы дают маршрут от проверки требований до CSV склада и плана поддержки старого приложения.
Разделение участников: PHP выполняет программу; native-расширение предоставляет cubrid_*; CCI передаёт запрос брокеру; CAS связывает запрос с сервером базы. Первые запросы и транзакции объясняют сами операции с данными. Здесь подробно разобраны подготовка и границы компонентов.
Примеры проверяются в отдельной среде совместимости: PHP 7.4.33, native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. Это не предложение менять PHP существующего сайта. Проверка требований, конфигурации и исходников работает без соединения с базой; три SQL-проекта создают и удаляют только собственную таблицу.
- PHP читает задание: две категории и нужные показатели
- Расширение cubrid предоставляет функции, например cubrid_prepare
- Библиотека CCI передаёт команды брокеру CUBRID; его CAS обслуживает соединение
- Сервер выполняет WHERE и GROUP BY над таблицей товаров
- PHP получает две строки, записывает CSV и освобождает ресурсы
Расширение — подключаемая часть PHP, которая добавляет функции. CCI — клиентская библиотека CUBRID, через которую native-драйвер общается с серверной частью. Broker принимает клиентские подключения; CAS — обслуживающий их процесс брокера. Это разные участники, поэтому рабочий PHP ещё не доказывает рабочий брокер. Модель показывает обязанности, а не требует, чтобы все компоненты были на разных компьютерах: в учебной среде они находятся на одном компьютере, но роли остаются разными.
Самый маленький первый шаг
$hasDriver = extension_loaded('cubrid');
if ($hasDriver) {
echo 'native cubrid загружен';
} else {
echo 'В этом PHP расширение не загружено';
}Знак = сохраняет результат в переменной $hasDriver. Кавычки задают текстовое имя расширения. Вызов extension_loaded возвращает логическое значение; if выбирает сообщение. В среде примеров вывод — «native cubrid загружен». Запроса к базе здесь нет. Первый проект развивает эту проверку до требований конкретного приложения.
| Нужно узнать | Подходящий шаг | Что он ещё не доказывает |
|---|---|---|
| Есть ли необходимый API в текущем PHP? | Контракт stock_export и проверка имён | Доступность базы и права пользователя |
| Допустимо ли задание «электрика, 20 строк»? | Проверка формы, типов и разрешённых значений | Что SQL уже выполнен или файл уже выгружен |
| Что действительно лежит на складе? | Подключение, подготовленный SQL и CSV | Что любой другой запрос или серверный профиль тоже настроен правильно |
Официальная ветка CUBRID включает установку и настройки, типы ресурсов, константы, примеры, native-функции, MySQL-совместимые функции и устаревшие имена. Здесь находится маршрут подготовки; следующая глава распределяет эти группы по задачам и владельцам ресурсов. PDO_CUBRID — отдельный интерфейс: наличие функции cubrid_connect не является проверкой PDO-драйвера. Общая документация сервера доступна на сайте CUBRID и в руководстве CUBRID 11.3.
Маршрут установки и настройки CUBRID
Маршрут Installing/Configuring в PHP Manual состоит из требований, установки, runtime-настроек и типов ресурсов. Пройдём их в том порядке, в котором устраняются реальные препятствия: сначала совместимые компоненты, затем загрузка расширения именно нужным PHP, после этого параметры приложения и соединение. Типы ресурсов понадобятся, когда запрос уже можно выполнить.
| Этап | Что подготовить или проверить | Куда перейти |
|---|---|---|
| 1. Требования | Выбранный PHP, native-драйвер, CCI, доступный сервер через брокер | Компоненты и совместимость |
| 2. Установка | Модуль выбранной сборки и его запись в php.ini нужного процесса | Сборка и включение расширения |
| 3. Настройки | Не смешивать php.ini, конфигурацию задания, реквизиты и параметры SQL | Четыре уровня конфигурации |
| 4. Ресурсы | Понимать владельца connection/request и способ их завершения | Карта API и владельцы ресурсов |
| 5. Полезный результат | Прочитать только нужную сводку и записать CSV | Рабочий отчёт склада |
В ZIP каждого проекта есть README.txt. Для первых двух проектов достаточно CLI с native cubrid: сервер и пароль не используются. Третий проект требует собственной учебной базы, поэтому README даёт полный рецепт изолированной среды Linux/WSL, брокера, маркера и запуска. Ограничители lab.php не предназначены для переноса в публичный обработчик сайта: они гарантируют, что самостоятельный пример создаёт и удаляет только свою учебную таблицу.
Для существующего проекта сначала выполните проверку в его фактическом процессе PHP. Не переключайте весь сайт на версию учебной среды ради запуска архива. Если выбранный production PHP не поддерживается нужной версией native-драйвера, это отдельное решение о совместимости и развёртывании, а не повод скопировать в него чужой бинарный модуль.
Требования: PHP, расширение, CCI, брокер и база
Официальное минимальное требование — установить CUBRID и собрать PHP-библиотеку с поддержкой CUBRID. На практике важно назвать каждый компонент: файлы PHP-кода сами по себе не содержат ни сервер базы, ни native-расширение, ни клиентскую библиотеку CCI. Удалённый сервер возможен; установка всего сервера на машине PHP не является смыслом самого вызова подключения.
| Компонент | Зачем нужен | Что согласовать |
|---|---|---|
| PHP | Выполняет example.php | Версия и ABI расширений, архитектура, режим TS/NTS для подходящей платформы, выбранный SAPI |
| Native cubrid | Добавляет cubrid_* и CUBRID_* | Исходник/бинарный модуль для именно выбранной сборки PHP |
| CCI | Клиентская часть обмена с CUBRID | Версия и зависимые библиотеки сборки драйвера; доступность при запуске процесса |
| Broker и CAS | Обслуживают клиентское подключение | Адрес, порт, допуск пользователя и базы, работа сервиса |
| Сервер и база | Хранят таблицы, выполняют SQL | Имя базы, учётная запись и нужные SQL-права |
ABI — договорённость о бинарном устройстве модуля и PHP: совпадающее имя файла ещё не означает, что модуль можно загрузить. TS/NTS обозначает вариант PHP с поддержкой потокобезопасности или без неё. Для Windows дополнительно сопоставляют архитектуру и комплект сборки/зависимые DLL. Нельзя взять модуль от PHP 5 или 7 и считать его совместимым с PHP 8 только потому, что директива extension указывает на существующий файл.
В примерах зафиксированы PHP 7.4.33, native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. Номера разных компонентов не обязаны буквально совпадать; это конкретная проверенная комбинация, а не заявление «самые новые версии» и не универсальная матрица совместимости. phpversion('cubrid') в этой сборке не сообщает версию модуля; функция cubrid_version сообщает. Поэтому решение о готовности опирается на необходимые символы, а отдельный отчёт возможностей соединения показывает различия версий клиента, расширения и сервера.
Composer устанавливает PHP-пакеты приложения, но наличие vendor/autoload.php не загружает native cubrid. Загрузка модуля и его зависимостей происходит раньше выполнения обычного PHP-кода. Для статического анализа исходников в следующей главе дополнительно нужен tokenizer; это расширение PHP, не возможность CUBRID и не условие выполнения складского SELECT.
Ошибка «функция не найдена» проверяется на уровне PHP и загрузки модуля. Отказ пароля или брокера возникает позже, при подключении. SQL-ошибка возникает ещё позже, при подготовке/выполнении команды. Эти три причины требуют разных действий; диагностика CUBRID разбирает код, область и сообщение ошибки без публикации реквизитов.
Проверить требования приложения до первого подключения
Перед переносом складского приложения нужно узнать, найдёт ли оно свои функции в выбранном PHP. Список требований полезнее общего сообщения «расширение установлено»: native cubrid может быть загружено, а старого GLO API в нём уже нет. Проект читает два небольших контракта из contracts.json и выдаёт объяснимое решение, не выполняя ни отсутствующие функции, ни SQL.
- contracts.json: stock_export и legacy_attachment
- Проверить расширение, имена функций и констант в текущем PHP
- stock_export: доступно; legacy_attachment: два отсутствующих вызова
- readiness.json: решение с причинами; соединений с базой нет
Минимальный вызов
if (function_exists('cubrid_prepare')) {
echo 'prepare доступна';
} else {
echo 'Этому PHP не хватает нужного API';
}Строка в кавычках — имя функции. function_exists возвращает true или false, но не запускает названную функцию. if выбирает одну ветвь; в нашем профиле вывод — «prepare доступна». defined аналогично проверяет имя константы. Полный проект повторяет эти два простых вопроса для каждого требования; массив missing_functions сохраняет конкретные отсутствующие имена.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Экспорт склада | Нужные native-функции доступны |
| Старый модуль вложений | Не хватает cubrid_new_glo, cubrid_send_glo |
| Отсутствующие функции вызваны | нет |
| Соединение с базой открыто | нет |
| Файл | readiness.json |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=c174LocalRuntime();
$input=file_get_contents(__DIR__.'/contracts.json');
if ($input===false) {throw new RuntimeException('Контракт не прочитан');}
$contracts=json_decode($input,true,512,JSON_THROW_ON_ERROR);
if (!is_array($contracts)) {throw new RuntimeException('Нужен список контрактов');}
$report=[];
foreach ($contracts as $contract) {
if (!is_array($contract) || !isset($contract['name'],$contract['extension'],$contract['functions'],$contract['constants'])
|| !is_string($contract['name']) || !is_string($contract['extension'])
|| !is_array($contract['functions']) || !is_array($contract['constants'])) {
throw new RuntimeException('Неверная форма контракта');
}
$missingFunctions=[];$missingConstants=[];
foreach ($contract['functions'] as $name) {
if (!is_string($name)) {throw new RuntimeException('Имя функции должно быть строкой');}
if (!function_exists($name)) {$missingFunctions[]=$name;}
}
foreach ($contract['constants'] as $name) {
if (!is_string($name)) {throw new RuntimeException('Имя константы должно быть строкой');}
if (!defined($name)) {$missingConstants[]=$name;}
}
$loaded=extension_loaded($contract['extension']);
$ready=$loaded && $missingFunctions===[] && $missingConstants===[];
$report[]=['application'=>$contract['name'],'extension_loaded'=>$loaded,'ready'=>$ready,
'missing_functions'=>$missingFunctions,'missing_constants'=>$missingConstants,
'decision'=>$ready?'Можно перейти к отдельной проверке SQL и прав':'Нужен перенос отсутствующих вызовов GLO'];
}
$encoded=json_encode($report,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/readiness.json',$encoded)!==strlen($encoded)) {throw new RuntimeException('Отчёт не записан полностью');}
$rows=[['Экспорт склада','Нужные native-функции доступны'],['Старый модуль вложений','Не хватает cubrid_new_glo, cubrid_send_glo'],
['Отсутствующие функции вызваны',false],['Соединение с базой открыто',false],['Файл','readiness.json']];
$checks=['two_contracts'=>count($report)===2,'export_ready'=>$report[0]['ready']===true,
'export_functions_complete'=>$report[0]['missing_functions']===[],'export_constants_complete'=>$report[0]['missing_constants']===[],
'legacy_refused'=>$report[1]['ready']===false,'exact_missing_functions'=>$report[1]['missing_functions']===['cubrid_new_glo','cubrid_send_glo'],
'native_present_both'=>$report[0]['extension_loaded']===true&&$report[1]['extension_loaded']===true,
'no_connection'=>$db===null&&$runtime['database']===null&&$runtime['transport']==='no_database_connection',
'artifact_roundtrip'=>json_decode(file_get_contents(__DIR__.'/readiness.json'),true,512,JSON_THROW_ON_ERROR)===$report];
$observations=['application_contracts'=>$contracts,'decisions'=>$report,'SAPI'=>PHP_SAPI,
'availability_is_not_execution'=>true,'server_and_permissions_checked'=>false];
c174Report('cub174-app-readiness','Проверить требования приложения до первого подключения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Экспорт склада — Нужные native-функции доступны
Старый модуль вложений — Не хватает cubrid_new_glo, cubrid_send_glo
Отсутствующие функции вызваны — нет
Соединение с базой открыто — нет
Файл — readiness.json
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит логику урока, lab.php — проверку отдельной среды и работу с собственными ресурсами. Этот проект читает локальные файлы и возможности CLI, не открывая соединение с базой. После запуска откройте result.html и скачиваемый ниже JSON или CSV.
Открыть результат выполнения · Посмотреть данные JSON · Скачать решения по требованиям приложения
Границы решения
ready означает только наличие перечисленных символов в этом процессе PHP. Это не проверка пароля, прав SELECT, доступности брокера, всех режимов API или совместимости приложения целиком. Проект сам выполняется в подготовленной native-лаборатории; отрицательная ветвь показывает отсутствующий GLO API, а не установку сломанного расширения. Файл контракта описывает фиксированные модули нашего примера. Для произвольного приложения формулировку причины отказа нужно привязать к его требованиям. Наличие cubrid не подтверждает наличие PDO_CUBRID. Подробный маршрут старого API — перенос GLO в LOB2.
Попробуйте изменить условие
Как отличить «есть функция» от «запрос действительно прочитал нужный товар»? Что добавить в план после положительного readiness?
Разбор
Сначала оставить этот быстрый этап без SQL. Затем отдельным действием открыть нужный профиль, проверить идентичность базы и выполнить ограниченный запрос с правами приложения. Так отказ подключения не маскируется под отсутствующую функцию. Складской отчёт ниже выполняет именно следующий этап.
Установка: выбрать совместимую сборку и нужный php.ini
Страница установки CUBRID в PHP Manual направляет к общим инструкциям PECL, странице пакета cubrid с исходниками/релизами/сопровождающими и журналом изменений, а для ручной сборки — к build-guide.html в архиве расширения. Она также сообщает об отсутствии доступной через эту страницу Windows DLL и отсылает к сборке под Windows. Это описание маршрутов получения, а не гарантия наличия готового модуля для любой сегодняшней версии PHP.
Страница пакета PECL cubrid служит точкой поиска опубликованных исходников и истории. Доступность конкретной загрузки и её совместимость проверяют отдельно. Общая документация установки PHP теперь также упоминает PIE; само это упоминание не подтверждает, что наш исторический пакет CUBRID поддерживает установку через PIE. Здесь воспроизводится конкретная native-сборка из исходника с фиксированными версиями.
Linux/WSL: связать сборку с выбранным PHP
В поставляемом build-guide.html тега v11.2.0.0002 описаны два пути: включение исходников расширения в сборку PHP и отдельная сборка расширения. Для второго пути phpize подготавливает систему сборки, а php-config сообщает параметры выбранного PHP. Используйте обе утилиты из одного префикса установки; случайный phpize из системного PATH может относиться к другому PHP.
# Шаблон отдельных шагов после подготовки зависимостей и распаковки исходника.
# PHP_PREFIX — ваш отдельный каталог установленного совместимого PHP.
"$PHP_PREFIX/bin/phpize"
./configure --with-cubrid --with-php-config="$PHP_PREFIX/bin/php-config"
make -j2
# modules/cubrid.so включают в отдельный php.ini этого же PHP.
Эти короткие команды показывают назначение инструментов, а не заменяют установку CCI, настройку путей библиотек и создание базы. Полный воспроизводимый рецепт со ссылками на архивы, собственными каталогами, переменными и проверками находится в README.txt скачанного проекта. Он не требует менять системный PHP или конфигурацию действующего сайта. В проверочной среде пакета повторно используются неизменные бинарники подготовленной версии, а база, конфигурация и секреты этого пакета отдельные.
Включение и проверка именно нужного процесса
; Отдельный php.ini, путь заменяют путём своей сборки:
extension=/your/lab/extensions/cubrid.so
"$PHP_PREFIX/bin/php" -c "$LAB_ROOT/php.ini" --ini
"$PHP_PREFIX/bin/php" -c "$LAB_ROOT/php.ini" -m
"$PHP_PREFIX/bin/php" -c "$LAB_ROOT/php.ini" --ri cubrid
"$PHP_PREFIX/bin/php" -c "$LAB_ROOT/php.ini" example.php
extension загружает модуль, extension_dir задаёт каталог поиска при относительном имени. -c явно выбирает ini, --ini показывает, какой файл и дополнительные ini прочитаны, -m — загруженные расширения, --ri cubrid — сведения модуля. Сообщения PHP Startup важны даже до первой строки программы. В нашем профиле готовность native-функций подтверждается первым проектом; отсутствие значения phpversion('cubrid') само по себе не равно отсутствию загруженного расширения.
CLI, PHP-FPM и модуль веб-сервера — разные способы запуска PHP, называемые SAPI. Один и тот же файл программы может исполняться разными бинарниками, с разными ini, рабочими каталогами и окружением библиотек. Даже совпавший путь php.ini не доказывает, что относительный extension_dir разрешается одинаково. Проверяйте PHP_SAPI, PHP_VERSION, php_ini_loaded_file() и фактическое extension_loaded('cubrid') в том процессе, где работает приложение. Не публикуйте полный phpinfo с окружением и путями как страницу сайта. Изменение ini требует применения к конкретному процессу: новый CLI прочитает его при старте; долгоживущий веб-процесс должен быть перезапущен штатным способом его сервиса.
Windows: согласовать модуль, PHP и библиотеки
Исторический build-guide содержит проекты Visual Studio и варианты TS/NTS, а также требования к совпадению исходников PHP с бинарной сборкой. Его примеры VC9/VC11/VC14 относятся к указанным там поколениям PHP. Они не подтверждают наличие готового Windows-модуля для PHP 8. Выбирайте исходники/модуль и зависимости для своей комбинации; простое переименование cubrid.so в DLL или копирование DLL другого PHP ничего не исправляет. Примеры этой главы реально выполняются в отдельном Linux-окружении; Windows здесь разобран как официальный путь сборки и его ограничения, без заявления о выполненной Windows-матрице.
Дополнительный tokenizer для анализа старого кода
Проект инвентаризации API использует token_get_all. В минимальной сборке PHP для учебных запросов tokenizer отсутствовал, поэтому для пакета он собран отдельно из тех же официальных исходников PHP 7.4.33. Его загружают рядом с cubrid в собственный ini, без изменения PHP-бинарника и без make install в системный каталог.
# PHP_SOURCE — распакованные исходники именно PHP 7.4.33.
cp -a "$PHP_SOURCE/ext/tokenizer" "$LAB_ROOT/tokenizer-source"
cd "$LAB_ROOT/tokenizer-source"
"$PHP_PREFIX/bin/phpize"
./configure --with-php-config="$PHP_PREFIX/bin/php-config" --enable-tokenizer=shared
make -j2
mkdir -p "$LAB_ROOT/extensions"
cp modules/tokenizer.so "$LAB_ROOT/extensions/tokenizer.so"
# Добавьте в отдельный php.ini:
# extension=/your/lab/extensions/tokenizer.so
Здесь важен принцип общей ABI: phpize, php-config и исходник tokenizer должны соответствовать выбранному PHP. Сам факт успешной компиляции ещё не означает, что модуль загружен; реальный проект анализа проверяет token_get_all и использует его для собственных файлов. Дополнительный модуль не устанавливает PDO_CUBRID и не меняет доступность функций базы.
Runtime-настройки: не смешивать PHP, приложение и соединение
На странице Runtime Configuration CUBRID прямо указано, что собственных runtime-настроек расширения сейчас нет. В проверенном php_cubrid7.c также не зарегистрирован набор INI-директив cubrid.*. Это узкое утверждение о расширении: PHP, брокер, база и само приложение по-прежнему имеют конфигурацию. Запись ini_set('cubrid.host', ...) не является способом передать хост в cubrid_connect.
| Уровень | Что задаёт в нашем приложении | Где находится |
|---|---|---|
| Запуск PHP | Загрузку cubrid.so, дополнительных расширений и общие правила PHP | Отдельный php.ini выбранного процесса |
| Задание приложения | Категорию, row_limit в строках, известное имя profile и format | Проверенный JSON; export-plan.json |
| Соединение и SQL | Адрес/порт/базу/пользователя, явный режим autocommit и сроки операций | Реквизиты вне публичного файла + аргументы connect и соответствующие функции |
| Серверная часть | Собственную базу, брокер, CAS, допуск соединений | Конфигурация сервера и брокера; выполняется администратором стенда |
Логический профиль warehouse_read позволяет программе выбрать заранее разрешённые реквизиты. Само имя не выдаёт прав чтения: их проверяет сервер для соответствующей учётной записи. Пароль не нужен закупщику в JSON и не попадает в CSV. Категория является значением SQL-параметра; имя таблицы и список столбцов определяет приложение. Поэтому изменение файла задания не превращается в произвольный адрес подключения или SQL-команду.
Общие настройки PHP, такие как extension, extension_dir или error_reporting, не становятся директивами CUBRID. Функции управления autocommit и транзакцией действуют на соединение, а параметры базы и срок запроса имеют собственные контракты и единицы. Их нельзя вывести из имени произвольного поля JSON. В примере timeout_seconds намеренно считается неизвестным ключом, чтобы опечатка не оставила операцию с неожиданным временем ожидания.
Минимальная понятная конфигурация задания
{
"profile": "warehouse_read",
"category": "электрика",
"row_limit": 20,
"format": "stock_csv"
}
Фигурные скобки задают JSON-объект с полями. row_limit — число, а profile и format — имена известных вариантов. Это переносимый файл с намерением пользователя: «выгрузи электрику в формате склада». В полном проекте сначала проверяются форма и значения; только разрешённый план можно передать коду SQL. Неизвестные поля отвергаются, поэтому у неправильного задания нет скрытой частично выполненной выгрузки.
Превратить JSON-настройки в разрешённый план выгрузки
Сотруднику нужен экспорт электрики, максимум 20 строк. Он выбирает понятное имя задания, а не присылает пароль, SQL или адрес сервера. Проект читает jobs.json, разрешает только известные ключи и значения и сохраняет export-plan.json. Три ошибочных задания получают точные причины отказа до открытия базы. Неправильный тип числа и опечатка в настройке не превращаются в молчаливое значение по умолчанию.
- jobs.json: категория, число строк, имя профиля и формата
- Проверить объект, известные поля, типы, диапазон и разрешённые значения
- Один разрешённый план; три отказа с причинами
- export-plan.json содержит только имена и параметры задачи; пароль остаётся вне файла
Минимальный вызов
$job = json_decode('{"row_limit":20}', false, 512, JSON_THROW_ON_ERROR);
if (!is_int($job->row_limit)) {
throw new RuntimeException('row_limit должен быть целым числом');
}
echo $job->row_limit;JSON — текст с именованными полями. Число 20 записано без кавычек; "20" было бы строкой. При associative=false JSON-объект становится stdClass, а стрелка -> читает его поле: здесь вывод 20. JSON-массив [] остаётся массивом, поэтому проект может отличить неверную форму от объекта {}. JSON_THROW_ON_ERROR останавливает разбор испорченного JSON; после синтаксиса всё равно нужны проверки смысла. Функция plan174 собирает эти небольшие проверки и возвращает данные решения, не выполняя SQL.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Принятое задание | электрика; максимум 20 строк; sku, title, qty |
| Строка вместо числа | row_limit: целое число от 1 до 100 |
| Неизвестная настройка | unknown: timeout_seconds |
| Массив вместо объекта | job: нужен JSON-объект |
| Соединение с базой открыто | нет |
| Файл | export-plan.json |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=c174LocalRuntime();
function plan174($job): array {
if (!is_object($job) || !($job instanceof stdClass)) {
return ['accepted'=>false,'plan'=>null,'errors'=>['job: нужен JSON-объект']];
}
$values=get_object_vars($job);$errors=[];
$allowed=['profile','category','row_limit','format'];
foreach (array_keys($values) as $key) {if (!in_array($key,$allowed,true)) {$errors[]='unknown: '.$key;}}
if (($values['profile']??null)!=='warehouse_read') {$errors[]='profile: неизвестный логический профиль';}
if (!isset($values['category']) || !is_string($values['category']) || !in_array($values['category'],['электрика','крепёж'],true)) {
$errors[]='category: разрешены электрика или крепёж';
}
if (!isset($values['row_limit']) || !is_int($values['row_limit']) || $values['row_limit']<1 || $values['row_limit']>100) {
$errors[]='row_limit: целое число от 1 до 100';
}
if (($values['format']??null)!=='stock_csv') {$errors[]='format: неизвестный формат';}
if ($errors!==[]) {return ['accepted'=>false,'plan'=>null,'errors'=>$errors];}
return ['accepted'=>true,'plan'=>['profile'=>'warehouse_read','category'=>$values['category'],
'row_limit'=>$values['row_limit'],'format'=>'stock_csv','columns'=>['sku','title','qty'],
'order_by'=>['id'],'connection_source'=>'private_environment'],'errors'=>[]];
}
$input=file_get_contents(__DIR__.'/jobs.json');
if ($input===false) {throw new RuntimeException('Настройки не прочитаны');}
$jobs=json_decode($input,false,512,JSON_THROW_ON_ERROR);
if (!is_array($jobs)) {throw new RuntimeException('Нужен список заданий');}
$report=[];
foreach ($jobs as $case) {
if (!($case instanceof stdClass) || !isset($case->label,$case->job) || !is_string($case->label)) {throw new RuntimeException('Неверная форма случая');}
$report[]=array_merge(['label'=>$case->label],plan174($case->job));
}
$encoded=json_encode($report,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)."\n";
if (file_put_contents(__DIR__.'/export-plan.json',$encoded)!==strlen($encoded)) {throw new RuntimeException('План не записан полностью');}
$rows=[['Принятое задание','электрика; максимум 20 строк; sku, title, qty'],['Строка вместо числа','row_limit: целое число от 1 до 100'],
['Неизвестная настройка','unknown: timeout_seconds'],['Массив вместо объекта','job: нужен JSON-объект'],['Соединение с базой открыто',false],['Файл','export-plan.json']];
$checks=['four_cases'=>count($report)===4,'one_accepted'=>array_column($report,'accepted')===[true,false,false,false],
'valid_category'=>$report[0]['plan']['category']==='электрика','valid_limit'=>$report[0]['plan']['row_limit']===20,
'allowed_columns'=>$report[0]['plan']['columns']===['sku','title','qty'],'stable_order'=>$report[0]['plan']['order_by']===['id'],
'numeric_string_rejected'=>$report[1]['errors']===['row_limit: целое число от 1 до 100'],
'unknown_and_range_rejected'=>$report[2]['errors']===['unknown: timeout_seconds','profile: неизвестный логический профиль','row_limit: целое число от 1 до 100','format: неизвестный формат'],
'array_rejected'=>$report[3]['errors']===['job: нужен JSON-объект'],
'invalid_has_no_plan'=>$report[1]['plan']===null&&$report[2]['plan']===null&&$report[3]['plan']===null,
'no_connection'=>$db===null&&$runtime['database']===null,'artifact_roundtrip'=>json_decode(file_get_contents(__DIR__.'/export-plan.json'),true,512,JSON_THROW_ON_ERROR)===$report];
$observations=['normalized_decisions'=>$report,'row_limit_unit'=>'rows','configuration_is_not_sql'=>true,
'credentials_in_plan'=>false,'connection_opened'=>false];
c174Report('cub174-job-configuration','Превратить JSON-настройки в разрешённый план выгрузки',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Принятое задание — электрика; максимум 20 строк; sku, title, qty
Строка вместо числа — row_limit: целое число от 1 до 100
Неизвестная настройка — unknown: timeout_seconds
Массив вместо объекта — job: нужен JSON-объект
Соединение с базой открыто — нет
Файл — export-plan.json
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит логику урока, lab.php — проверку отдельной среды и работу с собственными ресурсами. Этот проект читает локальные файлы и возможности CLI, не открывая соединение с базой. После запуска откройте result.html и скачиваемый ниже JSON или CSV.
Открыть результат выполнения · Посмотреть данные JSON · Скачать нормализованный план и причины отказов
Границы решения
Это небольшой валидатор конкретного формата, не реализация стандарта JSON Schema. Профиль warehouse_read — ключ разрешённой конфигурации приложения, а не доказательство SQL-прав пользователя. В этом проекте подключения нет. row_limit измеряется строками; timeout_seconds отвергнут как неизвестный ключ, а не преобразован в миллисекунды. Время ожидания задают отдельно и с известными единицами в настройках соединения и запроса. Имена столбцов берутся из кода: их нельзя заменить пользовательским SQL через параметр bind. Значения категории в будущем запросе передаются отдельно через подготовленный запрос.
Попробуйте изменить условие
Что произойдёт с row_limit:0, отсутствующим profile и пустым объектом {}? Нужно ли автоматически приводить "20" к числу?
Разбор
0 не входит в диапазон 1–100. Отсутствующий profile даёт отказ профиля; пустой объект не проходит ни одно обязательное поле. Автоматическое приведение скрыло бы ошибку формата. Если продукт хочет принимать числовые строки, это отдельное явно описанное правило, которого в данном контракте нет. Перед разбором более сложного JSON полезно прочитать json_decode: объект, массив и типы значений.
Модель складского отчёта: позиции, единицы и копейки
Теперь сведём окружение и прикладную задачу. Таблица содержит пять товарных позиций. qty означает количество единиц, price_cents — цену одной единицы в копейках. Одна строка каталога не равна одной единице товара: Кабель — одна позиция с тремя единицами. Это различие определяет, что считать COUNT, а что SUM.
| Товар и категория | Остаток и цена за единицу | Попадает в отчёт |
|---|---|---|
| Кабель / электрика | 3 × 12000 коп. | Да: 36000 коп. |
| Розетка / электрика | 2 × 2500 коп. | Да: 5000 коп. |
| Хомут / крепёж | 4 × 150 коп. | Да: 600 коп. |
| Болт / крепёж | 0 × 200 коп. | Нет: qty должен быть больше 0 |
| Коробка / упаковка | 8 × 500 коп. | Нет: категория не выбрана |
Результат для электрики: 2 позиции, 5 единиц, стоимость 41000 копеек. Для крепежа: 1 позиция, 4 единицы, 600 копеек. Эти же имена и числа используются в SQL, HTML и CSV ниже. PHP получает уже сгруппированный ответ. Преобразование двух строк в CSV полезно; перенос сортировки и группировки сотен тысяч товаров из SQL в PHP здесь не нужен.
Официальный пример CUBRID показывает общий путь: открыть соединение, выполнить SQL, получить описание столбцов, прочитать строки, закрыть результат и соединение. Наш складской проект применяет тот же путь к своим данным, UTF-8 и подготовленным значениям. Исторические demodb и euc-kr не являются обязательными именем базы и кодировкой, а старую функцию each не нужно переносить в новый PHP-код: здесь используется foreach. Полный разбор официального примера и выбора API показывает остальные его детали; метаданные полей объясняет имена и типы колонок результата.
Получить сводку нужных товарных категорий и CSV для закупщика
Закупщику нужна стоимость имеющихся товаров двух направлений. Ему не нужен массив всех складских строк. SQL оставляет электрику и крепёж с положительным остатком, группирует их и возвращает две строки. Программа записывает готовую сводку в stock-by-category.csv. На одном понятном наборе данных связываются подготовка PHP, подключение, параметры запроса, чтение результата и завершение ресурсов.
- 5 товаров: Кабель: 3, Розетка: 2, Хомут: 4, Болт: 0, Коробка: 8
- WHERE: электрика/крепёж и qty>0 → три позиции
- GROUP BY: крепёж — 1/4/600; электрика — 2/5/41000
- PHP получает две строки → CSV → request закрыт, транзакция завершена
Минимальный вызов
$sql = 'SELECT category,COUNT(*) AS positions,SUM(qty) AS units '
. 'FROM stock WHERE qty>0 GROUP BY category ORDER BY category LIMIT 10';
$request = cubrid_execute($db, $sql);
if ($request === false) { throw new RuntimeException('Сводка не получена'); }
$row = cubrid_fetch_assoc($request);
if (is_array($row)) { echo $row['category'], ': ', $row['units']; }
cubrid_close_request($request);Короткая запись предполагает уже открытый $db и таблицу stock. WHERE отбирает строки до группировки; GROUP BY собирает одну группу на категорию. COUNT(*) считает строки товаров внутри группы, SUM(qty) — единицы товара. AS задаёт имя поля PHP-массива. ORDER BY определяет порядок, LIMIT — максимум строк сводки. В коротком запросе первая строка наших данных даст «крепёж: 4»; полный проект дополнительно выбирает только две нужные категории через маркеры ? и считает стоимость. Звёздочка в COUNT(*) не передаёт все столбцы или товары клиенту и отличается от SELECT *.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| крепёж | 1 позиция; 4 единицы; 600 коп. |
| электрика | 2 позиции; 5 единиц; 41000 коп. |
| Строк сводки из SQL | 2 |
| Нулевые остатки и упаковка | Не включены в отчёт |
| Файл | stock-by-category.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c174Config();$db=c174Connect();$runtime=c174Runtime($db);
$name=c174CreateTable($db,$owned,'stock_report',[
['id'=>1,'sku'=>'CB01','title'=>'Кабель','category'=>'электрика','price_cents'=>12000,'qty'=>3,'note'=>null],
['id'=>2,'sku'=>'SO02','title'=>'Розетка','category'=>'электрика','price_cents'=>2500,'qty'=>2,'note'=>''],
['id'=>3,'sku'=>'CL03','title'=>'Хомут','category'=>'крепёж','price_cents'=>150,'qty'=>4,'note'=>null],
['id'=>4,'sku'=>'BL04','title'=>'Болт','category'=>'крепёж','price_cents'=>200,'qty'=>0,'note'=>null],
['id'=>5,'sku'=>'BX05','title'=>'Коробка','category'=>'упаковка','price_cents'=>500,'qty'=>8,'note'=>null]]);
$table=c174Qualified($name);
$sql='SELECT category,COUNT(*) AS positions,SUM(qty) AS units,SUM(price_cents*qty) AS value_cents '
.'FROM '.$table.' WHERE category IN (?,?) AND qty>0 GROUP BY category ORDER BY category LIMIT 10';
$prepared=c174Capture(static function()use($db,$sql){return cubrid_prepare($db,$sql);});
if (!is_resource($prepared['value']) || $prepared['warning']) {c174Failure('Подготовка сводки',$prepared);}
$request=$prepared['value'];unset($prepared);
$raw=[];
try {
foreach (['электрика','крепёж'] as $index=>$category) {
$bound=c174Capture(static function()use($request,$index,$category){return cubrid_bind($request,$index+1,$category,'string');});
if ($bound['value']!==true || $bound['warning']) {c174Failure('Категория не передана',$bound);}
}
$executed=c174Capture(static function()use($request){return cubrid_execute($request);});
if ($executed['value']!==true || $executed['warning']) {c174Failure('Сводка не выполнена',$executed);}
while (true) {
$call=c174Capture(static function()use($request){return cubrid_fetch_assoc($request);});
if ($call['warning'] || $call['code']!==0 || $call['facility']!==0) {c174Failure('Чтение сводки',$call);}
if ($call['value']===false) {break;}
if (!is_array($call['value'])) {throw new RuntimeException('Неожиданный тип строки сводки');}
$raw[]=$call['value'];
}
} finally {c174Release($request);}
c174Commit($db);
$summary=[];
foreach ($raw as $row) {$summary[]=['category'=>$row['category'],'positions'=>(int)$row['positions'],
'units'=>(int)$row['units'],'value_cents'=>(int)$row['value_cents']];}
$fp=fopen(__DIR__.'/stock-by-category.csv','wb');
if (!is_resource($fp)) {throw new RuntimeException('CSV не открыт');}
try {
if (fputcsv($fp,['category','positions','units','value_cents'])===false) {throw new RuntimeException('Заголовок CSV не записан');}
foreach ($summary as $row) {if (fputcsv($fp,array_values($row))===false) {throw new RuntimeException('Строка CSV не записана');}}
} finally {fclose($fp);}
$expected=[['category'=>'крепёж','positions'=>1,'units'=>4,'value_cents'=>600],['category'=>'электрика','positions'=>2,'units'=>5,'value_cents'=>41000]];
$rows=[['крепёж','1 позиция; 4 единицы; 600 коп.'],['электрика','2 позиции; 5 единиц; 41000 коп.'],
['Строк сводки из SQL',count($summary)],['Нулевые остатки и упаковка','Не включены в отчёт'],['Файл','stock-by-category.csv']];
$checks=['two_categories'=>count($summary)===2,'exact_summary'=>$summary===$expected,
'selected_categories'=>array_column($summary,'category')===['крепёж','электрика'],
'position_count'=>array_sum(array_column($summary,'positions'))===3,
'units_count'=>array_sum(array_column($summary,'units'))===9,'stock_value'=>array_sum(array_column($summary,'value_cents'))===41600,
'explicit_fields'=>array_keys($raw[0])===['category','positions','units','value_cents'],
'within_sql_limit'=>count($raw)<=10,
'artifact_bytes'=>file_get_contents(__DIR__.'/stock-by-category.csv')==="category,positions,units,value_cents\nкрепёж,1,4,600\nэлектрика,2,5,41000\n"];
$observations=['raw_sql_rows'=>$raw,'normalized_summary'=>$summary,'filter'=>['categories'=>['электрика','крепёж'],'qty_greater_than'=>0],
'money_unit'=>'копейки','aggregation_performed_by'=>'SQL GROUP BY','full_catalog_loaded_into_php'=>false];
} finally {
if (is_resource($db)) {try {$removed=c174Cleanup($db,$owned);} finally {c174Close($db);}}
}
c174Report('cub174-stock-report','Получить сводку нужных товарных категорий и CSV для закупщика',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
крепёж — 1 позиция; 4 единицы; 600 коп.
электрика — 2 позиции; 5 единиц; 41000 коп.
Строк сводки из SQL — 2
Нулевые остатки и упаковка — Не включены в отчёт
Файл — stock-by-category.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит логику урока, lab.php — проверку отдельной среды и работу с собственными ресурсами. Этот проект выполняет SQL и удаляет только свою учебную таблицу. После запуска откройте result.html и скачиваемый ниже JSON или CSV.
Открыть результат выполнения · Посмотреть данные JSON · Скачать складскую сводку CSV
Границы решения
COUNT(*) — количество позиций, а не сумма количества товара. Здесь стоимость хранится целыми копейками, поэтому 12000×3+2500×2=41000; диапазоны маленькой учебной таблицы проверены ожидаемыми значениями. Для больших денежных сумм заранее выбирают точный SQL-тип, допустимый диапазон и правила округления. LIMIT применяется к результату группировки: он не обещает, что сервер прочитает только десять исходных записей. Индексы и план выполнения разбираются отдельно. PHP преобразует небольшую полученную сводку в типы предметной области; фильтрация и группировка полного каталога происходят в базе. Поля, ключи и связи таблиц и границы транзакции помогают развить пример.
Попробуйте изменить условие
Товар Болт получил остаток 5. Как изменятся число позиций, количество и стоимость категории «крепёж»? Где внести фильтр по ещё одной категории?
Разбор
Крепёж станет: 2 позиции, 9 единиц, 1600 копеек (4×150+5×200). Новую категорию включают в WHERE и передают отдельным связанным значением; нельзя загрузить весь каталог, чтобы затем отсеивать его в PHP. Для нескольких независимых модулей отчёта следующий шаг — один владелец соединения и отдельные request каждого модуля.
Разобрать типичные отказы и перейти к устройству приложения
Реальная проблема: CLI загружает расширения, а HTTP — нет показывает, почему совпадения имени php.ini мало. Здесь предложена проверка фактического процесса и пути модуля; специфический обход старого Windows-пути из ответа не объявляется универсальным правилом.
Ошибка несовпадающих module API PHP помогает увидеть разницу между «файл существует» и «расширение совместимо с этим PHP». Мы не переносим команды переключения системных пакетов: учебная сборка выбирается явно и изолированно.
JSON-валидация, которая пропустила пустой адрес ставит отдельную проблему формы данных. Наш небольшой валидатор различает JSON-объект, массив, строку и число и проверяет смысл полей. Он не воспроизводит стороннюю библиотеку и не утверждает, что разбор синтаксиса JSON сам проверяет бизнес-правила.
Следующий шаг — выбрать семейство API и назначить владельцев соединения и запросов. Там два модуля работают через одно соединение, именованный профиль задаёт форму выгрузки, а анализатор составляет план поддержки старого кода. Если запросы и параметры ещё непривычны, сначала пройдите первые запросы и подготовленный каталог.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


