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

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

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

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

Глава 177 · После запросов MongoDB и знакомства с локальным key vault

MongoDB: явное шифрование полей заказа и поиск точных совпадений

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

Два представления одного заказа

Сотрудник склада видит заказ D-701 со статусом «К выдаче». На экране курьера рядом появляется инструкция «Окно учебной выдачи № 2». Обе страницы берут один документ из одной коллекции. Откройте два представления первого заказа, чтобы сравнить результат.

Что находится у каждой стороны
  1. В PHP есть учебный текст инструкции
  2. Клиент шифрует один текст → BSON Binary с подтипом 6
  3. MongoDB хранит открытый номер D-701 рядом с бинарным полем
  4. Клиент читает Binary, при наличии нужных ключей выполняет decrypt → на экране снова текст

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

$instruction = 'Окно учебной выдачи № 2';
echo $instruction;

Здесь $instruction — переменная для текста. Оператор = присваивает ей строку между кавычками, echo выводит строку. Если сохранить её напрямую, обычный клиент прочитает те же символы. Поэтому перед вставкой в закрытое поле заменим это значение результатом следующего вызова.

$options = [
    'keyId' => $keyId,
    'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM,
];
$sealed = $crypto->encrypt($instruction, $options);

Массив $options поясняет, каким ключом и алгоритмом обработать значение. $crypto — объект драйвера, уже связанный с хранилищем ключей. Первый аргумент в круглых скобках — сам текст. Стрелка -> вызывает метод. В $sealed окажется двоичное значение для записи в MongoDB. Как возникают $crypto и $keyId, показывает глава о хранилище ключей.

Какие поля открывать, по каким искать

Требование экранаКак представлены данныеРезультат запроса
Показать номер и статус без расшифровкиОставляем согласованные публичные поля обычными значениямиИх можно выбрать и упорядочить на сервере. Они доступны читателю коллекции.
Показать примечание только после выбора заказаСлучайный алгоритм для поля noteИщем по номеру; после проверки прав расшифровываем только выбранную инструкцию.
Найти точный внешний кодДетерминированное шифрование на записи и при создании значения фильтраВозможны совпадения по равенству. Одинаковые значения дают наблюдаемые повторы.
Сохранить несколько вложенных полей как один закрытый блокВесь BSON-документ зашифрован случайноБаза не читает внутренний путь; приложение получает его после decrypt.

Эти примеры относятся к классическому явному Client-Side Field Level Encryption, сокращённо CSFLE: приложение явно вызывает encrypt и decrypt. Для Queryable Encryption существуют отдельные правила настройки коллекции, индексов и построения запросов. Метод encryptExpression относится к другой задаче и отдельно в этих опытах не применяется.

Где выполнить и увидеть результат

Понадобятся PHP с поддержкой libmongocrypt в расширении mongodb, библиотека Composer и отдельный локальный сервер. Установленная среда — PHP 8.5.11 x64, ext-mongodb 2.5.5, mongodb/mongodb 2.5.0, MongoDB 8.0.34. Подключение и создание клиента объяснены в предыдущей главе. В ZIP каждого примера есть собственная инструкция README.txt.

composer require mongodb/mongodb:2.5.0
$env:PHPMAN_MONGODB_URI='mongodb://127.0.0.1:27075/?directConnection=true&retryWrites=false'
$env:PHPMAN_MONGODB_DATABASE='php_manual_lab'
php example.php

Запускайте программу из её отдельной папки. PHP создаст новую пару коллекций — одну для учебных заказов, вторую для ключей. Локальный мастер-ключ из 96 случайных байтов остаётся в памяти на время опыта; новый ключ данных (DEK) в key vault хранится завёрнутым. Исходные данные вымышлены. В конце программа удаляет обе свои коллекции и подтверждает отсутствие каждой. В result.html открывается экран, result.json содержит его табличные значения, metrics.json — проверки.

После закрытия процесса этот одноразовый мастер-ключ не предназначен для повторного открытия базы. Для долговременных рабочих данных сохранность и управление мастер-ключом и key vault являются отдельной задачей. Пример показывает локальную механику, а не организацию промышленного KMS или восстановления.

Одна запись для склада и для курьера

Склад отмечает, что заказ можно забирать. Где именно его выдают, знает приложение курьера. Откройте результат, чтобы увидеть два представления одной и той же записи.

Исходные данные

До вставки у PHP есть текст инструкции, объект ClientEncryption и сохранённый в key vault ключ данных. Метод encrypt() возвращает объект Binary. В документ заказов помещаем этот объект, а номер и состояние оставляем обычными строками.

Путь значения
  1. PHP шифрует инструкцию → получает Binary 6
  2. Обычная коллекция сохраняет D-701 и поле note
  3. Склад читает номер и состояние → клиент курьера расшифровывает только note

Минимальный вызов

$protected = $crypto->encrypt('Окно учебной выдачи № 2', [
    'keyId' => $keyId,
    'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM,
]);

Результат запуска

Заказ D-701 · Окно учебной выдачи № 2

