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

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

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

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

Глава 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-проекта создают и удаляют только собственную таблицу.

Как запрос превращается в складской отчёт
  1. PHP читает задание: две категории и нужные показатели
  2. Расширение cubrid предоставляет функции, например cubrid_prepare
  3. Библиотека CCI передаёт команды брокеру CUBRID; его CAS обслуживает соединение
  4. Сервер выполняет WHERE и GROUP BY над таблицей товаров
  5. 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.

Путь данных и полученный результат
  1. contracts.json: stock_export и legacy_attachment
  2. Проверить расширение, имена функций и констант в текущем PHP
  3. stock_export: доступно; legacy_attachment: два отсутствующих вызова
  4. 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. Три ошибочных задания получают точные причины отказа до открытия базы. Неправильный тип числа и опечатка в настройке не превращаются в молчаливое значение по умолчанию.

Путь данных и полученный результат
  1. jobs.json: категория, число строк, имя профиля и формата
  2. Проверить объект, известные поля, типы, диапазон и разрешённые значения
  3. Один разрешённый план; три отказа с причинами
  4. 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
Самостоятельный проект, исходник и архив
Превратить 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, подключение, параметры запроса, чтение результата и завершение ресурсов.

Путь данных и полученный результат
  1. 5 товаров: Кабель: 3, Розетка: 2, Хомут: 4, Болт: 0, Коробка: 8
  2. WHERE: электрика/крепёж и qty>0 → три позиции
  3. GROUP BY: крепёж — 1/4/600; электрика — 2/5/41000
  4. 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 коп.
Строк сводки из SQL2
Нулевые остатки и упаковкаНе включены в отчёт
Файлstock-by-category.csv
Самостоятельный проект, исходник и архив
Получить сводку нужных товарных категорий и 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.