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

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

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

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

Глава 123 · После первого каталога X DevAPI

MySQL Document Store: вычисляемые сводки и индексы каталога

От стоимости одной лампы до ведомости по категориям: Expression, groupBy/having, составной индекс и проверка исходных данных.

Получить сводку, которую можно показать закупщику

В прошлой главе мы получили карточки каталога. Теперь сотруднику нужны ответы другого масштаба: сколько денег лежит в активных запасах, какие категории требуют внимания и как подготовить доступ к растущему каталогу. Начинаем с одной лампы 1200 × 4, затем собираем ведомость; база вычисляет нужные строки, PHP показывает результат.

От карточки к отчёту
  1. LAMP: цена 1200, остаток 4 →4800
  2. STRIP:1500 × 2 →3000
  3. lighting объединяет обе карточки →7800
  4. having от 5000 оставляет эту категорию
  5. индекс по category/price помогает выбранному способу доступа; его пользу подтверждает отдельный план

Перед началом нужны сеанс, коллекция, поля и чтение и изменение одного документа. Подключение и создание собственной коллекции входят в каждый полный архив. Новые действия сначала показаны коротко; затем раскрывается весь файл с ожидаемыми строками и сохранённым результатом.

Используется отдельная лаборатория 139: PHP 8.3.35, mysql_xdevapi 8.0.30, MySQL 8.4.13 с X Protocol. Нужны PHPMAN_MYSQLX_URI и PHPMAN_MYSQLX_SCHEMA; секретов и адреса production в архиве нет. Путь настройки — установка и лаборатория. Индексам дополнительно нужны INDEX/ALTER на учебной схеме. README каждого архива содержит полную подготовку; PHP сам не расширяет права.

Вычислить стоимость остатка без лишнего поля в документе

Закупщику нужна стоимость запаса: у лампы цена 1200 рублей и остаток 4, значит на полке 4800 рублей. Хранить ещё одно поле inventory не обязательно: его легко забыть пересчитать. Сначала вычислим значение только в выдаче, затем сравним формулу с обычной строкой при записи.

Путь данных этой программы
  1. Документ LAMP хранит price 1200 и stock 4
  2. fields вычисляет 1200 × 4 → inventory 4800 только для ответа
  3. Повторное getOne: поля inventory в документе нет
  4. set(note, строка) сохраняет текст price * stock
  5. new Expression(stock + :extra), extra 2 → новое чтение stock 6
  6. expression(stock + :extra), extra 3 → новое чтение stock 9

Сначала короткое действие

$row = $collection->find('_id = :id')->bind(['id'=>'LAMP'])
    ->fields(['sku', 'price * stock AS inventory'])->execute()->fetchOne();
echo $row['inventory']; // 4800

fields выбирает данные ответа; * здесь умножает два поля, а AS inventory даёт вычисленному значению имя. PHP не перемножает строку запроса: выражение разбирает API, вычисляет сервер. new Expression создаёт объект-обёртку выражения; expression(...) создаёт такую обёртку функцией. В set обычная строка — значение, Expression — инструкция вычислить. Сначала поступили 2 лампы: объект через new даёт остаток 6. Затем поступили ещё 3: объект через функцию даёт остаток 9. :extra обозначает место для значения; текст формулы остаётся фиксированным. Сначала set добавляет выражение с :extra к операции, затем bind связывает оба параметра id и extra. В проверенном 8.0.30 попытка связать extra до добавления set завершается Error while binding a variable: такого параметра ещё нет в подготовленной операции.

Короткая запись использует уже полученную коллекцию. Полный файл ниже создаёт свои исходные документы, выполняет действие, читает данные и удаляет только свои коллекции.

Что вы получите

НаблюдениеРезультат
Стоимость четырёх ламп4800
В исходном документе появился inventoryнет
Обычная строка сохранена буквальноprice * stock
После new Expression: поступило 2 лампы6
После expression: поступило ещё 3 лампы9
Самостоятельный файл и архив для запуска
Вычислить стоимость остатка без лишнего поля в документеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';