Открыть фактический экран · Строки таблицы в JSON

ПредставлениеЗаказСостояниеИнструкция
СкладD-701К выдачеBinary, подтип 6
КурьерD-701К выдачеОкно учебной выдачи № 2

Почему получилось именно так

Оба представления построены из результата реального findOne. Склад видит незашифрованные поля и обозначение типа скрытого поля. Клиенту, который может получить и раскрыть подходящий ключ данных, decrypt() вернул исходный текст.

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

Эта схема разделяет представления. Кто из сотрудников вправе расшифровывать запись, должно проверять приложение и управление доступом к ключам. Само название «Курьер» доступ не выдаёт. Номер, состояние, имена полей и наличие записи остаются видимыми.

Полная программа и архив для запуска
Одна запись для склада и для курьераСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-stored-views', 'Одна запись для склада и для курьера', ['Представление', 'Заказ', 'Состояние', 'Инструкция'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        $note = 'Окно учебной выдачи № 2';
        $sealed = randomField($crypto, $keyId, $note);
        $receipt = $orders->insertOne(['_id' => 'D-701', 'status' => 'К выдаче', 'note' => $sealed]);
        $stored = $orders->findOne(['_id' => 'D-701']);
        if (!isSealed($stored['note'])) {
            throw new UnexpectedValueException('Expected a sealed field');
        }
        $storageLabel = 'Binary, подтип '.$stored['note']->getType();
        $instruction = $crypto->decrypt($stored['note']);
        $checks['one_insert'] = $receipt->getInsertedCount() === 1;
        $checks['raw_read_binary'] = $stored['note'] instanceof Binary;
        $checks['stored_subtype_six'] = $stored['note']->getType() === 6;
        $checks['status_readable'] = $stored['status'] === 'К выдаче';
        $checks['note_restored'] = $instruction === $note;
        $rows = [['Склад', $stored['_id'], $stored['status'], $storageLabel],
                 ['Курьер', $stored['_id'], $stored['status'], $instruction]];
        return [$rows, $checks, 'Заказ '.$stored['_id'].' · '.$instruction];
    }
);
Ожидаемый результат
Заказ D-701 · Окно учебной выдачи № 2

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

Измените учебную инструкцию перед encrypt, выполните программу заново и сравните обе панели. На складе останется подтип 6, в представлении курьера изменится текст.

Одинаковая инструкция в двух разных заказах

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

Исходные данные

В обоих вызовах используются одинаковые текст, ключ данных и алгоритм Random. В коллекцию пишутся два результата, затем оба заново читаются по разным _id.

Путь значения
  1. Одна фраза → два случайных шифрования
  2. Читаем два Binary → их полные значения различаются
  3. Расшифровываем оба с правильным ключом → одинаковая инструкция

Минимальный вызов

$options = ['keyId' => $keyId, 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM];
$first = $crypto->encrypt($note, $options);
$second = $crypto->encrypt($note, $options);

Результат запуска

Два заказа с одной инструкцией · Учебная рампа Б

Открыть фактический экран · Строки таблицы в JSON

НаблюдениеРезультат
Совпали сохранённые Binaryfalse
Совпали расшифрованные инструкцииtrue
Найдено по новому случайному значению0

Почему получилось именно так

Случайность входит в процесс шифрования. Даже третье шифрование правильной фразы дало значение, по которому обычный фильтр равенства не нашёл эти записи. Выбирать документы здесь следует по доступным поисковым полям, например номеру.

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

Сравнение выполнено внутри PHP, бинарные значения не выводятся. Разный шифротекст не скрывает размер коллекции, открытые поля и другие наблюдаемые признаки. Этот опыт не измеряет стойкость криптографии.

Полная программа и архив для запуска
Одинаковая инструкция в двух разных заказахСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-random-notes', 'Одинаковая инструкция в двух разных заказах', ['Наблюдение', 'Результат'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        $note = 'Учебная рампа Б';
        $first = randomField($crypto, $keyId, $note);
        $second = randomField($crypto, $keyId, $note);
        $orders->insertMany([['_id' => 'D-711', 'note' => $first], ['_id' => 'D-712', 'note' => $second]]);
        $a = $orders->findOne(['_id' => 'D-711'])['note'];
        $b = $orders->findOne(['_id' => 'D-712'])['note'];
        $sameCipher = $a == $b;
        $plainA = $crypto->decrypt($a);
        $plainB = $crypto->decrypt($b);
        $another = randomField($crypto, $keyId, $note);
        $matches = $orders->countDocuments(['note' => $another]);
        $checks['two_stored'] = $orders->countDocuments([]) === 2;
        $checks['both_sealed'] = isSealed($a) && isSealed($b);
        $checks['ciphertexts_differ'] = !$sameCipher;
        $checks['both_restore_note'] = $plainA === $note && $plainB === $note;
        $checks['fresh_token_does_not_match'] = $matches === 0;
        return [[['Совпали сохранённые Binary', $sameCipher], ['Совпали расшифрованные инструкции', $plainA === $plainB],
                 ['Найдено по новому случайному значению', $matches]], $checks, 'Два заказа с одной инструкцией · '.$plainA];
    }
);
Ожидаемый результат
Два заказа с одной инструкцией · Учебная рампа Б

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

