Anonymous 3D Imageboard http://cyberia.digital/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

962 lines
26KB

  1. <?php
  2. namespace Lazer\Classes;
  3. include "Libs/Lazer/Classes/Helpers/Validate.php";
  4. include "Libs/Lazer/Classes/LazerException.php";
  5. use Lazer\Classes\LazerException;
  6. use Lazer\Classes\Database;
  7. use Lazer\Classes\Relation;
  8. use Lazer\Classes\Helpers;
  9. /**
  10. * Core class of Lazer.
  11. *
  12. * There are classes to use JSON files like file database.
  13. *
  14. * Using style was inspired by ORM classes.
  15. *
  16. * @category Core
  17. * @author Grzegorz Kuźnik
  18. * @copyright (c) 2013, Grzegorz Kuźnik
  19. * @license http://opensource.org/licenses/MIT The MIT License
  20. * @link https://github.com/Greg0/Lazer-Database GitHub Repository
  21. */
  22. abstract class Core_Database implements \IteratorAggregate, \Countable {
  23. /**
  24. * Contain returned data from file as object or array of objects
  25. * @var mixed Data from table
  26. */
  27. protected $data;
  28. /**
  29. * Name of file (table)
  30. * @var string Name of table
  31. */
  32. protected $name;
  33. /**
  34. * Object with setted data
  35. * @var object Setted data
  36. */
  37. protected $set;
  38. /**
  39. * ID of current row if setted
  40. * @var integer Current ID
  41. */
  42. protected $currentId;
  43. /**
  44. * Key if current row if setted
  45. * @var integer Current key
  46. */
  47. protected $currentKey;
  48. /**
  49. * Pending functions with values
  50. * @see \Lazer\Classes\Core_Database::setPending()
  51. * @var array
  52. */
  53. protected $pending;
  54. /**
  55. * Information about to reset keys in array or not to
  56. * @var integer
  57. */
  58. protected $resetKeys = 1;
  59. /**
  60. * Factory pattern
  61. * @param string $name Name of table
  62. * @return \Lazer\Classes\Database
  63. * @throws LazerException If there's problems with load file
  64. */
  65. public static function table($name)
  66. {
  67. Helpers\Validate::table($name)->exists();
  68. $self = new Database;
  69. $self->name = $name;
  70. $self->setFields();
  71. $self->setPending();
  72. return $self;
  73. }
  74. /**
  75. * Get rows from table
  76. * @uses Lazer\Classes\Helpers\Data::get() to get data from file
  77. * @return array
  78. */
  79. protected function getData()
  80. {
  81. return Helpers\Data::table($this->name)->get();
  82. }
  83. /**
  84. * Setting data to Database::$data
  85. */
  86. protected function setData()
  87. {
  88. $this->data = $this->getData();
  89. }
  90. /**
  91. * Returns array key of row with specified ID
  92. * @param integer $id Row ID
  93. * @return integer Row key
  94. * @throws LazerException If there's no data with that ID
  95. */
  96. protected function getRowKey($id)
  97. {
  98. foreach ($this->getData() as $key => $data)
  99. {
  100. if ($data->id == $id)
  101. {
  102. return $key;
  103. break;
  104. }
  105. }
  106. throw new LazerException('No data found with ID: ' . $id);
  107. }
  108. /**
  109. * Set NULL for currentId and currentKey
  110. */
  111. protected function clearKeyInfo()
  112. {
  113. $this->currentId = $this->currentKey = NULL;
  114. }
  115. /**
  116. * Setting fields with default values
  117. * @uses Lazer\Classes\Helpers\Validate::isNumeric() to check if type of field is numeric
  118. */
  119. protected function setFields()
  120. {
  121. $this->set = new \stdClass();
  122. $schema = $this->schema();
  123. foreach ($schema as $field => $type)
  124. {
  125. if (Helpers\Validate::isNumeric($type) AND $field != 'id')
  126. {
  127. $this->set->{$field} = 0;
  128. }
  129. else
  130. {
  131. $this->set->{$field} = null;
  132. }
  133. }
  134. }
  135. /**
  136. * Set pending functions in right order with default values (Empty).
  137. */
  138. protected function setPending()
  139. {
  140. $this->pending = array(
  141. 'where' => array(),
  142. 'orderBy' => array(),
  143. 'limit' => array(),
  144. 'with' => array(),
  145. 'groupBy' => array(),
  146. );
  147. }
  148. /**
  149. * Clear info about previous queries
  150. */
  151. protected function clearQuery()
  152. {
  153. $this->setPending();
  154. $this->clearKeyInfo();
  155. }
  156. /**
  157. * Validating fields and setting variables to current operations
  158. * @uses Lazer\Classes\Helpers\Validate::field() to check that field exist
  159. * @uses Lazer\Classes\Helpers\Validate::type() to check that field type is correct
  160. * @param string $name Field name
  161. * @param mixed $value Field value
  162. */
  163. public function __set($name, $value)
  164. {
  165. if (Helpers\Validate::table($this->name)->field($name) && Helpers\Validate::table($this->name)->type($name, $value))
  166. {
  167. $this->set->{$name} = $value;
  168. }
  169. }
  170. /**
  171. * Returning variable from Object
  172. * @param string $name Field name
  173. * @return mixed Field value
  174. * @throws LazerException
  175. */
  176. public function __get($name)
  177. {
  178. if (isset($this->set->{$name}))
  179. return $this->set->{$name};
  180. throw new LazerException('There is no data');
  181. }
  182. /**
  183. * Check if the given field exists
  184. * @param string $name Field name
  185. * @return boolean True if the field exists, false otherwise
  186. */
  187. public function __isset($name)
  188. {
  189. return isset($this->set->{$name});
  190. }
  191. /**
  192. * Execute pending functions
  193. */
  194. protected function pending()
  195. {
  196. $this->setData();
  197. foreach ($this->pending as $func => $args)
  198. {
  199. if (!empty($args))
  200. {
  201. call_user_func(array($this, $func . 'Pending'));
  202. }
  203. }
  204. //clear pending values after executed query
  205. $this->clearQuery();
  206. }
  207. /**
  208. * Creating new table
  209. *
  210. * For example few fields:
  211. *
  212. * Database::create('news', array(
  213. * 'title' => 'string',
  214. * 'content' => 'string',
  215. * 'rating' => 'double',
  216. * 'author' => 'integer'
  217. * ));
  218. *
  219. * Types of field:
  220. * - boolean
  221. * - integer
  222. * - string
  223. * - double (also for float type)
  224. *
  225. * ID field isn't required (it will be created automatically) but you can specify it at first place.
  226. *
  227. * @uses Lazer\Classes\Helpers\Data::arrToLower() to lower case keys and values of array
  228. * @uses Lazer\Classes\Helpers\Data::exists() to check if data file exists
  229. * @uses Lazer\Classes\Helpers\Config::exists() to check if config file exists
  230. * @uses Lazer\Classes\Helpers\Validate::types() to check if type of fields are correct
  231. * @uses Lazer\Classes\Helpers\Data::put() to save data file
  232. * @uses Lazer\Classes\Helpers\Config::put() to save config file
  233. * @param string $name Table name
  234. * @param array $fields Field configuration
  235. * @throws LazerException If table exist
  236. */
  237. public static function create($name, array $fields)
  238. {
  239. $fields = Helpers\Validate::arrToLower($fields);
  240. if (Helpers\Data::table($name)->exists() && Helpers\Config::table($name)->exists())
  241. {
  242. throw new LazerException('helper\Table "' . $name . '" already exists');
  243. }
  244. $types = array_values($fields);
  245. Helpers\Validate::types($types);
  246. if (!array_key_exists('id', $fields))
  247. {
  248. $fields = array('id' => 'integer') + $fields;
  249. }
  250. $data = new \stdClass();
  251. $data->last_id = 0;
  252. $data->schema = $fields;
  253. $data->relations = new \stdClass();
  254. Helpers\Data::table($name)->put(array());
  255. Helpers\Config::table($name)->put($data);
  256. }
  257. /**
  258. * Removing table with config
  259. * @uses Lazer\Classes\Helpers\Data::remove() to remove data file
  260. * @uses Lazer\Classes\Helpers\Config::remove() to remove config file
  261. * @param string $name Table name
  262. * @return boolean|LazerException
  263. */
  264. public static function remove($name)
  265. {
  266. if (Helpers\Data::table($name)->remove() && Helpers\Config::table($name)->remove())
  267. {
  268. return TRUE;
  269. }
  270. return FALSE;
  271. }
  272. /**
  273. * Grouping results by one field
  274. * @param string $column
  275. * @return \Lazer\Classes\Core_Database
  276. */
  277. public function groupBy($column)
  278. {
  279. if (Helpers\Validate::table($this->name)->field($column))
  280. {
  281. $this->resetKeys = 0;
  282. $this->pending[__FUNCTION__] = $column;
  283. }
  284. return $this;
  285. }
  286. /**
  287. * Grouping array pending method
  288. */
  289. protected function groupByPending()
  290. {
  291. $column = $this->pending['groupBy'];
  292. $grouped = array();
  293. foreach ($this->data as $object)
  294. {
  295. $grouped[$object->{$column}][] = $object;
  296. }
  297. $this->data = $grouped;
  298. }
  299. /**
  300. * JOIN other tables
  301. * @param string $table relations separated by :
  302. * @return \Lazer\Classes\Core_Database
  303. */
  304. public function with($table)
  305. {
  306. $this->pending['with'][] = explode(':', $table);
  307. return $this;
  308. }
  309. /**
  310. * Pending function for with(), joining other tables to current
  311. */
  312. protected function withPending()
  313. {
  314. $joins = $this->pending['with'];
  315. foreach ($joins as $join)
  316. {
  317. $local = (count($join) > 1) ? array_slice($join, -2, 1)[0] : $this->name;
  318. $foreign = end($join);
  319. $relation = Relation::table($local)->with($foreign);
  320. $data = $this->data;
  321. foreach ($join as $part)
  322. {
  323. $data = $relation->build($data, $part);
  324. }
  325. }
  326. }
  327. /**
  328. * Sorting data by field
  329. * @param string $key Field name
  330. * @param string $direction ASC|DESC
  331. * @return \Lazer\Classes\Core_Database
  332. */
  333. public function orderBy($key, $direction = 'ASC')
  334. {
  335. if (Helpers\Validate::table($this->name)->field($key))
  336. {
  337. $directions = array(
  338. 'ASC' => SORT_ASC,
  339. 'DESC' => SORT_DESC
  340. );
  341. $this->pending[__FUNCTION__][$key] = isset($directions[$direction]) ? $directions[$direction] : 'ASC';
  342. }
  343. return $this;
  344. }
  345. /**
  346. * Sort an array of objects by more than one field.
  347. * @
  348. * @link http://blog.amnuts.com/2011/04/08/sorting-an-array-of-objects-by-one-or-more-object-property/ It's not mine algorithm
  349. */
  350. protected function orderByPending()
  351. {
  352. $properties = $this->pending['orderBy'];
  353. uasort($this->data, function($a, $b) use ($properties)
  354. {
  355. foreach ($properties as $column => $direction)
  356. {
  357. if (is_int($column))
  358. {
  359. $column = $direction;
  360. $direction = SORT_ASC;
  361. }
  362. $collapse = function($node, $props)
  363. {
  364. if (is_array($props))
  365. {
  366. foreach ($props as $prop)
  367. {
  368. $node = (!isset($node->$prop)) ? null : $node->$prop;
  369. }
  370. return $node;
  371. }
  372. else
  373. {
  374. return (!isset($node->$props)) ? null : $node->$props;
  375. }
  376. };
  377. $aProp = $collapse($a, $column);
  378. $bProp = $collapse($b, $column);
  379. if ($aProp != $bProp)
  380. {
  381. return ($direction == SORT_ASC) ? strnatcasecmp($aProp, $bProp) : strnatcasecmp($bProp, $aProp);
  382. }
  383. }
  384. return FALSE;
  385. });
  386. }
  387. /**
  388. * Where function, like SQL
  389. *
  390. * Operators:
  391. * - Standard operators (=, !=, >, <, >=, <=)
  392. * - IN (only for array value)
  393. * - NOT IN (only for array value)
  394. *
  395. * @param string $field Field name
  396. * @param string $op Operator
  397. * @param mixed $value Field value
  398. * @return \Lazer\Classes\Core_Database
  399. */
  400. public function where($field, $op, $value)
  401. {
  402. $this->pending['where'][] = array(
  403. 'type' => 'and',
  404. 'field' => $field,
  405. 'op' => $op,
  406. 'value' => $value,
  407. );
  408. return $this;
  409. }
  410. /**
  411. * Alias for where()
  412. * @param string $field Field name
  413. * @param string $op Operator
  414. * @param mixed $value Field value
  415. * @return \Lazer\Classes\Core_Database
  416. */
  417. public function andWhere($field, $op, $value)
  418. {
  419. $this->where($field, $op, $value);
  420. return $this;
  421. }
  422. /**
  423. * Alias for where(), setting OR for searching
  424. * @param string $field Field name
  425. * @param string $op Operator
  426. * @param mixed $value Field value
  427. * @return \Lazer\Classes\Core_Database
  428. */
  429. public function orWhere($field, $op, $value)
  430. {
  431. $this->pending['where'][] = array(
  432. 'type' => 'or',
  433. 'field' => $field,
  434. 'op' => $op,
  435. 'value' => $value,
  436. );
  437. return $this;
  438. }
  439. /**
  440. * Filter function for array_filter() in where()
  441. * @return boolean
  442. */
  443. protected function wherePending()
  444. {
  445. $operator = array(
  446. '=' => '==',
  447. '!=' => '!=',
  448. '>' => '>',
  449. '<' => '<',
  450. '>=' => '>=',
  451. '<=' => '<=',
  452. 'and' => '&&',
  453. 'or' => '||'
  454. );
  455. $this->data = array_filter($this->data, function($row) use ($operator)
  456. {
  457. $clause = '';
  458. $result = true;
  459. foreach ($this->pending['where'] as $key => $condition)
  460. {
  461. extract($condition);
  462. if (is_array($value) && $op == 'IN')
  463. {
  464. $value = (in_array($row->{$field}, $value)) ? 1 : 0;
  465. $op = '==';
  466. $field = 1;
  467. }
  468. elseif (!is_array($value) && in_array($op, array('LIKE', 'like')))
  469. {
  470. $regex = "/^" . str_replace('%', '(.*?)', preg_quote($value)) . "$/si";
  471. $value = preg_match($regex, $row->{$field});
  472. $op = '==';
  473. $field = 1;
  474. }
  475. elseif (!is_array($value) && $op != 'IN')
  476. {
  477. $value = is_string($value) ?
  478. '\'' . mb_strtolower($value) . '\'' :
  479. $value;
  480. $op = $operator[$op];
  481. $field = is_string($row->{$field}) ?
  482. 'mb_strtolower($row->' . $field .')' :
  483. '$row->' . $field;
  484. }
  485. $type = (!$key) ?
  486. null :
  487. $operator[$type];
  488. $query = array($type, $field, $op, $value);
  489. $clause .= implode(' ', $query) . ' ';
  490. eval('$result = ' . $clause . ';');
  491. }
  492. return $result;
  493. });
  494. }
  495. /**
  496. * Returning data as indexed or assoc array.
  497. * @param string $key Field that will be the key, NULL for Indexed
  498. * @param string $value Field that will be the value
  499. * @return array
  500. */
  501. public function asArray($key = null, $value = null)
  502. {
  503. if (!is_null($key))
  504. {
  505. Helpers\Validate::table($this->name)->field($key);
  506. }
  507. if (!is_null($value))
  508. {
  509. Helpers\Validate::table($this->name)->field($value);
  510. }
  511. $datas = array();
  512. if (!$this->resetKeys)
  513. {
  514. if (is_null($key) && is_null($value))
  515. {
  516. return $this->data;
  517. }
  518. else
  519. {
  520. foreach ($this->data as $rowKey => $data)
  521. {
  522. $datas[$rowKey] = array();
  523. foreach ($data as $row)
  524. {
  525. if (is_null($key))
  526. {
  527. $datas[$rowKey][] = $row->{$value};
  528. }
  529. elseif (is_null($value))
  530. {
  531. $datas[$rowKey][$row->{$key}] = $row;
  532. }
  533. else
  534. {
  535. $datas[$rowKey][$row->{$key}] = $row->{$value};
  536. }
  537. }
  538. }
  539. }
  540. }
  541. else
  542. {
  543. if (is_null($key) && is_null($value))
  544. {
  545. foreach ($this->data as $data)
  546. {
  547. $datas[] = get_object_vars($data);
  548. }
  549. }
  550. else
  551. {
  552. foreach ($this->data as $data)
  553. {
  554. if (is_null($key))
  555. {
  556. $datas[] = $data->{$value};
  557. }
  558. elseif (is_null($value))
  559. {
  560. $datas[$data->{$key}] = $data;
  561. }
  562. else
  563. {
  564. $datas[$data->{$key}] = $data->{$value};
  565. }
  566. }
  567. }
  568. }
  569. return $datas;
  570. }
  571. /**
  572. * Limit returned data
  573. *
  574. * Should be used at the end of chain, before end method
  575. * @param integer $number Limit number
  576. * @param integer $offset Offset number
  577. * @return \Lazer\Classes\Core_Database
  578. */
  579. public function limit($number, $offset = 0)
  580. {
  581. $this->pending['limit'] = array(
  582. 'offset' => $offset,
  583. 'number' => $number
  584. );
  585. return $this;
  586. }
  587. /**
  588. * Pending function for limit()
  589. */
  590. protected function limitPending()
  591. {
  592. $offset = $this->pending['limit']['offset'];
  593. $num = $this->pending['limit']['number'];
  594. $this->data = array_slice($this->data, $offset, $num);
  595. }
  596. /**
  597. * Add new fields to table, array schema like in create() function
  598. * @param array $fields Associative array
  599. */
  600. public function addFields(array $fields)
  601. {
  602. $fields = Helpers\Validate::arrToLower($fields);
  603. Helpers\Validate::types(array_values($fields));
  604. $schema = $this->schema();
  605. $fields = array_diff_assoc($fields, $schema);
  606. if (!empty($fields))
  607. {
  608. $config = $this->config();
  609. $config->schema = array_merge($schema, $fields);
  610. $data = $this->getData();
  611. foreach ($data as $key => $object)
  612. {
  613. foreach ($fields as $name => $type)
  614. {
  615. if (Helpers\Validate::isNumeric($type))
  616. $data[$key]->{$name} = 0;
  617. else
  618. $data[$key]->{$name} = null;
  619. }
  620. }
  621. Helpers\Data::table($this->name)->put($data);
  622. Helpers\Config::table($this->name)->put($config);
  623. }
  624. }
  625. /**
  626. * Delete fields from array
  627. * @param array $fields Indexed array
  628. */
  629. public function deleteFields(array $fields)
  630. {
  631. $fields = Helpers\Validate::arrToLower($fields);
  632. Helpers\Validate::table($this->name)->fields($fields);
  633. $config = $this->config();
  634. $config->schema = array_diff_key($this->schema(), array_flip($fields));
  635. $data = $this->getData();
  636. foreach ($data as $key => $object)
  637. {
  638. foreach ($fields as $name)
  639. {
  640. unset($data[$key]->{$name});
  641. }
  642. }
  643. Helpers\Data::table($this->name)->put($data);
  644. Helpers\Config::table($this->name)->put($config);
  645. }
  646. /**
  647. * Returns table name
  648. * @return string table name
  649. */
  650. public function name()
  651. {
  652. return $this->name;
  653. }
  654. /**
  655. * Returning object with config for table
  656. * @return object Config
  657. */
  658. public function config()
  659. {
  660. return Helpers\Config::table($this->name)->get();
  661. }
  662. /**
  663. * Return array with names of fields
  664. * @return array Fields
  665. */
  666. public function fields()
  667. {
  668. return Helpers\Config::table($this->name)->fields();
  669. }
  670. /**
  671. * Returning assoc array with types of fields
  672. * @return array Fields type
  673. */
  674. public function schema()
  675. {
  676. return Helpers\Config::table($this->name)->schema();
  677. }
  678. /**
  679. * Returning assoc array with relationed tables
  680. * @param string|null $tableName
  681. * @return array Fields type
  682. */
  683. public function relations($tableName = null)
  684. {
  685. return Helpers\Config::table($this->name)->relations($tableName, true);
  686. }
  687. /**
  688. * Returning last ID from table
  689. * @return integer Last ID
  690. */
  691. public function lastId()
  692. {
  693. return Helpers\Config::table($this->name)->lastId();
  694. }
  695. /**
  696. * Saving inserted or updated data
  697. */
  698. public function save()
  699. {
  700. $data = $this->getData();
  701. if (!$this->currentId)
  702. {
  703. $config = $this->config();
  704. $config->last_id++;
  705. $this->set->id = $config->last_id;
  706. array_push($data, $this->set);
  707. Helpers\Config::table($this->name)->put($config);
  708. }
  709. else
  710. {
  711. $this->set->id = $this->currentId;
  712. $data[$this->currentKey] = $this->set;
  713. }
  714. Helpers\Data::table($this->name)->put($data);
  715. // $this->setFields();
  716. }
  717. /**
  718. * Deleting loaded data
  719. * @return boolean
  720. */
  721. public function delete()
  722. {
  723. $data = $this->getData();
  724. if (isset($this->currentId))
  725. {
  726. unset($data[$this->currentKey]);
  727. }
  728. else
  729. {
  730. $this->pending();
  731. $old = $data;
  732. $data = array_diff_key($old, $this->data);
  733. }
  734. $this->data = array_values($data);
  735. return Helpers\Data::table($this->name)->put($this->data) ? true : false;
  736. }
  737. /**
  738. * Return count in integer or array of integers (if grouped)
  739. * @return mixed
  740. */
  741. public function count()
  742. {
  743. if (!$this->resetKeys)
  744. {
  745. $count = array();
  746. foreach ($this->data as $group => $data)
  747. {
  748. $count[$group] = count($data);
  749. }
  750. }
  751. else
  752. {
  753. $count = count($this->data);
  754. }
  755. return $count;
  756. }
  757. /**
  758. * Returns one row with specified ID
  759. * @param integer $id Row ID
  760. * @return \Lazer\Classes\Core_Database
  761. */
  762. public function find($id = NULL)
  763. {
  764. if ($id !== NULL)
  765. {
  766. $data = $this->getData();
  767. $this->currentId = $id;
  768. $this->currentKey = $this->getRowKey($id);
  769. foreach ($data[$this->currentKey] as $field => $value)
  770. {
  771. $this->set->{$field} = $value;
  772. }
  773. }
  774. else
  775. {
  776. $this->limit(1)->findAll();
  777. $data = $this->data;
  778. if (count($data))
  779. {
  780. foreach ($data[0] as $field => $value)
  781. {
  782. $this->set->{$field} = $value;
  783. }
  784. $this->currentId = $this->set->id;
  785. $this->currentKey = $this->getRowKey($this->currentId);
  786. }
  787. }
  788. return clone $this;
  789. }
  790. /**
  791. * Make data ready to read
  792. */
  793. public function findAll()
  794. {
  795. $this->pending();
  796. $this->data = $this->resetKeys ? array_values($this->data) : $this->data;
  797. return clone $this;
  798. }
  799. /**
  800. * Iterator for Data
  801. * @return \ArrayIterator
  802. */
  803. public function getIterator()
  804. {
  805. return new \ArrayIterator($this->data);
  806. }
  807. /**
  808. * Debug functions, prints whole query with values
  809. */
  810. public function debug()
  811. {
  812. $print = "Lazer::table(" . $this->name . ")\n";
  813. foreach ($this->pending as $function => $values)
  814. {
  815. if (!empty($values))
  816. {
  817. if (is_array($values))
  818. {
  819. if (is_array(reset($values)))
  820. {
  821. foreach ($values as $value)
  822. {
  823. if ($function == 'where')
  824. {
  825. array_shift($value);
  826. }
  827. if ($function == 'with')
  828. {
  829. $params = implode(':', $value);
  830. }
  831. else
  832. {
  833. $params = implode(', ', $value);
  834. }
  835. $print .= "\t" . '->' . $function . '(' . $params . ')' . "\n";
  836. }
  837. }
  838. else
  839. {
  840. $params = implode(', ', $values);
  841. $print .= "\t" . '->' . $function . '(' . $params . ')' . "\n";
  842. }
  843. }
  844. else
  845. {
  846. $print .= "\t" . '->' . $function . '(' . $values . ')' . "\n";
  847. }
  848. }
  849. }
  850. echo '<pre>' . print_r($print, true) . '</pre>';
  851. $this->clearQuery();
  852. }
  853. }