/src/ListAbstract.php

https://gitlab.com/mac_doggie/list · PHP · 361 lines · 240 code · 45 blank · 76 comment · 48 complexity · 96c8c45adfcace2e9f2428e09a9c1384 MD5 · raw file

  1. <?php
  2. namespace Macdoggie\Component\Lists;
  3. use Macdoggie\Component\Lists\Exceptions\InvalidDataTypeException;
  4. use Macdoggie\Component\Lists\Exceptions\InvalidDataValueException;
  5. use Macdoggie\Component\Lists\Exceptions\InvalidRangeException;
  6. use Macdoggie\Component\Lists\Expressions\ExpressionFactory;
  7. abstract class ListAbstract implements ListInterface
  8. {
  9. /**
  10. * @var mixed[]
  11. */
  12. protected $items;
  13. protected $position;
  14. public function __construct()
  15. {
  16. $this->items = [];
  17. $this->position = 0;
  18. $this->rewind();
  19. }
  20. /**
  21. * @param $item
  22. * @param int|null $offset
  23. */
  24. protected function addItem($item, int $offset = null)
  25. {
  26. if (null == $offset) {
  27. $this->items[] = $item;
  28. } else {
  29. array_splice($this->items, $offset, 0, $item);
  30. }
  31. }
  32. /**
  33. * @return mixed|null
  34. */
  35. public function getFirst()
  36. {
  37. $this->rewind();
  38. return ($this->valid()) ? $this->current() : null;
  39. }
  40. /**
  41. * @return mixed|null
  42. */
  43. public function getPrevious()
  44. {
  45. $this->prev();
  46. return ($this->valid()) ? $this->current() : null;
  47. }
  48. /**
  49. * @return mixed|null
  50. */
  51. public function getNext()
  52. {
  53. $this->next();
  54. return ($this->valid()) ? $this->current() : null;
  55. }
  56. /**
  57. * @return mixed|null
  58. */
  59. public function getLast()
  60. {
  61. $this->last();
  62. return ($this->valid()) ? $this->current() : null;
  63. }
  64. public function rewind()
  65. {
  66. $this->position = 0;
  67. }
  68. public function prev()
  69. {
  70. --$this->position;
  71. }
  72. public function next()
  73. {
  74. ++$this->position;
  75. }
  76. public function last()
  77. {
  78. $this->position = count($this->items) - 1;
  79. }
  80. /**
  81. * @return mixed
  82. */
  83. public function current()
  84. {
  85. return $this->items[$this->position];
  86. }
  87. /**
  88. * @return int
  89. */
  90. public function key()
  91. {
  92. return $this->position;
  93. }
  94. /**
  95. * @return bool
  96. */
  97. public function valid()
  98. {
  99. return isset($this->items[$this->position]);
  100. }
  101. /**
  102. * @param int $offset
  103. * @param int $length
  104. * @throws InvalidRangeException
  105. */
  106. public function getRange(int $offset, int $length)
  107. {
  108. if ($length < 0) {
  109. $lastIndex = count($this->items) + $length;
  110. if ($lastIndex < $offset) {
  111. throw new InvalidRangeException("Range out of bound: [{$offset}, {$length}] results in [{$offset}, {$lastIndex}], size is " . count($this->items));
  112. }
  113. }
  114. }
  115. /**
  116. * @param array $query
  117. * @param int $offset
  118. * @return mixed|null
  119. * @throws InvalidDataValueException
  120. */
  121. public function findOneBy(array $query, int $offset = 0)
  122. {
  123. $result = $this->findBy($query, 1, $offset);
  124. return ($result) ? $result : null;
  125. }
  126. /**
  127. * @param array $query
  128. * @param int $limit
  129. * @param int $offset
  130. * @return mixed[]
  131. * @throws InvalidDataTypeException
  132. */
  133. public function findBy($query = null, int $limit = 0, int $offset = 0)
  134. {
  135. if ($query == null) {
  136. $items = $this->items;
  137. } else {
  138. if (!is_array($query)) {
  139. throw new InvalidDataTypeException(sprintf("findBy expects parameter 1 to be an array, found: %s", gettype($query)));
  140. }
  141. /* Syntax:
  142. * [
  143. * ['ISO3Code', '$eqs', 'EUR' ],
  144. * ['valueBaseISO3Code', '$eq', 'USD' ],
  145. * ['value', '$gt', 1 ] ,
  146. * ['$or', [ 'value', '$lt', 2 ], [ 'value', '$gt', 50 ]]
  147. * ['$or', [ 'value', '$lt', 2 ], ['$and', [ 'value', '$gt', 10 ], [ 'value', '$lt', 30 ]]]
  148. * ]
  149. */
  150. $items = [];
  151. $limit = ($limit <= 0) ? count($this->items) : $limit;
  152. for ($i = $offset; $i < count($this->items) && count($items) < $limit; $i++) {
  153. if ($this->andClause($query, $this->items[$i])) {
  154. $items[] = $this->items[$i];
  155. }
  156. }
  157. }
  158. return $items;
  159. }
  160. /**
  161. * @param $orClauses
  162. * @param $item
  163. * @return bool
  164. * @throws InvalidDataTypeException
  165. * @throws InvalidDataValueException
  166. */
  167. private function orClause($orClauses, $item)
  168. {
  169. if (!is_array($orClauses)) {
  170. throw new InvalidDataValueException('orClauses should be an array with OR clauses.');
  171. }
  172. for ($i = 0, $result = false; $i < count($orClauses) && !$result; $i++) {
  173. if (!is_array($orClauses[$i])) {
  174. throw new InvalidDataValueException('orClause should contain array with OR clauses. Each orClause should be an array with 2 values and an operator $lt, $gt, $lte, $gte or $eq');
  175. }
  176. $result = $this->checkClause($orClauses[$i], $item);
  177. }
  178. return $result;
  179. }
  180. /**
  181. * @param $andClauses
  182. * @param $item
  183. * @return bool
  184. * @throws InvalidDataTypeException
  185. * @throws InvalidDataValueException
  186. */
  187. private function andClause($andClauses, $item)
  188. {
  189. if (empty($andClauses)) {
  190. $andClauses = [];
  191. }
  192. if (!is_array($andClauses)) {
  193. throw new InvalidDataValueException('andClauses should be an array with AND clauses.');
  194. }
  195. for ($i = 0, $result = true; $i < count($andClauses) && $result; $i++) {
  196. if (!is_array($andClauses[$i])) {
  197. throw new InvalidDataValueException('andClause should contain array with AND clauses. Each andClause should be an array with 2 values and an operator $lt, $gt, $lte, $gte or $eq');
  198. }
  199. $result = $this->checkClause($andClauses[$i], $item);
  200. }
  201. return $result;
  202. }
  203. /**
  204. * @param $clause
  205. * @param $item
  206. * @return bool
  207. * @throws InvalidDataTypeException
  208. * @throws InvalidDataValueException
  209. */
  210. private function checkClause($clause, $item)
  211. {
  212. if ($clause[0] == '$or') {
  213. $orClauses = array_slice($clause, 1);
  214. $result = $this->orClause($orClauses, $item);
  215. } elseif ($clause[0] == '$and') {
  216. $andClauses = array_slice($clause, 1);
  217. $result = $this->andClause($andClauses, $item);
  218. } else {
  219. $value1 = null;
  220. $value2 = null;
  221. if (is_object($item)) {
  222. $value1 = (is_string($clause[0]) && substr($clause[0], 0, 1) == ":") ?
  223. $this->extractValue(substr($clause[0], 1), $item) :
  224. $clause[0];
  225. $value2 = (is_string($clause[2]) && substr($clause[2], 0, 1) == ":") ?
  226. $this->extractValue(substr($clause[2], 1), $item) :
  227. $clause[2];
  228. } else {
  229. $value1 = (is_string($clause[0]) && substr($clause[0], 0, 1) == ":") ?
  230. $item :
  231. $clause[0];
  232. $value2 = (is_string($clause[2]) && substr($clause[2], 0, 1) == ":") ?
  233. $item :
  234. $clause[2];
  235. }
  236. $expressionFactory = new ExpressionFactory();
  237. $expression = $expressionFactory->createExpression($clause[1]);
  238. $result = $expression->validate($value1, $value2);
  239. }
  240. return $result;
  241. }
  242. private function extractValue($fieldName, $item)
  243. {
  244. $valueSet = false;
  245. $reflection = new \ReflectionObject($item);
  246. $value = null;
  247. if (\property_exists($item, $fieldName)) {
  248. $property = $reflection->getProperty($fieldName);
  249. if ($property->isPublic()) {
  250. $value = $item->$fieldName;
  251. $valueSet = true;
  252. }
  253. }
  254. if (!$valueSet) {
  255. $field = "get" . ucfirst($fieldName);
  256. if (\method_exists($item, $field)) {
  257. $method = $reflection->getMethod($field);
  258. if ($method->isPublic()) {
  259. $value = $item->$field();
  260. } else {
  261. throw new InvalidDataTypeException("The getter `{$field}` is not accessable, make sure it is public");
  262. }
  263. } else {
  264. throw new InvalidDataTypeException("The field is not accessable, make sure the field `{$fieldName}` is public or there is a public method called `{$field}`");
  265. }
  266. }
  267. return $value;
  268. }
  269. public function sum(string $fieldName, array $query = null)
  270. {
  271. $items = $this->findBy($query);
  272. return $this->sumItems($fieldName, $items);
  273. }
  274. public function average(string $fieldName, array $query = null)
  275. {
  276. $items = $this->findBy($query);
  277. $sum = $this->sumItems($fieldName, $items);
  278. return (count($items) > 0) ? ($sum / count($items)) : 0;
  279. }
  280. private function sumItems(string $fieldName, array $items)
  281. {
  282. $sum = 0;
  283. foreach($items as $item) {
  284. $sum += floatval($this->extractValue($fieldName, $item));
  285. }
  286. return $sum;
  287. }
  288. public function min(string $fieldName, array $query = null)
  289. {
  290. $items = $this->findBy($query);
  291. $min = null;
  292. foreach($items as $item) {
  293. $fieldValue = $this->extractValue($fieldName, $item);
  294. if($min == null || $fieldValue < $min) {
  295. $min = $fieldValue;
  296. }
  297. }
  298. return $min;
  299. }
  300. public function Max(string $fieldName, array $query = null)
  301. {
  302. $items = $this->findBy($query);
  303. $max = null;
  304. foreach($items as $item) {
  305. $fieldValue = $this->extractValue($fieldName, $item);
  306. if($max == null || $fieldValue > $max) {
  307. $max = $fieldValue;
  308. }
  309. }
  310. return $max;
  311. }
  312. public function Count()
  313. {
  314. return count($this->items);
  315. }
  316. }