Вместо совпадающих фраз передайте разные. Сравнение Binary по-прежнему должно показать разные значения. В отличие от первого опыта изменится и результат сравнения расшифрованного текста.

Найти заказы по точному внешнему коду

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

Исходные данные

Значения BOX-1273 и BOX-3008 — вымышленные коды. При записи каждый код зашифрован с одним DEK детерминированным алгоритмом. Используется обычная коллекция без автоматического шифрования запросов.

Путь значения
  1. Сохраняем три документа → два из них имеют равные закрытые коды
  2. Фильтр со строкой BOX-1273 → 0
  3. Шифруем аргумент тем же ключом и алгоритмом → D-721 и D-723

Минимальный вызов

$needle = $crypto->encrypt($wanted, [
    'keyId' => $keyId,
    'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC,
]);
$cursor = $orders->find(['externalCode' => $needle]);

Результат запуска

Для учебного кода найдены D-721, D-723

Открыть фактический экран · Строки таблицы в JSON

Условие поискаСовпаденийНомера
Обычная строка0—
Предшифрованное значение2D-721, D-723

Почему получилось именно так

Серверу передаётся сравнимое бинарное значение, исходная строка поискового аргумента в фильтр базы не попадает. Поэтому в этом примере нет перебора всех заказов и расшифровки коллекции в PHP.

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

Детерминированное представление раскрывает повторы и частоты. При малом числе возможных исходных значений это особенно чувствительно. Здесь продемонстрировано равенство. Сортировка бинарных данных не упорядочит исходные слова, поиск подстроки и диапазон этим приёмом не решаются.

Полная программа и архив для запуска
Найти заказы по точному внешнему кодуСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-exact-match', 'Найти заказы по точному внешнему коду', ['Условие поиска', 'Совпадений', 'Номера'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        foreach (['D-721' => 'BOX-1273', 'D-722' => 'BOX-3008', 'D-723' => 'BOX-1273'] as $number => $code) {
            $orders->insertOne(['_id' => $number, 'externalCode' => matchField($crypto, $keyId, $code)]);
        }
        $wanted = 'BOX-1273';
        $plainHits = $orders->countDocuments(['externalCode' => $wanted]);
        $needle = matchField($crypto, $keyId, $wanted);
        $found = $orders->find(['externalCode' => $needle], ['projection' => ['_id' => 1, 'externalCode' => 1],
            'sort' => ['_id' => 1], 'limit' => 10])->toArray();
        $ids = array_column($found, '_id');
        $checks['plain_hits_zero'] = $plainHits === 0;
        $checks['two_exact_matches'] = $ids === ['D-721','D-723'];
        $checks['repeat_cipher_equal'] = $found[0]['externalCode'] == $found[1]['externalCode'];
        $checks['needle_matches_stored'] = $needle == $found[0]['externalCode'];
        $checks['value_confirmed'] = $crypto->decrypt($found[0]['externalCode']) === $wanted;
        return [[['Обычная строка', $plainHits, '—'], ['Предшифрованное значение', count($ids), implode(', ', $ids)]], $checks,
            'Для учебного кода найдены '.implode(', ', $ids)];
    }
);
Ожидаемый результат
Для учебного кода найдены D-721, D-723

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

Укажите BOX-3008 в роли искомого значения. При совпадающих ключе и типе должна остаться одна строка D-722.

Почему $match с известной строкой возвращает пусто для зашифрованного поля? В учебной коллекции сравнить строковый фильтр и такой же по смыслу фильтр с вычисленным Binary. Вопрос разработчика на Stack Overflow.

Понять, почему одна цифра в фильтре не совпала

Импортёр записал номер ворот целым числом, а веб-форма прислала строку. Внешне оба значения выглядят как 7, но запрос может вернуть другие заказы.

Исходные данные

Три записи имеют исходные значения 7, "7" и "WING-A". Ключ, алгоритм и коллекция одни и те же. В PHP кавычки создают строку, число без кавычек имеет числовой тип.

Путь значения
  1. Зашифровать число 7 → D-731
  2. Зашифровать строку "7" → D-732
  3. Точное "WING-A" совпадает, вариант "wing-a" не совпадает

Минимальный вызов

$numeric = matchField($crypto, $keyId, 7);
$text = matchField($crypto, $keyId, '7');
$first = $orders->findOne(['dock' => $numeric]);
$second = $orders->findOne(['dock' => $text]);

Результат запуска

Целое 7 → D-731 · строка 7 → D-732

Открыть фактический экран · Строки таблицы в JSON

Искомое значениеНомерТип после decrypt
Целое 7D-731int
Строка 7D-732string
Строка wing-a——
Строка WING-AD-733string

Почему получилось именно так

Детерминированное шифрование применяется к типизированному значению BSON. PHP не приводит типы при сравнении уже зашифрованных байтов на стороне базы. Регистр и принятый формат кода также влияют на исходное значение.

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

