├── INSTALL
├── README.md
└── views
├── elements
├── csv
│ ├── column_actions.ctp
│ ├── grid_empty_row.ctp
│ ├── grid_full.ctp
│ ├── grid_headers.ctp
│ └── grid_row.ctp
└── table
│ ├── column_actions.ctp
│ ├── grid_empty_row.ctp
│ ├── grid_full.ctp
│ ├── grid_headers.ctp
│ └── grid_row.ctp
└── helpers
└── grid.php
/INSTALL:
--------------------------------------------------------------------------------
1 | git submodule add git://github.com/rross0227/CakeGrid.git app/plugins/cake_grid
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | _____ _ _____ _ _
2 | / ____| | | / ____| (_) | |
3 | | | __ _| | _____ | | __ _ __ _ __| |
4 | | | / _` | |/ / _ \ | | |_ | '__| |/ _` |
5 | | |___| (_| | < __/ | |__| | | | | (_| |
6 | \_____\__,_|_|\_\___| \_____|_| |_|\__,_|
7 |
8 | Easy tabular data for CakePHP (cakephp.org)
9 | -- by Robert Ross (rross@sdreader.com)
10 | -- available at http://github.com/rross0227
11 | -- requires CakePHP 1.3.x
12 |
13 | Copyright (c) 2011 The Daily Save, LLC. All rights reserved.
14 |
15 | This program is free software: you can redistribute it and/or modify
16 | it under the terms of the GNU General Public License as published by
17 | the Free Software Foundation, either version 3 of the License, or
18 | (at your option) any later version.
19 |
20 | This program is distributed in the hope that it will be useful,
21 | but WITHOUT ANY WARRANTY; without even the implied warranty of
22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 | GNU General Public License for more details.
24 |
25 | You should have received a copy of the GNU General Public License
26 | along with this program. If not, see .
27 |
28 | # Some notes
29 | CakeGrid was created because we create tables constantly. It's annoying how much we make tables. Especially in our admin.
30 | Our tables are simple to complex, so columns can use elements, or formatting. Doesn't matter.
31 | This plugin was created quickly and therefore has no test cases, But is currently in my todo list.
32 | I'll give a cookie to anyone that makes them.
33 |
34 | # How to use
35 |
36 | Install the plugin (Read the INSTALL file)
37 |
38 | In your controller (or for global usage include it in your AppController) include the helper by adding this line:
39 |
40 | var $helpers = array('CakeGrid.Grid');
41 |
42 | In your view file you can now create grids easily by doing something like this:
43 |
44 | $this->Grid->addColumn('Order Id', '/Order/id');
45 | $this->Grid->addColumn('Order Date', '/Order/created', array('type' => 'date'));
46 | $this->Grid->addColumn('Order Amount', '/Order/amount', array('type' => 'money'));
47 |
48 | $this->Grid->addAction('Edit', array('controller' => 'orders', 'action' => 'edit'), array('/Order/id'));
49 |
50 | echo $this->Grid->generate($results);
51 |
52 | This will create a 4 column grid (including actions) for all of your orders or whatever you like!
53 | CakeGrid uses the Set::extract format found here: http://book.cakephp.org/view/1501/extract
54 |
55 | If you're generating multiple tables per view, reset the grid and start over after you've generated your result set:
56 |
57 | $this->Grid->reset();
58 |
59 | # Actions Column
60 |
61 | @param string $name
62 | @param array $url
63 | @param array $trailingParams
64 |
65 | $this->Grid->addAction('Edit', array('controller' => 'orders', 'action' => 'edit'), array('/Order/id'));
66 |
67 | ## What this does:
68 |
69 | The First parameter if the link text (Edit, Delete, Rename, etc..)
70 | The Second parameter is the controller action that will be handling the action.
71 | The Third parameter is for the action parameters. So the id of the result, maybe a date? Whatever. Use your imagination.
72 |
73 |
74 | # Advanced Functionality
75 |
76 | CakeGrid allows you to make column results linkable. For example, if a column is for the order number, you can make the result a link to the actual order details.
77 |
78 | For example:
79 |
80 | $this->Grid->addColumn('ID', '/Order/id', array('linkable' => array(
81 | 'url' => array('action' => 'details'),
82 | 'trailingParams' => array('/Order/id')
83 | )));
84 |
85 | Linkable is the option parameter takes 3 sub options. url, trailingParams, and Html::link options (see http://book.cakephp.org/view/1442/link)
86 |
87 | The url could be considered the controller and action, and maybe a named parameter. The trailing parameters is the id or whatever you like. It will be pulled from the result.
88 | __Note:__ Named parameters are not yet supported, but so array('named' => array('id' => '/Order/id')) will not work, but array('id' => '/Order/id') will
89 |
90 | ## Total Row
91 |
92 | To create a "totals" row. You can set a column to total. Only money and numbers will work (obviously).
93 |
94 | The syntax is as follows:
95 |
96 | $this->addColumn('Amount', '/Order/amount', array('total' => true));
97 |
98 | This will produce a final row with the totals on it for the column. If the column type is set to money or number, it will format the totals as well.
99 |
100 | ## Concat and Format
101 |
102 | CakeGrid allows you to do concatenation and sprintf formatting on your cells. For example, if you have a first and last name but don't want to use CakePHP's virtualFields to merge them together, you can use CakeGrid to do it.
103 |
104 | ### Concat
105 |
106 | $this->Grid->addColumn('User', array(
107 | 'type' => 'concat',
108 | '/User/first_name',
109 | '/User/last_name'
110 | ));
111 |
112 | This will output in the cell the users first and last name together. Concat uses spaces as the default separator but can be changed in 2 ways.
113 |
114 | // Inline with the column options
115 | $this->Grid->addColumn('User', array(
116 | 'type' => 'concat',
117 | 'separator' => ' ',
118 | '/User/first_name',
119 | '/User/last_name'
120 | ));
121 |
122 | // Global usage
123 | $this->Grid->options(array(
124 | 'separator' => ' '
125 | ));
126 |
127 | ### Formatting
128 |
129 | $this->Grid->addColumn('Register Date', array(
130 | 'type' => 'format',
131 | 'with' => '%s (%s)',
132 | '/User/created',
133 | '/User/register_ip'
134 | ));
135 |
136 | ## Elements
137 |
138 | CakeGrid allows the usage of your own elements to be used in cells. This is useful if you're wanting to use a hasMany relationship into a dropdown or something similar.
139 | When using an element, a valuePath is not used. CakeGrid will pass the entire result of the row to the element.
140 |
141 | For Example:
142 |
143 | $this->Grid->addColumn('Purchases', null, array('element' => 'purchase_list'));
144 |
145 | Whatever the result is for the current row will get passed to the element as $result.
146 |
147 | So in your element (purchase_list.ctp for example)
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/views/elements/csv/column_actions.ctp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bobbytables/CakeGrid/1bbc49ea56e0fc70ab96d2d63c1b7164b53c121b/views/elements/csv/column_actions.ctp
--------------------------------------------------------------------------------
/views/elements/csv/grid_empty_row.ctp:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/elements/csv/grid_full.ctp:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/views/elements/csv/grid_headers.ctp:
--------------------------------------------------------------------------------
1 | Grid->csvData(Set::extract('/title', array_values($headers))); ?>
--------------------------------------------------------------------------------
/views/elements/csv/grid_row.ctp:
--------------------------------------------------------------------------------
1 | Grid->csvData($rowColumns); ?>
--------------------------------------------------------------------------------
/views/elements/table/column_actions.ctp:
--------------------------------------------------------------------------------
1 | $action): ?>
2 | Html->link($title, $action['url'], $action['options'] + array('class' => 'cg_action')); ?>
3 |
--------------------------------------------------------------------------------
/views/elements/table/grid_empty_row.ctp:
--------------------------------------------------------------------------------
1 |
2 | |
3 |
--------------------------------------------------------------------------------
/views/elements/table/grid_full.ctp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/views/elements/table/grid_headers.ctp:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/elements/table/grid_row.ctp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | |
6 |
7 |
--------------------------------------------------------------------------------
/views/helpers/grid.php:
--------------------------------------------------------------------------------
1 | options(array());
58 | }
59 |
60 | /**
61 | * Set options for headers and such
62 | *
63 | * @param string $options
64 | * @return void
65 | * @author Robert Ross
66 | */
67 | function options($options){
68 | $defaults = array(
69 | 'class_header' => 'cg_header',
70 | 'class_row' => 'cg_row',
71 | 'class_table' => 'cg_table',
72 | 'empty_message' => 'No Results',
73 | 'separator' => ' ',
74 | 'type' => 'table'
75 | );
76 |
77 | $options = array_merge($defaults, $options);
78 |
79 | $this->__settings = $options;
80 |
81 | //-- Set the directory we'll be looking for elements
82 | $this->elemDir = $this->__settings['type'];
83 | }
84 |
85 | /**
86 | * Resets columns and actions so multiple grids may be created
87 | *
88 | * @return void
89 | * @author Robert Ross
90 | */
91 | function reset(){
92 | $this->__columns = array();
93 | $this->__actions = array();
94 | }
95 |
96 | /**
97 | * Adds a column to the grid
98 | *
99 | * @param string $title
100 | * @param string $valuePath
101 | * @param array $options
102 | * @return void
103 | * @author Robert Ross
104 | */
105 | function addColumn($title, $valuePath, array $options = array()){
106 | $defaults = array(
107 | 'editable' => false,
108 | 'type' => 'string',
109 | 'element' => false,
110 | 'linkable' => false,
111 | 'total' => false
112 | );
113 |
114 | $options = array_merge($defaults, $options);
115 |
116 | $titleSlug = Inflector::slug($title);
117 |
118 | $this->__columns[$titleSlug] = array(
119 | 'title' => $title,
120 | 'valuePath' => $valuePath,
121 | 'options' => $options
122 | );
123 |
124 | if($options['total'] == true){
125 | $this->__totals[$title] = 0;
126 | }
127 |
128 | return $titleSlug;
129 | }
130 |
131 | /**
132 | * Adds an actions column if it doesnt exist, then creates
133 | *
134 | * @param string $name
135 | * @param array $url
136 | * @param array $trailingParams - This is the stuff after /controller/action. Such as /orders/edit/{id}. It's the action parameters in other words
137 | * @return void
138 | * @author Robert Ross
139 | */
140 | function addAction($name, array $url, array $trailingParams = array(), array $options = array()){
141 | $this->__actions[$name] = array(
142 | 'url' => $url,
143 | 'trailingParams' => $trailingParams,
144 | 'options' => $options
145 | );
146 |
147 | if(!isset($this->__columns['actions'])){
148 | $this->addColumn('Actions', null, array('type' => 'actions'));
149 | }
150 |
151 | return true;
152 | }
153 |
154 | /**
155 | * Generates the entire grid including headers and results
156 | *
157 | * @param string $results
158 | * @return void
159 | * @author Robert Ross
160 | */
161 | function generate($results){
162 | $View = $this->__view();
163 |
164 | $directory = $this->__settings['type'];
165 |
166 | if($this->__settings['type'] == 'csv' && !empty($this->__totals)){
167 | array_unshift($this->__columns, array(
168 | 'title' => '',
169 | 'valuePath' => '',
170 | 'options' => array(
171 | 'type' => 'empty'
172 | )
173 | ));
174 | }
175 |
176 | //-- Build the columns
177 | $headers = $View->element($this->elemDir . DS . 'grid_headers', array(
178 | 'plugin' => $this->plugin_name,
179 | 'headers' => $this->__columns,
180 | 'options' => $this->__settings
181 | ));
182 | $results = $this->results($results);
183 |
184 | $generated = $View->element($this->elemDir . DS . 'grid_full', array(
185 | 'plugin' => $this->plugin_name,
186 | 'headers' => $headers,
187 | 'results' => $results,
188 | 'options' => $this->__settings
189 | ));
190 |
191 | return $generated;
192 | }
193 |
194 | /**
195 | * Creates the result set inclusive of the actions column (if applied)
196 | *
197 | * @param string $results
198 | * @return void
199 | * @author Robert Ross
200 | */
201 | function results($results = array()){
202 | $rows = array();
203 | $View = $this->__view();
204 |
205 | foreach($results as $key => $result){
206 | //-- Loop through columns
207 | $rowColumns = array();
208 |
209 | foreach($this->__columns as $column){
210 | $rowColumns[] = $this->__generateColumn($result, $column);
211 | }
212 |
213 | $rows[] = $View->element($this->elemDir . DS . 'grid_row', array(
214 | 'plugin' => $this->plugin_name,
215 | 'zebra' => $key % 2 == 0 ? 'odd' : 'even',
216 | 'rowColumns' => $rowColumns,
217 | 'options' => $this->__settings
218 | ));
219 | }
220 |
221 | if(!empty($this->__totals)){
222 | $totalColumns = array();
223 |
224 | $i = 0;
225 | foreach($this->__columns as $column){
226 | if($i == 0){
227 | $totalColumns[] = 'Total';
228 | $i++;
229 | continue;
230 | }
231 | $i++;
232 |
233 | if(isset($this->__totals[$column['title']])){
234 | if($column['options']['type'] == 'money'){
235 | $total = money_format("%n", $this->__totals[$column['title']]);
236 | } else if($column['options']['type'] == 'number'){
237 | $total = number_format($this->__totals[$column['title']]);
238 | }
239 |
240 | if($this->__settings['type'] == 'csv'){
241 | $total = floatval(str_replace(array('$', ','), '', $total));
242 | $totalColumns[] = $total;
243 | continue;
244 | }
245 |
246 | $totalColumns[] = $total . ' (total)';
247 | continue;
248 | }
249 |
250 | $totalColumns[] = '';
251 | }
252 |
253 | $rows[] = $View->element($this->elemDir . DS . 'grid_row', array(
254 | 'plugin' => $this->plugin_name,
255 | 'rowColumns' => $totalColumns,
256 | 'options' => $this->__settings,
257 | 'zebra' => 'totals'
258 | ));
259 | }
260 |
261 | //-- Upon review, this if statement is hilarious
262 | if(empty($rows) && !empty($this->__settings['empty_message'])){
263 | $rows[] = $View->element($this->elemDir . DS . 'grid_empty_row', array(
264 | 'plugin' => $this->plugin_name,
265 | 'colspan' => sizeof($this->__columns) + (sizeof($this->__actions) ? 1 : 0),
266 | 'options' => $this->__settings
267 | ));
268 | }
269 |
270 | return implode("\n", $rows);
271 | }
272 |
273 | /**
274 | * Creates the column based on the type. If there's no type, just a plain ol' string.
275 | *
276 | * @param string $result
277 | * @param string $column
278 | * @return void
279 | * @author Robert Ross
280 | */
281 | private function __generateColumn($result, $column){
282 | if($column['options']['type'] == 'empty'){
283 | return '';
284 | }
285 |
286 | if(!isset($column['valuePath'])){
287 | $value = $result;
288 | } else if(!is_array($column['valuePath'])) {
289 | $value = Set::extract($column['valuePath'], $result);
290 | $value = array_pop($value);
291 | } else if(is_array($column['valuePath'])){
292 | $valuePath = $column['valuePath'];
293 |
294 | if($valuePath['type'] == 'concat'){
295 | $separator = isset($valuePath['separator']) ? $valuePath['separator'] : $this->__settings['separator'];
296 | unset($valuePath['type'], $valuePath['separator']);
297 |
298 | $values = array();
299 | foreach($valuePath as $path){
300 | $extracted = Set::extract($path, $result);
301 | $values[] = array_pop($extracted);
302 | }
303 |
304 | $value = implode($separator, $values);
305 | } else if($valuePath['type'] == 'format'){
306 | $format = $valuePath['with'];
307 | unset($valuePath['type'], $valuePath['with']);
308 |
309 | $values = array($format);
310 | foreach($valuePath as $path){
311 | $extracted = (array) Set::extract($path, $result);
312 | $values[] = array_pop($extracted);
313 | }
314 |
315 | $value = call_user_func_array('sprintf', $values);
316 | }
317 | }
318 |
319 | //-- Total things up if needed
320 | if(isset($column['options']['total']) && $column['options']['total'] == true){
321 | $this->__totals[$column['title']] += $value;
322 | }
323 |
324 | if(isset($column['options']['element']) && $column['options']['element'] != false){
325 | $View = $this->__view();
326 |
327 | return $View->element($this->elemDir . DS . $column['options']['element'], array('result' => $value));
328 | } else {
329 | if(isset($column['options']['type']) && $column['options']['type'] == 'date'){
330 | $value = date('m/d/Y', strtotime($value));
331 | } else if(isset($column['options']['type']) && $column['options']['type'] == 'datetime'){
332 | $value = date('m/d/Y h:ia', strtotime($value));
333 | } else if(isset($column['options']['type']) && $column['options']['type'] == 'money' && $this->__settings['type'] != 'csv'){
334 | $value = money_format('%n', $value);
335 | } else if(isset($column['options']['type']) && $column['options']['type'] == 'actions'){
336 | $View = $this->__view();
337 | $actions = array();
338 |
339 | //-- Need to retrieve the results of the trailing params
340 | foreach($this->__actions as $name => $action){
341 | //-- Check to see if the action is supposed to be hidden for this result (set in the controller)
342 | if(isset($result['show_actions']) && is_array($result['show_actions']) && !in_array($name, $result['show_actions'])){
343 | continue;
344 | }
345 |
346 | //-- Need to find the trailing parameters (id, action type, etc)
347 | $trailingParams = array();
348 | if(!empty($action['trailingParams'])){
349 | foreach($action['trailingParams'] as $key => $param){
350 | $trailingParams[$key] = array_pop(Set::extract($param, $result));
351 | }
352 | }
353 |
354 | $actions[$name] = array(
355 | 'url' => Router::url($action['url'] + $trailingParams),
356 | 'options' => $action['options']
357 | );
358 | }
359 |
360 | return $View->element($this->elemDir . DS . 'column_actions', array('plugin' => $this->plugin_name, 'actions' => $actions), array('Html'));
361 | }
362 | }
363 |
364 | //-- Check if it's linkable
365 | if(is_array($column['options']['linkable']) && !empty($column['options']['linkable'])){
366 | $trailingParams = array();
367 |
368 | $linkable = $column['options']['linkable'];
369 |
370 | if(!empty($linkable['trailingParams']) && is_array($linkable['trailingParams'])){
371 | foreach($linkable['trailingParams'] as $key => $param){
372 | $trailingParams[$key] = array_pop(Set::extract($param, $result));
373 | }
374 | }
375 |
376 | $url = $linkable['url'] + $trailingParams;
377 | $linkable['options'] = !isset($linkable['options']) ? array() : $linkable['options'];
378 |
379 | $value = $this->Html->link($value, $url, $linkable['options']);
380 | }
381 |
382 | return $value;
383 | }
384 |
385 | /**
386 | * Function to return escaped csv data since PHP doesn't have a function out of the box
387 | * Taken from http://php.net/manual/en/function.fputcsv.php comment
388 | *
389 | * @param string $data
390 | * @return void
391 | * @author Robert Ross
392 | */
393 | function csvData($data){
394 | $fp = false;
395 | $eol = "\n";
396 |
397 | if ($fp === false) {
398 | $fp = fopen('php://temp', 'r+');
399 | } else {
400 | rewind($fp);
401 | }
402 |
403 | if (fputcsv($fp, $data) === false) {
404 | return false;
405 | }
406 |
407 | rewind($fp);
408 | $csv = fgets($fp);
409 |
410 | if ($eol != PHP_EOL){
411 | $csv = substr($csv, 0, (0 - strlen(PHP_EOL))) . $eol;
412 | }
413 |
414 | //-- For out purpose... we don't want another \n
415 | $csv = substr($csv, 0, strlen($eol) * -1);
416 |
417 | return $csv;
418 | }
419 |
420 | /**
421 | * Retrieves the view instance from the registry
422 | *
423 | * @return void
424 | * @author Robert Ross
425 | */
426 | private function __view() {
427 | if (!empty($this->globalParams['viewInstance'])) {
428 | $View = $this->globalParams['viewInstance'];
429 | } else {
430 | $View = ClassRegistry::getObject('view');
431 | }
432 |
433 | return $View;
434 | }
435 | }
--------------------------------------------------------------------------------