├── 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------