Согласуйте тип, нормализацию и обработку ведущих нулей у всех писателей и читателей до шифрования. Если 007 и 7 — разные бизнес-коды, приведение обоих к числу уничтожит это различие. Серверная collation над бинарным представлением не нормализует текст внутри.

Полная программа и архив для запуска
Понять, почему одна цифра в фильтре не совпалаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-typed-lookup', 'Понять, почему одна цифра в фильтре не совпала', ['Искомое значение', 'Номер', 'Тип после decrypt'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        foreach (['D-731' => 7, 'D-732' => '7', 'D-733' => 'WING-A'] as $number => $code) {
            $orders->insertOne(['_id' => $number, 'dock' => matchField($crypto, $keyId, $code)]);
        }
        $rows = [];
        foreach ([['Целое 7', 7], ['Строка 7', '7'], ['Строка wing-a', 'wing-a'], ['Строка WING-A', 'WING-A']] as [$label, $value]) {
            $hit = $orders->findOne(['dock' => matchField($crypto, $keyId, $value)]);
            $rows[] = [$label, $hit['_id'] ?? '—', $hit ? get_debug_type($crypto->decrypt($hit['dock'])) : '—'];
        }
        $checks['integer_hit'] = $rows[0] === ['Целое 7','D-731','int'];
        $checks['text_hit'] = $rows[1] === ['Строка 7','D-732','string'];
        $checks['case_sensitive'] = $rows[2][1] === '—' && $rows[3][1] === 'D-733';
        $checks['three_stored'] = $orders->countDocuments([]) === 3;
        $checks['types_produce_different_binary'] = matchField($crypto, $keyId, 7) != matchField($crypto, $keyId, '7');
        return [$rows, $checks, 'Целое 7 → '.$rows[0][1].' · строка 7 → '.$rows[1][1]];
    }
);
Ожидаемый результат
Целое 7 → D-731 · строка 7 → D-732

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

Добавьте отдельный заказ с исходной строкой "007". Проверьте, что фильтр с "7" оставляет этот новый заказ за пределами результата.

Передать зашифрованную схему выдачи без лишнего JSON

Инструкция состоит из названия стойки и номера этажа. Курьеру нужны оба значения вместе, а диспетчеру важно понимать, сможет ли база отфильтровать такие заказы по этажу.

Исходные данные

В PHP (object) создаёт из именованных полей документ для BSON. Этаж хранится целым числом. Вся структура передаётся в encrypt как одно значение.

Путь значения
  1. Целый документ + Deterministic → операция отвергнута
  2. Random → в MongoDB единое закрытое поле
  3. Чтение по D-741 → decrypt → стойка и целое число этажа

Минимальный вызов

$instructions = (object)['desk' => 'Стойка Б', 'floor' => 2];
$sealed = randomField($crypto, $keyId, $instructions);
$orders->insertOne(['_id' => 'D-741', 'instructions' => $sealed]);

Результат запуска

Стойка Б · этаж 2

Открыть фактический экран · Строки таблицы в JSON

ПроверкаЗначение
Детерминированное шифрование документаотклонено
Найдено по instructions.floor в базе0
Стойка после decryptСтойка Б
Этаж после decrypt2

Почему получилось именно так

При поиске внутри instructions в коллекции нет вложенного открытого поля floor. Для сервера весь блок является Binary. На клиенте decrypt восстанавливает документ, поэтому можно взять цифру 2 как число. Не нужно сначала превращать документ в текст JSON и при чтении разбирать этот текст обратно.

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

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

Полная программа и архив для запуска
Передать зашифрованную схему выдачи без лишнего JSONСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-sealed-document', 'Передать зашифрованную схему выдачи без лишнего JSON', ['Проверка', 'Значение'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        $instructions = (object)['desk' => 'Стойка Б', 'floor' => 2];
        $denied = false;
        try {
            matchField($crypto, $keyId, $instructions);
        } catch (EncryptionException | MongoDB\Driver\Exception\InvalidArgumentException $error) {
            $denied = true;
        }
        $orders->insertOne(['_id' => 'D-741', 'instructions' => randomField($crypto, $keyId, $instructions)]);
        $raw = $orders->findOne(['_id' => 'D-741'])['instructions'];
        $restored = $crypto->decrypt($raw);
        $fields = (array)$restored;
        $hits = $orders->countDocuments(['instructions.floor' => 2]);
        $checks['deterministic_document_rejected'] = $denied;
        $checks['random_document_stored'] = isSealed($raw);
        $checks['decoded_document'] = is_object($restored) || is_array($restored);
        $checks['typed_floor'] = $fields['floor'] === 2;
        $checks['typed_desk'] = $fields['desk'] === 'Стойка Б';
        $checks['no_dotted_match'] = $hits === 0;
        return [[['Детерминированное шифрование документа', $denied ? 'отклонено' : 'принято'],
                 ['Найдено по instructions.floor в базе', $hits], ['Стойка после decrypt', $fields['desk']],
                 ['Этаж после decrypt', $fields['floor']]], $checks, $fields['desk'].' · этаж '.$fields['floor']];
    }
);
Ожидаемый результат
Стойка Б · этаж 2

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

