Filesystem.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Data\Collection;
  7. use Magento\Framework\Data\Collection;
  8. /**
  9. * Filesystem items collection
  10. *
  11. * Can scan a folder for files and/or folders recursively.
  12. * Creates \Magento\Framework\DataObject instance for each item, with its filename and base name
  13. *
  14. * Supports regexp masks that are applied to files and folders base names.
  15. * These masks apply before adding items to collection, during filesystem scanning
  16. *
  17. * Supports dirsFirst feature, that will make directories be before files, regardless of sorting column.
  18. *
  19. * Supports some fancy filters.
  20. *
  21. * At least one target directory must be set
  22. *
  23. * @api
  24. * @since 100.0.2
  25. */
  26. class Filesystem extends \Magento\Framework\Data\Collection
  27. {
  28. /**
  29. * Target directory
  30. *
  31. * @var string
  32. */
  33. protected $_targetDirs = [];
  34. /**
  35. * Whether to collect files
  36. *
  37. * @var bool
  38. */
  39. protected $_collectFiles = true;
  40. /**
  41. * Whether to collect directories before files
  42. *
  43. * @var bool
  44. */
  45. protected $_dirsFirst = true;
  46. /**
  47. * Whether to collect recursively
  48. *
  49. * @var bool
  50. */
  51. protected $_collectRecursively = true;
  52. /**
  53. * Whether to collect dirs
  54. *
  55. * @var bool
  56. */
  57. protected $_collectDirs = false;
  58. /**
  59. * \Directory names regex pre-filter
  60. *
  61. * @var string
  62. */
  63. protected $_allowedDirsMask = '/^[a-z0-9\.\-\_]+$/i';
  64. /**
  65. * Filenames regex pre-filter
  66. *
  67. * @var string
  68. */
  69. protected $_allowedFilesMask = '/^[a-z0-9\.\-\_]+\.[a-z0-9]+$/i';
  70. /**
  71. * Disallowed filenames regex pre-filter match for better versatility
  72. *
  73. * @var string
  74. */
  75. protected $_disallowedFilesMask = '';
  76. /**
  77. * Filter rendering helper variable
  78. *
  79. * @var int
  80. * @see Collection::$_filter
  81. * @see Collection::$_isFiltersRendered
  82. */
  83. private $_filterIncrement = 0;
  84. /**
  85. * Filter rendering helper variable
  86. *
  87. * @var array
  88. * @see Collection::$_filter
  89. * @see Collection::$_isFiltersRendered
  90. */
  91. private $_filterBrackets = [];
  92. /**
  93. * Filter rendering helper variable
  94. *
  95. * @var string
  96. * @see Collection::$_filter
  97. * @see Collection::$_isFiltersRendered
  98. */
  99. private $_filterEvalRendered = '';
  100. /**
  101. * Collecting items helper variable
  102. *
  103. * @var array
  104. */
  105. protected $_collectedDirs = [];
  106. /**
  107. * Collecting items helper variable
  108. *
  109. * @var array
  110. */
  111. protected $_collectedFiles = [];
  112. /**
  113. * Allowed dirs mask setter
  114. * Set empty to not filter
  115. *
  116. * @param string $regex
  117. * @return $this
  118. */
  119. public function setDirsFilter($regex)
  120. {
  121. $this->_allowedDirsMask = (string)$regex;
  122. return $this;
  123. }
  124. /**
  125. * Allowed files mask setter
  126. * Set empty to not filter
  127. *
  128. * @param string $regex
  129. * @return $this
  130. */
  131. public function setFilesFilter($regex)
  132. {
  133. $this->_allowedFilesMask = (string)$regex;
  134. return $this;
  135. }
  136. /**
  137. * Disallowed files mask setter
  138. * Set empty value to not use this filter
  139. *
  140. * @param string $regex
  141. * @return $this
  142. */
  143. public function setDisallowedFilesFilter($regex)
  144. {
  145. $this->_disallowedFilesMask = (string)$regex;
  146. return $this;
  147. }
  148. /**
  149. * Set whether to collect dirs
  150. *
  151. * @param bool $value
  152. * @return $this
  153. */
  154. public function setCollectDirs($value)
  155. {
  156. $this->_collectDirs = (bool)$value;
  157. return $this;
  158. }
  159. /**
  160. * Set whether to collect files
  161. *
  162. * @param bool $value
  163. * @return $this
  164. */
  165. public function setCollectFiles($value)
  166. {
  167. $this->_collectFiles = (bool)$value;
  168. return $this;
  169. }
  170. /**
  171. * Set whether to collect recursively
  172. *
  173. * @param bool $value
  174. * @return $this
  175. */
  176. public function setCollectRecursively($value)
  177. {
  178. $this->_collectRecursively = (bool)$value;
  179. return $this;
  180. }
  181. /**
  182. * Target directory setter. Adds directory to be scanned
  183. *
  184. * @param string $value
  185. * @return $this
  186. * @throws \Exception
  187. */
  188. public function addTargetDir($value)
  189. {
  190. $value = (string)$value;
  191. if (!is_dir($value)) {
  192. throw new \Exception('Unable to set target directory.');
  193. }
  194. $this->_targetDirs[$value] = $value;
  195. return $this;
  196. }
  197. /**
  198. * Set whether to collect directories before files
  199. * Works *before* sorting.
  200. *
  201. * @param bool $value
  202. * @return $this
  203. */
  204. public function setDirsFirst($value)
  205. {
  206. $this->_dirsFirst = (bool)$value;
  207. return $this;
  208. }
  209. /**
  210. * Get files from specified directory recursively (if needed)
  211. *
  212. * @param string|array $dir
  213. * @return void
  214. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  215. * @SuppressWarnings(PHPMD.NPathComplexity)
  216. */
  217. protected function _collectRecursive($dir)
  218. {
  219. $collectedResult = [];
  220. if (!is_array($dir)) {
  221. $dir = [$dir];
  222. }
  223. foreach ($dir as $folder) {
  224. if ($nodes = glob($folder . '/*', GLOB_NOSORT)) {
  225. foreach ($nodes as $node) {
  226. $collectedResult[] = $node;
  227. }
  228. }
  229. }
  230. if (empty($collectedResult)) {
  231. return;
  232. }
  233. foreach ($collectedResult as $item) {
  234. if (is_dir($item) && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item)))) {
  235. if ($this->_collectDirs) {
  236. if ($this->_dirsFirst) {
  237. $this->_collectedDirs[] = $item;
  238. } else {
  239. $this->_collectedFiles[] = $item;
  240. }
  241. }
  242. if ($this->_collectRecursively) {
  243. $this->_collectRecursive($item);
  244. }
  245. } elseif ($this->_collectFiles && is_file(
  246. $item
  247. ) && (!$this->_allowedFilesMask || preg_match(
  248. $this->_allowedFilesMask,
  249. basename($item)
  250. )) && (!$this->_disallowedFilesMask || !preg_match(
  251. $this->_disallowedFilesMask,
  252. basename($item)
  253. ))
  254. ) {
  255. $this->_collectedFiles[] = $item;
  256. }
  257. }
  258. }
  259. /**
  260. * Lauch data collecting
  261. *
  262. * @param bool $printQuery
  263. * @param bool $logQuery
  264. * @return $this
  265. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  266. * @throws \Exception
  267. */
  268. public function loadData($printQuery = false, $logQuery = false)
  269. {
  270. if ($this->isLoaded()) {
  271. return $this;
  272. }
  273. if (empty($this->_targetDirs)) {
  274. throw new \Exception('Please specify at least one target directory.');
  275. }
  276. $this->_collectedFiles = [];
  277. $this->_collectedDirs = [];
  278. $this->_collectRecursive($this->_targetDirs);
  279. $this->_generateAndFilterAndSort('_collectedFiles');
  280. if ($this->_dirsFirst) {
  281. $this->_generateAndFilterAndSort('_collectedDirs');
  282. $this->_collectedFiles = array_merge($this->_collectedDirs, $this->_collectedFiles);
  283. }
  284. // calculate totals
  285. $this->_totalRecords = count($this->_collectedFiles);
  286. $this->_setIsLoaded();
  287. // paginate and add items
  288. $from = ($this->getCurPage() - 1) * $this->getPageSize();
  289. $to = $from + $this->getPageSize() - 1;
  290. $isPaginated = $this->getPageSize() > 0;
  291. $cnt = 0;
  292. foreach ($this->_collectedFiles as $row) {
  293. $cnt++;
  294. if ($isPaginated && ($cnt < $from || $cnt > $to)) {
  295. continue;
  296. }
  297. $item = new $this->_itemObjectClass();
  298. $this->addItem($item->addData($row));
  299. if (!$item->hasId()) {
  300. $item->setId($cnt);
  301. }
  302. }
  303. return $this;
  304. }
  305. /**
  306. * With specified collected items:
  307. * - generate data
  308. * - apply filters
  309. * - sort
  310. *
  311. * @param string $attributeName '_collectedFiles' | '_collectedDirs'
  312. * @return void
  313. */
  314. private function _generateAndFilterAndSort($attributeName)
  315. {
  316. // generate custom data (as rows with columns) basing on the filenames
  317. foreach ($this->{$attributeName} as $key => $filename) {
  318. $this->{$attributeName}[$key] = $this->_generateRow($filename);
  319. }
  320. // apply filters on generated data
  321. if (!empty($this->_filters)) {
  322. foreach ($this->{$attributeName} as $key => $row) {
  323. if (!$this->_filterRow($row)) {
  324. unset($this->{$attributeName}[$key]);
  325. }
  326. }
  327. }
  328. // sort (keys are lost!)
  329. if (!empty($this->_orders)) {
  330. usort($this->{$attributeName}, [$this, '_usort']);
  331. }
  332. }
  333. /**
  334. * Callback for sorting items
  335. * Currently supports only sorting by one column
  336. *
  337. * @param array $a
  338. * @param array $b
  339. * @return int|void
  340. */
  341. protected function _usort($a, $b)
  342. {
  343. foreach ($this->_orders as $key => $direction) {
  344. $result = $a[$key] > $b[$key] ? 1 : ($a[$key] < $b[$key] ? -1 : 0);
  345. return self::SORT_ORDER_ASC === strtoupper($direction) ? $result : -$result;
  346. break;
  347. }
  348. }
  349. /**
  350. * Set select order
  351. * Currently supports only sorting by one column
  352. *
  353. * @param string $field
  354. * @param string $direction
  355. * @return Collection
  356. */
  357. public function setOrder($field, $direction = self::SORT_ORDER_DESC)
  358. {
  359. $this->_orders = [$field => $direction];
  360. return $this;
  361. }
  362. /**
  363. * Generate item row basing on the filename
  364. *
  365. * @param string $filename
  366. * @return array
  367. */
  368. protected function _generateRow($filename)
  369. {
  370. return ['filename' => $filename, 'basename' => basename($filename)];
  371. }
  372. /**
  373. * Set a custom filter with callback
  374. * The callback must take 3 params:
  375. * string $field - field key,
  376. * mixed $filterValue - value to filter by,
  377. * array $row - a generated row (before generating varien objects)
  378. *
  379. * @param string $field
  380. * @param mixed $value
  381. * @param string $type 'and'|'or'
  382. * @param callback $callback
  383. * @param bool $isInverted
  384. * @return $this
  385. */
  386. public function addCallbackFilter($field, $value, $type, $callback, $isInverted = false)
  387. {
  388. $this->_filters[$this->_filterIncrement] = [
  389. 'field' => $field,
  390. 'value' => $value,
  391. 'is_and' => 'and' === $type,
  392. 'callback' => $callback,
  393. 'is_inverted' => $isInverted,
  394. ];
  395. $this->_filterIncrement++;
  396. return $this;
  397. }
  398. /**
  399. * The filters renderer and caller
  400. * Applies to each row, renders once.
  401. *
  402. * @param array $row
  403. * @return bool
  404. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  405. * @SuppressWarnings(PHPMD.EvalExpression)
  406. */
  407. protected function _filterRow($row)
  408. {
  409. // render filters once
  410. if (!$this->_isFiltersRendered) {
  411. $eval = '';
  412. for ($i = 0; $i < $this->_filterIncrement; $i++) {
  413. if (isset($this->_filterBrackets[$i])) {
  414. $eval .= $this->_renderConditionBeforeFilterElement(
  415. $i,
  416. $this->_filterBrackets[$i]['is_and']
  417. ) . $this->_filterBrackets[$i]['value'];
  418. } else {
  419. $f = '$this->_filters[' . $i . ']';
  420. $eval .= $this->_renderConditionBeforeFilterElement(
  421. $i,
  422. $this->_filters[$i]['is_and']
  423. ) .
  424. ($this->_filters[$i]['is_inverted'] ? '!' : '') .
  425. '$this->_invokeFilter(' .
  426. "{$f}['callback'], array({$f}['field'], {$f}['value'], " .
  427. '$row))';
  428. }
  429. }
  430. $this->_filterEvalRendered = $eval;
  431. $this->_isFiltersRendered = true;
  432. }
  433. $result = false;
  434. if ($this->_filterEvalRendered) {
  435. eval('$result = ' . $this->_filterEvalRendered . ';');
  436. }
  437. return $result;
  438. }
  439. /**
  440. * Invokes specified callback
  441. * Skips, if there is no filtered key in the row
  442. *
  443. * @param callback $callback
  444. * @param array $callbackParams
  445. * @return bool
  446. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  447. */
  448. protected function _invokeFilter($callback, $callbackParams)
  449. {
  450. list($field, $value, $row) = $callbackParams;
  451. if (!array_key_exists($field, $row)) {
  452. return false;
  453. }
  454. return call_user_func_array($callback, $callbackParams);
  455. }
  456. /**
  457. * Fancy field filter
  458. *
  459. * @param string $field
  460. * @param mixed $cond
  461. * @param string $type 'and' | 'or'
  462. * @see Db::addFieldToFilter()
  463. * @return $this
  464. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  465. * @SuppressWarnings(PHPMD.NPathComplexity)
  466. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  467. */
  468. public function addFieldToFilter($field, $cond, $type = 'and')
  469. {
  470. $inverted = true;
  471. // simply check whether equals
  472. if (!is_array($cond)) {
  473. return $this->addCallbackFilter($field, $cond, $type, [$this, 'filterCallbackEq']);
  474. }
  475. // versatile filters
  476. if (isset($cond['from']) || isset($cond['to'])) {
  477. $this->_addFilterBracket('(', 'and' === $type);
  478. if (isset($cond['from'])) {
  479. $this->addCallbackFilter(
  480. $field,
  481. $cond['from'],
  482. 'and',
  483. [$this, 'filterCallbackIsLessThan'],
  484. $inverted
  485. );
  486. }
  487. if (isset($cond['to'])) {
  488. $this->addCallbackFilter(
  489. $field,
  490. $cond['to'],
  491. 'and',
  492. [$this, 'filterCallbackIsMoreThan'],
  493. $inverted
  494. );
  495. }
  496. return $this->_addFilterBracket(')');
  497. }
  498. if (isset($cond['eq'])) {
  499. return $this->addCallbackFilter($field, $cond['eq'], $type, [$this, 'filterCallbackEq']);
  500. }
  501. if (isset($cond['neq'])) {
  502. return $this->addCallbackFilter($field, $cond['neq'], $type, [$this, 'filterCallbackEq'], $inverted);
  503. }
  504. if (isset($cond['like'])) {
  505. return $this->addCallbackFilter($field, $cond['like'], $type, [$this, 'filterCallbackLike']);
  506. }
  507. if (isset($cond['nlike'])) {
  508. return $this->addCallbackFilter(
  509. $field,
  510. $cond['nlike'],
  511. $type,
  512. [$this, 'filterCallbackLike'],
  513. $inverted
  514. );
  515. }
  516. if (isset($cond['in'])) {
  517. return $this->addCallbackFilter($field, $cond['in'], $type, [$this, 'filterCallbackInArray']);
  518. }
  519. if (isset($cond['nin'])) {
  520. return $this->addCallbackFilter(
  521. $field,
  522. $cond['nin'],
  523. $type,
  524. [$this, 'filterCallbackInArray'],
  525. $inverted
  526. );
  527. }
  528. if (isset($cond['notnull'])) {
  529. return $this->addCallbackFilter(
  530. $field,
  531. $cond['notnull'],
  532. $type,
  533. [$this, 'filterCallbackIsNull'],
  534. $inverted
  535. );
  536. }
  537. if (isset($cond['null'])) {
  538. return $this->addCallbackFilter($field, $cond['null'], $type, [$this, 'filterCallbackIsNull']);
  539. }
  540. if (isset($cond['moreq'])) {
  541. return $this->addCallbackFilter(
  542. $field,
  543. $cond['moreq'],
  544. $type,
  545. [$this, 'filterCallbackIsLessThan'],
  546. $inverted
  547. );
  548. }
  549. if (isset($cond['gt'])) {
  550. return $this->addCallbackFilter($field, $cond['gt'], $type, [$this, 'filterCallbackIsMoreThan']);
  551. }
  552. if (isset($cond['lt'])) {
  553. return $this->addCallbackFilter($field, $cond['lt'], $type, [$this, 'filterCallbackIsLessThan']);
  554. }
  555. if (isset($cond['gteq'])) {
  556. return $this->addCallbackFilter(
  557. $field,
  558. $cond['gteq'],
  559. $type,
  560. [$this, 'filterCallbackIsLessThan'],
  561. $inverted
  562. );
  563. }
  564. if (isset($cond['lteq'])) {
  565. return $this->addCallbackFilter(
  566. $field,
  567. $cond['lteq'],
  568. $type,
  569. [$this, 'filterCallbackIsMoreThan'],
  570. $inverted
  571. );
  572. }
  573. if (isset($cond['finset'])) {
  574. $filterValue = $cond['finset'] ? explode(',', $cond['finset']) : [];
  575. return $this->addCallbackFilter($field, $filterValue, $type, [$this, 'filterCallbackInArray']);
  576. }
  577. // add OR recursively
  578. foreach ($cond as $orCond) {
  579. $this->_addFilterBracket('(', 'and' === $type);
  580. $this->addFieldToFilter($field, $orCond, 'or');
  581. $this->_addFilterBracket(')');
  582. }
  583. return $this;
  584. }
  585. /**
  586. * Prepare a bracket into filters
  587. *
  588. * @param string $bracket
  589. * @param bool $isAnd
  590. * @return $this
  591. */
  592. protected function _addFilterBracket($bracket = '(', $isAnd = true)
  593. {
  594. $this->_filterBrackets[$this->_filterIncrement] = [
  595. 'value' => $bracket === ')' ? ')' : '(',
  596. 'is_and' => $isAnd,
  597. ];
  598. $this->_filterIncrement++;
  599. return $this;
  600. }
  601. /**
  602. * Render condition sign before element, if required
  603. *
  604. * @param int $increment
  605. * @param bool $isAnd
  606. * @return string
  607. */
  608. protected function _renderConditionBeforeFilterElement($increment, $isAnd)
  609. {
  610. if (isset($this->_filterBrackets[$increment]) && ')' === $this->_filterBrackets[$increment]['value']) {
  611. return '';
  612. }
  613. $prevIncrement = $increment - 1;
  614. $prevBracket = false;
  615. if (isset($this->_filterBrackets[$prevIncrement])) {
  616. $prevBracket = $this->_filterBrackets[$prevIncrement]['value'];
  617. }
  618. if ($prevIncrement < 0 || $prevBracket === '(') {
  619. return '';
  620. }
  621. return $isAnd ? ' && ' : ' || ';
  622. }
  623. /**
  624. * Does nothing. Intentionally disabled parent method
  625. * @param string $field
  626. * @param string $value
  627. * @param string $type
  628. * @return $this
  629. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  630. */
  631. public function addFilter($field, $value, $type = 'and')
  632. {
  633. return $this;
  634. }
  635. /**
  636. * Get all ids of collected items
  637. *
  638. * @return array
  639. */
  640. public function getAllIds()
  641. {
  642. return array_keys($this->_items);
  643. }
  644. /**
  645. * Callback method for 'like' fancy filter
  646. *
  647. * @param string $field
  648. * @param mixed $filterValue
  649. * @param array $row
  650. * @return bool
  651. * @see addFieldToFilter()
  652. * @see addCallbackFilter()
  653. */
  654. public function filterCallbackLike($field, $filterValue, $row)
  655. {
  656. $filterValue = trim(stripslashes($filterValue), '\'');
  657. $filterValue = trim($filterValue, '%');
  658. $filterValueRegex = '(.*?)' . preg_quote($filterValue, '/') . '(.*?)';
  659. return (bool)preg_match("/^{$filterValueRegex}\$/i", $row[$field]);
  660. }
  661. /**
  662. * Callback method for 'eq' fancy filter
  663. *
  664. * @param string $field
  665. * @param mixed $filterValue
  666. * @param array $row
  667. * @return bool
  668. * @see addFieldToFilter()
  669. * @see addCallbackFilter()
  670. */
  671. public function filterCallbackEq($field, $filterValue, $row)
  672. {
  673. return $filterValue == $row[$field];
  674. }
  675. /**
  676. * Callback method for 'in' fancy filter
  677. *
  678. * @param string $field
  679. * @param mixed $filterValue
  680. * @param array $row
  681. * @return bool
  682. * @see addFieldToFilter()
  683. * @see addCallbackFilter()
  684. */
  685. public function filterCallbackInArray($field, $filterValue, $row)
  686. {
  687. return in_array($row[$field], $filterValue);
  688. }
  689. /**
  690. * Callback method for 'isnull' fancy filter
  691. *
  692. * @param string $field
  693. * @param mixed $filterValue
  694. * @param array $row
  695. * @return bool
  696. * @see addFieldToFilter()
  697. * @see addCallbackFilter()
  698. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  699. */
  700. public function filterCallbackIsNull($field, $filterValue, $row)
  701. {
  702. return null === $row[$field];
  703. }
  704. /**
  705. * Callback method for 'moreq' fancy filter
  706. *
  707. * @param string $field
  708. * @param mixed $filterValue
  709. * @param array $row
  710. * @return bool
  711. * @see addFieldToFilter()
  712. * @see addCallbackFilter()
  713. */
  714. public function filterCallbackIsMoreThan($field, $filterValue, $row)
  715. {
  716. return $row[$field] > $filterValue;
  717. }
  718. /**
  719. * Callback method for 'lteq' fancy filter
  720. *
  721. * @param string $field
  722. * @param mixed $filterValue
  723. * @param array $row
  724. * @return bool
  725. * @see addFieldToFilter()
  726. * @see addCallbackFilter()
  727. */
  728. public function filterCallbackIsLessThan($field, $filterValue, $row)
  729. {
  730. return $row[$field] < $filterValue;
  731. }
  732. }