$config = x140Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
    $schema = $session->getSchema($config['schema']);
    if (!$schema->existsInDatabase()) {
        throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
    }
    $runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
        'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
        'host'=>$config['host'], 'port'=>$config['port']];
    $name = x140Name('computed');
    $handle = $schema->getCollection($name);
    $existed = $handle->existsInDatabase();
    if ($existed) {throw new RuntimeException('Случайное имя уже занято.');}
    $collection = $schema->createCollection($name);
    $owned[] = $name;

    $collection->add(['_id'=>'LAMP','sku'=>'LAMP','price'=>1200,'stock'=>4])->execute();
    $minimal = $collection->find('_id = :id')->bind(['id'=>'LAMP'])
        ->fields(['sku','price * stock AS inventory'])->execute()->fetchOne();
    $before = $collection->getOne('LAMP');
    $collection->modify('_id = :id')->bind(['id'=>'LAMP'])->set('note','price * stock')->execute();
    $literal = $collection->getOne('LAMP');
    $constructed = new mysql_xdevapi\Expression('stock + :extra');
    $collection->modify('_id = :id')->set('stock',$constructed)->bind(['id'=>'LAMP','extra'=>2])->execute();
    $afterConstructor = $collection->getOne('LAMP');
    $formula = mysql_xdevapi\expression('stock + :extra');
    $collection->modify('_id = :id')->set('stock',$formula)->bind(['id'=>'LAMP','extra'=>3])->execute();
    $after = $collection->getOne('LAMP');
    $minimalCorrect = (is_int($minimal['inventory']) || is_float($minimal['inventory']))
        && (float)$minimal['inventory'] === 4800.0;
    $constructorCorrect = (is_int($afterConstructor['stock']) || is_float($afterConstructor['stock']))
        && (float)$afterConstructor['stock'] === 6.0;
    $incrementCorrect = (is_int($after['stock']) || is_float($after['stock']))
        && (float)$after['stock'] === 9.0;
    $rows = [['Стоимость четырёх ламп', $minimalCorrect ? (int)$minimal['inventory'] : 'Неожиданное число'],
        ['В исходном документе появился inventory', array_key_exists('inventory',$before)],
        ['Обычная строка сохранена буквально', $literal['note']],
        ['После new Expression: поступило 2 лампы', $constructorCorrect ? (int)$afterConstructor['stock'] : 'Неожиданное число'],
        ['После expression: поступило ещё 3 лампы', $incrementCorrect ? (int)$after['stock'] : 'Неожиданное число']];
    $checks = ['minimal_value'=>$minimalCorrect,
        'projection_sku'=>$minimal['sku'] === 'LAMP',
        'projection_read_only'=>!array_key_exists('inventory',$before) && $before['stock'] === 4,
        'literal_preserved'=>$literal['note'] === 'price * stock',
        'function_expression'=>$formula instanceof mysql_xdevapi\Expression,
        'constructor_expression'=>$constructed instanceof mysql_xdevapi\Expression,
        'constructor_readback'=>$constructorCorrect,
        'increment_readback'=>$incrementCorrect && $after['price'] === 1200];
    $observations = ['minimal_projection'=>$minimal,
        'before_write'=>$before,'literal_readback'=>$literal,'after_constructor'=>$afterConstructor,'after_function_expression'=>$after,
        'expression_name_property'=>$constructed->name,
        'numeric_types'=>['inventory'=>get_debug_type($minimal['inventory']),
            'after_constructor_stock'=>get_debug_type($afterConstructor['stock']),
            'after_function_stock'=>get_debug_type($after['stock'])]];
} finally {
    try {
        if ($schema instanceof mysql_xdevapi\Schema) {
            $removed = x140Cleanup($schema, $owned);
        }
    } finally {
        $session->close();
    }
}
x140Report('mysql140-x-computed-stock', 'Вычислить стоимость остатка без лишнего поля в документе', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Стоимость четырёх ламп — 4800
В исходном документе появился inventory — нет
Обычная строка сохранена буквально — price * stock
После new Expression: поступило 2 лампы — 6
После expression: поступило ещё 3 лампы — 9

Откройте result.html после запуска: строки получены из реальных ответов MySQL. result.json сохраняет документы и цепочки отказов; metrics.json — результаты проверок и очистки.

Таблица сохранённого запуска · Полные данные результата.

Границы и ошибки

Expression не экранирует произвольный ввод и не заменяет bind. Не вставляйте строку из формы в формулу или имя поля: разрешите несколько известных вариантов, а числа передавайте параметрами. В этом профиле результат арифметики X возвращается в PHP как float: inventory 4800.0, остатки 6.0 и 9.0. Число 6.0 не равно целому 6 при строгом ===, хотя их числовые значения совпадают. Проверка сначала допускает только int/float, затем сравнивает с точными ожидаемыми целыми значениями в формате float; строка "6" и дробь 6.5 проверку не пройдут. Только после такой проверки отображение превращает подтверждённые 6.0 и 9.0 в целые 6 и 9. Исходные ответы и get_debug_type сохраняются в observations без изменения. Это не правило безусловно обрезать дробь через (int). Цены здесь целые рубли для учебного набора; в реальном учёте выбирайте единицу и диапазон (например, целые копейки) и не выводите денежную точность из float. Вычисление проекции не сохраняется; set уже меняет документ.

Попробуйте изменить

Поменяйте price на 1250, оставив stock 4. Что изменится до записи и что останется прежним?

Ожидаемое решение

Проекция через fields даст 5000; inventory всё ещё не появится в документе. После поступлений 2 и 3 остаток станет сначала 6, затем 9. Измените ожидаемую стоимость и проверку price, затем сравните result.json с новой ценой.

Получить ведомость по категориям и складам

Вместо четырёх карточек закупщик хочет две строки: сколько действующих артикулов, штук и денег приходится на каждую категорию. Группировка объединяет документы с одинаковым ключом. Она не меняет сами документы: архивный OLD остаётся в базе, но не участвует в выбранной ведомости.

Путь данных этой программы
  1. find(active = true) оставляет LAMP, STRIP, CABLE
  2. groupBy(category) собирает LAMP + STRIP в lighting, CABLE в cable
  3. COUNT(*) считает SKU; SUM(stock) — штуки; SUM(price * stock) — деньги
  4. sort(category ASC) задаёт порядок двух строк
  5. PHP получает только сводку; вариант category + warehouse даёт 3 группы

Сначала короткое действие

$groups = $collection->find('active = true')
    ->fields(['category', 'SUM(stock) AS units'])
    ->groupBy('category')->sort('category ASC')->execute()->fetchAll();

groupBy получает ключ группировки. SUM складывает значение для каждого документа группы; COUNT(*) считает документы, а не сумму остатков. Скобки COUNT(*) здесь означают подсчёт строк группы, не выдачу всех полей как SELECT *. fields перечисляет ровно необходимые результаты. Вариант массива category,warehouse формирует группу по паре: одинаковая категория на разных складах даёт разные строки. foreach в конце только оформляет готовую небольшую сводку, не сортирует весь каталог в PHP.

Короткая запись использует уже полученную коллекцию. Полный файл ниже создаёт свои исходные документы, выполняет действие, читает данные и удаляет только свои коллекции.

Что вы получите

НаблюдениеРезультат
cable1 SKU / 10 шт. / 3000 руб.
lighting2 SKU / 6 шт. / 7800 руб.
Групп категория + склад3
Исходных документов в базе4
Самостоятельный файл и архив для запуска
Получить ведомость по категориям и складамСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';

$config = x140Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
    $schema = $session->getSchema($config['schema']);
    if (!$schema->existsInDatabase()) {
        throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
    }
    $runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
        'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
        'host'=>$config['host'], 'port'=>$config['port']];
    $name = x140Name('totals');
    $handle = $schema->getCollection($name);
    $existed = $handle->existsInDatabase();
    if ($existed) {throw new RuntimeException('Случайное имя уже занято.');}
    $collection = $schema->createCollection($name);
    $owned[] = $name;

    $collection->add(
        ['_id'=>'LAMP','sku'=>'LAMP','category'=>'lighting','warehouse'=>'central','price'=>1200,'stock'=>4,'active'=>true],
        ['_id'=>'STRIP','sku'=>'STRIP','category'=>'lighting','warehouse'=>'annex','price'=>1500,'stock'=>2,'active'=>true],
        ['_id'=>'CABLE','sku'=>'CABLE','category'=>'cable','warehouse'=>'central','price'=>300,'stock'=>10,'active'=>true],
        ['_id'=>'OLD','sku'=>'OLD','category'=>'lighting','warehouse'=>'central','price'=>99,'stock'=>100,'active'=>false]
    )->execute();

    $byCategory = $collection->find('active = true')
        ->fields(['category','COUNT(*) AS skus','SUM(stock) AS units','SUM(price * stock) AS amount'])
        ->groupBy('category')->sort('category ASC')->execute()->fetchAll();
    $byWarehouse = $collection->find('active = true')
        ->fields(['category','warehouse','SUM(stock) AS units'])
        ->groupBy(['category','warehouse'])->sort('category ASC','warehouse ASC')->execute()->fetchAll();
    $rows = [];
    foreach ($byCategory as $group) {
        $rows[] = [$group['category'], (int)$group['skus'].' SKU / '.(int)$group['units'].' шт. / '.(int)$group['amount'].' руб.'];
    }
    $rows[] = ['Групп категория + склад',count($byWarehouse)];
    $rows[] = ['Исходных документов в базе',$collection->count()];
    $checks = ['two_categories'=>count($byCategory) === 2,
        'cable_totals'=>$byCategory[0]['category'] === 'cable' && (float)$byCategory[0]['skus'] === 1.0 && (float)$byCategory[0]['units'] === 10.0 && (float)$byCategory[0]['amount'] === 3000.0,
        'lighting_totals'=>$byCategory[1]['category'] === 'lighting' && (float)$byCategory[1]['skus'] === 2.0 && (float)$byCategory[1]['units'] === 6.0 && (float)$byCategory[1]['amount'] === 7800.0,
        'two_key_grouping'=>count($byWarehouse) === 3 && $byWarehouse[1]['warehouse'] === 'annex' && (float)$byWarehouse[1]['units'] === 2.0,
        'documents_preserved'=>$collection->count() === 4];
    $observations = ['category_rows'=>$byCategory,'category_warehouse_rows'=>$byWarehouse,
        'all_documents'=>$collection->find()->sort('_id ASC')->execute()->fetchAll()];
} finally {
    try {
        if ($schema instanceof mysql_xdevapi\Schema) {
            $removed = x140Cleanup($schema, $owned);
        }
    } finally {
        $session->close();
    }
}
x140Report('mysql140-x-category-totals', 'Получить ведомость по категориям и складам', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
cable — 1 SKU / 10 шт. / 3000 руб.
lighting — 2 SKU / 6 шт. / 7800 руб.
Групп категория + склад — 3
Исходных документов в базе — 4

Откройте result.html после запуска: строки получены из реальных ответов MySQL. result.json сохраняет документы и цепочки отказов; metrics.json — результаты проверок и очистки.

Таблица сохранённого запуска · Полные данные результата.

Границы и ошибки

Не добавляйте sku в такую проекцию без группировки по sku или понятного агрегата: у lighting два разных артикула, и единственного правильного sku для группы нет. Нельзя лечить неоднозначность отключением ONLY_FULL_GROUP_BY. groupBy не гарантирует порядок, поэтому sort задан отдельно. Для больших наборов нужны подходящий индекс, план и объём реально прочитанных данных; сам факт сводки не делает запрос бесплатным. Если требуется весь экспорт, читайте его отдельно порциями, а не используйте экран сводки как выгрузку всех документов.

В сводку добавляют поле конкретной строки и получают неоднозначный результат. Разбор реальной ситуации на Stack Overflow. Проекция содержит ключи группы и агрегаты, не случайный SKU; сортировка и связанные параметры явно заданы. Старый SQL-код ответа не копируется.

Попробуйте изменить

Добавьте активный товар lighting на склад annex с остатком 1 и ценой 500. Сколько групп получится в обоих вариантах?

Ожидаемое решение

Категорий останется 2; lighting станет 3SKU/7 шт/8300 руб. Пар категория+склад останется 3: новый товар попал в существующую пару lighting+annex; её units станет 3.

Отобрать дорогие запасы после подсчёта групп

Нужны категории, где в запасах не меньше 5000 рублей. Порог относится к сумме группы, поэтому проверяется после сложения. Если заранее убрать товары с маленьким остатком, исчезнут две ленты STRIP по 1500: ведомость lighting ошибочно уменьшится с 7800 до 4800.

Путь данных этой программы
  1. find(active = true) выбирает участвующие документы
  2. groupBy(category): lighting = LAMP 4800 + STRIP 3000
  3. having(SUM(...) >= :minimum), minimum5000 → остаётся lighting 7800
  4. Альтернатива stock >=3 в find заранее убирает STRIP → lighting 4800
  5. Порог 10000 даёт пустой список, не ошибку соединения

Сначала короткое действие

$rows = $collection->find('active = true')
    ->fields(['category','SUM(price * stock) AS amount'])
    ->groupBy('category')->having('SUM(price * stock) >= :minimum')
    ->bind(['minimum'=>5000])->execute()->fetchAll();

find задаёт условие для каждого исходного документа. having задаёт условие для полученной группы; в нём допустимы агрегатные функции. Параметр :minimum связан с целым 5000, а не склеен со строкой. Имена методов можно читать как рецепт: выбрать активные → собрать категории → посчитать стоимость → оставить группы нужной суммы → выполнить. [] означает, что запрос исполнен, но подходящих групп нет.

Короткая запись использует уже полученную коллекцию. Полный файл ниже создаёт свои исходные документы, выполняет действие, читает данные и удаляет только свои коллекции.

Что вы получите

НаблюдениеРезультат
Категория с суммой от 5000lighting
Сумма всей категории7800
lighting после раннего отбора stock >=34800
Групп с суммой от 100000
Самостоятельный файл и архив для запуска
Отобрать дорогие запасы после подсчёта группСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';

$config = x140Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
    $schema = $session->getSchema($config['schema']);
    if (!$schema->existsInDatabase()) {
        throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
    }
    $runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
        'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
        'host'=>$config['host'], 'port'=>$config['port']];
    $name = x140Name('threshold');
    $handle = $schema->getCollection($name);
    $existed = $handle->existsInDatabase();
    if ($existed) {throw new RuntimeException('Случайное имя уже занято.');}
    $collection = $schema->createCollection($name);
    $owned[] = $name;

    $collection->add(
        ['_id'=>'LAMP','sku'=>'LAMP','category'=>'lighting','warehouse'=>'central','price'=>1200,'stock'=>4,'active'=>true],
        ['_id'=>'STRIP','sku'=>'STRIP','category'=>'lighting','warehouse'=>'annex','price'=>1500,'stock'=>2,'active'=>true],
        ['_id'=>'CABLE','sku'=>'CABLE','category'=>'cable','warehouse'=>'central','price'=>300,'stock'=>10,'active'=>true],
        ['_id'=>'OLD','sku'=>'OLD','category'=>'lighting','warehouse'=>'central','price'=>99,'stock'=>100,'active'=>false]
    )->execute();

    $over = $collection->find('active = true')
        ->fields(['category','SUM(price * stock) AS amount'])
        ->groupBy('category')->having('SUM(price * stock) >= :minimum')->bind(['minimum'=>5000])
        ->sort('category ASC')->execute()->fetchAll();
    $early = $collection->find('active = true AND stock >= :stock')->bind(['stock'=>3])
        ->fields(['category','SUM(price * stock) AS amount'])->groupBy('category')
        ->sort('category ASC')->execute()->fetchAll();
    $none = $collection->find('active = true')
        ->fields(['category','SUM(price * stock) AS amount'])->groupBy('category')
        ->having('SUM(price * stock) >= :minimum')->bind(['minimum'=>10000])->execute()->fetchAll();
    $rows = [['Категория с суммой от 5000',$over[0]['category']],['Сумма всей категории',(int)$over[0]['amount']],
        ['lighting после раннего отбора stock >=3',(int)$early[1]['amount']], ['Групп с суммой от 10000',count($none)]];
    $checks = ['having_one_group'=>count($over) === 1 && $over[0]['category'] === 'lighting',
        'complete_group_amount'=>(float)$over[0]['amount'] === 7800.0,
        'different_population'=>count($early) === 2 && $early[1]['category'] === 'lighting' && (float)$early[1]['amount'] === 4800.0,
        'empty_is_array'=>$none === [],'documents_not_deleted'=>$collection->count() === 4];
    $observations = ['after_group_filter'=>$over,'before_group_filter'=>$early,'no_matching_groups'=>$none];
} finally {
    try {
        if ($schema instanceof mysql_xdevapi\Schema) {
            $removed = x140Cleanup($schema, $owned);
        }
    } finally {
        $session->close();
    }
}
x140Report('mysql140-x-group-threshold', 'Отобрать дорогие запасы после подсчёта групп', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Категория с суммой от 5000 — lighting
Сумма всей категории — 7800
lighting после раннего отбора stock >=3 — 4800
Групп с суммой от 10000 — 0

Откройте result.html после запуска: строки получены из реальных ответов MySQL. result.json сохраняет документы и цепочки отказов; metrics.json — результаты проверок и очистки.

Таблица сохранённого запуска · Полные данные результата.

Границы и ошибки

Не переносите SUM в условие find: сумма группы ещё не существует на этом этапе. В having нельзя без смысла выбирать случайное негруппированное поле. Ошибка синтаксиса или сети выбрасывает исключение и не должна превращаться в сообщение «дорогих запасов нет». Одного limit недостаточно для сокращения стоимости агрегации: сервер сначала должен получить необходимые суммы. Наш набор мал и неизменяем; консистентность отчёта при параллельных записях зависит от транзакции и уровня изоляции.

Перенос условия до или после группировки меняет смысл ведомости. Разбор реальной ситуации на Stack Overflow. Сравниваем на тех же товарах ранний фильтр stock>=3 с поздним порогом суммы; формулу и результат определяют первичные документы и запуск.

Попробуйте изменить

Замените порог 5000 на 3000. Сколько строк будет? Почему равенство тоже проходит?

Ожидаемое решение

Будут cable 3000 и lighting 7800: сравнение >= включает границу. При >3000 останется только lighting. Менять нужно значение bind или оператор в известном шаблоне, а не дописывать произвольное выражение пользователя.

Создать индекс каталога и удалить только его

Каталог регулярно выбирает категорию и верхнюю границу цены. Индекс создаёт отдельную структуру доступа по этим полям; его устройство выбирают под запрос. Здесь проверим жизненный цикл индекса и сохранность выдачи, не будем выдавать микросекунды четырёх документов за доказательство ускорения большой базы.

Путь данных этой программы
  1. Из документов берутся category и price
  2. JSON fields описывает $.category → TEXT(40), $.price → INT UNSIGNED
  3. createIndex создаёт category_price сразу, без execute
  4. Тот же запрос даёт те же LAMP и OLD; повтор имени отклонён
  5. dropIndex возвращает true, повторный dropIndex false и E_WARNING 1091; 4 документа остались

Сначала короткое действие

$description = '{"fields":[{"field":"$.category","type":"TEXT(40)"}]}';
$collection->createIndex('category_lookup', $description);
$removed = $collection->dropIndex('category_lookup');

Первый аргумент — имя индекса внутри этой коллекции; второй — строка JSON с массивом fields. В каждом элементе field содержит полный путь: $.category, а не просто category. type — тип индексируемого представления; TEXT(40) ограничивает длину, UNSIGNED выбирает беззнаковый числовой вариант. json_encode превращает PHP-массив описания в JSON; JSON_THROW_ON_ERROR не позволяет тихо отправить false вместо строки. createIndex/dropIndex выполняются сразу; у них нет последующего execute.

Короткая запись использует уже полученную коллекцию. Полный файл ниже создаёт свои исходные документы, выполняет действие, читает данные и удаляет только свои коллекции.

Что вы получите

НаблюдениеРезультат
Артикулы выбранной страницыLAMP, OLD
Выдача до и после индекса совпадаетда
Повторное имя отклоненода
Свой индекс удалёнда
Повторное удалениенет
Предупреждений отсутствующего индекса1
Документов после удаления индекса4
Самостоятельный файл и архив для запуска
Создать индекс каталога и удалить только егоСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';

$config = x140Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
    $schema = $session->getSchema($config['schema']);
    if (!$schema->existsInDatabase()) {
        throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
    }
    $runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
        'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
        'host'=>$config['host'], 'port'=>$config['port']];
    $name = x140Name('indexes');
    $handle = $schema->getCollection($name);
    $existed = $handle->existsInDatabase();
    if ($existed) {throw new RuntimeException('Случайное имя уже занято.');}
    $collection = $schema->createCollection($name);
    $owned[] = $name;

    $collection->add(
        ['_id'=>'LAMP','sku'=>'LAMP','category'=>'lighting','warehouse'=>'central','price'=>1200,'stock'=>4,'active'=>true],
        ['_id'=>'STRIP','sku'=>'STRIP','category'=>'lighting','warehouse'=>'annex','price'=>1500,'stock'=>2,'active'=>true],
        ['_id'=>'CABLE','sku'=>'CABLE','category'=>'cable','warehouse'=>'central','price'=>300,'stock'=>10,'active'=>true],
        ['_id'=>'OLD','sku'=>'OLD','category'=>'lighting','warehouse'=>'central','price'=>99,'stock'=>100,'active'=>false]
    )->execute();

    $before = $collection->find('category = :category AND price <= :price')
        ->bind(['category'=>'lighting','price'=>1300])->fields(['sku','price'])->sort('sku ASC')->execute()->fetchAll();
    $description = json_encode(['fields'=>[
        ['field'=>'$.category','type'=>'TEXT(40)','required'=>true],
        ['field'=>'$.price','type'=>'INT UNSIGNED','required'=>true]
    ]], JSON_THROW_ON_ERROR);
    $creation = $collection->createIndex('category_price',$description);
    $duplicate = x140Failure(static fn()=>$collection->createIndex('category_price',$description));
    $after = $collection->find('category = :category AND price <= :price')
        ->bind(['category'=>'lighting','price'=>1300])->fields(['sku','price'])->sort('sku ASC')->execute()->fetchAll();
    $dropped = $collection->dropIndex('category_price');
    $dropWarnings = [];
    set_error_handler(static function(int $severity, string $message) use (&$dropWarnings): bool {
        if ($severity === E_WARNING && str_contains($message,'Collection::dropIndex()')
            && str_contains($message,'[1091][42000]') && str_contains($message,"'category_price'")) {
            $dropWarnings[] = $message;
            return true;
        }
        return false;
    });
    try {$absent = $collection->dropIndex('category_price');}
    finally {restore_error_handler();}
    $rows = [['Артикулы выбранной страницы',implode(', ',array_column($after,'sku'))],
        ['Выдача до и после индекса совпадает',$before === $after],
        ['Повторное имя отклонено',$duplicate !== null],['Свой индекс удалён',$dropped],['Повторное удаление',$absent],
        ['Предупреждений отсутствующего индекса',count($dropWarnings)],
        ['Документов после удаления индекса',$collection->count()]];
    $checks = ['same_result'=>$before === $after && array_column($after,'sku') === ['LAMP','OLD'],
        'duplicate_failed'=>is_array($duplicate) && count($duplicate)>0,
        'create_return_observed'=>$creation === true,
        'drop_existing'=>$dropped === true,'drop_absent'=>$absent === false,
        'specific_absence_warning'=>count($dropWarnings) === 1,'documents_intact'=>$collection->count() === 4];
    $observations = ['index_description'=>json_decode($description,true,512,JSON_THROW_ON_ERROR),
        'before'=>$before,'after'=>$after,'create_return_type'=>get_debug_type($creation),
        'duplicate_exception_chain'=>$duplicate,'dropped'=>$dropped,'drop_absent'=>$absent,'drop_absent_warnings'=>$dropWarnings];
} finally {
    try {
        if ($schema instanceof mysql_xdevapi\Schema) {
            $removed = x140Cleanup($schema, $owned);
        }
    } finally {
        $session->close();
    }
}
x140Report('mysql140-x-index-lifecycle', 'Создать индекс каталога и удалить только его', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Артикулы выбранной страницы — LAMP, OLD
Выдача до и после индекса совпадает — да
Повторное имя отклонено — да
Свой индекс удалён — да
Повторное удаление — нет
Предупреждений отсутствующего индекса — 1
Документов после удаления индекса — 4

Откройте result.html после запуска: строки получены из реальных ответов MySQL. result.json сохраняет документы и цепочки отказов; metrics.json — результаты проверок и очистки.

Таблица сохранённого запуска · Полные данные результата.

Границы и ошибки

Фильтр намеренно не содержит active=true, поэтому OLD входит в выдачу: индекс не исправляет забытое бизнес-условие. Категория первой, цена второй — порядок составного ключа; иные запросы могут требовать другого индекса. Индексы занимают место и увеличивают стоимость записи. DDL с индексами выполняйте как отдельное изменение схемы, вне транзакции резервирования: возможен неявный commit. Права INDEX/ALTER выдаются только на отдельную учебную схему. В PHP-модуле 8.0.30 повторный dropIndex возвращает false и дополнительно выдаёт E_WARNING с кодом 1091. Обработчик только вокруг этого вызова перехватывает строго ожидаемые метод, код и имя собственного индекса, сохраняет текст в result.json и восстанавливается в finally. Любое другое предупреждение передаётся обычному обработчику через return false; @ не используется. Отсутствие индекса и отказ доступа — разные результаты.

Индекс отвергает описание, хотя нужные свойства есть в документе. Разбор реальной ситуации на Stack Overflow. Разбираем полный JSON path $.category и явный TEXT(40), а не переносим старые обходы unique через ручное изменение внутренней таблицы.

Попробуйте изменить

Добавьте active=true в оба запроса до/после индекса. Какая строка исчезнет и нужно ли удалять документ OLD?

Ожидаемое решение

Останется LAMP, OLD отфильтруется, но count останется 4. Измените ожидаемую страницу и проверку same_result. Не удаляйте архивные документы, чтобы подогнать результат запроса.

Проверить отсутствующее поле и ошибочное описание индекса

Служба доставки вводит индекс по весу. У BOX вес уже есть, у BAG его ещё нет. Нельзя считать, что описание required само исправит старые данные. Проверим optional, затем обязательность при построении и новой записи; отдельно различим ошибку JSON, неизвестную опцию и неподходящий тип. Вторая задача — проверить, действительно ли unique:true защитил артикул. В этом профиле параметр принимается без отказа, но уникальность не устанавливается: узнаем это по структуре индекса и реальной повторной записи.

Путь данных этой программы
  1. BOX weight 2, BAG без weight → optional индекс допустим
  2. required проверяет уже существующий BAG → создание отклонено
  3. Явно записываем BAG weight 1 → required индекс создаётся
  4. Новый EMPTY без weight отвергнут и не читается обратно
  5. unique:true принят; SHOW INDEX сообщает Non_unique 1
  6. Новый _id DUP с прежним sku BOX записывается → уже 3 документа

Сначала короткое действие

$description = '{"fields":[{"field":"$.weight","type":"INT","required":true}]}';
$collection->createIndex('weight_required', $description);

required:true связывает индексируемое поле с требованием присутствия/ненулевого индексируемого значения. false разрешает отсутствие; поле не возникает само. В программе исключения сохраняются цепочкой: верхний код расширения может оборачивать более конкретную ошибку сервера. x140Failure ловит именно mysql_xdevapi\Exception, возвращает данные об отказе и не объявляет случайную PHP-ошибку нормальной ситуацией. SHOW INDEX читает метаданные: Non_unique 1 означает, что повтор значения разрешён, 0 — что индекс уникальный. Имя unique_sku само по себе ничего не гарантирует. Имена учебной схемы и коллекции берутся из проверенного конфигурационного шаблона и генератора; SQL-идентификаторы нельзя подставлять из непроверенного HTTP-ввода.

Короткая запись использует уже полученную коллекцию. Полный файл ниже создаёт свои исходные документы, выполняет действие, читает данные и удаляет только свои коллекции.

Что вы получите

НаблюдениеРезультат
Optional сохранил документ без weightда
Required отклонён на старых неполных данныхда
Вес после исправления исходного документа1
Новая запись без weight отклоненада
Ошибочный JSON отклонёнда
Неизвестная опция отклоненада
Неизвестный тип отклонёнда
Вызов с unique:true принятда
SHOW INDEX: Non_unique для unique_sku1
Повторный артикул с новым _id записалсяда
Сохранённых документов3
Самостоятельный файл и архив для запуска
Проверить отсутствующее поле и ошибочное описание индексаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';

$config = x140Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
    $schema = $session->getSchema($config['schema']);
    if (!$schema->existsInDatabase()) {
        throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
    }
    $runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
        'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
        'host'=>$config['host'], 'port'=>$config['port']];
    $name = x140Name('contract');
    $handle = $schema->getCollection($name);
    $existed = $handle->existsInDatabase();
    if ($existed) {throw new RuntimeException('Случайное имя уже занято.');}
    $collection = $schema->createCollection($name);
    $owned[] = $name;

    $collection->add(['_id'=>'BOX','sku'=>'BOX','weight'=>2],['_id'=>'BAG','sku'=>'BAG'])->execute();
    $optional = '{"fields":[{"field":"$.weight","type":"INT","required":false}]}';
    $required = '{"fields":[{"field":"$.weight","type":"INT","required":true}]}';
    $collection->createIndex('weight_optional',$optional);
    $optionalRead = $collection->getOne('BAG');
    $collection->dropIndex('weight_optional');
    $missingAtBuild = x140Failure(static fn()=>$collection->createIndex('weight_required',$required));
    $collection->modify('_id = :id')->bind(['id'=>'BAG'])->set('weight',1)->execute();
    $collection->createIndex('weight_required',$required);
    $missingAtInsert = x140Failure(static fn()=>$collection->add(['_id'=>'EMPTY','sku'=>'EMPTY'])->execute());
    $badJson = x140Failure(static fn()=>$collection->createIndex('bad_json','{"fields":['));
    $unknownField = x140Failure(static fn()=>$collection->createIndex('unknown_option',
        '{"fields":[{"field":"$.weight","type":"INT","unsupported_option":true}]}'));
    $badType = x140Failure(static fn()=>$collection->createIndex('bad_type','{"fields":[{"field":"$.weight","type":"NOT_A_SQL_TYPE"}]}'));
    $uniqueCreated = $collection->createIndex('unique_sku',
        '{"fields":[{"field":"$.sku","type":"TEXT(40)","required":true}],"unique":true}');
    $duplicateSku = x140Failure(static fn()=>$collection->add(['_id'=>'DUP','sku'=>'BOX','weight'=>3])->execute());
    $indexRows = $session->sql('SHOW INDEX FROM `'.$config['schema'].'`.`'.$name.'`')->execute()->fetchAll();
    $skuNonUnique = null;
    foreach ($indexRows as $indexRow) {
        if ($indexRow['Key_name'] === 'unique_sku') {$skuNonUnique = (int)$indexRow['Non_unique'];}
    }
    $bag = $collection->getOne('BAG');
    $empty = $collection->getOne('EMPTY') ?? null;
    $duplicate = $collection->getOne('DUP') ?? null;
    $rows = [['Optional сохранил документ без weight',!array_key_exists('weight',$optionalRead)],
        ['Required отклонён на старых неполных данных',$missingAtBuild !== null],
        ['Вес после исправления исходного документа',$bag['weight']],
        ['Новая запись без weight отклонена',$missingAtInsert !== null],
        ['Ошибочный JSON отклонён',$badJson !== null],['Неизвестная опция отклонена',$unknownField !== null],
        ['Неизвестный тип отклонён',$badType !== null],['Вызов с unique:true принят',$uniqueCreated === true],
        ['SHOW INDEX: Non_unique для unique_sku',$skuNonUnique],
        ['Повторный артикул с новым _id записался',$duplicateSku === null && $duplicate !== null],
        ['Сохранённых документов',$collection->count()]];
    $checks = ['optional_missing'=>!array_key_exists('weight',$optionalRead),
        'required_existing_missing_failed'=>$missingAtBuild !== null,
        'repaired_document'=>$bag['weight'] === 1,'required_new_missing_failed'=>$missingAtInsert !== null && $empty === null,
        'malformed_json_failed'=>$badJson !== null,'unknown_field_failed'=>$unknownField !== null,'unknown_type_failed'=>$badType !== null,
        'index_request_accepted'=>$uniqueCreated === true,
        'unique_flag_not_enforced'=>$skuNonUnique === 1,
        'duplicate_value_persisted'=>$duplicateSku === null && $duplicate !== null && $duplicate['sku'] === 'BOX' && $duplicate['weight'] === 3,
        'expected_document_count'=>$collection->count() === 3];
    $observations = ['optional_document'=>$optionalRead,'repaired_document'=>$bag,'absent_failed_insert'=>$empty,
        'missing_at_build'=>$missingAtBuild,'missing_at_insert'=>$missingAtInsert,
        'bad_json'=>$badJson,'unknown_option'=>$unknownField,'bad_type'=>$badType,
        'index_return_type'=>get_debug_type($uniqueCreated),'indexes'=>$indexRows,
        'duplicate_sku_exception'=>$duplicateSku,'duplicate_readback'=>$duplicate,
        'documents'=>$collection->find()->sort('_id ASC')->execute()->fetchAll()];
} finally {
    try {
        if ($schema instanceof mysql_xdevapi\Schema) {
            $removed = x140Cleanup($schema, $owned);
        }
    } finally {
        $session->close();
    }
}
x140Report('mysql140-x-index-data-contract', 'Проверить отсутствующее поле и ошибочное описание индекса', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Optional сохранил документ без weight — да
Required отклонён на старых неполных данных — да
Вес после исправления исходного документа — 1
Новая запись без weight отклонена — да
Ошибочный JSON отклонён — да
Неизвестная опция отклонена — да
Неизвестный тип отклонён — да
Вызов с unique:true принят — да
SHOW INDEX: Non_unique для unique_sku — 1
Повторный артикул с новым _id записался — да
Сохранённых документов — 3

Откройте result.html после запуска: строки получены из реальных ответов MySQL. result.json сохраняет документы и цепочки отказов; metrics.json — результаты проверок и очистки.

Таблица сохранённого запуска · Полные данные результата.

Границы и ошибки

Индекс — не полная схема валидации документа. Преобразование JSON к числовому индексному типу следует правилам сервера и SQL mode: из требования INT нельзя вывести обещание, что любая строка обязательно отвергнется. Поэтому упражнение проверяет отсутствующее обязательное поле, а не выдуманный универсальный отказ на строке. JSON null и отсутствие — разные значения документа; изучайте их отдельно. В PECL 8.0.30 разбор описания не читает unique и отправляет серверу false: успешный createIndex не доказывает уникальность. После повторной вставки в этом опыте намеренно остаются BOX и DUP с одинаковым sku. Для модели «один документ на неизменный канонический артикул» можно задавать этим артикулом _id: его уникальность обеспечивается отдельно. Этот выбор требует единой нормализации и политики изменения артикула; более сложные бизнес-ключи требуют подходящей схемы и проверенного механизма ограничений. Предварительный find перед add не заменяет ограничение при конкурентных запросах. При миграции сначала исследуйте исходные данные; не ставьте вес 0 неизвестным товарам ради успешного CREATE.

Попробуйте изменить

Как безопасно добавить required для каталога, где часть товаров имеет неизвестный вес? Должен ли код автоматически заменить его нулём?

Ожидаемое решение

Сначала получить список таких товаров, уточнить вес или явно исключить их из соответствующего процесса, затем повторить создание. Ноль — реальное значение веса, а не синоним неизвестности. Пример намеренно исправляет только BAG на известное 1 и повторно читает сохранённый документ.

Выбрать функцию или метод по задаче

APIДля чего
expressionОбозначить строку как выражение, а не как буквальное значение.
ExpressionОбёртка выражения для вычисляемых полей и изменений.
Expression::__constructСоздать обёртку фиксированного выражения.
CollectionFind::groupByПолучить одну строку на категорию или пару категория+склад.
CollectionFind::havingОтобрать группы по подсчитанной сумме.
Collection::createIndexСоздать индекс по свойствам документов с явными путями и типами.
Collection::dropIndexУдалить выбранный индекс, сохранив документы.

От сводки к согласованной выдаче товара

Отчёт отвечает, что было видно при чтении. Он не резервирует последнюю лампу. Чтобы два сотрудника не выдали одну единицу дважды, следующий шаг — два сеанса, блокировки и частичный откат. Для принципов выбора индекса — индексы под запрос.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

mysql_xdevapi\expressionОбозначить строку как выражение, а не как буквальное значение.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

mysql_xdevapi\expression(string $expression): object

Expression помечает строку как выражение X DevAPI. Она может описывать вычисление над полями или документную проекцию. Создание обёртки само не исполняет запрос: сервер получает действие при execute у find/modify. В рабочем сценарии урока оба способа создания обёртки применены к понятному изменению остатка; вычисляемая выдача отдельно показана через fields со списком полей.

Параметр $expression — строка; функция возвращает объект Expression. В PHP Manual краткое описание ошибочно связывает функцию с привязкой параметров, а основной текст не заполнен. В этих примерах роли разделены: expression помечает выражение, bind передаёт значения в его placeholders.

Из формулы в вычисление
  1. Автор задаёт stock + :extra
  2. Expression хранит текст выражения, не число 6
  3. modify задаёт фильтр; set добавляет формулу с :extra
  4. bind задаёт id LAMP и extra 2
  5. execute вычисляет 4 + 2 на сервере
  6. getOne читает stock 6
Вход или границаЗначение
expressionСтрока синтаксиса X DevAPI. Не произвольный PHP-код и не готовый SQL SELECT.
ОшибкаНеподходящий тип аргумента либо синтаксис может вызвать отказ при создании/использовании/execute. Объект не сертификат корректности всей операции.
Литералset("note", "price * stock") записывает текст; обёртка означает вычисление.
ПараметрЧисло extra передаётся bind; выбор разрешённого поля/формулы задаётся кодом приложения.

Два практических применения

Показать стоимость запаса в ответе, не сохраняя дублирующее поле. В этой проекции fields уже разбирает выражение из строки; создавать объект Expression для неё не требуется.

$row = $collection->find('_id = :id')->bind(['id'=>'LAMP'])
    ->fields(['sku','price * stock AS inventory'])->execute()->fetchOne();

Полные исходные данные, программа и её результат.

Записать поступление товара по фиксированной формуле и отдельно прочитать сохранённый остаток 6. Сначала set добавляет параметр extra к операции, затем bind передаёт его значение. Полная программа дополнительно использует функцию expression для следующего поступления 3 ламп и читает остаток 9.

$formula = new mysql_xdevapi\Expression('stock + :extra');
// Равнозначное создание обёртки: mysql_xdevapi\expression('stock + :extra')
$collection->modify('_id = :id')->set('stock',$formula)
    ->bind(['id'=>'LAMP','extra'=>2])->execute();

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Версионная граница: официальный пример разрешает документную проекцию через new Expression с JSON-подобной формой. Однако диагностика PHP 8.3.35/mysql_xdevapi 8.0.30/MySQL 8.4.13 для {"sku": sku, "inventory": price * stock} вернула вложенный ответ {"stock":{"sku":"BOX","inventory":4800}}, а не ожидаемые верхние ключи sku/inventory. Урок использует подтверждённую плоскую проекцию fields([...]). Не следует наугад брать первый элемент массива или считать ключ stock универсальным контрактом; перенос объектной проекции на другой профиль требует отдельной проверки формы ответа.

Обёртка не делает пользовательскую строку безопасной. Не собирайте код формулы из HTTP-поля; оставляйте структуру известной, значения передавайте через bind. Для обычной строки set обёртка не нужна. Возвращённую проекцию и сохранённый документ различайте явно.

mysql_xdevapi\ExpressionОбёртка выражения для вычисляемых полей и изменений.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

class mysql_xdevapi\Expression

Expression помечает строку как выражение X DevAPI. Она может описывать вычисление над полями или документную проекцию. Создание обёртки само не исполняет запрос: сервер получает действие при execute у find/modify. В рабочем сценарии урока оба способа создания обёртки применены к понятному изменению остатка; вычисляемая выдача отдельно показана через fields со списком полей.

class mysql_xdevapi\Expression {
    public $name;
    public function __construct(string $expression);
}

Класс доступен с PECL mysql_xdevapi 8.0.11. Публичное свойство name присутствует в официальном обзоре, но его смысл там не раскрыт. В исходнике 8.0.30 оно объявлено как null для Reflection API, а текст выражения хранится отдельно во внутренних данных объекта. В result.json показано реальное чтение свойства; изменение name не является способом переписать выражение. Создайте новую обёртку для нового фиксированного выражения.

Из формулы в вычисление
  1. Автор задаёт stock + :extra
  2. Expression хранит текст выражения, не число 6
  3. modify задаёт фильтр; set добавляет формулу с :extra
  4. bind задаёт id LAMP и extra 2
  5. execute вычисляет 4 + 2 на сервере
  6. getOne читает stock 6
Вход или границаЗначение
expressionСтрока синтаксиса X DevAPI. Не произвольный PHP-код и не готовый SQL SELECT.
ОшибкаНеподходящий тип аргумента либо синтаксис может вызвать отказ при создании/использовании/execute. Объект не сертификат корректности всей операции.
Литералset("note", "price * stock") записывает текст; обёртка означает вычисление.
ПараметрЧисло extra передаётся bind; выбор разрешённого поля/формулы задаётся кодом приложения.

Два практических применения

Показать стоимость запаса в ответе, не сохраняя дублирующее поле. В этой проекции fields уже разбирает выражение из строки; создавать объект Expression для неё не требуется.

$row = $collection->find('_id = :id')->bind(['id'=>'LAMP'])
    ->fields(['sku','price * stock AS inventory'])->execute()->fetchOne();

Полные исходные данные, программа и её результат.

Записать поступление товара по фиксированной формуле и отдельно прочитать сохранённый остаток 6. Сначала set добавляет параметр extra к операции, затем bind передаёт его значение. Полная программа дополнительно использует функцию expression для следующего поступления 3 ламп и читает остаток 9.

$formula = new mysql_xdevapi\Expression('stock + :extra');
// Равнозначное создание обёртки: mysql_xdevapi\expression('stock + :extra')
$collection->modify('_id = :id')->set('stock',$formula)
    ->bind(['id'=>'LAMP','extra'=>2])->execute();

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Версионная граница: официальный пример разрешает документную проекцию через new Expression с JSON-подобной формой. Однако диагностика PHP 8.3.35/mysql_xdevapi 8.0.30/MySQL 8.4.13 для {"sku": sku, "inventory": price * stock} вернула вложенный ответ {"stock":{"sku":"BOX","inventory":4800}}, а не ожидаемые верхние ключи sku/inventory. Урок использует подтверждённую плоскую проекцию fields([...]). Не следует наугад брать первый элемент массива или считать ключ stock универсальным контрактом; перенос объектной проекции на другой профиль требует отдельной проверки формы ответа.

Обёртка не делает пользовательскую строку безопасной. Не собирайте код формулы из HTTP-поля; оставляйте структуру известной, значения передавайте через bind. Для обычной строки set обёртка не нужна. Возвращённую проекцию и сохранённый документ различайте явно.

mysql_xdevapi\Expression::__constructСоздать обёртку фиксированного выражения.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

public mysql_xdevapi\Expression::__construct(string $expression)

Expression помечает строку как выражение X DevAPI. Она может описывать вычисление над полями или документную проекцию. Создание обёртки само не исполняет запрос: сервер получает действие при execute у find/modify. В рабочем сценарии урока оба способа создания обёртки применены к понятному изменению остатка; вычисляемая выдача отдельно показана через fields со списком полей.

new выделяет объект, __construct получает строку $expression и задаёт его выражение. Конструктор публичный, в отличие от многих объектов соединения этого расширения. Вызов new возвращает созданный Expression; у самого конструктора нет отдельного прикладного возвращаемого значения.

Из формулы в вычисление
  1. Автор задаёт stock + :extra
  2. Expression хранит текст выражения, не число 6
  3. modify задаёт фильтр; set добавляет формулу с :extra
  4. bind задаёт id LAMP и extra 2
  5. execute вычисляет 4 + 2 на сервере
  6. getOne читает stock 6
Вход или границаЗначение
expressionСтрока синтаксиса X DevAPI. Не произвольный PHP-код и не готовый SQL SELECT.
ОшибкаНеподходящий тип аргумента либо синтаксис может вызвать отказ при создании/использовании/execute. Объект не сертификат корректности всей операции.
Литералset("note", "price * stock") записывает текст; обёртка означает вычисление.
ПараметрЧисло extra передаётся bind; выбор разрешённого поля/формулы задаётся кодом приложения.

Два практических применения

Показать стоимость запаса в ответе, не сохраняя дублирующее поле. В этой проекции fields уже разбирает выражение из строки; создавать объект Expression для неё не требуется.

$row = $collection->find('_id = :id')->bind(['id'=>'LAMP'])
    ->fields(['sku','price * stock AS inventory'])->execute()->fetchOne();

Полные исходные данные, программа и её результат.

Записать поступление товара по фиксированной формуле и отдельно прочитать сохранённый остаток 6. Сначала set добавляет параметр extra к операции, затем bind передаёт его значение. Полная программа дополнительно использует функцию expression для следующего поступления 3 ламп и читает остаток 9.

$formula = new mysql_xdevapi\Expression('stock + :extra');
// Равнозначное создание обёртки: mysql_xdevapi\expression('stock + :extra')
$collection->modify('_id = :id')->set('stock',$formula)
    ->bind(['id'=>'LAMP','extra'=>2])->execute();

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Версионная граница: официальный пример разрешает документную проекцию через new Expression с JSON-подобной формой. Однако диагностика PHP 8.3.35/mysql_xdevapi 8.0.30/MySQL 8.4.13 для {"sku": sku, "inventory": price * stock} вернула вложенный ответ {"stock":{"sku":"BOX","inventory":4800}}, а не ожидаемые верхние ключи sku/inventory. Урок использует подтверждённую плоскую проекцию fields([...]). Не следует наугад брать первый элемент массива или считать ключ stock универсальным контрактом; перенос объектной проекции на другой профиль требует отдельной проверки формы ответа.

Обёртка не делает пользовательскую строку безопасной. Не собирайте код формулы из HTTP-поля; оставляйте структуру известной, значения передавайте через bind. Для обычной строки set обёртка не нужна. Возвращённую проекцию и сохранённый документ различайте явно.

mysql_xdevapi\CollectionFind::groupByПолучить одну строку на категорию или пару категория+склад.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

CollectionFind::groupBy(string $sort_expr): mysql_xdevapi\CollectionFind

groupBy объединяет документы с одинаковыми значениями выбранных полей, чтобы получить одну строку сводки на группу.

ПараметрКак использовать
sort_exprКлюч группировки: строка или массив строк для нескольких ключей. Краткая сигнатура Manual показывает string; описание дополнительно разрешает массив.
ВозвратCollectionFind для продолжения настройки. Данные возвращает последующее execute и чтение DocResult.
Стадии одной ведомости
  1. find: активные документы
  2. groupBy: одинаковая category
  3. fields: category, SUM(price * stock) AS amount
  4. having: сумма не меньше 5000
  5. sort/limit: порядок и объём итоговых групп → execute → чтение

COUNT(*) считает документы, SUM(stock) считает штуки. В fields остаются ключи группы и осмысленные агрегаты; sku конкретной карточки не определён для группы lighting из двух SKU. Не отключайте ONLY_FULL_GROUP_BY ради случайного значения. Группировка сама не задаёт порядок — используйте sort.

Два практических применения

Ведомость двух категорий: cable 10 и lighting 6.

$rows = $collection->find('active = true')->fields(['category','SUM(stock) AS units'])
    ->groupBy('category')->sort('category ASC')->execute()->fetchAll();

Полные исходные данные, программа и её результат.

Разделить ту же категорию по складам: отдельные lighting+annex и lighting+central.

$rows = $collection->find('active = true')->fields(['category','warehouse','SUM(stock) AS units'])
    ->groupBy(['category','warehouse'])->sort('category ASC','warehouse ASC')->execute()->fetchAll();

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Общий принцип работы запроса: сначала ограничить участвующие данные, затем агрегировать и передать PHP именно нужный результат. CollectionFind::having дополняет этот шаг. При выборе производительности учитывайте индексы и план: маленькая выдача не доказывает маленькое чтение на сервере.

mysql_xdevapi\CollectionFind::havingОтобрать группы по подсчитанной сумме.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

CollectionFind::having(string $sort_expr): mysql_xdevapi\CollectionFind

having оставляет группы по условию над уже подсчитанными значениями.

ПараметрКак использовать
sort_exprУ having — одна строка условия, допустимы агрегаты. Название sort_expr в сигнатуре не означает сортировку.
ВозвратCollectionFind для продолжения настройки. Данные возвращает последующее execute и чтение DocResult.
Стадии одной ведомости
  1. find: активные документы
  2. groupBy: одинаковая category
  3. fields: category, SUM(price * stock) AS amount
  4. having: сумма не меньше 5000
  5. sort/limit: порядок и объём итоговых групп → execute → чтение

Сумма относится к группе. Порог bind задаёт minimum 5000 сравнивается с 7800 для lighting, а не с каждой ценой отдельно. Агрегат — операция над набором значений: SUM складывает, COUNT считает, MIN/MAX выбирают границу. Без подходящих групп получится пустой список; отказ запроса не равен пустому ответу.

having не требует обязательного groupBy: официальный пример применяет его после fields к проекции отдельных документов. Здесь он нужен именно для агрегатной ведомости. Для обычного отбора по исходному price предпочтительнее понятный find(price > :minimum); не переносите фильтр после проекции без причины.

Два практических применения

Отобрать категории, где на складе лежит не меньше 5000 рублей.

$q = $collection->find('active = true')->fields(['category','SUM(price * stock) AS amount'])
    ->groupBy('category')->having('SUM(price * stock) >= :minimum')->bind(['minimum'=>5000]);
$groups = $q->execute()->fetchAll();

Полные исходные данные, программа и её результат.

Корректно показать пустую ведомость при пороге 10000, не объявляя её ошибкой подключения.

$groups = $collection->find('active = true')->fields(['category','SUM(price * stock) AS amount'])
    ->groupBy('category')->having('SUM(price * stock) >= :minimum')->bind(['minimum'=>10000])
    ->execute()->fetchAll();

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Общий принцип работы запроса: сначала ограничить участвующие данные, затем агрегировать и передать PHP именно нужный результат. CollectionFind::groupBy дополняет этот шаг. При выборе производительности учитывайте индексы и план: маленькая выдача не доказывает маленькое чтение на сервере.

mysql_xdevapi\Collection::createIndexСоздать индекс по свойствам документов с явными путями и типами.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

Collection::createIndex(string $index_name, string $index_desc_json): void

Индекс — отдельная структура доступа к значениям свойств. createIndex принимает имя и JSON-описание и сразу создаёт её в коллекции. Документы не превращаются в PHP-массив и не выгружаются для сортировки. Индекс подбирают к конкретным фильтрам, порядку и объёму данных.

АргументКонтракт
index_nameИмя, допустимое для SQL CREATE INDEX; уникально в этой коллекции. Имя из миграции, а не из свободного поля формы.
index_desc_jsonСтрока JSON с непустым массивом fields; каждый элемент описывает одно индексируемое свойство. Верхний type: INDEX по умолчанию либо SPATIAL.
Возврат/ошибкаPHP Manual указывает void, но в профиле mysql_xdevapi 8.0.30/MySQL 8.4.13 фактически возвращается bool true при успехе. Это наблюдение конкретной реализации, а не обещание для всех версий. Повтор имени, неправильное описание, несовместимые данные или права вызывают отказ; вызов не требует execute.
Поле описанияЧто задаёт
fieldПолный путь к свойству, например $.category или $.shipping.weight.
typeТип индексируемого представления: целые/вещественные SQL-типы, DATE/TIME/TIMESTAMP/DATETIME, TEXT(length), GEOJSON. Для числового типа возможен UNSIGNED. Это не тип всей JSON-коллекции.
requiredЛогическое значение; по умолчанию false, для GEOJSON true. Влияет на допустимость отсутствующего/NULL индексируемого значения, включая старые данные при построении.
optionsЦелое: параметры разбора GEOJSON, не произвольные флаги индекса.
sridЦелое: система координат при преобразовании GEOJSON. Не номер масштаба и не способ превратить неверные координаты в верные.

Для чисел используйте INT/TINYINT/SMALLINT/MEDIUMINT/INTEGER/BIGINT, REAL/FLOAT/DOUBLE, DECIMAL/NUMERIC с допустимыми для сервера уточнениями. TEXT(40) задаёт длину индексируемого текста; не копируйте огромную длину без учёта ограничения ключа. JSON преобразуется к выбранному SQL-типу: наличие числового индекса не заменяет проверку входных данных.

Два практических применения

Подготовить составной доступ: сначала точная категория, затем диапазон цены.

$collection->createIndex('category_price', json_encode(['fields'=>[
    ['field'=>'$.category','type'=>'TEXT(40)','required'=>true],
    ['field'=>'$.price','type'=>'INT UNSIGNED','required'=>true]
]], JSON_THROW_ON_ERROR));

Полные исходные данные, программа и её результат.

Построить индекс веса после явного исправления документа BAG; увидеть отказ, если старые данные неполны.

$collection->createIndex('weight_required',
    '{"fields":[{"field":"$.weight","type":"INT","required":true}]}');

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Пространственные данные и полный состав описания

Для точек доставки верхний type выбирают SPATIAL, поле home описывают как GEOJSON. Это отдельный прикладной случай: координаты GeoJSON имеют свой порядок и систему отсчёта. Фрагмент ниже показывает все соответствующие поля; он не объявляется выполненной географической программой этого пакета.

$description = '{"type":"SPATIAL","fields":[{"field":"$.home",
    "type":"GEOJSON","required":true,"options":1,"srid":4326}]}';