Добавьте в инструкцию целое количество мест. В расшифрованном документе должна появиться цифра с сохранённым типом. Путь instructions.places в запросе к хранилищу по-прежнему недоступен.

Можно ли передать документ как значение без промежуточной JSON-строки? Рандомизированно зашифровать документ, проверить недоступность вложенных полей обычному запросу и восстановить типы у клиента. Вопрос разработчика на Stack Overflow.

Обработать ошибку чтения защищённой инструкции

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

Исходные данные

В том же временном key vault лежит правильный ключ данных. Новый Manager с disableClientPersistence=true создаёт свежий ClientEncryption, но получает другой случайный локальный мастер-ключ. У него нет ранее раскрытого DEK в памяти.

Путь значения
  1. Номер и статус из findOne доступны → note имеет тип Binary 6
  2. Клиент с чужим мастер-ключом пытается раскрыть DEK → EncryptionException
  3. Показываем состояние ошибки поля; исходная запись остаётся на месте

Минимальный вызов

try {
    $instruction = $otherClient->decrypt($order['note']);
} catch (EncryptionException $error) {
    $message = 'Инструкция недоступна';
}

Результат запуска

D-751 · Инструкция недоступна

Открыть фактический экран · Строки таблицы в JSON

НаблюдениеРезультат
Номер заказаD-751
СостояниеНазначен
Экран клиента с другим ключомИнструкция недоступна
Класс причиныMongoDB\Driver\Exception\EncryptionException

Почему получилось именно так

В этом воспроизводимом случае причина заведомо известна. Общий класс исключения сам по себе не указывает на единственную возможную причину. Правильный клиент прочитал тот же сохранённый Binary после отказа второго. Для технического отчёта достаточно этапа и класса, без вывода аргументов и ключевых данных.

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

Опыт отделяет проблему расшифровки от отсутствия заказа. В рабочей системе надо также различать недоступность базы и полномочия доступа к key vault. Создание нового случайного мастер-ключа не восстанавливает доступ к уже завёрнутому DEK.

Полная программа и архив для запуска
Обработать ошибку чтения защищённой инструкцииСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-wrong-master', 'Обработать ошибку чтения защищённой инструкции', ['Наблюдение', 'Результат'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        $orders->insertOne(['_id' => 'D-751', 'status' => 'Назначен',
            'note' => randomField($crypto, $keyId, 'Учебный сектор В')]);
        $order = $orders->findOne(['_id' => 'D-751']);
        $unrelatedMaster = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
        $otherClient = newFieldClient($uri, $vault, $unrelatedMaster);
        $message = 'Доступно';
        $kind = null;
        try {
            $otherClient->decrypt($order['note']);
        } catch (EncryptionException $error) {
            $message = 'Инструкция недоступна';
            $kind = get_class($error);
        }
        $stillThere = $orders->findOne(['_id' => 'D-751']);
        $checks['wrong_master_failed'] = $kind === EncryptionException::class;
        $checks['public_status_visible'] = $order['status'] === 'Назначен';
        $checks['original_cipher_preserved'] = $stillThere['note'] == $order['note'];
        $checks['correct_client_reads'] = $crypto->decrypt($stillThere['note']) === 'Учебный сектор В';
        $checks['incident_one_order'] = $orders->countDocuments([]) === 1;
        return [[['Номер заказа', $order['_id']], ['Состояние', $order['status']],
                 ['Экран клиента с другим ключом', $message], ['Класс причины', $kind]], $checks,
                 $order['_id'].' · '.$message];
    }
);
Ожидаемый результат
D-751 · Инструкция недоступна

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

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

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

Диспетчеру нужна очередь по срочности. Чтение закрытых примечаний всех заказов ради вывода двух номеров увеличило бы объём работы и раскрыло бы лишние сведения приложению.

Исходные данные

Три заказа имеют открытые номера, состояние и приоритет. Отдельное поле note уже зашифровано. Список запрашивает две строки, затем оператор открывает первую карточку.

Путь значения
  1. Открытые status и priority → сортировка в MongoDB
  2. Проекция → два номера с приоритетом, без поля note
  3. Отдельный поиск одного номера → decrypt выбранного примечания

Минимальный вызов

$page = $orders->find(['status' => 'Готов'], [
    'projection' => ['_id' => 1, 'priority' => 1],
    'sort' => ['priority' => -1, '_id' => 1],
    'limit' => 2,
]);

Результат запуска

Первым выдаётся D-761 · Учебный стеллаж А

Открыть фактический экран · Строки таблицы в JSON

ПредставлениеНомерПриоритет или инструкция
Очередь 1D-7613
Очередь 2D-7622
Открытая карточкаD-761Учебный стеллаж А

Почему получилось именно так

На странице можно сортировать по priority, поскольку в нашей модели он открыт. В таблицу пришли только две нужные строки. Шифрование note не закрывает соседние поля и не превращает сортировку по шифротексту в сортировку исходных строк.

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

