БлогNot. PHP: делаем "мини-БД" на текстовом файле

PHP: делаем "мини-БД" на текстовом файле

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

Прежде всего, определимся, что все файлы будут в кодировке Юникод (utf-8). Использовать в качестве текстового редактора стандартный Блокнот нельзя из-за добавляемых им в начало файла меток BOM, а использовать Notepad++ нужно с осторожностью - на пустом файле сразу скажите Кодировки - Кодировать в UTF-8 (без BOM), затем Синтаксис - PHP, потом уже пишите или вставляйте текст и выполняйте Файл - Сохранить. Лучше всего, конечно, встроенный редактор файл-менеджера Far, в котором Вы нажали комбинацию клавиш Shift+F8 и выбрали кодовую страницу 65001 (UTF-8).

Иначе, из-за лишней и невидимой BOM в начале файла, будете получать вот такую ерунду, ставящую в полный тупик начинающих:

Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers
already sent (output started at ...) in ...\function.php on line 2

Определим требования к скрипту. Он должен:

  • состоять из отдельных коротких модулей, чтобы можно было легко читать и модифицировать код;
  • корректно отображаться, например, сразу "понимать" Юникод;
  • не быть чувствительным к настройкам кавычек в PHP и позволять хранить в текстовом файле любые строки, в том числе, со спецсимволами PHP/HTML;
  • разумно фильтровать ввод пользователя, например, избавлять его от лишних разделителей;
  • уметь добавлять, удалять, редактировать и сортировать записи.

В стороне пока оставляем следующие вопросы:

Формат записи базы определим тоже простейшим - в одной строке текстового файла будут содержаться некое Имя (произвольная строка длиной до 30 символов включительно) и Число (целое значение, занимающие до 6 знакомест). Разделитель записей внутри строки назначим определённой в файле конфигурации константой DIV.

Напишем файлы head.php и foot.php - общие верх и низ всех страниц, подключать их будем оператором include.

Файл head.php
<?php
echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>МИНИ-БД</title>
</head>
<body>'."\n";
?>
Файл foot.php
<?php
 echo "\n".'</body></html>';
?>

Мы включили только минимально необходимые мета-теги - тип документа и кодировку.

Файл config.php будет содержать общие настройки скрипта, пока это только имя файла с данными (предполагается, что он в текущей папке, той же, где и все остальные файлы) и разделитель записей в строке данных. Назначим этим разделителем табуляцию, а защиту от случайного или намеренного ввода пользователем табуляции внутри строк "Имя" и "Число" (например, даже в однострочное поле <input type="text"> можно вставить символ табуляции из Буфера Обмена) возложим на функции разбора параметров, которые все разделители в строке сделают пробелами.

Файл config.php
<?php
 define ('FILENAME','data.txt');
 define ('DIV',"\t");
?>

Перейдём к функционалу. Как и любой скрипт, позволяющий пользователю что-то вводить в HTML-формы, наш продукт должен будет фильтровать пользовательский ввод, передаваемый методами GET, POST, и, возможно, через глобальный массив $_SESSION. Как минимум, нужно "критичные" для разметки символы заменить на их "HTML-изображения" стандартной функцией htmlspecialchars, разобраться с настройками "магических кавычек", всё ещё актуальными в PHP версий до 5.4.0 (стандартной функции нет, напишем собственную с именем magic), удалить в данных пользователя лишние разделители в начале и конце строк, а также между словами (стандартной функции нет, реализуем собственную с именем trimall).

Чтобы не писать в каждом модуле однотипного кода по обработке параметров, напишем "универсальный" модуль-обработчик параметров params.php, а в конкретных модулях для работы с ним нужно будет определить в массиве с именем $params разрешённые имена получаемых "извне" переменных, например

$params = array('action','name','number','status');

и подключить сам модуль строкой кода

require_once ('params.php');

Всё остальное он сделает сам, в том числе, создаст "пустые" переменные для случаев, если какая-то из разрешённых величин не передана в скрипт.