options передаётся разбору ST_GeomFromGeoJSON:1 отвергает координаты с более чем двумя измерениями;2,3,4 в текущей документации допускают их с удалением дополнительных измерений. srid4326 выбирает соответствующую географическую систему. Неподходящие геометрия, координаты или SRID вызывают ошибку; не подменяйте неизвестную точку координатами 0,0. Подробности преобразования и поведения без явного srid смотрите в MySQL 8.4: ST_GeomFromGeoJSON.

PHP Manual запрещает лишние поля IndexDefinition/IndexField; придерживайтесь указанного формата. Реализация 8.0.30 строго проверяет неизвестные свойства внутри fields, но не проверяет все лишние поля верхнего уровня. На странице PHP Manual примеры содержат unique, хотя перечень параметров его не объясняет. В этом модуле разбор верхнего уровня читает type и fields, а unique оставляет false: переданное unique:true принимается, но не включает уникальность. Полная программа сохраняет SHOW INDEX с Non_unique 1 и реально добавляет новый _id с прежним sku BOX; повтор читается обратно. Успех вызова и имя unique_sku не доказывают защиту бизнес-ключа.

Если модель требует ровно один документ на неизменный канонический артикул, можно использовать этот артикул как _id и опираться на уникальность идентификатора. Нормализация и правило переименования должны быть определены заранее; это не универсальная замена произвольному составному уникальному ключу. Явный идентификатор и отличие add от обновления разобраны в создании и чтении документа. Проверка find перед add сама по себе не защищает от параллельной вставки.

