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

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

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

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

Глава 137 · После простой таблицы, SELECT, массивов и первого чтения результата

MySQL X DevAPI: совместимость API и проверка ценового CSV

Сверить Driver и FieldMetadata с установленным модулем, сохранить полный официальный baseline и проверить реальную проекцию ColumnResult до выгрузки двух точных цен.

Найти поддерживаемый API и передать правильный файл цен

Нужно передать другой системе файл цен: первая колонка sku, вторая price, две строки CABLE и CLAMP. На странице справочника нашлись Driver и FieldMetadata, но установленный модуль их не предоставляет. Разберём обе проблемы последовательно: найдём настоящий доступный API, затем проверим результат SQL до создания файла. Получатель не должен обнаруживать ошибку заголовка или типа цены уже после загрузки.

От запроса до принятого файла
  1. PHP с загруженным mysql_xdevapi: проверяем фактические имена API
  2. SELECT возвращает только sku и DECIMAL price двух товаров
  3. ColumnResult описывает поля; сами строки содержат CABLE/120.00 и CLAMP/25.50
  4. Контракт сравнивает порядок, имена, тип и масштаб цены
  5. Только после успешной проверки открывается export.csv
  6. Файл читается обратно: заголовок и обе цены должны сохраниться точно

Представьте накладную: надписи над колонками объясняют, что означает содержимое. Metadata — такие сведения о колонках SQL-ответа, а данные — строки товаров под ними. Схема базы — третья сущность: описание хранилища. Ответ SELECT может выбрать часть полей, переименовать их или вычислить выражение, поэтому сведения о всей таблице нельзя автоматически принимать за контракт выгрузки.

Сначала проверим одно имя

$exists = class_exists('mysql_xdevapi\\FieldMetadata', false);
var_dump($exists); // bool(false) в PECL mysql_xdevapi 8.0.30

В скобках передано имя класса строкой. Двойной обратный слеш внутри строкового литерала даёт один разделитель пространства имён. Второй аргумент false запрещает автозагрузке подставить класс из пользовательского файла. Знак = сохраняет ответ, а var_dump показывает его тип и значение. Это проверка определённого класса в текущем процессе; она не подключается к серверу и не читает таблицу.

Отдельно phpversion("mysql_xdevapi") возвращает версию загруженного расширения или false. Отсутствие FieldMetadata при установленном модуле — другая ситуация, чем незагруженное расширение. Мы не исправляем её объявлением собственного класса mysql_xdevapi\FieldMetadata: так можно скрыть ошибку, но нельзя получить метаданные от сервера. Установка и активный PHP и пространства имён помогают отделить эти уровни.

У строки есть значение, у столбца — описание

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

$columns = $result->getColumns();
$price = $columns[1];
echo $price->getColumnLabel(); // price
echo $price->getFractionalDigits(); // 2

ColumnResult не содержит цену конкретного кабеля: он описывает столбец. Число 2 означает два дробных знака metadata, а не стоимость. Реальное значение 120.00 получаем из строки результата и сохраняем строкой. Здесь $result и $columns остаются живыми до окончания чтения. Полный архив ниже создаёт таблицу, записывает данные и получает этот результат самостоятельно; короткий фрагмент показывает только текущую операцию.

Контракт получателя записываем явно

ПолеПорядокЧто ожидает получательКак проверяем
sku1Строковый артикул выбранного товараИмя проекции, тип metadata и значение строки.
price2Цена с двумя цифрами после точкиDECIMAL, масштаб 2, затем строковое значение без float.

Контракт — договорённость о конкретном файле. Он не выводится автоматически из первого товара и не меняется вслед за случайным переименованием SQL-поля. В полном примере проверяется ровно проекция sku,price; лишний столбец тоже является несовпадением. Это полезно, когда файл читает программа доставки, учёта или обновления каталога с фиксированными колонками.

$headers = ['sku', 'price'];
$fields = [$row['sku'], $row['price']];

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

Что означает CSV в этой интеграции

CSV — текст с разделёнными полями, а не таблица Excel с настройками ячеек. Наш получатель ожидает UTF-8 без BOM, запятую, обычные двойные кавычки при необходимости и LF в конце каждой строки. Значения примера состоят из ASCII-символов, которые одновременно являются корректным UTF-8. Цена 120.00 передаётся строкой, без промежуточного float. Выбор программы для просмотра файла не изменяет его байты, но может изменить отображение числа.

fputcsv($stream, ['sku', 'price'], ',', '"', '', "\n");
fputcsv($stream, ['CABLE', '120.00'], ',', '"', '', "\n");

