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

Дерево руководства
Глава 41 · После создания и чтения DBF
dBase: изменить данные, удалить строки и подготовить импорт
Поиск по SKU, безопасная форма массива, пометки удаления, pack на копии, фильтрация, CSV и понятные ошибки.
Изменить остаток по артикулу и сохранить логическое поле
У A-100 остаток изменился с 12 на 15. Найдём SKU, соберём всю строку в порядке схемы и заменим запись. После закрытия прочитаем файл: активность должна остаться true, а B-200 — без изменений.
- SKU A-100 → поиск непомеченной записи
- Ассоциативная строка → QTY=15
- Порядок заголовка; L обратно в T/F/?
- replace_record → close → чтение
Что получится после повторного чтения
| SKU | Before QTY | After QTY | Active retained |
|---|---|---|---|
| A-100 | 12 | 15 | true |
| B-200 | 0 | 0 | false |
$row['QTY'] = 15;
$values = [];
foreach ($header as $field) {
$value = $row[$field['name']];
if ($field['type'] === 'boolean') { $value = $value === null ? '?' : ($value ? 'T' : 'F'); }
$values[] = $value;
}
dbase_replace_record($db, $values, $number);replace_record заменяет целую запись, поэтому получает все поля. Проходим заголовок и берём $row[$field["name"]] по порядку; deleted в схеме нет. Прочитанный bool для L превращаем обратно в T/F, неопределённое L — в ?. Слепой array_values всей прочитанной строки дал бы лишний элемент и неверный формат L. Флаг $changed отличает найденный товар от отсутствующего; finally закрывает handle при исключении.
Открыть таблицу реального запуска · Скачать данные результата. HTML генерирует сама PHP-программа из прочитанных значений; для просмотра этой таблицы сервер не нужен.
Учебные DBF: updated.dbf.
<?php
declare(strict_types=1);
require __DIR__ . '/dbf105-helper.php';
dbfSeed('updated.dbf', [['SKU', 'C', 6], ['NAME', 'C', 12], ['QTY', 'N', 4, 0], ['PRICE', 'N', 8, 2], ['ADDED', 'D'], ['ACTIVE', 'L']], [['A-100', 'Tea', 12, 19.90, '20261026', 'T'], ['B-200', 'Coffee', 0, 42.50, '20261025', 'F']]);
$before = dbfRows('updated.dbf');
$db = dbfOpen('updated.dbf', DBASE_RDWR);
try {
$header = dbase_get_header_info($db);
if ($header === false) { throw new RuntimeException('Cannot read schema'); }
$changed = false;
for ($number = 1; $number <= dbase_numrecords($db); $number++) {
$row = dbase_get_record_with_names($db, $number);
if ($row === false) { throw new RuntimeException('Cannot read row'); }
if ($row['deleted'] || rtrim($row['SKU'], ' ') !== 'A-100') { continue; }
$row['QTY'] = 15;
$values = [];
foreach ($header as $field) {
$value = $row[$field['name']];
if ($field['type'] === 'boolean') { $value = $value === null ? '?' : ($value ? 'T' : 'F'); }
$values[] = $value;
}
if (!dbase_replace_record($db, $values, $number)) { throw new RuntimeException('Cannot replace row'); }
$changed = true; break;
}
if (!$changed) { throw new RuntimeException('SKU not found'); }
} finally { dbase_close($db); }
$after = dbfRows('updated.dbf'); $rows = [];
foreach ($after as $index => $row) { $rows[] = [$row['SKU'], $before[$index]['QTY'], $row['QTY'], $row['ACTIVE']]; }
dbfReport(['SKU', 'Before QTY', 'After QTY', 'Active retained'], $rows, ['reopened' => true, 'identified_by' => 'SKU', 'deleted_key_written' => false]);
echo "updated.dbf: A-100 12 -> 15; ACTIVE remains true\n";
Ожидаемый результат
updated.dbf: A-100 12 -> 15; ACTIVE remains true
Скачайте комплект, распакуйте в пустой каталог и выполните php example.php с dbase. Helper и README включены; input.csv приложен к импорту. Helper содержит общие операции создания/чтения и отчёт; его исходник доступен в комплекте. Все DBF принадлежат этому примеру. Если файл уже существует, создание останавливается: для повтора возьмите другую пустую папку. Изменения проверяются новым открытием; таблица сохраняется в result.html и result.json.
SKU уникален здесь по договору; DBF не создаёт UNIQUE или индекс автоматически. Проекция рассчитана на обычную схему: nullable FoxPro требует сохранить null, а не всегда превращать его в ?. Несколько замен не образуют SQL-транзакцию: для отката сохраняйте закрытую копию или готовьте отдельную версию. Причину падения старой неофициальной PHP7-сборки из обсуждения нельзя достоверно перенести на текущую; здесь решаем воспроизводимую ошибку формы массива.
Реальная задача из обсуждения Dbf file modification script: Автор читает именованный массив DBF и передаёт все значения в add/replace; в массиве есть служебный deleted. Здесь написано собственное решение для обозначенного учебного договора; вопрос служит источником ситуации, а правила API сверены с PHP/PECL.
Убрать товар из активной выгрузки, сохранив помеченную запись
B-200 больше не должен попадать в выгрузку, но исходник нужен для сверки. Пометим запись и покажем решение импортёра для каждой строки. Физических строк останется три, экспортируемых товаров — два.
- delete_record(2) → метка
- numrecords → 3
- get_record → deleted=1 у B-200
- deleted → skip, остальные → export
Что получится после повторного чтения
| Record | SKU | QTY | deleted | Import decision |
|---|---|---|---|---|
| 1 | A-100 | 12 | 0 | export |
| 2 | B-200 | 0 | 1 | skip |
| 3 | C-300 | 7 | 0 | export |
if (rtrim($row['SKU'], ' ') === 'B-200' && !$row['deleted']) {
dbase_delete_record($db, $number);
}delete_record только помечает запись. Аргументы — ресурс и номер 1..numrecords, результат — true/false. Данные ещё читаются, счётчик включает эту строку. Импортёр решает: пропустить через continue или вывести в журнал исключений. Handle после пометки закрывается; новый reader показывает сохранённое состояние.
Открыть таблицу реального запуска · Скачать данные результата. HTML генерирует сама PHP-программа из прочитанных значений; для просмотра этой таблицы сервер не нужен.
Учебные DBF: marked.dbf.
<?php
declare(strict_types=1);
require __DIR__ . '/dbf105-helper.php';
dbfSeed('marked.dbf', [['SKU', 'C', 6], ['QTY', 'N', 4, 0]], [['A-100', 12], ['B-200', 0], ['C-300', 7]]);
$db = dbfOpen('marked.dbf', DBASE_RDWR);
try {
for ($number = 1; $number <= dbase_numrecords($db); $number++) {
$row = dbase_get_record_with_names($db, $number);
if ($row === false) { throw new RuntimeException('Cannot read record'); }
if (rtrim($row['SKU'], ' ') === 'B-200' && !$row['deleted']) {
if (!dbase_delete_record($db, $number)) { throw new RuntimeException('Cannot mark record'); }
}
}
} finally { dbase_close($db); }
$db = dbfOpen('marked.dbf');
try {
$physical = dbase_numrecords($db); $active = 0; $rows = [];
for ($number = 1; $number <= $physical; $number++) {
$row = dbase_get_record_with_names($db, $number);
if ($row === false) { throw new RuntimeException('Cannot read record'); }
if (!$row['deleted']) { $active++; }
$rows[] = [$number, rtrim($row['SKU'], ' '), $row['QTY'], $row['deleted'], $row['deleted'] ? 'skip' : 'export'];
}
} finally { dbase_close($db); }
dbfReport(['Record', 'SKU', 'QTY', 'deleted', 'Import decision'], $rows, ['physical_records' => $physical, 'active_records' => $active]);
echo "marked.dbf: physical=3 active=2; B-200 is marked, not physically removed\n";
Ожидаемый результат
marked.dbf: physical=3 active=2; B-200 is marked, not physically removed
Скачайте комплект, распакуйте в пустой каталог и выполните php example.php с dbase. Helper и README включены; input.csv приложен к импорту. Helper содержит общие операции создания/чтения и отчёт; его исходник доступен в комплекте. Все DBF принадлежат этому примеру. Если файл уже существует, создание останавливается: для повтора возьмите другую пустую папку. Изменения проверяются новым открытием; таблица сохраняется в result.html и result.json.
Удаление записи не удаляет файл. Получатель, игнорирующий метку, увидит B-200. Физическое удаление делает pack — отдельное решение с перенумерацией. deleted не колонка и не значение для add_record. Бизнес-поле ACTIVE также не ставит системную пометку.
Реальная задача из обсуждения Dbase oledb provider load data without this deleted records?: Импорт в SQL неожиданно включает записи, помеченные удалёнными. Здесь написано собственное решение для обозначенного учебного договора; вопрос служит источником ситуации, а правила API сверены с PHP/PECL.
Уплотнить копию DBF и увидеть новые номера записей
Получателю нужен компактный DBF без удалённых строк, но нам нужен откат. Закроем исходник, скопируем в другой путь, уплотним копию и сравним. C-300 был записью 3, станет 2; артикул сохранится.
- before.dbf: A-100, *B-200, C-300
- Закрытый файл → packed.dbf
- pack → A-100, C-300
- SKU C-300: 3 → 2; исходник неизменен
Что получится после повторного чтения
| SKU | Before record | After record | QTY |
|---|---|---|---|
| A-100 | 1 | 1 | 12 |
| C-300 | 3 | 2 | 7 |
if (!copy(__DIR__ . '/before.dbf', dbfPath('packed.dbf'))) { throw new RuntimeException('Cannot copy DBF'); }
$db = dbfOpen('packed.dbf', DBASE_RDWR);
dbase_pack($db);
dbase_close($db);copy идёт после закрытия writer, не посреди записи. pack окончательно удаляет помеченные строки и обрезает файл. После повторного открытия видны 2 строки. Сопоставляем их по SKU; номера нужны лишь для демонстрации. Хеш before.dbf до и после подтверждает неизменность исходника.
Открыть таблицу реального запуска · Скачать данные результата. HTML генерирует сама PHP-программа из прочитанных значений; для просмотра этой таблицы сервер не нужен.
Учебные DBF: before.dbf, packed.dbf.
<?php
declare(strict_types=1);
require __DIR__ . '/dbf105-helper.php';
dbfSeed('before.dbf', [['SKU', 'C', 6], ['QTY', 'N', 4, 0]], [['A-100', 12], ['B-200', 0], ['C-300', 7]]);
$db = dbfOpen('before.dbf', DBASE_RDWR);
try { if (!dbase_delete_record($db, 2)) { throw new RuntimeException('Cannot mark row'); } }
finally { dbase_close($db); }
$backupHash = hash_file('sha256', __DIR__ . '/before.dbf');
if (!copy(__DIR__ . '/before.dbf', dbfPath('packed.dbf'))) { throw new RuntimeException('Cannot copy DBF'); }
$db = dbfOpen('packed.dbf', DBASE_RDWR);
try { if (!dbase_pack($db)) { throw new RuntimeException('Cannot pack'); } }
finally { dbase_close($db); }
$before = dbfRows('before.dbf'); $after = dbfRows('packed.dbf');
$oldNumbers = []; foreach ($before as $index => $row) { $oldNumbers[$row['SKU']] = $index + 1; }
$rows = []; foreach ($after as $index => $row) { $rows[] = [$row['SKU'], $oldNumbers[$row['SKU']], $index + 1, $row['QTY']]; }
dbfReport(['SKU', 'Before record', 'After record', 'QTY'], $rows, ['before_count' => count($before), 'after_count' => count($after), 'backup_unchanged' => hash_file('sha256', __DIR__ . '/before.dbf') === $backupHash]);
echo "packed.dbf: 3 -> 2 records; C-300 record 3 -> 2; backup unchanged\n";
Ожидаемый результат
packed.dbf: 3 -> 2 records; C-300 record 3 -> 2; backup unchanged
Скачайте комплект, распакуйте в пустой каталог и выполните php example.php с dbase. Helper и README включены; input.csv приложен к импорту. Helper содержит общие операции создания/чтения и отчёт; его исходник доступен в комплекте. Все DBF принадлежат этому примеру. Если файл уже существует, создание останавливается: для повтора возьмите другую пустую папку. Изменения проверяются новым открытием; таблица сохраняется в result.html и result.json.
Это локальная копия, не снимок изменяемого сторонней программой файла. Внешних писателей нужно согласованно остановить или заблокировать. Номер записи нельзя хранить как постоянную внешнюю ссылку. Pack не восстанавливает удалённое, поэтому before.dbf сохраняется. Он не исправляет произвольные повреждения. Большое уплотнение требует места и времени на проход файла.
Экспортировать только живые строки без изменения исходного файла
Исходный файл поставщика нельзя менять, но нужен чистый импорт. Создадим live.dbf и перенесём только живые строки. Для сравнения уплотним отдельную копию compact.dbf и проверим равенство прочитанных данных.
Что получится после повторного чтения
| SKU | QTY | deleted |
|---|---|---|
| A-100 | 12 | 0 |
| C-300 | 7 | 0 |
if ($row['deleted']) { continue; }
unset($row['deleted']);
dbase_add_record($target, array_values($row));get_record возвращает числовые индексы и строковый deleted. Удаляем ключ через unset, array_values собирает плотный список. Здесь только C и N, их можно переносить так. L требует обратного T/F/?; nullable и кодировка — отдельного решения по схеме. $live === $compacted сравнивает значения и типы, а не даты заголовка и физические байты.
Открыть таблицу реального запуска · Скачать данные результата. HTML генерирует сама PHP-программа из прочитанных значений; для просмотра этой таблицы сервер не нужен.
Учебные DBF: source.dbf, live.dbf, compact.dbf.
<?php
declare(strict_types=1);
require __DIR__ . '/dbf105-helper.php';
$schema = [['SKU', 'C', 6], ['QTY', 'N', 4, 0]];
dbfSeed('source.dbf', $schema, [['A-100', 12], ['B-200', 0], ['C-300', 7]]);
$db = dbfOpen('source.dbf', DBASE_RDWR);
try { if (!dbase_delete_record($db, 2)) { throw new RuntimeException('Cannot mark row'); } }
finally { dbase_close($db); }
$sourceHash = hash_file('sha256', __DIR__ . '/source.dbf');
$source = dbfOpen('source.dbf');
$target = dbase_create(dbfPath('live.dbf'), $schema);
if ($target === false) { dbase_close($source); throw new RuntimeException('Cannot create export'); }
try {
for ($number = 1; $number <= dbase_numrecords($source); $number++) {
$row = dbase_get_record($source, $number);
if ($row === false) { throw new RuntimeException('Cannot read source'); }
if ($row['deleted']) { continue; }
unset($row['deleted']);
if (!dbase_add_record($target, array_values($row))) { throw new RuntimeException('Cannot export row'); }
}
} finally { dbase_close($target); dbase_close($source); }
// Второй способ на отдельной закрытой копии: сравниваем данные, а не байты заголовка.
if (!copy(__DIR__ . '/source.dbf', dbfPath('compact.dbf'))) { throw new RuntimeException('Cannot clone source'); }
$compact = dbfOpen('compact.dbf', DBASE_RDWR);
try { if (!dbase_pack($compact)) { throw new RuntimeException('Cannot compact clone'); } }
finally { dbase_close($compact); }
$live = dbfRows('live.dbf'); $compacted = dbfRows('compact.dbf'); $rows = [];
foreach ($live as $row) { $rows[] = [$row['SKU'], $row['QTY'], $row['deleted']]; }
dbfReport(['SKU', 'QTY', 'deleted'], $rows, ['source_unchanged' => hash_file('sha256', __DIR__ . '/source.dbf') === $sourceHash, 'same_rows_as_packed_copy' => $live === $compacted]);
echo "live.dbf: 2 records; source unchanged; same rows as packed copy\n";
Ожидаемый результат
live.dbf: 2 records; source unchanged; same rows as packed copy
Скачайте комплект, распакуйте в пустой каталог и выполните php example.php с dbase. Helper и README включены; input.csv приложен к импорту. Helper содержит общие операции создания/чтения и отчёт; его исходник доступен в комплекте. Все DBF принадлежат этому примеру. Если файл уже существует, создание останавливается: для повтора возьмите другую пустую папку. Изменения проверяются новым открытием; таблица сохраняется в result.html и result.json.
Пересоздание позволяет выбирать поля и проверять строки; pack сохраняет структуру и убирает пометки. Это не миграция любого DBF: memo и индексы не поддержаны, системные поля FoxPro скрыты API. Два handle открыты для разных файлов. Хеш источника после формирования метки проверяется в конце.
Проверить CSV перед записью и выдать отчёт о принятых строках
CSV содержит повтор артикула, отрицательное количество и длинный SKU. Сначала проверим текст, затем создадим DBF принятых строк. Отдельно применим подтверждённую поправку B-200 с нуля до двух. input.csv приложен; номера отказов и причины попадут в result.json.
- CSV SKU;QTY → массив строки
- SKU + цифры 0..9999 + уникальность
- Принятые → imported.dbf
- Поправка B-200 → replace_record
- Переоткрытие → 12, 2, 7
Что получится после повторного чтения
| SKU | Confirmed QTY |
|---|---|
| A-100 | 12 |
| B-200 | 2 |
| C-300 | 7 |
Входной CSV
| Строка | Вход | Решение |
|---|---|---|
| 2 | A-100;12 | Принять |
| 3 | B-200;0 | Принять |
| 4 | A-100;9 | Повтор SKU |
| 5 | C-300;-2 | Количество вне 0..9999 |
| 6 | TOO-LONG;1 | SKU длиннее 6 |
| 7 | C-300;7 | Принять: прежняя строка была отклонена |
if (($textQty === '' || strspn($textQty, '0123456789') !== strlen($textQty)) || strlen($textQty) > 4) { $rejected[] = [$line, 'QTY must be 0..9999']; continue; }
if (isset($seen[$sku])) { $rejected[] = [$line, 'duplicate SKU']; continue; }
$seen[$sku] = true; $accepted[] = [$sku, (int)$textQty];fgetcsv использует ;, кавычки " и явный пустой escape. strspn считает допустимые символы; равенство strlen требует допустимость всей строки. Наш договор: SKU — 1..6 ASCII-букв/цифр/дефисов, количество — 1..4 цифры, повтор принятого SKU отклоняется. $seen хранит принятые артикулы, $accepted — строки, $rejected — номер и причину. Заголовок CSV имеет номер 1, первая строка данных — 2.
Открыть таблицу реального запуска · Скачать данные результата. HTML генерирует сама PHP-программа из прочитанных значений; для просмотра этой таблицы сервер не нужен.
Учебные DBF: imported.dbf.
<?php
declare(strict_types=1);
require __DIR__ . '/dbf105-helper.php';
$csv = fopen(__DIR__ . '/input.csv', 'rb');
if ($csv === false) { throw new RuntimeException('Cannot read CSV'); }
$accepted = []; $rejected = []; $seen = [];
try {
$header = fgetcsv($csv, 0, ';', '"', '');
if ($header !== ['SKU', 'QTY']) { throw new InvalidArgumentException('Unexpected CSV columns'); }
$line = 1;
while (($row = fgetcsv($csv, 0, ';', '"', '')) !== false) {
$line++;
if (count($row) !== 2 || $row[0] === '' || strlen($row[0]) > 6 || strspn($row[0], 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-') !== strlen($row[0])) {
$rejected[] = [$line, 'invalid SKU']; continue;
}
[$sku, $textQty] = $row;
if (($textQty === '' || strspn($textQty, '0123456789') !== strlen($textQty)) || strlen($textQty) > 4) { $rejected[] = [$line, 'QTY must be 0..9999']; continue; }
if (isset($seen[$sku])) { $rejected[] = [$line, 'duplicate SKU']; continue; }
$seen[$sku] = true; $accepted[] = [$sku, (int)$textQty];
}
} finally { fclose($csv); }
dbfSeed('imported.dbf', [['SKU', 'C', 6], ['QTY', 'N', 4, 0]], $accepted);
$rows = []; foreach (dbfRows('imported.dbf') as $row) { $rows[] = [$row['SKU'], $row['QTY']]; }
// Отдельная корректировка подтверждённого остатка после импорта.
$db = dbfOpen('imported.dbf', DBASE_RDWR);
try {
$second = dbase_get_record($db, 2);
if ($second === false || rtrim($second[0], ' ') !== 'B-200') { throw new RuntimeException('Unexpected second record'); }
unset($second['deleted']); $second[1] = 2;
if (!dbase_replace_record($db, array_values($second), 2)) { throw new RuntimeException('Cannot apply stock correction'); }
} finally { dbase_close($db); }
$rows = []; foreach (dbfRows('imported.dbf') as $row) { $rows[] = [$row['SKU'], $row['QTY']]; }
dbfReport(['SKU', 'Confirmed QTY'], $rows, ['accepted' => count($accepted), 'rejected' => $rejected, 'correction' => 'B-200: 0 -> 2']);
echo "imported.dbf: accepted=3 rejected=3; B-200 corrected to 2\n";
Ожидаемый результат
imported.dbf: accepted=3 rejected=3; B-200 corrected to 2
Скачайте комплект, распакуйте в пустой каталог и выполните php example.php с dbase. Helper и README включены; input.csv приложен к импорту. Helper содержит общие операции создания/чтения и отчёт; его исходник доступен в комплекте. Все DBF принадлежат этому примеру. Если файл уже существует, создание останавливается: для повтора возьмите другую пустую папку. Изменения проверяются новым открытием; таблица сохраняется в result.html и result.json.
Отрицательное число и пустота не превращаются в 0. Политика принимает корректные строки и выдаёт список отказов; для «всё или ничего» отмените создание при непустом rejected. Ограничивайте размер входа; большой импорт нужен потоковый. Поправка после импорта — отдельное подтверждённое действие, не право случайного дубликата перезаписать товар. Для переноса в SQL следующий шаг — подключение и INSERT через PDO.
Различить ошибку доступа, неверный номер и закрытый ресурс
«Не работает DBF» может означать разные причины. В отдельном каталоге воспроизведём отсутствие файла, режим 1, запись через reader, номер 0, закрытый ресурс и лишний deleted. Исходная запись должна остаться целой.
- Один ожидаемый отказ → перехват E_WARNING
- false отдельно от исключения
- ValueError: номер 0
- TypeError: закрытый ресурс
- Чтение после отказов → A-100=12
Что получится после повторного чтения
| Situation | Outcome | Warning captured | Exception |
|---|---|---|---|
| missing file | false | true | NULL |
| write-only mode | false | true | NULL |
| read-only write | false | true | NULL |
| record zero | exception | false | ValueError |
| closed handle | exception | false | TypeError |
| extra deleted key | false | true | NULL |
$missing = dbfAttempt(fn() => dbase_open(__DIR__ . '/absent.dbf', DBASE_RDONLY));
$zero = dbfAttempt(fn() => dbase_get_record($db, 0));dbfAttempt из helper временно сохраняет E_WARNING и восстанавливает обработчик в finally. Она различает false и Throwable. Это наблюдаемый контракт PHP 8.5.11 / dbase 7.1.1: файловые отказы дают warning+false, номер 0 — ValueError, закрытый ресурс — TypeError. Не ставьте @ перед всеми операциями: неожиданный сбой должен быть виден.
Открыть таблицу реального запуска · Скачать данные результата. HTML генерирует сама PHP-программа из прочитанных значений; для просмотра этой таблицы сервер не нужен.
Учебные DBF: safe.dbf.
<?php
declare(strict_types=1);
require __DIR__ . '/dbf105-helper.php';
dbfSeed('safe.dbf', [['SKU', 'C', 6], ['QTY', 'N', 4, 0]], [['A-100', 12]]);
$missing = dbfAttempt(fn() => dbase_open(__DIR__ . '/absent.dbf', DBASE_RDONLY));
$writeOnly = dbfAttempt(fn() => dbase_open(__DIR__ . '/safe.dbf', 1));
$db = dbfOpen('safe.dbf');
try {
$readOnly = dbfAttempt(fn() => dbase_add_record($db, ['B-200', 5]));
$zero = dbfAttempt(fn() => dbase_get_record($db, 0));
} finally { dbase_close($db); }
$closed = dbfAttempt(fn() => dbase_numrecords($db));
$db = dbfOpen('safe.dbf', DBASE_RDWR);
try {
$withFlag = dbase_get_record($db, 1);
$extra = dbfAttempt(fn() => dbase_add_record($db, $withFlag));
} finally { dbase_close($db); }
$rows = [];
foreach (['missing file' => $missing, 'write-only mode' => $writeOnly, 'read-only write' => $readOnly, 'record zero' => $zero, 'closed handle' => $closed, 'extra deleted key' => $extra] as $case => $result) {
$rows[] = [$case, $result['kind'], $result['warning'], $result['class']];
}
$saved = dbfRows('safe.dbf');
dbfReport(['Situation', 'Outcome', 'Warning captured', 'Exception'], $rows, ['records_after_errors' => count($saved), 'original_qty' => $saved[0]['QTY']]);
echo "safe.dbf: 6 failure cases handled; original record unchanged\n";
Ожидаемый результат
safe.dbf: 6 failure cases handled; original record unchanged
Скачайте комплект, распакуйте в пустой каталог и выполните php example.php с dbase. Helper и README включены; input.csv приложен к импорту. Helper содержит общие операции создания/чтения и отчёт; его исходник доступен в комплекте. Все DBF принадлежат этому примеру. Если файл уже существует, создание останавливается: для повтора возьмите другую пустую папку. Изменения проверяются новым открытием; таблица сохраняется в result.html и result.json.
Текст warning зависит от ОС, пути и версии. В публичный отчёт не выдаём абсолютные серверные пути; в рабочем приложении сохраняйте первичную диагностику в закрытый журнал. Эти примеры не проверяют права FPM или многопроцессные гонки. Режим 1 не исправляется памятью, лишняя колонка — кодировкой. Устраняйте причину, не повторяйте запись бесконечно.
Самостоятельная задача и следующий шаг
Задача: из исходного DBF выдать только живые товары с остатком больше нуля; источник сохранить. Предложенное решение: открыть источник DBASE_RDONLY, проверить заголовок, для каждого номера 1..numrecords получить строку, пропустить deleted=1 и QTY<=0, собрать поля по известной схеме, создать новый файл и добавить принятые строки. После закрытия заново прочитать результат и сверить артикулы/количество. Это развитие примера экспорта живых строк, а не удаление из источника. Для схемы с L перед записью верните T/F/?.
Следующий шаг зависит от задачи: файловый словарь DBA, импорт в SQL через PDO, потоковые файлы. Даты и денежные правила задаются договором приложения, а не форматом DBF.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
dbase_numrecordsПолучить границы обхода записей
PECL dbase7.0+; examples PHP8.2+ · dbase
dbase_numrecords(resource $database): int|falseПолучить границы обхода записей.
| Параметр | Что передать |
|---|---|
| database | Ресурс dbase, введённый вместо integer в 7.0.0. |
Результат и отказ. Число физических записей или false при отказе. Помеченные на удаление записи входят в число. Если записей нет, результат 0, а не false. Цикл должен идти с 1 по это число включительно.
Границы применения. Число активных товаров вычисляется отдельно по deleted и бизнес-условиям. После delete счётчик может не измениться, после pack уменьшается. Если вы меняете число записей во время обхода, заранее продумайте границы и порядок: учебные сценарии отделяют чтение, запись и уплотнение.
Два применения с выполненными программами
Пройти записи с единицы и вычислить общий остаток: stock.dbf: 3 records; total_qty=19
Убрать товар из активной выгрузки, сохранив помеченную запись: marked.dbf: physical=3 active=2; B-200 is marked, not physically removed
Связанные операции: dbase_numfields, dbase_pack.
dbase_replace_recordЗаменить содержимое существующей записи
PECL dbase7.0+; examples PHP8.2+ · dbase
dbase_replace_record(resource $database, array $data, int $number): boolЗаменить содержимое существующей записи.
| Параметр | Что передать |
|---|---|
| database | Ресурс с доступом чтение/запись; с 7.0.0 вместо integer. |
| data | Позиционные значения всех полей в порядке схемы, без deleted. Число элементов должно совпасть с numfields. |
| number | Существующий номер 1..numrecords, найденный по устойчивому ключу вроде SKU. |
Результат и отказ. true при успешной замене, false при отказе. Это замена целой записи, не изменение единственного названного поля и не upsert по SKU. Ошибка формы data приводит к отказу; неверные значения аргументов современной сборки могут выбросить исключение.
Границы применения. Официальная заметка предупреждает об обратной записи L как 0/1; в актуальной версии чтение даёт bool/null, но проблема формата записи остаётся: нужны T/F/?. Удалите служебный ключ и проецируйте по схеме. Не обещайте многострочный откат или SQL-изоляцию; сохраняйте отдельный исходник для пакетных изменений.
Два применения с выполненными программами
Изменить остаток по артикулу и сохранить логическое поле: updated.dbf: A-100 12 -> 15; ACTIVE remains true
Проверить CSV перед записью и выдать отчёт о принятых строках: imported.dbf: accepted=3 rejected=3; B-200 corrected to 2
Связанные операции: dbase_add_record, dbase_get_header_info.
dbase_delete_recordПометить строку для удаления
PECL dbase7.0+; examples PHP8.2+ · dbase
dbase_delete_record(resource $database, int $number): boolПометить строку для удаления.
| Параметр | Что передать |
|---|---|
| database | Открытый для записи ресурс dbase; с 7.0.0 вместо integer. |
| number | Номер существующей строки 1..numrecords. |
Результат и отказ. true при успешной пометке, false при отказе. Запись физически остаётся, get_record/get_record_with_names возвращают deleted=1, numrecords продолжает её считать. Окончательное удаление выполняет pack.
Границы применения. Не считайте это DELETE из SQL: получатель, игнорирующий метку, увидит строку. Сначала решите, нужен ли журнал исключений и сохранение исходника. Номер не постоянен после pack; находите товар по SKU. Метка не равно ACTIVE, а удаление записи не означает unlink файла.
Два применения с выполненными программами
Убрать товар из активной выгрузки, сохранив помеченную запись: marked.dbf: physical=3 active=2; B-200 is marked, not physically removed
Уплотнить копию DBF и увидеть новые номера записей: packed.dbf: 3 -> 2 records; C-300 record 3 -> 2; backup unchanged
Связанные операции: dbase_pack, dbase_get_record_with_names.
dbase_packФизически удалить помеченные строки и уплотнить файл
PECL dbase7.0+; examples PHP8.2+ · dbase
dbase_pack(resource $database): boolФизически удалить помеченные строки и уплотнить файл.
| Параметр | Что передать |
|---|---|
| database | Ресурс файла, открытый для записи; с 7.0.0 вместо integer. |
Результат и отказ. true при успешном уплотнении, false при отказе. Все записи, ранее помеченные delete_record, удаляются окончательно. После успешного pack файл усекается, в отличие от исторической команды PACK dBASE III, упомянутой PHP manual.
Границы применения. Оставшиеся записи перенумеровываются. Для восстановления сохраняйте закрытую согласованную копию перед операцией. Pack не создаёт индекс, не лечит произвольное повреждение и не превращает DBF в транзакционную базу. Альтернатива для неизменяемого источника — отфильтровать данные и создать новый файл по проверенной схеме.
Два применения с выполненными программами
Уплотнить копию DBF и увидеть новые номера записей: packed.dbf: 3 -> 2 records; C-300 record 3 -> 2; backup unchanged
Экспортировать только живые строки без изменения исходного файла: live.dbf: 2 records; source unchanged; same rows as packed copy
Связанные операции: dbase_delete_record, dbase_create.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