Видимость номера и порядка заказов — сознательное решение модели. Если приоритет чувствителен, открывать его для ускорения запроса без согласования нельзя. Проекция сокращает передаваемые поля, но права пользователя всё равно должен проверять сервис.

Полная программа и архив для запуска
Построить общую очередь и расшифровать одно поле выбранного заказаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-public-queue', 'Построить общую очередь и расшифровать одно поле выбранного заказа', ['Представление', 'Номер', 'Приоритет или инструкция'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        foreach ([['D-761',3,'Учебный стеллаж А'], ['D-762',2,'Учебный стеллаж Б'], ['D-763',1,'Учебный стеллаж В']] as [$number,$priority,$note]) {
            $orders->insertOne(['_id' => $number, 'priority' => $priority, 'status' => 'Готов',
                'note' => randomField($crypto, $keyId, $note)]);
        }
        $page = $orders->find(['status' => 'Готов'], [
            'projection' => ['_id' => 1, 'priority' => 1], 'sort' => ['priority' => -1, '_id' => 1], 'limit' => 2,
        ])->toArray();
        $selected = $orders->findOne(['_id' => $page[0]['_id']], ['projection' => ['_id' => 1, 'note' => 1]]);
        $instruction = $crypto->decrypt($selected['note']);
        $checks['correct_ordering'] = array_column($page, '_id') === ['D-761', 'D-762'];
        $checks['priorities_visible'] = array_column($page, 'priority') === [3,2];
        $checks['no_notes_in_queue'] = !isset($page[0]['note']) && !isset($page[1]['note']);
        $checks['selected_only'] = $selected['_id'] === 'D-761' && isSealed($selected['note']);
        $checks['selected_decryption'] = $instruction === 'Учебный стеллаж А';
        return [[['Очередь 1', $page[0]['_id'], $page[0]['priority']], ['Очередь 2', $page[1]['_id'], $page[1]['priority']],
                 ['Открытая карточка', $selected['_id'], $instruction]], $checks, 'Первым выдаётся '.$selected['_id'].' · '.$instruction];
    }
);
Ожидаемый результат
Первым выдаётся D-761 · Учебный стеллаж А

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

Установите для D-762 приоритет 4. Этот заказ должен стать первым, и закрытая часть карточки будет прочитана уже для него.

Какие поля реально подходят для сортировки, когда часть заказа шифруется у клиента? Открытые приоритет и номер задают порядок выдачи; скрытый текст расшифровывается только для выбранной карточки. Вопрос разработчика на Stack Overflow.

Изменить закрытую инструкцию, сохранив правку другого сотрудника

Два сотрудника открыли одну и ту же старую инструкцию. Изменение принимается, только если никто не успел сохранить следующую редакцию.

Исходные данные

Номер заказа и целое поле revision доступны серверу. Содержимое note шифруется на клиенте. Оба обращения используют одну и ту же прочитанную исходную revision=1.

Путь значения
  1. Первый updateOne находит номер и версию 1 → меняет шифротекст, версия 2
  2. Второй updateOne требует версию 1 → 0 совпадений
  3. Чтение версии 2 и decrypt → сохранилось указание «Учебный вход Б»

Минимальный вызов

$filter = ['_id' => $number, 'revision' => $versionFromTheForm];
$update = ['$set' => ['note' => randomField($crypto, $keyId, $newNote)], '$inc' => ['revision' => 1]];
$receipt = $orders->updateOne($filter, $update);

Результат запуска

Сохранена версия 2 · Учебный вход Б

Открыть фактический экран · Строки таблицы в JSON

ШагВерсия или число совпаденийНа экране
Исходная версия1Учебный вход А
После правки2Учебный вход Б
Совпало для устаревшей правки0Обновить карточку

Почему получилось именно так

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

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

Это проверка конфликта редакций одного документа в последовательном воспроизведении двух устаревших клиентов. Она не проверяет реальный параллелизм, транзакцию нескольких документов или сетевой повтор. В приложении ноль совпадений возможен и после удаления заказа.

Полная программа и архив для запуска
Изменить закрытую инструкцию, сохранив правку другого сотрудникаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/field-lab.php';

use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ClientEncryption;
use MongoDB\Driver\Exception\EncryptionException;