Файл params.php
<?php
 if (isset($params) && !empty($params)) {
  while (list($num,$var) = each($params)) {
   if (isset($_POST[$var])) $$var = trimall(htmlspecialchars(magic($_POST[$var])));
   else if (isset($_GET[$var])) $$var = trimall(htmlspecialchars(magic($_GET[$var])));
   else if (isset($_SESSION[$var])) {
    $$var = trimall(htmlspecialchars(magic($_SESSION[$var])));
    unset ($_SESSION[$var]);
   }
   else $$var = '';
  }
 }
?>

Что исправить в модуле для PHP 5.5 и выше?

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

Хорошо, когда все служебные функции объединены в один модуль, который основные модули подключают через директиву require_once. Мы свой модуль служебных функций назовём function.php и, кроме упомянутых методов trimall и magic, включим туда следующие функции:

  • read() - будет читать текущую базу и возвращать массив записей;
  • write($a) - будет записывать массив записей $a в файл;
  • get_index_by_name ($a,$name) - будет искать по имени $name соответствующую запись и возвращать её номер (с нуля) или значение -1, если запись не найдена. Это пригодится, чтобы отличать добавление новой записи от редактирования существующей.

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

Файл function.php
<?php
session_start();
require_once ('config.php');
function trimall ($string) { //убрать лишние разделители
 return preg_replace("/(^\s*)|(\s*$)/","",preg_replace("/\s+/"," ",trim($string)));
}
function magic($path) { //убрать возможные проблемы с настройками кавычек
 ini_set('magic_quotes_runtime', '0'); ini_set('magic_quotes_sybase', '0');
 return (@get_magic_quotes_gpc()=='1' ? stripslashes($path) : $path);
}
function read () {
 return array_filter (explode("\n",@file_get_contents (FILENAME)),function ($var) { return(!empty($var)); });
}
function write ($a) {
 @file_put_contents (FILENAME,implode("\n",$a));
}
function get_index_by_name ($a,$name) {
 $name = mb_strtolower(trimall($name),'UTF-8');
 foreach ($a as $index=>$item) {
  list ($myname,$mynumber) = explode (DIV, $item);
  if ($name == mb_strtolower(trimall($myname),'UTF-8')) return $index;
 }
 return -1;
}
?>

Обратите внимание, что функция read дополнительно фильтрует массив записей от пустых строк (на всякий случай, вообще-то их не должно возникать), а trimall сначала заменит любую непустую цепочку разделителей на один пробел (вторая preg_replace), а затем удалит возможные лишние разделители в начале и конце оставшейся строки (первая preg_replace).

Также есть нюанс с функцией read в смысле совместимости кода. Так как в единственной её строке есть анонимная функция, предполагается использование PHP версии не ниже 5.3. Если надо ниже - замените код на такой, где функция фильтрации именована или совсем не используется, как тут:

function read () {
 $str=@file_get_contents (FILENAME);
 $a=explode("\n",$str);
 return $a;
}

Также важно корректное приведение строки в Юникоде к нижнему регистру (см. mb_strtolower в коде). А локалью мы здесь не пользуемся.

Кажется, настало время писать основной файл index.php. Он будет решать следующие задачи:

  • выводить форму для добавления новой записи, которую обрабатывает модуль add.php;
  • рядом с формой добавления выведем дополнительные команды - очистка формы "самовызовом" скрипта без параметров (кнопка <input type="reset"> здесь не подойдёт, т.к. не передаёт на сервер данных) и ссылку для обращения к модулю сортировки записей по имени sort.php;
  • получать от других модулей результаты их работы в виде числовой переменной $status и выводить соответствующие сообщения (массив $status_msg). Значение $status, равное нулю, будет принято по умолчанию, ему соответствует вывод краткой справки о работе программы;
  • если база непуста, показать её записи и обеспечить переход к редактированию или удалению.

Чтобы не перегружать таблицу дополнительными кнопками и ссылками, сделаем щелчок по имени переходом к редактированию записи модулем edit.php, а щелчку по полю "Число" будет соответствовать удаление записи модулем del.php.

Вот как всё выглядит при нескольких добавленных записях:

Внешний вид скрипта "Мини-БД на текстовом файле"
Внешний вид скрипта "Мини-БД на текстовом файле"
Файл index.php
<?php
 require_once ('function.php');
 $params = array('name','number','status');
 require_once ('params.php');
 include 'head.php';
 $a = read ();
 echo '<form action="add.php" method="post">
  Имя: <input type="text" name="name" value="'.$name.'" size="30" maxlength="30">
  Число: <input type="text" name="number" value="'.intval($number).'" size="6" maxlength="6">
  <input type="submit" value="Добавить"> <a href="index.php">Очистить</a> <a href="sort.php">Сортировать</a></form>';
 
 $status_msg = array (
  'Щёлкните по Имени для редактирования или по Числу для удаления записи',
  'Запись уже существует, исправлено значение в ней',
  'Не удалось найти запись с указанным номером',
  'Не переданы данные для добавления'
 );
 if (empty($status)) $status = 0;
 echo '<p>'.$status_msg[$status].'</p>';

 if (count($a)>0) {
  echo '<table cellpadding="4" cellspacing="0" border="1">'."\n".
   '<tr><th>Имя</th><th>Число</th></tr>'."\n";
  foreach ($a as $index=>$item) {
   echo '<tr>'."\n";
   list ($name, $number) = explode (DIV,$item);
   echo '<td><a href="edit.php?id='.$index.'"><span title="Править">'.htmlspecialchars(trimall($name)).
        '</span></a></td>'."\n".
        '<td><a href="del.php?id='.$index.'"><span title="Удалить">'.intval(trimall($number)).
        '</span></a></td>'."\n".
        '</td></tr>'."\n";
  }
  echo "</table>"."\n";
 }
 include 'foot.php';
?>

Теперь займёмся недостающими модулями. У add.php, кажется, простая задача - получить от index.php переменные $name и $number и записать их в файл. Однако, модуль должен проверить, что ему переданы непустые данные, а также уметь отличать ситуацию, когда введено уже существующее в базе имя от ввода новой записи (см. if ($id>-1) { ... } else { ... } в коде). В последнем случае запись всегда добавляется в конец, ведь будет модуль сортировки строк по алфавиту.

Также важно, что строка $name, "пропущенная" через обработчик параметров, уже лишена "критичных" для разметки символов вроде ', ", < и >, а в массиве $a, прочитанном из файла данных, все строки лежат "как есть", и <>123 не будет найдена, если $name после обработки превратилась &lt;&gt;123. Поэтому функции поиска записи, названной нами get_index_by_name, передаётся строка, преобразованная "обратно" к первоначальному виду с помощью стандартной функции htmlspecialchars_decode (доступна с PHP 5.1). В том же виде строка возвращается обратно в index.php через массив $_SESSION. Это обеспечит некоторое удобство работы - после ввода новой записи её данные останутся в форме и можно будет внести ещё одну запись, мало отличающуюся по имени ("Иванова" после "Иванов").

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

Файл add.php
<?php
 require_once ('function.php');
 $params = array('name','number');
 require_once ('params.php');
 $status = 0;
 if (!empty($name) && isset($number)) {
  $number = intval ($number);
  $a = read();
  $id = get_index_by_name ($a, htmlspecialchars_decode($name));
  $new_string = htmlspecialchars_decode($name).DIV.intval($number);
  if ($id>-1) {
   list ($name,$number0) = explode (DIV,$a[$id]);
   $a[$id] = $new_string;;
   $status=1;
  }
  else {
   $id = count($a)+1;
   array_push ($a,$new_string);
  }
  write ($a);
  $_SESSION['id'] = intval($id);
 }
 else $status=3;
 $_SESSION['name'] = htmlspecialchars_decode($name);
 $_SESSION['number'] = intval($number);
 header ('Location: index.php'.($status==0?'':'?status='.$status));
?>

Теперь о редактировании, оно будет реализовано в edit.php. Форма редактирования - почти такая же, как форма добавления, мы просто поленились сделать отдельную функцию для вывода формы. Важнее то, что модуль сам будет обработчиком данных, переданных через форму редактирования, а значит, должен отличать ситуацию, когда он только вызван, от той, когда пользователь нажал "Сохранить". Последней задаче служит проверка