stream — уже открытый файл, второй аргумент — массив полей. Далее явно заданы разделитель, кавычка, пустой escape и перевод строки. Пустой escape отключает собственный backslash-механизм PHP; стандартное удвоение кавычки внутри поля остаётся. Параметр конца строки доступен с PHP 8.1; явный escape также не зависит от устаревшего начиная с PHP 8.4 значения по умолчанию. fputcsv возвращает число записанных байтов или false: результат нужно проверить. В самостоятельном файле поток открывается только после проверки metadata и значений.

Первый архив не требует базы: он составляет карту доступности API. Второй использует одну собственную временную таблицу в заранее подготовленной учебной схеме, формирует export.csv и проверяет его чтением через fgetcsv. Каждая ошибочная проекция отвергается до открытия rejected.csv. Таблица удаляется в finally; полезный файл и отчёты остаются в папке примера. Данные подключения берутся из окружения по README, не записываются в результат.

Старый пример не запускается: отделить отсутствующий модуль от отсутствующего класса

Вы нашли старый код, который обращается к Driver::version и FieldMetadata. Нужен короткий отчёт для переноса: что реально загружено и каким API получать версию и сведения о цене. Соединение с MySQL для этой проверки не требуется.

Путь от задачи до проверяемого результата
  1. Текущий PHP загружает свой набор расширений
  2. phpversion читает версию именно этого модуля
  3. class_exists без автозагрузки проверяет три конкретных имени
  4. method_exists проверяет четыре getter у ColumnResult
  5. Получаем карту маршрутов; сервер и данные не трогаем

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

var_dump(extension_loaded('mysql_xdevapi')); // bool(true)
echo phpversion('mysql_xdevapi'); // 8.0.30

extension_loaded отвечает логическим true или false. phpversion получает имя модуля и возвращает его версию строкой либо false. Оба вызова исследуют процесс PHP; это не запрос SELECT VERSION() к серверу. Полная программа дополнительно проверяет классы и методы, а массив routes записывает, каким API решается каждая задача.

Что получится