runFieldLesson('mongodb132-field-revision-guard', 'Изменить закрытую инструкцию, сохранив правку другого сотрудника', ['Шаг', 'Версия или число совпадений', 'На экране'],
    function (Collection $orders, ClientEncryption $crypto, Binary $keyId, string $uri, string $vault): array {
        $orders->insertOne(['_id' => 'D-771', 'revision' => 1, 'note' => randomField($crypto, $keyId, 'Учебный вход А')]);
        $opened = $orders->findOne(['_id' => 'D-771']);
        $starting = $crypto->decrypt($opened['note']);
        $firstUpdate = $orders->updateOne(['_id' => 'D-771', 'revision' => $opened['revision']], [
            '$set' => ['note' => randomField($crypto, $keyId, 'Учебный вход Б')], '$inc' => ['revision' => 1],
        ]);
        $staleUpdate = $orders->updateOne(['_id' => 'D-771', 'revision' => $opened['revision']], [
            '$set' => ['note' => randomField($crypto, $keyId, 'Устаревшая правка')], '$inc' => ['revision' => 1],
        ]);
        $saved = $orders->findOne(['_id' => 'D-771']);
        $actual = $crypto->decrypt($saved['note']);
        $checks['original_read'] = $starting === 'Учебный вход А';
        $checks['updated_one'] = $firstUpdate->getModifiedCount() === 1;
        $checks['stale_refused'] = $staleUpdate->getMatchedCount() === 0 && $staleUpdate->getModifiedCount() === 0;
        $checks['revision_two'] = $saved['revision'] === 2;
        $checks['correct_new_note'] = $actual === 'Учебный вход Б' && isSealed($saved['note']);
        return [[['Исходная версия', $opened['revision'], $starting], ['После правки', $saved['revision'], $actual],
                 ['Совпало для устаревшей правки', $staleUpdate->getMatchedCount(), 'Обновить карточку']], $checks,
                 'Сохранена версия '.$saved['revision'].' · '.$actual];
    }
);
Ожидаемый результат
Сохранена версия 2 · Учебный вход Б

В архиве есть field-lab.php, который создаёт локальные ключи и учебные коллекции и очищает их после опыта. HTML содержит только явно выбранные сведения.

Самостоятельная проверка

После нулевого результата предложите перечитать карточку и показать пользователю уже сохранённую инструкцию. Исходную устаревшую правку нельзя автоматически применять поверх новой.

Методы работы с полем и диагностика

Метод или классКогда понадобится
MongoDB\Driver\ClientEncryption::encryptЗашифровать одно значение BSON на клиенте до записи или для точного поиска.
MongoDB\Driver\ClientEncryption::decryptВернуть исходное значение защищённого поля для разрешённого представления.
MongoDB\Driver\Exception\EncryptionExceptionОбработать сбой клиентского шифрования или расшифровки в контексте конкретного поля.

От поля к целому экрану

Уже можно собрать список по открытым полям, открыть выбранную карточку и получить закрытое значение. Для самого экрана пригодится пример PHP-очереди, для смены ключевых настроек — руководство по key vault, для конкурентного редактирования — атомарные изменения по условию.

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

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

MongoDB\Driver\ClientEncryption::encryptЗашифровать одно значение BSON на клиенте до записи или для точного поиска.

С ext-mongodb 1.7.0. В этой главе выполняются два классических алгоритма; сигнатура сверена с расширением 2.5.5. · mongodb

final public function MongoDB\Driver\ClientEncryption::encrypt(mixed $value, ?array $options = null): MongoDB\BSON\Binary

Вход и параметры

Передайте значение и массив опций. Обязательно задайте algorithm и ровно один способ найти DEK: keyId — BSON Binary с подтипом UUID 4, либо keyAltName — ранее созданное уникальное строковое имя в key vault. Пометка ?array в сигнатуре не создаёт нужные опции за вызывающий код. Для классического CSFLE доступны константы AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM и AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC.

Результат

Метод возвращает MongoDB\BSON\Binary с подтипом 6. Это значение можно сразу вставить в поле документа через драйвер. Сама операция ещё не создала заказ в прикладной коллекции. В синтаксисе PHP стрелка -> вызывает метод у клиента, а двойное двоеточие :: обращается к константе класса.

Ошибки и ограничения

Тип значения должен иметь допустимое BSON-представление и поддерживаться выбранным алгоритмом. Классический детерминированный алгоритм исключает целые документы и массивы. Аргументы неправильной формы могут привести к InvalidArgumentException; ошибка выполнения шифрования — к EncryptionException. Для равенства нужны тот же алгоритм, ключ, значение и BSON-тип. Частоты повторов сохраняются. Случайный алгоритм предпочтителен для читаемого целиком поля без поиска по нему.

Для Queryable Encryption у метода также существуют алгоритмы Indexed, Unindexed и Range. Параметры queryType (equality/range) и целочисленный contentionFactor относятся к поддерживаемым Indexed/Range режимам. Для Range задают rangeOpts согласованно с encryptedFields целевой коллекции. Это может включать min, max, положительное sparsity, precision для double/decimal128 и trimFactor. У double/decimal128 min, max и precision задаются согласованно. Метаданные коллекции и процедура запроса там отдельные. Опции contentionFactor/queryType добавлены в 1.14, rangeOpts в 1.16, trimFactor в 1.20, когда sparsity стала необязательной.

Версия

С ext-mongodb 1.7.0. В этой главе выполняются два классических алгоритма; сигнатура сверена с расширением 2.5.5.

Сохранить инструкцию, оставив статус видимым

$note = $crypto->encrypt($instruction, [
    'keyId' => $keyId, 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM,
]);
$orders->insertOne(['_id' => 'D-701', 'status' => 'К выдаче', 'note' => $note]);