if (!empty($_POST['submit']) && !empty($name) && isset($number) && isset($id)) {

определяющая, была ли нажата кнопка и переданы все данные. Вторая ветка -

else if (isset($a[$id])) {

предназначена для ситуации, когда в массиве $a есть запись, номер которой передан скрипту и она должна быть отредактирована. Номер записи сохраняется в скрытом HTML-поле <input type="hidden">.

Файл edit.php
<?php
 require_once ('function.php');
 $params = array('id','name','number');
 require_once ('params.php');
 $a = read ();
 $status = 0;
 if (!empty($_POST['submit']) && !empty($name) && isset($number) && isset($id)) {
  $a = read ();
  $a[$id] = htmlspecialchars_decode($name).DIV.intval($number);
  write ($a);
 }
 else if (isset($a[$id])) {
  list ($name,$number) = explode (DIV, $a[$id]);
  include 'head.php';
  echo '<form action="edit.php" method="post">
   <input type="hidden" name="id" value="'.$id.'">
   Имя: <input type="text" name="name" value="'.htmlspecialchars(trimall($name)).'" size="30" maxlength="30">
   Число: <input type="text" name="number" value="'.intval(trimall($number)).'" size="6" maxlength="6">
   <input type="submit" name="submit" value="Сохранить"> <a href="index.php">Назад</a></form>';
  include 'foot.php';  
  exit (0);
 }
 else $status = 2;
 header ('Location: index.php'.($status==0?'':'?status='.$status));
?>

Модуль удаления записи del.php будет достаточно простым, всё, что ему понадобится - получить допустимый $id записи (номер элемента в массиве $a), убрать соответствующий элемент из массива, переписать файл и вернуться на страницу главного модуля.

Файл del.php
<?php
  require_once ('function.php');
  $params = array('id');
  require_once ('params.php');
  $status = 0;
  if (isset($id)) {
   $a=read();
   if (isset($a[$id])) unset ($a[$id]);
   else $status = 2;
   write ($a);
  }
  header ('Location: index.php'.($status==0?'':'?status='.$status));
?>

Наконец, модуль сортировки sort.php породит новую проблему - как сортировать строки в Юникоде по алфавиту, не различая больших и маленьких букв? "Прямая" сортировка с помощью стандартной функции sort подойдёт едва ли - она считает строчную и прописную букву разными символами. Локаль мы не ставили, тем более, для её установки единой формы записи для всех операционок нет.

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

Применяемое "прямое" сравнение односимвольных строк Юникода, думается, не совсем корректно, но strcmp сравнивает строки побайтово и нам не подойдёт, а вообще-то корректное сравнение любых строк в Юникоде - очень непростая задача... У меня для русского и английского в системе всё сработало, например, после сортировки получались естественные порядки слов, такие как

абба, Авка, авклит, бася, Боби, Бобик, бобика, Бобина	

Файл sort.php
<?php
 require_once ('function.php');
 $a = read();

 function cmp ($a0,$b0) {
  $a = mb_strtolower(trimall($a0),'UTF-8');
  $b = mb_strtolower(trimall($b0),'UTF-8');
  $alen = mb_strlen ($a,'UTF-8');
  $blen = mb_strlen ($b,'UTF-8');
  for ($i=0; $i<min($alen,$blen); $i++) {
   $ca = mb_substr ($a, $i, 1, 'UTF-8');
   $cb = mb_substr ($b, $i, 1, 'UTF-8');
   if ($ca<$cb) return -1;
   else if ($ca>$cb) return 1;
  }
  if ($alen<$blen) return -1;
  else if ($alen>$blen) return 1;
  else return 0;
 }

 usort ($a,'cmp');
 write ($a);
 header ('Location: index.php');
?>

Нам остаётся создать в папке файл с именем .htaccess, где мы пропишем кодировкой по умолчанию Юникод и укажем директивы настройки кавычек для сайта, чтоб всё работало даже на Денвере...

В этой же папке создадим пустой (0 байт) файл data.txt (необязательно, если все права настроены).

Файл .htaccess
AddDefaultCharset utf-8
php_flag magic_quotes_gpc off
php_flag magic_quotes_runtime off
php_flag magic_quotes_sybase off

Можете посмотреть, что получилось, и о найденных проблемах сообщить мне, я написал скрипт очень оперативно, в 2 приёма, и мог что-то не продумать :)

 Скачать все файлы скрипта в архиве .zip (5 Кб)

15.10.2014, 13:51 [62723 просмотра]


теги: php textprocessing html программирование форматы

показать комментарии (5)