Архивная версия сайта e-luge.net. Последняя запись сделана 1 марта 2011 года.
Город съехавших крыш
08-03-2010 05:15   |  Метки: php, бд, разработка, велосипед, файлы

Время от времени на форуме появляются вопросы связанные с использованием файлов. Что-то вроде самописной базы данных. Чаще всего проблемы возникают с задачами поиска в файле и обновлении данных. Естественно, чаще всего товарищу советуют перестать заниматься ерундой и начать изучать нормальные СУБД (mysql, postgresql и т.д.). Да оно и правильно, вроде. Зачем пеписывать на php то, что давным-давно реализовано на более быстрых языках? А довод, что большинство БД хранят эти самые данные в файлах, воспринимается больше как щутка.

А почему бы не поробовать-таки сделать что-то более-менее приемлимое с использованием файлов? Толку, конечно, не много — сочинять или выискивать хитрые алгоритны работы с файлами глупо. Всё равно всю скорость алгоритмов сведёт на нет скорость работы интерпретатора php. Но просто по принципу «возжа под хвост попала» сделать захотелось. К тому же, будет куда ссылки давать в качестве примера.

Ничего особенного делать не собирался. Поставил себе задачу сделать небольшую библиотеку для записи / чтения / поиска в файл и обёртку для неё с возможностью, передав массив параметров, получить необходимый результат. Ну не писать же свой язык запросов :)Так же хотелось сделать что-то, что можно было бы использовать если не в реальном, то хотя бы, в учебном проекте. Т.е. не просто записали слово в файл - прочитали это слово, а более-менее (скорее всё же менее:)) функциональную СУБД. Не слабо так замахнулся, да?

Решил ничего не усложнять. Писать в файл сериализованный массив и номер строки. Таким образом имеем ряд данных (массив) и некое подобие индекса для поиска (номер строки). Индекс хранится для каждой таблицы в отдельном файле. Хотя, не индекс, просто счётчик строк для таблицы, позволяющий организовать некое подобие автоинкремента.

Вот что в итоге получилось:

helperFiles

  1. <?php
  2. /**
  3.  * Класс работы с файловой псевдо-БД
  4.  *
  5.  * @author Павел Новицкий <lugebox@gmail.com>
  6.  * @link http://www.e-luge.net/
  7.  */
  8. class helperFiles {
  9.  
  10. /**
  11. * Расширение для файлов бд
  12. *
  13. * @var string
  14. */
  15. private static $ext;
  16.  
  17. /**
  18. * Расширение для файлов счётчика индексов
  19. *
  20. * @var string
  21. */
  22. private static $ext_index;
  23.  
  24. /**
  25. * Путь к файлам бд
  26. *
  27. * @var string
  28. */
  29. private static $path;
  30.  
  31. /**
  32. * Счётчик найденных в БД строк
  33. *
  34. * @var int
  35. */
  36. private $found_lines;
  37.  
  38.  
  39. /**
  40. * Конструктор, установить путь к файлам БД,
  41. * расширения для файлов
  42. *
  43. * @param string $path
  44. * @return void
  45. */
  46. public function __construct($path)
  47. {
  48. $this->ext = '.phpbase';
  49. $this->ext_index = '.phpindex';
  50.  
  51. $this->path = $path;
  52. $this->found_lines = 0;
  53. }
  54.  
  55. /**
  56. * Деструктор, уходя, госите свет
  57. *
  58. * @return void
  59. */
  60. public function __destruct() {
  61. foreach ( get_class_vars ( __CLASS__ ) as $k => $v ) {
  62. $this->$k = null;
  63. }
  64. }
  65.  
  66. /**
  67. * Записать массив или строку в файл
  68. *
  69. * @param string $name
  70. * @param mixed $data (массив или строка)
  71. * @return mixed (количество записанных байт / false)
  72. */
  73. protected final function write($name, $data)
  74. {
  75. $filename = $this->getFileName($name);
  76.  
  77. $f = $this->openWrite($filename);
  78.  
  79. $res = is_array($data)?
  80. $this->writeArr($f, $name, $data):
  81. $this->writeLn($f, $name, $data);
  82.  
  83. $this->closeFile($f);
  84.  
  85. if ($res === FALSE) {
  86. trigger_error( 'Запись в файл <b>'.$file.'</b> не удалась', E_USER_ERROR );
  87. return false;
  88. } else {
  89. return $res;
  90. }
  91. }
  92.  
  93. /**
  94. * Записать строку в файл
  95. *
  96. * @param resource $handler
  97. * @param string $name
  98. * @param string $line
  99. * @return mixed (количество записанных байт / false)
  100. */
  101. private final function writeLn($handler, $name, $line)
  102. {
  103. $idx = $this->getIdx($name);
  104. $idx++;
  105. $r = fwrite($handler, $idx.' '.$line."\n");
  106. file_put_contents($this->getIndexFileName($name), $idx);
  107. return $r;
  108. }
  109.  
  110. /**
  111. * Получить значения счётчика индекса
  112. *
  113. * @param string $name
  114. * @return int
  115. */
  116. private final function getIdx($name)
  117. {
  118. return (int)file_get_contents($this->getIndexFileName($name));
  119. }
  120.  
  121. /**
  122. * Записать массив строк в файл
  123. *
  124. * @param resource $handler
  125. * @param string $name
  126. * @param array $data
  127. * @return mixed (количество записанных байт / false)
  128. */
  129. private final function writeArr($handler, $name, $data)
  130. {
  131. $r = 0;
  132. foreach($data as $v) {
  133. $res = $this->writeLn($handler, $name, $v);
  134. if ($res === false) {
  135. return false;
  136. } else {
  137. $r += $res;
  138. }
  139. }
  140. return $r;
  141. }
  142.  
  143. /**
  144. * Найти запись в файле
  145. *
  146. * @param string $name
  147. * @param string $key (имя поля)
  148. * @param string $search (имя значения для поиска)
  149. * @param string $type
  150. * (сравнение результатов eq - равенство, con - содержит в себе строку)
  151. * @param array $num (сколько записей вернуть, all - все найденные)
  152. * @return array
  153. */
  154. protected final function search($name, $key, $search, $type = 'con', $num = 'all')
  155. {
  156. $filename = $this->getFileName($name);
  157. if (empty($key) && empty($search)) {
  158. $search = 'all';
  159. }
  160.  
  161. $f = $this->openRead($filename);
  162.  
  163. $buffer = '';
  164. $this->found_lines = 0;
  165. $res = false;
  166.  
  167. while (!feof($f)) {
  168. $buffer = fgets($f);
  169. if (strpos($buffer, $search) !== false || $search == 'all') {
  170.  
  171. $r = $this->getSearchResults($buffer, $search, $key, $type);
  172. if ($r !== false) {
  173. $res[] = $r;
  174. unset($r);
  175.  
  176. $this->found_lines++;
  177.  
  178. if (($num != 'all') && ($this->found_lines == $num)) {
  179. break;
  180. }
  181. }
  182. }
  183. }
  184. $this->closeFile($f);
  185. return $res;
  186. }
  187.  
  188. /**
  189. * Найти запись в файле по индексу
  190. *
  191. * @param string $name
  192. * @param string $idx
  193. * @param array $num (сколько записей вернуть)
  194. * @return array
  195. */
  196. protected final function searchIdx($name, $idx, $num = 'all')
  197. {
  198. $filename = $this->getFileName($name);
  199. $id = $this->getIdx($name);
  200.  
  201. if ($id < $idx) {
  202. return false;
  203. }
  204.  
  205. $f = $this->openRead($filename);
  206.  
  207. $buffer = '';
  208. $this->found_lines = 0;
  209. $res = false;
  210.  
  211. while (!feof($f)) {
  212. $buffer = fgets($f);
  213. if (strpos($buffer, (string)$idx) !== false) {
  214.  
  215. $arr = $this->parseLine($buffer);
  216. if ($arr[0] == $idx) {
  217. $r = array('res' => unserialize($arr[1]),
  218. 'idx' => $arr[0]);
  219. $res[] = $r;
  220. unset($r);
  221. $this->found_lines++;
  222.  
  223. if (($num != 'all') && ($this->found_lines == $num)) {
  224. break;
  225. }
  226. }
  227.  
  228. }
  229. }
  230. $this->closeFile($f);
  231. return $res;
  232. }
  233.  
  234. /**
  235. * Распарсить полученную из БД строку
  236. *
  237. * @param string $line
  238. * @return array
  239. */
  240. private final function parseLine($line)
  241. {
  242. $pos = strpos($line, ' ');
  243. $len = strlen($line);
  244. $arr[0] = substr($line, 0, $pos+1);
  245. $arr[1] = substr($line, $pos+1, $len+1);
  246.  
  247. $arr = array_map('trim', $arr);
  248. $arr[0] = (int)$arr[0];
  249. return $arr;
  250. }
  251.  
  252. /**
  253. * Проверить, соответствует ли строка критериям поиска
  254. *
  255. * @param string $line
  256. * @return mixed (array / false)
  257. */
  258. private final function getSearchResults($line, $search, $key, $type)
  259. {
  260. $res = false;
  261. $arr = $this->parseLine($line);
  262.  
  263. $d = unserialize(trim($arr[1]));
  264.  
  265. if ($search == 'all') {
  266. $res = $d;
  267. } elseif ($type == 'eq') {
  268. if ($d[$key] == $search) {
  269. $res = $d;
  270. }
  271. } else {
  272. if (stripos($d[$key], $search) !== false) {
  273. $res = $d;
  274. }
  275. }
  276. if ((bool)$res) {
  277. return array('res' => $res, 'idx' => $arr[0]);
  278. } else {
  279. return $res;
  280. }
  281. }
  282.  
  283. /**
  284. * Вернуть количество найденных строк
  285. *
  286. * @return int
  287. */
  288. public function getNumRows()
  289. {
  290. return $this->found_lines;
  291. }
  292.  
  293. /**
  294. * Снять блокировку, закрыть файл
  295. * после чтения / записи
  296. *
  297. * @param resource $handler
  298. * @return void
  299. */
  300. private final function closeFile($handler)
  301. {
  302. flock($handler, LOCK_UN);
  303. fclose($handler);
  304. }
  305.  
  306. /**
  307. * Открыть файл для записи
  308. *
  309. * @param string $filename
  310. * @param string $flag (аналог флагов у fopen())
  311. * @return resource
  312. */
  313. private final function openWrite($filename, $flag = 'a')
  314. {
  315. try {
  316. if (is_writable($filename)) {
  317. if ($f = fopen($filename, $flag.'b')) {
  318. if (flock($f, LOCK_EX)) {
  319. return $f;
  320. } else {
  321. throw new Exception('Не удалось залочить файл <b>'.$filename.'</b> для записи');
  322. }
  323. } else {
  324. throw new Exception('Не могу открыть файл <b>'.$filename.'</b> для записи');
  325. }
  326. } else {
  327. throw new Exception('Не могу записать в файл <b>'.$filename.'</b>');
  328. }
  329.  
  330. } catch (Exception $e) {
  331. trigger_error($e->getMessage(), E_USER_ERROR);
  332. }
  333. }
  334.  
  335. /**
  336. * Открыть файл для чтения
  337. *
  338. * @param string $filename
  339. * @return resource
  340. */
  341. private final function openRead($filename)
  342. {
  343. try {
  344. if (is_readable($filename)) {
  345. if ($f = fopen($filename, 'rb')) {
  346. if (flock($f, LOCK_SH)) {
  347. return $f;
  348. } else {
  349. throw new Exception('Не удалось залочить файл <b>'.$filename.'</b> для чтения');
  350. }
  351. } else {
  352. throw new Exception('Не могу открыть файл <b>'.$filename.'</b> для чтения');
  353. }
  354. } else {
  355. throw new Exception('Не могу прочитать файл <b>'.$filename.'</b> ');
  356. }
  357. } catch (Exception $e) {
  358. trigger_error($e->getMessage(), E_USER_ERROR);
  359. }
  360. }
  361.  
  362. /**
  363. * Получить полный путь к файлу бд
  364. *
  365. * @param string $name
  366. * @return string
  367. */
  368. private final function getFileName($name)
  369. {
  370. return $this->path.DIRECTORY_SEPARATOR.$name.$this->ext;
  371. }
  372.  
  373. /**
  374. * Получить полный путь к файлу счётчика индекса
  375. *
  376. * @param string $name
  377. * @return string
  378. */
  379. private final function getIndexFileName($name)
  380. {
  381. return $this->path.DIRECTORY_SEPARATOR.$name.$this->ext_index;
  382. }
  383.  
  384. /**
  385. * Удалить запись по индексу
  386. *
  387. * @param string $name
  388. * @param int $idx
  389. * @return string
  390. */
  391. protected final function deleteByIdx($name, $idx)
  392. {
  393. $filename = $this->getFileName($name);
  394.  
  395. $id = (int)file_get_contents($this->getIndexFileName($name));
  396. if ($id < $idx) {
  397. return false;
  398. }
  399. $data = file($filename, FILE_IGNORE_NEW_LINES);
  400. foreach ($data as $k => $v) {
  401. $arr = $this->parseLine($v);
  402. if ($arr[0] == $idx) {
  403. unset($data[$k]);
  404. $this->updateFile($filename, $data);
  405. break;
  406. }
  407. }
  408. return true;
  409. }
  410.  
  411. /**
  412. * Обновить файл после замены содержимого или удаления
  413. *
  414. * @param string $filename
  415. * @param array $data
  416. * @return string
  417. */
  418. private final function updateFile($filename, $data)
  419. {
  420. $f = $this->openWrite($filename, 'w');
  421. fwrite($f, implode("\n",$data)."\n");
  422. $this->closeFile($f);
  423. }
  424.  
  425. /**
  426. * Изменить запись по индексу
  427. *
  428. * @param string $name
  429. * @param int $idx
  430. * @param array $replaces
  431. * @return bool
  432. */
  433. protected final function replaceByIx($name, $idx, $replaces)
  434. {
  435. $filename = $this->getFileName($name);
  436.  
  437. $id = (int)file_get_contents($this->getIndexFileName($name));
  438. if ($id < $idx) {
  439. return false;
  440. }
  441. $data = file($filename, FILE_IGNORE_NEW_LINES);
  442. foreach ($data as $k => $v) {
  443. $arr = $this->parseLine($v);
  444. if ($arr[0] == $idx) {
  445. $arr[1] = unserialize($arr[1]);
  446. foreach ($replaces as $key => $val) {
  447. $arr[1][$key] = $val;
  448. }
  449. $arr[1] = serialize($arr[1]);
  450. $data[$k] = implode(' ', $arr);
  451. $this->updateFile($filename, $data);
  452. break;
  453. }
  454. }
  455. return true;
  456. }
  457. }
  458. ?>

