Grido@master
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  • Download

Namespaces

  • Grido
    • Components
      • Actions
      • Columns
      • Filters
    • DataSources
    • PropertyAccessors
    • Translations

Classes

  • Grido\Translations\FileTranslator
  1 <?php
  2 
  3 /**
  4  * This file is part of the Grido (https://github.com/o5/grido)
  5  *
  6  * Copyright (c) 2011 Petr Bugyík (http://petr.bugyik.cz)
  7  *
  8  * For the full copyright and license information, please view
  9  * the file LICENSE.md that was distributed with this source code.
 10  */
 11 
 12 namespace Grido;
 13 
 14 use Grido\Components\Columns\Column;
 15 use Grido\Components\Filters\Filter;
 16 use Grido\Components\Paginator;
 17 
 18 /**
 19  * Grido - DataGrid for Nette Framework.
 20  *
 21  * @package     Grido
 22  * @author      Petr Bugyík
 23  *
 24  * @property-read int $count
 25  * @property-read mixed $data
 26  * @property-read \Nette\Utils\Html $tablePrototype
 27  * @property-write string $templateFile
 28  * @property bool $rememberState
 29  * @property array $defaultPerPage
 30  * @property array $defaultFilter
 31  * @property array $defaultSort
 32  * @property array $perPageList
 33  * @property \Nette\Localization\ITranslator $translator
 34  * @property Paginator $paginator
 35  * @property string $primaryKey
 36  * @property string $filterRenderType
 37  * @property DataSources\IDataSource $model
 38  * @property PropertyAccessors\IPropertyAccessor $propertyAccessor
 39  * @property callback $rowCallback
 40  */
 41 class Grid extends Components\Container
 42 {
 43     /***** DEFAULTS ****/
 44     const BUTTONS = 'buttons';
 45 
 46     const CLIENT_SIDE_OPTIONS = 'grido-options';
 47 
 48     /** @var int @persistent */
 49     public $page = 1;
 50 
 51     /** @var int @persistent */
 52     public $perPage;
 53 
 54     /** @var array @persistent */
 55     public $sort = array();
 56 
 57     /** @var array @persistent */
 58     public $filter = array();
 59 
 60     /** @var array event on all grid's components registered */
 61     public $onRegistered;
 62 
 63     /** @var array event on render */
 64     public $onRender;
 65 
 66     /** @var array event for modifying data */
 67     public $onFetchData;
 68 
 69     /** @var callback returns tr html element; function($row, Html $tr) */
 70     protected $rowCallback;
 71 
 72     /** @var \Nette\Utils\Html */
 73     protected $tablePrototype;
 74 
 75     /** @var bool */
 76     protected $rememberState = FALSE;
 77 
 78     /** @var string */
 79     protected $primaryKey = 'id';
 80 
 81     /** @var string */
 82     protected $filterRenderType;
 83 
 84     /** @var array */
 85     protected $perPageList = array(10, 20, 30, 50, 100);
 86 
 87     /** @var int */
 88     protected $defaultPerPage = 20;
 89 
 90     /** @var array */
 91     protected $defaultFilter = array();
 92 
 93     /** @var array */
 94     protected $defaultSort = array();
 95 
 96     /** @var DataSources\IDataSource */
 97     protected $model;
 98 
 99     /** @var int total count of items */
100     protected $count;
101 
102     /** @var mixed */
103     protected $data;
104 
105     /** @var Paginator */
106     protected $paginator;
107 
108     /** @var \Nette\Localization\ITranslator */
109     protected $translator;
110 
111     /** @var PropertyAccessors\IPropertyAccessor */
112     protected $propertyAccessor;
113 
114     /**
115      * Sets a model that implements the interface Grido\DataSources\IDataSource or data-source object.
116      * @param mixed $model
117      * @param bool $forceWrapper
118      * @throws \InvalidArgumentException
119      * @return Grid
120      */
121     public function setModel($model, $forceWrapper = FALSE)
122     {
123         $this->model = $model instanceof DataSources\IDataSource && $forceWrapper === FALSE
124             ? $model
125             : new DataSources\Model($model);
126 
127         return $this;
128     }
129 
130     /**
131      * Sets a property accesor that implements the interface Grido\PropertyAccessors\IPropertyAccessor.
132      * @param PropertyAccessors\IPropertyAccessor $propertyAccessor
133      * @return Grid
134      */
135     public function setPropertyAccessor(PropertyAccessors\IPropertyAccessor $propertyAccessor)
136     {
137         $this->propertyAccessor = $propertyAccessor;
138         return $this;
139     }
140 
141     /**
142      * Sets the default number of items per page.
143      * @param int $perPage
144      * @return Grid
145      */
146     public function setDefaultPerPage($perPage)
147     {
148         $perPage = (int) $perPage;
149         $this->defaultPerPage = $perPage;
150 
151         if (!in_array($perPage, $this->perPageList)) {
152             $this->perPageList[] = $perPage;
153             sort($this->perPageList);
154         }
155 
156         return $this;
157     }
158 
159     /**
160      * Sets default filtering.
161      * @param array $filter
162      * @return Grid
163      */
164     public function setDefaultFilter(array $filter)
165     {
166         $this->defaultFilter = array_merge($this->defaultFilter, $filter);
167         return $this;
168     }
169 
170     /**
171      * Sets default sorting.
172      * @param array $sort
173      * @return Grid
174      * @throws \InvalidArgumentException
175      */
176     public function setDefaultSort(array $sort)
177     {
178         static $replace = array('asc' => Column::ORDER_ASC, 'desc' => Column::ORDER_DESC);
179 
180         foreach ($sort as $column => $dir) {
181             $dir = strtr(strtolower($dir), $replace);
182             if (!in_array($dir, $replace)) {
183                 throw new \InvalidArgumentException("Dir '$dir' for column '$column' is not allowed.");
184             }
185 
186             $this->defaultSort[$column] = $dir;
187         }
188 
189         return $this;
190     }
191 
192     /**
193      * Sets items to per-page select.
194      * @param array $perPageList
195      * @return Grid
196      */
197     public function setPerPageList(array $perPageList)
198     {
199         $this->perPageList = $perPageList;
200 
201         if ($this->hasFilters(FALSE) || $this->hasOperation(FALSE)) {
202             $this['form']['count']->setItems($this->getItemsForCountSelect());
203         }
204 
205         return $this;
206     }
207 
208     /**
209      * Sets translator.
210      * @param \Nette\Localization\ITranslator $translator
211      * @return Grid
212      */
213     public function setTranslator(\Nette\Localization\ITranslator $translator)
214     {
215         $this->translator = $translator;
216         return $this;
217     }
218 
219     /**
220      * Sets type of filter rendering.
221      * Defaults inner (Filter::RENDER_INNER) if column does not exist then outer filter (Filter::RENDER_OUTER).
222      * @param string $type
223      * @throws \InvalidArgumentException
224      * @return Grid
225      */
226     public function setFilterRenderType($type)
227     {
228         $type = strtolower($type);
229         if (!in_array($type, array(Filter::RENDER_INNER, Filter::RENDER_OUTER))) {
230             throw new \InvalidArgumentException('Type must be Filter::RENDER_INNER or Filter::RENDER_OUTER.');
231         }
232 
233         $this->filterRenderType = $type;
234         return $this;
235     }
236 
237     /**
238      * Sets custom paginator.
239      * @param Paginator $paginator
240      * @return Grid
241      */
242     public function setPaginator(Paginator $paginator)
243     {
244         $this->paginator = $paginator;
245         return $this;
246     }
247 
248     /**
249      * Sets grid primary key.
250      * Defaults is "id".
251      * @param string $key
252      * @return Grid
253      */
254     public function setPrimaryKey($key)
255     {
256         $this->primaryKey = $key;
257         return $this;
258     }
259 
260     /**
261      * Sets file name of custom template.
262      * @param string $file
263      * @return Grid
264      */
265     public function setTemplateFile($file)
266     {
267         $this->getTemplate()->setFile($file);
268         return $this;
269     }
270 
271     /**
272      * Sets saving state to session.
273      * @param bool $state
274      * @return Grid
275      */
276     public function setRememberState($state = TRUE)
277     {
278         $this->getPresenter(); //component must be attached to presenter
279         $this->getRememberSession(TRUE); //start session if not
280         $this->rememberState = (bool) $state;
281 
282         return $this;
283     }
284 
285     /**
286      * Sets callback for customizing tr html object.
287      * Callback returns tr html element; function($row, Html $tr).
288      * @param $callback
289      * @return Grid
290      */
291     public function setRowCallback($callback)
292     {
293         $this->rowCallback = $callback;
294         return $this;
295     }
296 
297     /**
298      * Sets client-side options.
299      * @param array $options
300      * @return Grid
301      */
302     public function setClientSideOptions(array $options)
303     {
304         $this->getTablePrototype()->data[self::CLIENT_SIDE_OPTIONS] = json_encode($options);
305         return $this;
306     }
307 
308     /**********************************************************************************************/
309 
310     /**
311      * Returns total count of data.
312      * @return int
313      */
314     public function getCount()
315     {
316         if ($this->count === NULL) {
317             $this->count = $this->model->getCount();
318         }
319 
320         return $this->count;
321     }
322 
323     /**
324      * Returns default per page.
325      * @return int
326      */
327     public function getDefaultPerPage()
328     {
329         if (!in_array($this->defaultPerPage, $this->perPageList)) {
330             $this->defaultPerPage = $this->perPageList[0];
331         }
332 
333         return $this->defaultPerPage;
334     }
335 
336     /**
337      * Returns default filter.
338      * @return array
339      */
340     public function getDefaultFilter()
341     {
342         return $this->defaultFilter;
343     }
344 
345     /**
346      * Returns default sort.
347      * @return array
348      */
349     public function getDefaultSort()
350     {
351         return $this->defaultSort;
352     }
353 
354     /**
355      * Returns list of possible items per page.
356      * @return array
357      */
358     public function getPerPageList()
359     {
360         return $this->perPageList;
361     }
362 
363     /**
364      * Returns primary key.
365      * @return string
366      */
367     public function getPrimaryKey()
368     {
369         return $this->primaryKey;
370     }
371 
372     /**
373      * Returns remember state.
374      * @return bool
375      */
376     public function getRememberState()
377     {
378         return $this->rememberState;
379     }
380 
381     /**
382      * Returns row callback.
383      * @return callback
384      */
385     public function getRowCallback()
386     {
387         return $this->rowCallback;
388     }
389 
390     /**
391      * Returns items per page.
392      * @return int
393      */
394     public function getPerPage()
395     {
396         return $this->perPage === NULL
397             ? $this->getDefaultPerPage()
398             : $this->perPage;
399     }
400 
401     /**
402      * Returns actual filter values.
403      * @param string $key
404      * @return mixed
405      */
406     public function getActualFilter($key = NULL)
407     {
408         $filter = $this->filter ? $this->filter : $this->defaultFilter;
409         return $key && isset($filter[$key]) ? $filter[$key] : $filter;
410     }
411 
412     /**
413      * Returns fetched data.
414      * @param bool $applyPaging
415      * @param bool $useCache
416      * @throws \Exception
417      * @return array
418      */
419     public function getData($applyPaging = TRUE, $useCache = TRUE)
420     {
421         if ($this->model === NULL) {
422             throw new \Exception('Model cannot be empty, please use method $grid->setModel().');
423         }
424 
425         $data = $this->data;
426         if ($data === NULL || $useCache === FALSE) {
427             $this->applyFiltering();
428             $this->applySorting();
429 
430             if ($applyPaging) {
431                 $this->applyPaging();
432             }
433 
434             $data = $this->model->getData();
435 
436             if ($useCache === TRUE) {
437                 $this->data = $data;
438             }
439 
440             if ($applyPaging && $data && !in_array($this->page, range(1, $this->getPaginator()->pageCount))) {
441                 trigger_error("Page is out of range.", E_USER_NOTICE);
442                 $this->page = 1;
443             }
444 
445             if ($this->onFetchData) {
446                 $this->onFetchData($this);
447             }
448         }
449 
450         return $data;
451     }
452 
453     /**
454      * Returns translator.
455      * @return Translations\FileTranslator
456      */
457     public function getTranslator()
458     {
459         if ($this->translator === NULL) {
460             $this->setTranslator(new Translations\FileTranslator);
461         }
462 
463         return $this->translator;
464     }
465 
466     /**
467      * Returns remember session for set expiration, etc.
468      * @param bool $forceStart - if TRUE, session will be started if not
469      * @return \Nette\Http\SessionSection|NULL
470      */
471     public function getRememberSession($forceStart = FALSE)
472     {
473         $presenter = $this->getPresenter();
474         $session = $presenter->getSession();
475 
476         if (!$session->isStarted() && $forceStart) {
477             $session->start();
478         }
479 
480         return $session->isStarted()
481             ? $session->getSection($presenter->getName() . '\\' . ucfirst($this->getName()))
482             : NULL;
483     }
484 
485     /**
486      * Returns table html element of grid.
487      * @return \Nette\Utils\Html
488      */
489     public function getTablePrototype()
490     {
491         if ($this->tablePrototype === NULL) {
492             $this->tablePrototype = \Nette\Utils\Html::el('table');
493             $this->tablePrototype->id($this->getName())
494                 ->class[] = 'table table-striped table-hover';
495         }
496 
497         return $this->tablePrototype;
498     }
499 
500     /**
501      * @return string
502      * @internal
503      */
504     public function getFilterRenderType()
505     {
506         if ($this->filterRenderType !== NULL) {
507             return $this->filterRenderType;
508         }
509 
510         $this->filterRenderType = Filter::RENDER_OUTER;
511         if ($this->hasColumns() && $this->hasFilters() && $this->hasActions()) {
512             $this->filterRenderType = Filter::RENDER_INNER;
513 
514             $filters = $this[Filter::ID]->getComponents();
515             foreach ($filters as $filter) {
516                 if (!$this[Column::ID]->getComponent($filter->name, FALSE)) {
517                     $this->filterRenderType = Filter::RENDER_OUTER;
518                     break;
519                 }
520             }
521         }
522 
523         return $this->filterRenderType;
524     }
525 
526     /**
527      * @return DataSources\IDataSource
528      * @internal
529      */
530     public function getModel()
531     {
532         return $this->model;
533     }
534 
535     /**
536      * @return PropertyAccessors\IPropertyAccessor
537      * @internal
538      */
539     public function getPropertyAccessor()
540     {
541         if ($this->propertyAccessor === NULL) {
542             $this->propertyAccessor = new PropertyAccessors\ArrayObjectAccessor;
543         }
544 
545         return $this->propertyAccessor;
546     }
547 
548     /**
549      * @return Paginator
550      * @internal
551      */
552     public function getPaginator()
553     {
554         if ($this->paginator === NULL) {
555             $this->paginator = new Paginator;
556             $this->paginator->setItemsPerPage($this->getPerPage())
557                 ->setGrid($this);
558         }
559 
560         return $this->paginator;
561     }
562 
563     /**
564      * @param mixed $row item from db
565      * @return \Nette\Utils\Html
566      * @internal
567      */
568     public function getRowPrototype($row)
569     {
570         try {
571             $primaryValue = $this->getPropertyAccessor()->getProperty($row, $this->getPrimaryKey());
572         } catch (\Exception $e) {
573             $primaryValue = NULL;
574         }
575 
576         $tr = \Nette\Utils\Html::el('tr');
577         $primaryValue ? $tr->class[] = "grid-row-$primaryValue" : NULL;
578 
579         if ($this->rowCallback) {
580             $tr = callback($this->rowCallback)->invokeArgs(array($row, $tr));
581         }
582 
583         return $tr;
584     }
585 
586     /**
587      * Returns client-side options.
588      * @return array
589      */
590     public function getClientSideOptions()
591     {
592         return (array) json_decode($this->getTablePrototype()->data[self::CLIENT_SIDE_OPTIONS]);
593     }
594 
595     /**********************************************************************************************/
596 
597     /**
598      * Loads state informations.
599      * @param array $params
600      * @internal
601      */
602     public function loadState(array $params)
603     {
604         //loads state from session
605         $session = $this->getRememberSession();
606         if ($session && $this->getPresenter()->isSignalReceiver($this)) {
607             $session->remove();
608         } elseif ($session && !$params && $session->params) {
609             $params = (array) $session->params;
610         }
611 
612         parent::loadState($params);
613     }
614 
615     /**
616      * Saves state informations for next request.
617      * @param array $params
618      * @param \Nette\Application\UI\PresenterComponentReflection $reflection (internal, used by Presenter)
619      * @internal
620      */
621     public function saveState(array &$params, $reflection = NULL)
622     {
623         $this->onRegistered && $this->onRegistered($this);
624         return parent::saveState($params, $reflection);
625     }
626 
627     /**
628      * Ajax method.
629      * @internal
630      */
631     public function handleRefresh()
632     {
633         $this->reload();
634     }
635 
636     /**
637      * @param int $page
638      * @internal
639      */
640     public function handlePage($page)
641     {
642         $this->reload();
643     }
644 
645     /**
646      * @param array $sort
647      * @internal
648      */
649     public function handleSort(array $sort)
650     {
651         $this->page = 1;
652         $this->reload();
653     }
654 
655     /**
656      * @param \Nette\Forms\Controls\SubmitButton $button
657      * @internal
658      */
659     public function handleFilter(\Nette\Forms\Controls\SubmitButton $button)
660     {
661         $values = $button->form->values[Filter::ID];
662         $session = $this->rememberState //session filter
663             ? isset($this->getRememberSession(TRUE)->params['filter'])
664                 ? $this->getRememberSession(TRUE)->params['filter']
665                 : array()
666             : array();
667 
668         foreach ($values as $name => $value) {
669             if (is_numeric($value) || !empty($value) || isset($this->defaultFilter[$name]) || isset($session[$name])) {
670                 $this->filter[$name] = $this->getFilter($name)->changeValue($value);
671             } elseif (isset($this->filter[$name])) {
672                 unset($this->filter[$name]);
673             }
674         }
675 
676         $this->page = 1;
677         $this->reload();
678     }
679 
680     /**
681      * @param \Nette\Forms\Controls\SubmitButton $button
682      * @internal
683      */
684     public function handleReset(\Nette\Forms\Controls\SubmitButton $button)
685     {
686         $this->sort = array();
687         $this->filter = array();
688         $this->perPage = NULL;
689 
690         if ($session = $this->getRememberSession()) {
691             $session->remove();
692         }
693 
694         $button->form->setValues(array(Filter::ID => $this->defaultFilter), TRUE);
695 
696         $this->page = 1;
697         $this->reload();
698     }
699 
700     /**
701      * @param \Nette\Forms\Controls\SubmitButton $button
702      * @internal
703      */
704     public function handlePerPage(\Nette\Forms\Controls\SubmitButton $button)
705     {
706         $perPage = (int) $button->form['count']->value;
707         $this->perPage = $perPage == $this->defaultPerPage
708             ? NULL
709             : $perPage;
710 
711         $this->page = 1;
712         $this->reload();
713     }
714 
715     /**
716      * Refresh wrapper.
717      * @return void
718      * @internal
719      */
720     public function reload()
721     {
722         if ($this->presenter->isAjax()) {
723             $this->presenter->payload->grido = TRUE;
724             $this->invalidateControl();
725         } else {
726             $this->redirect('this');
727         }
728     }
729 
730     /**********************************************************************************************/
731 
732     /**
733      * @param string $class
734      * @return \Nette\Templating\FileTemplate
735      * @internal
736      */
737     public function createTemplate($class = NULL)
738     {
739         $template = parent::createTemplate($class);
740         $template->setFile(__DIR__ . '/Grid.latte');
741         $template->registerHelper('translate', callback($this->getTranslator(), 'translate'));
742 
743         return $template;
744     }
745 
746     /**
747      * @internal
748      * @throws \Exception
749      */
750     public function render()
751     {
752         if (!$this->hasColumns()) {
753             throw new \Exception('Grid must have defined a column, please use method $grid->addColumn*().');
754         }
755 
756         $this->saveRememberState();
757         $data = $this->getData();
758 
759         if ($this->onRender) {
760             $this->onRender($this);
761         }
762 
763         $this->template->data = $data;
764         $this->template->form = $form = $this['form'];
765         $this->template->paginator = $this->paginator;
766 
767         $form['count']->setValue($this->getPerPage());
768 
769         $this->template->render();
770     }
771 
772     protected function saveRememberState()
773     {
774         if ($this->rememberState) {
775             $session = $this->getRememberSession(TRUE);
776             $params = array_keys($this->getReflection()->getPersistentParams());
777             foreach ($params as $param) {
778                 $session->params[$param] = $this->$param;
779             }
780         }
781     }
782 
783     protected function applyFiltering()
784     {
785         $conditions = $this->__getConditions($this->getActualFilter());
786         $this->model->filter($conditions);
787     }
788 
789     /**
790      * @param array $filter
791      * @return array
792      * @internal
793      */
794     public function __getConditions(array $filter)
795     {
796         $conditions = array();
797         if ($filter) {
798             $this['form']->setDefaults(array(Filter::ID => $filter));
799 
800             foreach ($filter as $column => $value) {
801                 if ($component = $this->getFilter($column, FALSE)) {
802                     if ($condition = $component->__getCondition($value)) {
803                         $conditions[] = $condition;
804                     }
805                 } else {
806                     trigger_error("Filter with name '$column' does not exist.", E_USER_NOTICE);
807                 }
808             }
809         }
810 
811         return $conditions;
812     }
813 
814     protected function applySorting()
815     {
816         $sort = array();
817         $this->sort = $this->sort ? $this->sort : $this->defaultSort;
818 
819         foreach ($this->sort as $column => $dir) {
820             $component = $this->getColumn($column, FALSE);
821             if (!$component) {
822                 if (!isset($this->defaultSort[$column])) {
823                     trigger_error("Column with name '$column' does not exist.", E_USER_NOTICE);
824                     break;
825                 }
826 
827             } elseif (!$component->isSortable()) {
828                 if (isset($this->defaultSort[$column])) {
829                     $component->setSortable();
830                 } else {
831                     trigger_error("Column with name '$column' is not sortable.", E_USER_NOTICE);
832                     break;
833                 }
834             }
835 
836             if (!in_array($dir, array(Column::ORDER_ASC, Column::ORDER_DESC))) {
837                 if ($dir == '' && isset($this->defaultSort[$column])) {
838                     unset($this->sort[$column]);
839                     break;
840                 }
841 
842                 trigger_error("Dir '$dir' is not allowed.", E_USER_NOTICE);
843                 break;
844             }
845 
846             $sort[$component ? $component->column : $column] = $dir == Column::ORDER_ASC ? 'ASC' : 'DESC';
847         }
848 
849         if ($sort) {
850             $this->model->sort($sort);
851         }
852     }
853 
854     protected function applyPaging()
855     {
856         $paginator = $this->getPaginator()
857             ->setItemCount($this->getCount())
858             ->setPage($this->page);
859 
860         $perPage = $this->getPerPage();
861         if ($perPage !== NULL && !in_array($perPage, $this->perPageList)) {
862             trigger_error("The number '$perPage' of items per page is out of range.", E_USER_NOTICE);
863             $perPage = $this->defaultPerPage;
864         }
865 
866         $this->model->limit($paginator->getOffset(), $paginator->getLength());
867     }
868 
869     protected function createComponentForm($name)
870     {
871         $form = new \Nette\Application\UI\Form($this, $name);
872         $form->setTranslator($this->getTranslator());
873         $form->setMethod($form::GET);
874 
875         $buttons = $form->addContainer(self::BUTTONS);
876         $buttons->addSubmit('search', 'Grido.Search')
877             ->onClick[] = $this->handleFilter;
878         $buttons->addSubmit('reset', 'Grido.Reset')
879             ->onClick[] = $this->handleReset;
880         $buttons->addSubmit('perPage', 'Grido.ItemsPerPage')
881             ->onClick[] = $this->handlePerPage;
882 
883         $form->addSelect('count', 'Count', $this->getItemsForCountSelect())
884             ->controlPrototype->attrs['title'] = $this->getTranslator()->translate('Grido.ItemsPerPage');
885     }
886 
887     /**
888      * @return array
889      */
890     protected function getItemsForCountSelect()
891     {
892         return array_combine($this->perPageList, $this->perPageList);
893     }
894 }
895 
Grido@master API documentation generated by ApiGen