НаблюдениеРезультат
Расширение загруженода
Версия расширения8.0.30
MYSQLX_VERSION совпадает с phpversionда
Класс Driver доступеннет
Константа Driver::version определенанет
Класс FieldMetadata доступеннет
Класс ColumnResult доступенда
Четыре нужных getter доступныда
Открыто подключение к базенет
Полный файл и архив для самостоятельного запуска
Старый пример не запускается: отделить отсутствующий модуль от отсутствующего классаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$session=$schema=$config=null;
$owned=$removed=$rows=$checks=$observations=[];
$runtime=x147Runtime();
try {
    $loaded = extension_loaded('mysql_xdevapi');
    $extensionVersion = phpversion('mysql_xdevapi');
    $nativeVersion = defined('MYSQLX_VERSION') ? constant('MYSQLX_VERSION') : null;
    $nativeVersionId = defined('MYSQLX_VERSION_ID') ? constant('MYSQLX_VERSION_ID') : null;
    $classes = [];
    foreach (['Driver', 'FieldMetadata', 'ColumnResult'] as $shortName) {
        $classes[$shortName] = class_exists('mysql_xdevapi\\'.$shortName, false);
    }
    $driverConstant = defined('mysql_xdevapi\\Driver::version');
    $requiredGetters = ['getColumnLabel', 'getColumnName', 'getType', 'getFractionalDigits'];
    $getters = [];
    foreach ($requiredGetters as $method) {
        $getters[$method] = method_exists('mysql_xdevapi\\ColumnResult', $method);
    }
    $metadataRouteReady = !in_array(false, $getters, true);
    $versionMatches = is_string($extensionVersion) && $extensionVersion === $nativeVersion;
    $observations = [
        'extension_loaded'=>$loaded, 'extension_version'=>$extensionVersion,
        'native_version'=>$nativeVersion, 'native_version_id'=>$nativeVersionId,
        'classes'=>$classes, 'driver_version_constant_defined'=>$driverConstant,
        'metadata_getters'=>$getters, 'database_connection_opened'=>$session !== null,
        'routes'=>[
            ['need'=>'версия загруженного модуля', 'api'=>'phpversion("mysql_xdevapi")'],
            ['need'=>'подпись столбца ответа', 'api'=>'ColumnResult::getColumnLabel'],
            ['need'=>'тип и масштаб цены', 'api'=>'ColumnResult::getType / getFractionalDigits'],
        ],
    ];
    $rows = [
        ['Расширение загружено', $loaded],
        ['Версия расширения', $extensionVersion],
        ['MYSQLX_VERSION совпадает с phpversion', $versionMatches],
        ['Класс Driver доступен', $classes['Driver']],
        ['Константа Driver::version определена', $driverConstant],
        ['Класс FieldMetadata доступен', $classes['FieldMetadata']],
        ['Класс ColumnResult доступен', $classes['ColumnResult']],
        ['Четыре нужных getter доступны', $metadataRouteReady],
        ['Открыто подключение к базе', $session !== null],
    ];
    $checks = [
        'extension_loaded'=>$loaded,
        'version_matches_native'=>$versionMatches && $nativeVersionId === 80030,
        'legacy_classes_absent'=>$classes['Driver'] === false && $classes['FieldMetadata'] === false,
        'legacy_constant_absent'=>$driverConstant === false,
        'metadata_route'=>$classes['ColumnResult'] && $metadataRouteReady,
        'no_database'=>$session === null && $schema === null && $owned === [],
    ];
} finally {
    if ($session instanceof mysql_xdevapi\Session) {
        try {
            if ($schema instanceof mysql_xdevapi\Schema) {$removed=x147Cleanup($schema,$owned);}
        } finally {$session->close();}
    }
}
x147Report('mysql147-x-legacy-api-map', 'Старый пример не запускается: отделить отсутствующий модуль от отсутствующего класса', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Расширение загружено — да
Версия расширения — 8.0.30
MYSQLX_VERSION совпадает с phpversion — да
Класс Driver доступен — нет
Константа Driver::version определена — нет
Класс FieldMetadata доступен — нет
Класс ColumnResult доступен — да
Четыре нужных getter доступны — да
Открыто подключение к базе — нет

Распакуйте каждый архив в отдельную пустую папку и следуйте README. Диагностическая программа не открывает соединение. Выгрузка создаёт одну собственную учебную таблицу, читает два товара, проверяет CSV и удаляет свою таблицу в finally. result.html и result.json сохраняют фактический результат, metrics.json — проверки.

Таблица сохранённого запуска · Фактические наблюдения JSON.

Границы, ошибки и следующий масштаб задачи

Вывод относится к проверенному PECL mysql_xdevapi 8.0.30. Если другое окружение покажет иные классы или версию, это повод исследовать его, а не подгонять результат. Отсутствие класса не равнозначно официальной отметке deprecated. method_exists показывает наличие метода, но не проверяет доступ к таблицам и не создаёт объект результата. При незагруженном расширении исправляют установку и конфигурацию конкретного PHP, а не добавляют одноимённый пользовательский класс. Автозагрузка отключена только у class_exists; для установленного ColumnResult метод уже принадлежит расширению.

Пакет Composer сообщает, что требуемого ext-mysql_xdevapi нет. Разработчик пытается решить вопрос как установку обычной PHP-библиотеки. Исходная практическая проблема. Сначала проверяем загруженный модуль в текущем процессе. Затем отдельно проверяем нужные классы: установленное расширение не обязано содержать каждое историческое имя из документации. Нативный модуль устанавливается и включается в PHP, а не создаётся файлом с подставным классом.

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

Добавьте в карту маршрут для имени исходной таблицы. Не подключайтесь к серверу и не создавайте ColumnResult вручную.

Разбор изменения

Добавьте getTableName в requiredGetters и строку маршрута ColumnResult::getTableName. Проверка подтвердит наличие метода; его фактическое значение получают только у метаданных настоящего SQL-ответа, как в предыдущей главе.

Не отправить чужой формат: проверить проекцию цены до создания CSV

Система учёта принимает только sku и точную цену price. Правка SELECT может переименовать price или превратить DECIMAL в текст, хотя две видимые цены останутся прежними. Нужно остановить такую выгрузку до появления ошибочного файла и получить корректный CSV из фактических данных MySQL.

Путь от задачи до проверяемого результата
  1. Своя таблица содержит CABLE:120.00 и CLAMP:25.50
  2. Неверный alias меняет заголовок — запись не начинается
  3. CAST AS CHAR сохраняет видимые цены, но меняет metadata — запись не начинается
  4. Верный SELECT даёт sku и DECIMAL price с масштабом 2
  5. Проверяем значения, создаём CSV и читаем его обратно
  6. Таблица удаляется; готовые файл и отчёт остаются

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

$metadata = x147PriceMetadata($result);
x147RequirePriceProjection($metadata);
// Только после успеха можно переходить к записи файла.

Здесь result — уже исполненный SQL-ответ. Первая наша функция собирает описания его столбцов в массив; вторая сверяет имена, порядок, типы и масштаб цены. При несовпадении throw прерывает этот путь исключением UnexpectedValueException. В полном файле показаны обе функции, запросы, запись и catch только для намеренно неправильных проекций. Ошибка подключения или SQL не превращается в ожидаемый отказ проверки.

array_column собирает подписи столбцов; строгое сравнение !== учитывает их порядок и число. Вторая проверка использует именованные константы расширения, а не число из головы. Значение масштаба 2 сравнивается отдельно. Наши функции имеют явный вход: объект ответа либо массив metadata. Первая возвращает массив, вторая с : void не выдаёт полезного значения — она либо завершается, либо бросает исключение. Как устроены вход, return и вызов функции.

Валидация строк решает другую задачу. Два коротких регулярных выражения проверяют весь артикул и всю цену: разрешены только оговорённые символы, у цены — ровно две цифры после точки. Цена остаётся строкой. Это не следствие типа metadata: мы отдельно проверяем реальные данные до fopen. Для первого знакомства можно проследить два фиксированных товара, а затем менять одно правило за раз.

SELECT sku, price FROM ваша_учебная_таблица ORDER BY sku;
-- Хорошая проекция: sku и DECIMAL price.
SELECT sku, CAST(price AS CHAR) AS price FROM ваша_учебная_таблица ORDER BY sku;
-- Подпись и видимые цифры прежние, тип столбца уже строковый.

В этом сравнении имя таблицы — пояснительная метка. В полном PHP-файле оно подставлено безопасным helper из собственного созданного имени, а INSERT передаёт каждый артикул и цену отдельным bind. Код не загружает лишние столбцы и не сортирует массив товаров в PHP: ORDER BY выполняет сервер. Выборка только нужных полей и строк.

Что получится

НаблюдениеРезультат
Заголовок CSV после чтенияsku,price
Цена CABLE в файле120.00
Цена CLAMP в файле25.50
Строк товаров в файле2
Переименованное price отвергнутода
Текстовое price отвергнутода
CAST сохранил значения, но не типда
Создан rejected.csvнет

Содержимое готового файла export.csv

sku,price
CABLE,120.00
CLAMP,25.50

В конце последней строки тоже есть LF. Здесь показан текст файла; оба trailing zero в 120.00 действительно сохранены. Самостоятельный запуск заново записывает и читает файл, а result.json хранит фактические строки, metadata и причину каждого отказа.

Полный файл и архив для самостоятельного запуска
Не отправить чужой формат: проверить проекцию цены до создания CSVСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$session=$schema=$config=null;
$owned=$removed=$rows=$checks=$observations=[];
$runtime=x147Runtime();
try {

    $config=x147Config();
    $session=mysql_xdevapi\getSession($config['uri']);
    $schema=$session->getSchema($config['schema']);
    if (!$schema->existsInDatabase()) {throw new RuntimeException('Подготовьте отдельную учебную базу по README.');}
    $runtime=x147Runtime($session,$config);
    function x147PriceMetadata($result): array {
        $metadata = [];
        foreach ($result->getColumns() as $column) {
            $metadata[] = [
                'label'=>$column->getColumnLabel(), 'name'=>$column->getColumnName(),
                'type'=>$column->getType(), 'scale'=>$column->getFractionalDigits(),
            ];
        }
        return $metadata;
    }
    function x147RequirePriceProjection(array $metadata): void {
        if (array_column($metadata, 'label') !== ['sku', 'price']) {
            throw new UnexpectedValueException('Ожидались ровно sku,price в этом порядке.');
        }
        if ($metadata[0]['type'] !== MYSQLX_TYPE_BYTES ||
            $metadata[1]['type'] !== MYSQLX_TYPE_DECIMAL || $metadata[1]['scale'] !== 2) {
            throw new UnexpectedValueException('Ожидались строковый sku и DECIMAL price с масштабом 2.');
        }
    }
    function x147WritePrices($result, string $path): array {
        // Result остаётся живым, пока читаем его ColumnResult и строки.
        $metadata = x147PriceMetadata($result);
        x147RequirePriceProjection($metadata);
        $data = $result->fetchAll();
        $csvRows = [];
        foreach ($data as $row) {
            if (!is_string($row['sku']) || preg_match('/\A[A-Z][A-Z0-9-]{0,19}\z/', $row['sku']) !== 1 ||
                !is_string($row['price']) || preg_match('/\A[0-9]+\.[0-9]{2}\z/', $row['price']) !== 1) {
                throw new UnexpectedValueException('Артикул или цена не соответствуют контракту файла.');
            }
            $csvRows[] = [$row['sku'], $row['price']];
        }
        // Файл появляется только после проверки metadata И всех строк этого маленького набора.
        $stream = fopen($path, 'xb');
        if ($stream === false) {throw new RuntimeException('Не удалось создать новый CSV.');}
        try {
            foreach (array_merge([['sku', 'price']], $csvRows) as $fields) {
                if (fputcsv($stream, $fields, ',', '"', '', "\n") === false) {
                    throw new RuntimeException('Запись CSV не завершена.');
                }
            }
        } finally {fclose($stream);}
        return ['metadata'=>$metadata, 'data'=>$data];
    }
    $tableName = x147OwnTable($session, $schema, $owned, 'price_export',
        '(sku VARCHAR(20) PRIMARY KEY, price DECIMAL(10,2) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
    $qualified = x147Qualified($schema, $tableName);
    foreach ([['CABLE','120.00'], ['CLAMP','25.50']] as $product) {
        $session->sql('INSERT INTO '.$qualified.' (sku, price) VALUES (?, ?)')
            ->bind($product[0])->bind($product[1])->execute();
    }
    $rejectedPath = __DIR__.'/rejected.csv';
    $exportPath = __DIR__.'/export.csv';
    if (file_exists($rejectedPath) || file_exists($exportPath)) {
        throw new RuntimeException('Запустите архив в отдельной пустой папке: файлы уже существуют.');
    }
    $rejectedCases = [];
    foreach ([
        'renamed'=>'sku, price AS renamed_price',
        'text'=>'sku, CAST(price AS CHAR) AS price',
    ] as $case=>$projection) {
        $badResult = $session->sql('SELECT '.$projection.' FROM '.$qualified.' ORDER BY sku')->execute();
        $badMetadata = x147PriceMetadata($badResult);
        $refused = false;
        $reason = null;
        try {
            x147WritePrices($badResult, $rejectedPath);
        } catch (UnexpectedValueException $error) {
            $refused = true;
            $reason = x147SafeMessage($error->getMessage());
        }
        $rejectedCases[$case] = [
            'rejected'=>$refused, 'reason'=>$reason, 'metadata'=>$badMetadata,
            'actual_rows'=>$badResult->fetchAll(), 'file_exists'=>file_exists($rejectedPath),
        ];
        if (!$refused || file_exists($rejectedPath)) {
            throw new RuntimeException('Некорректная проекция не была остановлена до записи.');
        }
    }
    $result = $session->sql('SELECT sku, price FROM '.$qualified.' ORDER BY sku')->execute();
    $written = x147WritePrices($result, $exportPath);
    $reader = fopen($exportPath, 'rb');
    if ($reader === false) {throw new RuntimeException('Не удалось прочитать созданный CSV.');}
    try {
        $exportColumns = fgetcsv($reader, null, ',', '"', '');
        $exportRows = [];
        while (($record = fgetcsv($reader, null, ',', '"', '')) !== false) {
            $exportRows[] = $record;
        }
        if (!feof($reader)) {throw new RuntimeException('Чтение CSV прервалось до конца файла.');}
    } finally {fclose($reader);}
    $exportText = file_get_contents($exportPath);
    if ($exportText === false) {throw new RuntimeException('Не удалось проверить текст CSV.');}
    $expectedData = [['sku'=>'CABLE','price'=>'120.00'], ['sku'=>'CLAMP','price'=>'25.50']];
    $sameValues = $rejectedCases['text']['actual_rows'] === $written['data'];
    $observations = [
        'valid_metadata'=>$written['metadata'], 'selected_rows'=>$written['data'],
        'export_columns'=>$exportColumns, 'export_rows'=>$exportRows, 'export_text'=>$exportText,
        'rejected_cases'=>$rejectedCases, 'rejected_file_exists'=>file_exists($rejectedPath),
        'cast_has_same_values'=>$sameValues,
    ];
    $rows = [
        ['Заголовок CSV после чтения', implode(',', $exportColumns)],
        ['Цена CABLE в файле', $exportRows[0][1]],
        ['Цена CLAMP в файле', $exportRows[1][1]],
        ['Строк товаров в файле', count($exportRows)],
        ['Переименованное price отвергнуто', $rejectedCases['renamed']['rejected']],
        ['Текстовое price отвергнуто', $rejectedCases['text']['rejected']],
        ['CAST сохранил значения, но не тип', $sameValues],
        ['Создан rejected.csv', file_exists($rejectedPath)],
    ];
    $checks = [
        'selected_data'=>$written['data'] === $expectedData,
        'metadata_contract'=>array_column($written['metadata'], 'label') === ['sku','price']
            && $written['metadata'][0]['type'] === MYSQLX_TYPE_BYTES
            && $written['metadata'][1]['type'] === MYSQLX_TYPE_DECIMAL
            && $written['metadata'][1]['scale'] === 2,
        'csv_readback'=>$exportColumns === ['sku','price']
            && $exportRows === [['CABLE','120.00'], ['CLAMP','25.50']],
        'csv_exact_bytes'=>$exportText === "sku,price\nCABLE,120.00\nCLAMP,25.50\n",
        'renamed_refused'=>$rejectedCases['renamed']['rejected']
            && $rejectedCases['renamed']['metadata'][1]['label'] === 'renamed_price',
        'text_refused'=>$rejectedCases['text']['rejected']
            && $rejectedCases['text']['metadata'][1]['type'] === MYSQLX_TYPE_BYTES
            && $rejectedCases['text']['metadata'][1]['scale'] === 0,
        'same_values_different_type'=>$sameValues,
        'rejected_absent'=>!file_exists($rejectedPath)
            && !$rejectedCases['renamed']['file_exists'] && !$rejectedCases['text']['file_exists'],
    ];
} finally {
    if ($session instanceof mysql_xdevapi\Session) {
        try {
            if ($schema instanceof mysql_xdevapi\Schema) {$removed=x147Cleanup($schema,$owned);}
        } finally {$session->close();}
    }
}
x147Report('mysql147-x-export-schema-guard', 'Не отправить чужой формат: проверить проекцию цены до создания CSV', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Заголовок CSV после чтения — sku,price
Цена CABLE в файле — 120.00
Цена CLAMP в файле — 25.50
Строк товаров в файле — 2
Переименованное price отвергнуто — да
Текстовое price отвергнуто — да
CAST сохранил значения, но не тип — да
Создан rejected.csv — нет

Распакуйте каждый архив в отдельную пустую папку и следуйте README. Диагностическая программа не открывает соединение. Выгрузка создаёт одну собственную учебную таблицу, читает два товара, проверяет CSV и удаляет свою таблицу в finally. result.html и result.json сохраняют фактический результат, metrics.json — проверки.

Таблица сохранённого запуска · Фактические наблюдения JSON · Готовый CSV с двумя товарами.

Границы, ошибки и следующий масштаб задачи

В проверенной связке 8.0.30/8.4.13 строковый VARCHAR имеет metadata MYSQLX_TYPE_BYTES (21), а DECIMAL — MYSQLX_TYPE_DECIMAL (0), масштаб 2. Это тип ответа протокола, а не PHP get_debug_type: обе цены читаются строками. Наличие metadata не доказывает корректность значений или безопасность CSV. Пример отдельно допускает только артикулы из заглавных латинских букв, цифр и дефиса, начинающиеся с буквы, и неотрицательные цены с двумя знаками. Это узкий бизнес-контракт, не универсальная обработка пользовательского ввода. Пустая SQL-выдача в этой версии может не дать metadata: проверка остановится; для файла только с заголовком нужен отдельный явно согласованный сценарий. Здесь два товара; загрузка всего большого каталога через fetchAll потребует иной организации потоковой записи и выборки. Режим xb не перезаписывает существующий файл, b сохраняет явные LF на Windows. При дисковой ошибке возможен частичный файл; для производственной публикации используют временный файл с последующим переносом только после успеха. Не отправляйте частичный результат получателю.

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

В ценовом CSV вместо ожидаемых двух знаков после точки оказывается целое значение. Исходная практическая проблема. Для нашего контракта DECIMAL читается и сохраняется как строка: 120.00 и 25.50 не преобразуются во float. Проверяем не только внешний вид таблицы, но и реальные байты файла и строки после обратного чтения.

Строки запроса выгружаются, а заголовки пытаются получить отдельно из описания всей таблицы; список полей может перестать соответствовать SELECT. Исходная практическая проблема. Определяем явный контракт sku,price, сверяем его с metadata конкретной проекции и только затем открываем файл. Заголовок записывается один раз, значения идут в том же порядке. При неправильном alias не подставляем пустую колонку, а останавливаем эту выгрузку.

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

Измените неправильный SELECT на price AS price. Затем замените DECIMAL(10,2) в учебной таблице на DECIMAL(10,3). Какие проверки должны измениться?

Разбор изменения

Первый вариант сохраняет имя price, тип DECIMAL и масштаб 2, поэтому это уже допустимая проекция, и её нельзя держать среди ожидаемых отказов. При масштабе 3 проверка намеренно останавливает экспорт: сначала согласуйте новый контракт получателя. Если нужны ровно два знака, явно решите правило округления в запросе и проверьте результат, а не удаляйте проверку metadata.

Официальные классы и фактические маршруты

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

Дальше: результат запроса, пустой набор и внутренние классы

Теперь можно объяснить, почему определённое имя API не найдено, и передать файл цен с проверенным форматом. Для происхождения полей, NULL, строк и точных чисел перейдите к реальным метаданным результата. Для нескольких наборов ответа — к чтению ответа до конца. Чтобы не путать доступный класс с публичной фабрикой объекта, продолжите границы Statement и SqlStatement. Для нового окружения начните с установки и проверки X DevAPI.

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

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

mysql_xdevapi\DriverПонять, откуда брать фактическую версию установленного драйвера, если класс из справочника недоступен.

Официальная страница не указывает версию появления. Проверенный профиль главы: PHP 8.3.35, PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. Номер PHP, версия расширения и версия сервера — разные сведения. · mysql_xdevapi

class mysql_xdevapi\Driver {
    const string version = '8.0.3';
}

Понять, откуда брать фактическую версию установленного драйвера, если класс из справочника недоступен.

От задачи к доступному API
  1. Старый пример обращается к Driver::version
  2. Сначала проверяем class_exists без автозагрузки
  3. Если класса нет, не обращаемся к его константе
  4. phpversion(mysql_xdevapi) читает загруженный модуль
  5. Отдельная проверка ColumnResult показывает пригодный маршрут для metadata

Полный официальный synopsis и его границы

class mysql_xdevapi\Driver {
    const string version = '8.0.3';
}

Официальный synopsis содержит класс Driver и одну строковую константу version со значением 8.0.3. Методы, параметры, возвращаемые значения и пояснение назначения класса на этой странице не описаны. Запись выше передаёт этот synopsis; она не создаёт класс в нашем PHP и не сообщает версию вашей установки.

Два применения с доступными средствами

При переносе приложения сначала отличите отсутствие самого расширения от несовпадения API. Если модуль не загружен, phpversion для него возвращает false. Наличие пакета Composer и его версия не заменяют эту проверку.

$loaded = extension_loaded('mysql_xdevapi');
$version = phpversion('mysql_xdevapi');
var_dump($loaded); // true в учебном профиле
echo $version; // 8.0.30 в учебном профиле

Полный самостоятельный файл и видимый результат.

Перед использованием исторического имени проверяем его доступность. false во втором параметре class_exists отключает автозагрузку. Полный запуск также сверяет настоящую глобальную MYSQLX_VERSION с версией расширения и формирует понятный маршрут замены, не создавая фиктивный Driver.

$driverAvailable = class_exists('mysql_xdevapi\\Driver', false);
$constantAvailable = defined('mysql_xdevapi\\Driver::version');
var_dump($driverAvailable, $constantAvailable); // false, false в профиле

Полный самостоятельный файл и видимый результат.

Ошибки, ограничения и выбор другого пути

В matching-исходнике PECL 8.0.30 Driver не зарегистрирован, а версия публикуется глобальными MYSQLX_VERSION и MYSQLX_VERSION_ID. Отсутствие одного класса не доказывает отсутствие всего расширения. Нельзя заменять проверку постоянной строкой 8.0.3 из справочника, объявлять её текущей или считать номером сервера MySQL. Страница не помечена deprecated: мы отмечаем конкретное расхождение реализации, а не придумываем дату удаления. Проверка наличия метода или класса не доказывает права доступа к базе и не выполняет запрос.

Установка и активный PHP подробно проверяются в диагностике расширения. Наличие другого внутреннего класса ещё не означает, что его нужно создавать через new: см. границы Statement.

Версии. Официальная страница не указывает версию появления. Проверенный профиль главы: PHP 8.3.35, PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. Номер PHP, версия расширения и версия сервера — разные сведения.

mysql_xdevapi\FieldMetadataПроверить, какие столбцы действительно вернул запрос, прежде чем отдавать ценовой файл другой системе.

Официальная страница не указывает версию появления. Проверенный профиль главы: PHP 8.3.35, PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. Номер PHP, версия расширения и версия сервера — разные сведения. · mysql_xdevapi

class mysql_xdevapi\FieldMetadata {
    public $type;
    public $type_name;
    public $name;
    public $original_name;
    public $table;
    public $original_table;
    public $schema;
    public $catalog;
    public $collation;
    public $fractional_digits;
    public $length;
    public $flags;
    public $content_type;
}

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

От задачи к доступному API
  1. SELECT выбирает sku и price
  2. getColumns возвращает реальные ColumnResult этой проекции
  3. Сравниваем имена, тип цены и два дробных знака с контрактом получателя
  4. Подходящие значения сохраняются в CSV в заданном порядке
  5. Неподходящая проекция останавливается до открытия файла

Полный официальный synopsis и его границы

class mysql_xdevapi\FieldMetadata {
    public $type;
    public $type_name;
    public $name;
    public $original_name;
    public $table;
    public $original_table;
    public $schema;
    public $catalog;
    public $collation;
    public $fractional_digits;
    public $length;
    public $flags;
    public $content_type;
}

В официальном synopsis перечислены все 13 публичных свойств. Их типы, значения по умолчанию, единицы и подробные описания не указаны; методов и параметров нет. В PECL mysql_xdevapi 8.0.30 FieldMetadata не зарегистрирован. Реальные сведения о столбцах в этой главе приходят как ColumnResult, а не как созданный нами объект с похожими свойствами.

Два применения с доступными средствами

Поставщик ожидает два поля. Если разработчик заменил alias price на unit_price, текущая версия экспортёра должна дать ясный отказ. Результат $result уже получен и удерживается в переменной; полный файл содержит SQL и проверку отсутствия rejected.csv.

$columns = $result->getColumns();
$labels = array_map(static fn($column) => $column->getColumnLabel(), $columns);
if ($labels !== ['sku', 'price']) {
    throw new RuntimeException('Ожидаются столбцы sku,price');
}

Полный самостоятельный файл и видимый результат.

После успешной проверки имён отдельно проверяем семантику цены. CAST(price AS CHAR) может оставить текст 120.00 на вид прежним, но меняет контракт результата. Полный запуск показывает отказ для такой проекции и сохраняет правильный файл из настоящего DECIMAL.

$priceColumn = $columns[1];
if ($priceColumn->getType() !== MYSQLX_TYPE_DECIMAL
    || $priceColumn->getFractionalDigits() !== 2) {
    throw new RuntimeException('Цена должна быть DECIMAL с масштабом 2');
}

Полный самостоятельный файл и видимый результат.

Ошибки, ограничения и выбор другого пути

Не используйте свойства отсутствующего FieldMetadata и не подменяйте его классом пользователя. Таблица ниже — карта доступных задач через ColumnResult, а не гарантированная эквивалентность недокументированных свойств. Metadata описывает конкретный ответ запроса; вычисления и alias могут отличаться от таблицы. Оно не проверяет все значения, допустимые цены, уникальность артикулов, права, SQL-инъекцию или безопасность будущего открытия файла. При пустом SQL-ответе в выбранном профиле metadata может отсутствовать: этот экспортёр не угадывает тип, а отказывает; маршрут явного пустого экрана уже разобран отдельно. Result нужно удерживать, пока читаются связанные сведения о столбцах.

Официальное свойствоЧто указано в baselineДоступный путь и граница
typeСвойство объявлено public; тип значения и описание на странице не заданы.Для реально полученного ColumnResult используем getType и константы MYSQLX_TYPE_*; это тип столбца ответа, не get_debug_type значения PHP.
type_nameПубличное свойство без указанного типа и пояснения.Прямого getTypeName у ColumnResult нет. Свой словарь названий допустим как прикладное отображение известных констант; неизвестный тип оставляем неизвестным.
nameПубличное свойство без документированной семантики.Для подписи результата есть getColumnLabel. Это полезный маршрут для alias, но не доказательство поведения отсутствующего FieldMetadata.
original_nameПубличное свойство без документированной семантики.У ColumnResult getColumnName описывает имя исходного столбца. Для вычисления оно может быть пустым; не выдумываем физический источник.
tableПубличное свойство без документированной семантики.getTableLabel позволяет увидеть имя/псевдоним таблицы в ответе. Отличаем его от физического имени.
original_tableПубличное свойство без документированной семантики.getTableName даёт исходное имя для доступного происхождения. У выражения источник может отсутствовать.
schemaПубличное свойство без указанного типа и пояснения.getSchemaName относится к происхождению столбца, а не автоматически к выбранной по умолчанию базе Session.
catalogПубличное свойство без указанного типа и пояснения.Прямого метода ColumnResult для catalog нет. Не добавляем выдуманное значение def.
collationПубличное свойство без указанного типа, единиц и пояснения.getCollationName возвращает имя сопоставления, когда оно доступно. Имя нельзя объявить точной заменой неизвестного числового collation.
fractional_digitsПубличное свойство без указанного типа и пояснения.getFractionalDigits позволяет проверить масштаб результата DECIMAL: в выгрузке требуется 2. Это не форматирование текста само по себе.
lengthПубличное свойство без документированных единиц.getLength относится к metadata протокола. Его нельзя без разбора назначить maxlength HTML-поля: символы и байты различаются.
flagsПубличное свойство без описанной битовой маски.Есть отдельные isNumberSigned и isPadded. Они не дают основания восстановить произвольную исходную маску flags.
content_typeПубличное свойство без указанного типа и пояснения.Прямого общедоступного getter у ColumnResult нет. Не подставляем MIME-тип файла или догадку о содержимом.

Различие подписи и происхождения поля: getColumnLabel и getColumnName. Пустая выдача раскрыта в отдельном экране без строк, а формат чисел — в точной цене и больших идентификаторах.

Версии. Официальная страница не указывает версию появления. Проверенный профиль главы: PHP 8.3.35, PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. Номер PHP, версия расширения и версия сервера — разные сведения.

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

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

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

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

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