Индекс создаётся административным этапом, не при каждом показе страницы. DDL может неявно фиксировать транзакцию. Ошибочный JSON и повтор имени разделены в программах; Фактическая цепочка исключений сохранена в JSON. После ненужного индекса используйте Collection::dropIndex. Общая модель и стоимость — выбор индекса под запрос.

Краткая сигнатура void не полностью описывает реализацию 8.0.30: исходник createIndex передаёт в возвращаемое значение результат внутреннего create_collection_index. Программа сохраняет get_debug_type результата: в лаборатории это bool; значение true проверяется строго, без приведения. Создание индекса подтверждается отказом при повторе имени и последующим удалением. Его конкретные свойства проверяются отдельно: метаданные и реальная повторная запись показывают, что unique здесь не обеспечен.

mysql_xdevapi\Collection::dropIndexУдалить выбранный индекс, сохранив документы.

Expression: PECL 8.0.11+; проверяемый профиль 8.0.30; отдельные страницы не указывают первую версию · mysql_xdevapi

Collection::dropIndex(string $index_name): bool

Когда изменились запросы, старый индекс может стать лишним. dropIndex удаляет индекс по имени в данной коллекции; документы остаются. Удалять индекс только ради освобождения места без проверки рабочих запросов опасно для времени ответа.