Db

  1. <?php
  2. require_once 'helperFiles.php';
  3.  
  4. /**
  5.  * Обёртка для работы с БД на файлах
  6.  *
  7.  * @author Павел Новицкий <lugebox@gmail.com>
  8.  * @link http://www.e-luge.net/
  9.  */
  10. class Db extends helperFiles {
  11.  
  12. /**
  13. * Конструктор, установить путь к файлам БД
  14. *
  15. * @param string $path
  16. * @return void
  17. */
  18. public function __construct($path)
  19. {
  20. parent::__construct ($path);
  21. }
  22.  
  23. /**
  24. * Деструктор
  25. *
  26. * @return void
  27. */
  28. public function __destruct() {
  29. parent::__destruct();
  30. foreach ( get_class_vars ( __CLASS__ ) as $k => $v ) {
  31. $this->$k = null;
  32. }
  33. }
  34.  
  35. /**
  36. * Выбрать записи из БД
  37. *
  38. * @param array $search
  39. * array(
  40. * 'from'=>'имя таблицы',
  41. * 'fields' => array('поле')/'*'/'поле', (имена полей, которые надо вернуть после зпроса)
  42. * 'whereId'=>1, (индекс записи)
  43. * или
  44. * where => array( (поиск по полю)
  45. * key = field, (поле)
  46. * val = search, (значение)
  47. * type='con' / 'val'), (сравнение, см. helperFiles)
  48. * num => 1)
  49. *
  50. * @return mixed (array / false)
  51. */
  52. public function select($search)
  53. {
  54. if (isset($search['where']) && isset($search['whereId'])) {
  55. trigger_error('Select. Выбраны 2 условия. Выборка по полю и выборка по ключу');
  56. }
  57.  
  58. $num = isset($search['num'])?
  59. ($search['num'] == 'all')?
  60. 'all':
  61. (int)$search['num']:
  62. 'all';
  63.  
  64. if (isset($search['where'])) {
  65. $type = (isset($search['where']['type']) && $search['where']['type'] == 'eq')?'eq':'con';
  66. $res = parent::search($search['from'], $search['where']['key'], $search['where']['val'], $type, $num);
  67. } else {
  68. $res = parent::searchIdx($search['from'], $search['whereId'], $num);
  69. }
  70.  
  71. if ($res === false) {
  72. return false;
  73. } else {
  74. return $this->filterResults($search['fields'], $res);
  75. }
  76. }
  77.  
  78. /**
  79. * Вернуть количество найденных строк
  80. *
  81. * @return int
  82. */
  83. public function getNumRows()
  84. {
  85. return parent::getNumRows();
  86. }
  87.  
  88. /**
  89. * Подготовить массив результата, основываясь на указанных в select() полях
  90. *
  91. * @param mixed $fields
  92. * @param array $data
  93. * @return array
  94. */
  95. private function filterResults($fields, &$data)
  96. {
  97. if ($fields == '*') {
  98. return $data;
  99. } else {
  100. if (!is_array($fields)) {
  101. $fields = array($fields);
  102. }
  103. foreach ($data as $k => $v) {
  104. foreach($v['res'] as $key => $val) {
  105. if (!in_array($key, $fields)) {
  106. unset($data[$k]['res'][$key]);
  107. }
  108. }
  109. }
  110. return $data;
  111. }
  112. }
  113.  
  114. /**
  115. * Выбрать записи из БД
  116. *
  117. * @param array $search
  118.   * array(
  119. * 'from'=>'имя таблицы',
  120. * 'update' => array('поле' => 'значение')
  121. * 'whereId'=>1, (индекс записи)
  122. * или
  123. * where => array( (поиск по полю)
  124. * key = field, (поле)
  125. * val = search)) (значение)
  126.   *
  127. * @return bool
  128. */
  129. public function update($search)
  130. {
  131. if (isset($search['where']) && isset($search['whereId'])) {
  132. trigger_error('Update. Выбраны 2 условия. Выборка по полю и выборка по ключу');
  133. }
  134.  
  135. if (isset($search['where'])) {
  136. $find = array(
  137. 'from'=>$search['from'],
  138. 'fields' => '*',
  139. 'where' => array(
  140. 'key' => $search['where']['key'],
  141. 'val' => $search['where']['val'],
  142. 'type' => 'eq'),
  143. 'num' => 'all');
  144. $r = $this->select($find);
  145. if ($r == true) {
  146. foreach ($r as $v) {
  147. parent::replaceByIx($search['from'], $v['idx'], $search['update']);
  148. }
  149. return true;
  150. } else {
  151. return false;
  152. }
  153.  
  154. } else {
  155. return parent::replaceByIx($search['from'], $search['whereId'], $search['update']);
  156. }
  157. return true;
  158. }
  159.  
  160. /**
  161. * вставить запись в БД
  162. *
  163. * @param string $name
  164. * @param array $data
  165. * массив или массивы пар поле => значение
  166. * @param bool $multiple
  167.   *
  168. * @return bool
  169. */
  170. public function insert($name, array $data, $multiple = false)
  171. {
  172. if ($multiple) {
  173. $arr = array();
  174. foreach ($data as $v) {
  175. $arr[] = serialize($v);
  176. }
  177. return parent::write($name, $arr);
  178. }
  179.  
  180. return parent::write($name, serialize($data));
  181. }
  182.  
  183. /**
  184. * удалить запись
  185. *
  186. * @param array $search
  187. * массив или массивы пар поле => значение
  188. * @param bool $multiple
  189.   * array(
  190. * 'from'=>'имя таблицы',
  191. * 'whereId'=>1, (индекс записи)
  192. * или
  193. * where => array( (поиск по полю)
  194. * key = field, (поле)
  195. * val = search)) (значение)
  196. *
  197. * @return bool
  198. */
  199. public function delete($search)
  200. {
  201. if (isset($search['where']) && isset($search['whereId'])) {
  202. trigger_error('Delete. Выбраны 2 условия. Выборка по полю и выборка по ключу');
  203. }
  204.  
  205. if (isset($search['where'])) {
  206. $find = array(
  207. 'from'=>$search['from'],
  208. 'fields' => '*',
  209. 'where' => array(
  210. 'key' => $search['where']['key'],
  211. 'val' => $search['where']['val'],
  212. 'type' => 'eq'),
  213. 'num' => 'all');
  214. $r = $this->select($find);
  215. if ($r == true) {
  216. foreach ($r as $v) {
  217. parent::deleteByIdx($search['from'], $v['idx']);
  218. }
  219. return true;
  220. } else {
  221. return false;
  222. }
  223.  
  224. } else {
  225. return parent::deleteByIdx($search['from'], $search['whereId']);
  226. }
  227. return true;
  228. }
  229. }
  230. ?>

Если интересно как оно всё работает, то можно скачать (и похвалить :)) или посмотреть как оно ведёт себя вживую.

p.s. притензии по вопросу организации общей структуры и отсутствия всяких мвц и еже с ними не принимаются %)

Комментарии (0):

Комментариев пока нет. Станете первым?

Это архив блога. Добавление комментариев запрещено.

© Павел Новицкий 2009 - 2011
(: time: 23s, sql: 76, memory: 322Mb :)