На складе видны номер и статус, а тип закрытого поля — Binary 6.

Исходные данные, полная программа и результат

Подготовить значение фильтра

$token = matchField($crypto, $keyId, 'BOX-1273');
$hits = $orders->find(['externalCode' => $token]);

В полном примере функция matchField вызывает encrypt с детерминированным алгоритмом. Фильтр находит два заказа вместо нуля при попытке найти ту же строку открытым текстом.

Исходные данные, полная программа и результат

Подробнее · Создание DEK · Расшифровка.

MongoDB\Driver\ClientEncryption::decryptВернуть исходное значение защищённого поля для разрешённого представления.

С ext-mongodb 1.7.0. На стенде 2.5.5 принимается Binary и возвращается mixed. · mongodb

final public function MongoDB\Driver\ClientEncryption::decrypt(MongoDB\BSON\Binary $value): mixed

Вход и параметры

Нужен MongoDB\BSON\Binary с подтипом 6, прочитанный, например, из поля note. Второго параметра и явного ID ключа этот метод не принимает. Сведения в шифрованном значении, конфигурация ClientEncryption, key vault и доступ к мастер-ключу позволяют найти и раскрыть нужный DEK.

Результат

Возвращается расшифрованное значение в PHP-представлении соответствующего типа BSON. Для строки это string. Для числового поля сохраняется отличие от строки. Целиком зашифрованный документ восстанавливается как документ, чьи поля читает приложение. Метод не записывает открытое значение обратно в MongoDB и сам ничего не выводит в браузер.

Ошибки и ограничения

Проверьте, что заказ найден, поле присутствует и его подтип корректен. При ошибочных аргументах документация указывает InvalidArgumentException, при ошибке расшифровки — EncryptionException. Возвращаемое значение следует проверять по схеме приложения. Любой пользовательский текст кодируйте на границе HTML с htmlspecialchars(). Сохраняйте минимум открытых значений в логах и отдавайте их лишь после проверки прав читателя.

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

Версия

С ext-mongodb 1.7.0. На стенде 2.5.5 принимается Binary и возвращается mixed.

Вынести в карточку инструкцию к одному заказу

$order = $orders->findOne(['_id' => 'D-701']);
$instruction = $crypto->decrypt($order['note']);

Сохранённый Binary даёт текст «Окно учебной выдачи № 2». Соседнее состояние остаётся открытым.

Исходные данные, полная программа и результат

Прочитать структурированную инструкцию

$route = (array)$crypto->decrypt($stored['instructions']);
echo $route['desk'], ' · этаж ', $route['floor'];

Получаем стойку и целый этаж. В коллекции обращение к instructions.floor ничего не находило, так как документ был зашифрован целиком.

Исходные данные, полная программа и результат

Подробнее · Шифрование поля · Выбор key vault.

MongoDB\Driver\Exception\EncryptionExceptionОбработать сбой клиентского шифрования или расшифровки в контексте конкретного поля.

Исключение расширения mongodb с 1.7.0. Для отрицательного примера на 2.5.5 используется настоящий новый клиент с несоответствующим локальным мастер-ключом. · mongodb

class MongoDB\Driver\Exception\EncryptionException extends MongoDB\Driver\Exception\RuntimeException implements MongoDB\Driver\Exception\Exception

Вход и параметры

Драйвер создаёт объект исключения при ошибке выполнения криптографической операции. В обработчике catch указывают класс, а переменной дают имя, например $error. Самостоятельно создавать его для проверки правильного ключа не требуется.

Результат

У выброшенного исключения нет результата decrypt или encrypt, который можно подставить в форму. Перехват позволяет передать пользователю понятное состояние ошибки. Унаследованы getMessage(), getCode(), getPrevious(), getFile(), getLine(), getTrace(), getTraceAsString() и hasErrorLabel(). Метод hasErrorLabel() нужен при наличии документированного правила для конкретной операции. Сам класс исключения такого правила не задаёт.

Ошибки и ограничения

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

Метаданные причины полезны для технического журнала с контролем доступа. В демонстрационном HTML виден только класс и выбранное сообщение интерфейса.

Версия

Исключение расширения mongodb с 1.7.0. Для отрицательного примера на 2.5.5 используется настоящий новый клиент с несоответствующим локальным мастер-ключом.

Сохранить читаемые части заказа при ошибке

try {
    $instruction = $otherClient->decrypt($order['note']);
} catch (EncryptionException $error) {
    $message = 'Инструкция недоступна';
}

Заказ D-751 найден. Другой мастер-ключ не раскрывает DEK; после отказа запись сохраняется и правильный клиент по-прежнему читает поле.

Исходные данные, полная программа и результат

Отделить неподдерживаемый тип от успешной записи

try {
    matchField($crypto, $keyId, $instructions);
} catch (EncryptionException | MongoDB\Driver\Exception\InvalidArgumentException $error) {
    $denied = true;
}

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

Исходные данные, полная программа и результат

Подробнее · Как работает try/catch · Ошибки серверной записи.

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

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

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

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

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