ЭлементЗначение
index_nameТочное имя ранее созданного индекса. Не имя свойства и не название коллекции.
Возвратtrue при успешном удалении; false, если индекс отсутствует. В проверенном модуле 8.0.30 отсутствие также выдаёт PHP-предупреждение E_WARNING [1091][42000]; это не исключение, но и не бесшумный false.
ОтказПроблемы соединения/прав/сервера не следует приравнивать к false «индекса нет». Публичный ответ приложения не должен раскрывать параметры подключения.

Полная программа намеренно удаляет category_price дважды. Только второй вызов окружён временным set_error_handler: обработчик принимает лишь ожидаемое E_WARNING от Collection::dropIndex с кодом 1091 и именем category_price, сохраняет его текст в наблюдениях, затем finally восстанавливает прежний обработчик. Для любого другого предупреждения возвращается false, чтобы сохранить обычную обработку PHP. Это точечный разбор ожидаемого результата, а не сокрытие ошибок через @.

Два практических применения

Завершить эксперимент с составным индексом и проверить число документов.

$removed = $collection->dropIndex('category_price');
// true: удалён только этот индекс; документы сохранены.

Полные исходные данные, программа и её результат.

Заменить прежний optional-доступ на обязательный после подготовленной миграции данных.

$collection->dropIndex('weight_optional');
// Затем отдельно: исправить BAG и создать weight_required.

Полные исходные данные, программа и её результат.

Короткие записи показывают вызов в контексте уже полученной коллекции. Самостоятельный архив по ссылке содержит подключение, данные, проверки и очистку; фрагменты не считаются отдельными выполненными программами.

Повторное dropIndex(category_price) в программе возвращает false, а count остаётся 4. Между drop и новым create нет автоматической атомарной замены: при отказе создания прежнего индекса уже нет. Планируйте миграцию и восстановление определения отдельно; не удаляйте все индексы или коллекцию в обработчике ошибки. Collection::createIndex описывает формат восстановления.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.