FRED™  3.0
FRED™: Framework for Rapid and Easy Development
Table.php
Go to the documentation of this file.
1 <?php
2 
4 
5 use \Rsi\Fred\Controller;
6 use \Rsi\Fred\Controller\Provider;
7 
8 class Table extends Char{
9 
10  const PROVIDER = 'provider'; //!< Data provider.
11  const KEY = 'key'; //!< System key (hidden from user).
12  const SEQUENCE = 'sequence'; //!< Sequence key (higher = newer).
13  const CODE = 'code'; //!< User code (what the user sees; default same as key).
14  const DESCR = 'descr'; //!< Full description.
15  const PARENT_KEY = 'parentKey'; //!< Parent key column.
16  const COLUMNS = 'columns'; //!< Grid columns (key = column name, value = column config; code and description if empty).
17  const LINK = 'link'; //!< Link (URL) for a record ("[columnName]" is replaced by the value of that column, for that record).
18  const BATCH_SIZE = 'batchSize'; //!< Number of records to return per call.
19  const DOWNLOAD_FORMATS = 'downloadFormats'; //!< Available download formats (key = description, value = function (use '*' to
20  // refer to this widget); default all downloadXxx functions; set to false for none).
21  const DOWNLOAD_NAME = 'downloadName'; //!< Filename (without extension) used for download.
22 
23  //column options
24  const ORDER = 'order'; //!< True if ordering the result by this column is allowed (default true if omitted).
25  const SEARCH = 'search'; //!< Allowed search methods for this column (see \\Rsi\\Fred\\Controller\\Provider::SEARCH_*
26  // constants).
27  const TOTAL = 'total'; //!< Show total for this column (see \\Rsi\\Fred\\Controller\\Provider::TOTAL_* constants; one or
28  // multiple in an array).
29  const GROUP = 'group'; //!< Show grouped data for this column (array with keys 'group' = column to group by - defaults to
30  // first column; 'column' = column to group the value for - defaults to current column; 'total' = see
31  // \\Rsi\\Fred\\Controller\\Provider::TOTAL_* constants; 'limit' = number of results: positive = top-x, negative = bottom-x,
32  // 0 = all, sorted by 'group' column).
33  const FORMAT = 'format'; //!< Formatting method.
34  const FORMAT_PARAMS = 'formatParams'; //!< Extra parameters for the formatting method.
35  const VISIBLE = 'visible'; //!< Default visibility (see VISIBLE_* and DOWNLOAD_* constants).
36 
37  const VISIBLE_DEFAULT = 1; //!< Show by deault (may be unselected by the user).
38  const VISIBLE_OPTIONAL = 2; //!< Dont't show by default, but make it selectable for the user.
39  const VISIBLE_NEVER = 4; //!< Do not show (also not optional).
40  const DOWNLOAD_DEFAULT = 16; //!< Download by default (may be unselected by the user).
41  const DOWNLOAD_VISIBLE = 32; //!< Download when visible.
42  const DOWNLOAD_OPTIONAL = 64; //!< Do not download by default, but make it selectable for the user.
43 
44  public $provider = null;
45  public $batchSize = 100;
46  public $link = null;
47  public $sequence = null;
48  public $sequenceMessage = '{[count<0]!}{[count>0]{[count|number()]}}'; //!< Message for new records (available tags: count,
49  //id, and caption; empty result = no message / no refresh; '!' = auto-refresh). Note: count = -1 means: latest record gone.
50  public $pdfWriter = null;
51  public $downloadName = 'data';
52  public $downloadCookieKey = 'fred-controller-table-downloading';
53  public $downloadBusyMessage = null;
54  public $downloadTimeout = null;
55 
56  protected $_key = null;
57  protected $_code = null;
58  protected $_descr = null;
59  protected $_parentKey = null;
60  protected $_search = null;
61  protected $_columns = null;
62  protected $_downloadFormats = null;
63 
64  protected function init(){
65  parent::init();
66  $this->publish(['key','descr','parentKey','search']);
67  }
68 
69  public function columnCaption($column){
70  return array_key_exists(self::CAPTION,$config = $this->columns[$column] ?: [])
71  ? $this->trans->str($config[self::CAPTION])
72  : $this->trans->id($column);
73  }
74 
75  public function sessionKey($key){
76  return array_merge(['widget','table',$this->id ?: $this->provider->id],is_array($key) ? $key : [$key]);
77  }
78 
79  public function clientConfig(){
80  $session = $this->_controller->session;
81  $columns = [];
82  foreach($this->columns as $column => $config){
83  if($caption = $this->columnCaption($column)) $config[self::CAPTION] = $caption;
84  $columns[$column] = $config;
85  }
86  $config = array_merge(parent::clientConfig(),$this->get([self::CODE,self::DESCR,self::PARENT_KEY,self::LINK,self::SEARCH]),[self::COLUMNS => $columns]);
87  if($search = $session->get($this->sessionKey('search'))) $config[self::SEARCH] = $search;
88  if($order = $session->get($this->sessionKey('order')) ?: $this->provider->order) $config['defaultOrder'] = $order;
89  if($this->downloadFormats) $config[self::DOWNLOAD_FORMATS] = array_keys($this->downloadFormats);
90  $config['downloadCookieKey'] = $this->downloadCookieKey;
91  if($this->downloadBusyMessage) $config['downloadBusyMessage'] = $this->trans->str($this->downloadBusyMessage);
92  if($this->downloadTimeout) $config = $this->downloadTimeout;
93  return $config;
94  }
95 
96  public function convert($value){
97  return $this->provider->trans($this->key,$this->code,$value,$this->params);
98  }
99 
100  public function clientConvert($value){
101  return $this->convert($value);
102  }
103 
104  public function format($value){
105  return $this->provider->trans($this->code,$this->key,$value,$this->params);
106  }
107 
108  public function clientFormat($value){
109  return $this->format($value);
110  }
111 
112  protected function checkRange($value,$index = null){
113  return $this->provider->exists($this->key,$value,$this->params);
114  }
115 
116  protected function download($records,$format,$ext = null,$content_type = null){
117  $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
118  $sheet = $spreadsheet->setActiveSheetIndex(0)->setTitle($this->downloadName);
119  if(!($columns = $this->request->record('columns')))
120  $this->fred->externalError('Invalid columns',['columns' => $columns]);
121  $columnChars = [];
122  foreach(array_values($columns) as $index => $column){
123  if(!array_key_exists($column,$this->columns)) $this->fred->externalError('Unknown column',['column' => $column]);
124  $char = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index);
125  $sheet->setCellValue($cell = $char . 1,$this->columnCaption($column));
126  $sheet->getStyle($cell)->getFont()->setBold(true);
127  foreach($records as $index => $record) $sheet->setCellValue($char . ($index + 2),$record[$column] ?? null);
128  $sheet->getColumnDimension($char)->setAutoSize(true);
129  }
130  \Rsi\Http::downloadHeaders($this->downloadName . '.' . ($ext ?: strtolower($format)),$content_type);
131  \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet,$format)->save('php://output');
132  }
133 
134  protected function downloadExcel($records){
135  $this->download($records,'Xls');
136  }
137 
138  protected function downloadXLSX($records){
139  $this->download($records,'Xlsx');
140  }
141 
142  protected function downloadCSV($records){
143  $this->download($records,'Csv');
144  }
145 
146  protected function downloadPDF($records){
147  $this->download($records,$this->pdfWriter,'pdf');
148  }
149 
150  protected function downloadXML($records){
151  $root = new \SimpleXmlElement('<records />');
152  foreach($records as $record){
153  $node = $root->addChild('record');
154  foreach($record as $key => $value) $node->addChild($key,htmlspecialchars($value));
155  }
156  \Rsi\Http::downloadHeaders($this->downloadName . '.xml');
157  print($root->asXML());
158  }
159 
160  protected function downloadJSON($records){
161  \Rsi\Http::downloadHeaders($this->downloadName . '.json');
162  print(json_encode($records));
163  }
164 
165  public function searchParams(&$search,&$order){
166  $search = $order = [];
167  if(is_array($param = $this->request->complex('search'))) foreach($param as $column => $values)
168  if(
169  array_key_exists($column,$this->columns) && is_array($values) &&
170  ($types = \Rsi\Record::get($this->columns[$column],self::SEARCH,[Provider::SEARCH_LIKE]))
171  ){
172  foreach($values as $type => $value) if(!in_array($type,$types))
173  $this->fred->externalError('Invalid search type',['class' => get_called_class()] + compact('column','type'));
174  $search[$column] = $values;
175  }
176  if(is_array($param = $this->request->complex('order'))) foreach($param as $column => $type)
177  if(array_key_exists($column,$this->columns) && \Rsi\Record::get($this->columns[$column],self::ORDER,true))
178  $order[$column] = (int)$type;
179  }
180 
181  protected function actionRecords(){
182  $this->searchParams($search,$order);
183  $session = $this->_controller->session;
184  $session->set($this->sessionKey('search'),$search);
185  $session->set($this->sessionKey('order'),$order);
186  if($this->parentKey) $search[$this->parentKey] = [Provider::SEARCH_EXACT => $this->convert($this->request->parent)];
187  $offset = max(0,(int)$this->request->offset);
188  $limit = min(max(0,(int)$this->request->limit) ?: $this->batchSize,$this->batchSize);
189  $records = $this->provider->search($search,$order,$params = $this->params,$offset,$limit);
190  if($this->parentKey) foreach($records as $index => $record) $records[$index][$this->parentKey] =
191  ($value = $record[$this->key] ?? null) && $this->provider->exists($this->parentKey,$value,$params);
192  $this->request->result = [
193  'records' => $records,
194  'eof' => count($records) < $limit
195  ];
196  }
197 
198  protected function actionTotals(){
199  $this->searchParams($search,$order);
200  $this->request->result = $totals = [];
201  foreach($this->columns as $column => $config)
202  if($total = \Rsi\Record::get($config,self::TOTAL)) $totals[$column] = $total;
203  if($this->sequence) $totals[$this->sequence] =
204  array_unique(array_merge(\Rsi\Record::explode($totals[$this->sequence] ?? null),[Controller::TOTAL_MAX]));
205  if($totals){
206  $this->request->result['totals'] = $totals = $this->provider->totals($search,$totals,$this->params);
207  if($this->sequence) $this->request->result['latest'] = $totals[$this->sequence][Controller::TOTAL_MAX];
208  }
209  }
210 
211  protected function actionGroup(){
212  if($config = \Rsi\Record::get($this->columns,[$column = $this->request->column,self::GROUP])){
213  $this->searchParams($search,$order);
214  $this->request->result = $this->provider->group(
215  $search,
216  $config['group'] ?? \Rsi\Record::key($this->columns),
217  $config['column'] ?? $column,
218  $config['total'],
219  $config['limit'] ?? null
220  );
221  }
222  }
223 
224  protected function actionDownload(){
225  if($method = \Rsi\Record::get($this->downloadFormats,$this->request->format)){
226  $this->request->cookie($this->downloadCookieKey,null,-1,['httponly' => false]);
227  $this->searchParams($search,$order);
228  call_user_func($method,$this->provider->search($search,$order,$this->params));
229  $this->fred->halt();
230  }
231  }
232 
233  protected function actionNew(){
234  if($this->sequence){
235  $this->searchParams($search,$order);
236  if(!array_key_exists($this->sequence,$search)) $search[$this->sequence] = [];
237  if($latest = $this->request->latest) $search[$this->sequence][Provider::SEARCH_FROM] = $latest;
238  $count = \Rsi\Record::get(
239  $this->provider->totals($search,[$this->sequence => Controller::TOTAL_COUNT],$this->params),
241  );
242  if($latest) $count--;
243  if($this->request->result['count'] = $count) $this->request->result['message'] = $this->trans->str(
244  $this->sequenceMessage,
245  ['count' => $count,'id' => $this->id,'caption' => $this->caption()]
246  );
247  }
248  }
249 
250  protected function getCode(){
251  return $this->config(self::CODE) ?: $this->_key;
252  }
253 
254  protected function getColumns(){
255  if(!$this->_columns){
256  $this->_columns = $this->config(self::COLUMNS);
257  if(!$this->_columns){
258  $this->_columns = [$this->code => null];
259  if($this->descr) $this->_columns[$this->descr] = null;
260  }
261  }
262  return $this->_columns;
263  }
264 
265  protected function getDownloadFormats(){
266  if($this->_downloadFormats === null){
267  $this->_downloadFormats = $this->config(self::DOWNLOAD_FORMATS);
268  if($this->_downloadFormats === null){
269  $this->_downloadFormats = [];
270  foreach(get_class_methods($this) as $method) if((strlen($method) > 8) && (substr($method,0,8) == 'download'))
271  $this->_downloadFormats[substr($method,8)] = [$this,$method];
272  }
273  elseif($this->_downloadFormats) foreach($this->_downloadFormats as &$function)
274  if(is_array($function) && ($function[0] == '*')) $function[0] = $this;
275  unset($function);
276  }
278  }
279 
280 }
const DESCR
Full description.
Definition: Table.php:14
const VISIBLE_OPTIONAL
Dont&#39;t show by default, but make it selectable for the user.
Definition: Table.php:38
const SEARCH_EXACT
Search for values identical to the reference value (this may also be an array with.
Definition: Provider.php:9
searchParams(&$search, &$order)
Definition: Table.php:165
const BATCH_SIZE
Number of records to return per call.
Definition: Table.php:18
const DOWNLOAD_VISIBLE
Download when visible.
Definition: Table.php:41
const FORMAT
Formatting method.
Definition: Table.php:33
const DOWNLOAD_DEFAULT
Download by default (may be unselected by the user).
Definition: Table.php:40
const FORMAT_PARAMS
Extra parameters for the formatting method.
Definition: Table.php:34
const VISIBLE_DEFAULT
Show by deault (may be unselected by the user).
Definition: Table.php:37
$sequenceMessage
Message for new records (available tags: count,.
Definition: Table.php:48
const PROVIDER
Data provider.
Definition: Table.php:10
const TOTAL
Show total for this column (see \Rsi\Fred\Controller\Provider::TOTAL_* constants; one or...
Definition: Table.php:27
const SEARCH_LIKE
Search for values that match reference value with wildcard (? = 1 char, * = multiple chars)...
Definition: Provider.php:12
checkRange($value, $index=null)
Definition: Table.php:112
const GROUP
Show grouped data for this column (array with keys &#39;group&#39; = column to group by - defaults to...
Definition: Table.php:29
const VISIBLE_NEVER
Do not show (also not optional).
Definition: Table.php:39
const DOWNLOAD_NAME
Filename (without extension) used for download.
Definition: Table.php:21
const LINK
Link (URL) for a record ("[columnName]" is replaced by the value of that column, for that record)...
Definition: Table.php:17
const DOWNLOAD_OPTIONAL
Do not download by default, but make it selectable for the user.
Definition: Table.php:42
const COLUMNS
Grid columns (key = column name, value = column config; code and description if empty).
Definition: Table.php:16
const SEARCH_FROM
Search for values larger than the reference value.
Definition: Provider.php:7
publish($property, $visibility=self::READABLE)
Publish a property (or hide it again).
Definition: Thing.php:27
const ORDER
True if ordering the result by this column is allowed (default true if omitted).
Definition: Table.php:24
const SEQUENCE
Sequence key (higher = newer).
Definition: Table.php:12
download($records, $format, $ext=null, $content_type=null)
Definition: Table.php:116
const PARENT_KEY
Parent key column.
Definition: Table.php:15
config($key, $default=null)
Get single configuration value.
Definition: Widget.php:93
const SEARCH
Allowed search methods for this column (see \Rsi\Fred\Controller\Provider::SEARCH_*.
Definition: Table.php:25
const DOWNLOAD_FORMATS
Available download formats (key = description, value = function (use &#39;*&#39; to.
Definition: Table.php:19
const CODE
User code (what the user sees; default same as key).
Definition: Table.php:13
const KEY
System key (hidden from user).
Definition: Table.php:11
const VISIBLE
Default visibility (see VISIBLE_* and DOWNLOAD_* constants).
Definition: Table.php:35