├── .editorconfig ├── .scrutinizer.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _config.php ├── code ├── CsvDataFormatter.php ├── ExcelDataFormatter.php ├── GridFieldExcelExportAction.php ├── GridFieldExcelExportButton.php ├── OldExcelDataFormatter.php └── SplitButton.php ├── composer.json ├── css └── splitbutton.css └── templates └── SplitButton.ss /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | checks: 4 | php: 5 | verify_property_names: true 6 | verify_argument_usable_as_reference: true 7 | verify_access_scope_valid: true 8 | useless_calls: true 9 | use_statement_alias_conflict: true 10 | variable_existence: true 11 | unused_variables: true 12 | unused_properties: true 13 | unused_parameters: true 14 | unused_methods: true 15 | unreachable_code: true 16 | too_many_arguments: true 17 | sql_injection_vulnerabilities: true 18 | simplify_boolean_return: true 19 | side_effects_or_types: true 20 | security_vulnerabilities: true 21 | return_doc_comments: true 22 | return_doc_comment_if_not_inferrable: true 23 | require_scope_for_properties: true 24 | require_scope_for_methods: true 25 | require_php_tag_first: true 26 | psr2_switch_declaration: true 27 | psr2_class_declaration: true 28 | property_assignments: true 29 | prefer_while_loop_over_for_loop: true 30 | precedence_mistakes: true 31 | precedence_in_conditions: true 32 | phpunit_assertions: true 33 | php5_style_constructor: true 34 | parse_doc_comments: true 35 | parameter_non_unique: true 36 | parameter_doc_comments: true 37 | param_doc_comment_if_not_inferrable: true 38 | optional_parameters_at_the_end: true 39 | one_class_per_file: true 40 | no_unnecessary_if: true 41 | no_trailing_whitespace: true 42 | no_property_on_interface: true 43 | no_non_implemented_abstract_methods: true 44 | no_error_suppression: true 45 | no_duplicate_arguments: true 46 | no_commented_out_code: true 47 | newline_at_end_of_file: true 48 | missing_arguments: true 49 | method_calls_on_non_object: true 50 | instanceof_class_exists: true 51 | foreach_traversable: true 52 | fix_line_ending: true 53 | fix_doc_comments: true 54 | duplication: true 55 | deprecated_code_usage: true 56 | deadlock_detection_in_loops: true 57 | code_rating: true 58 | closure_use_not_conflicting: true 59 | catch_class_exists: true 60 | blank_line_after_namespace_declaration: false 61 | avoid_multiple_statements_on_same_line: true 62 | avoid_duplicate_types: true 63 | avoid_conflicting_incrementers: true 64 | avoid_closing_tag: true 65 | assignment_of_null_return: true 66 | argument_type_checks: true 67 | 68 | filter: 69 | paths: [code/*, tests/*] 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Tag 0.2.0 2 | 3 | ## Wednesday, May 25, 2016 4 | * Updated ExcelDataFormatter::addRow() and ExcelDataFormatter::getFieldsForObj() so that DataObjects can suggest a default set of Fields to Export. 5 | * Added the ability for the ExcelDataFormatter to use field labels instead of just displaying the Database Field name. 6 | * Added a change log. 7 | * Updated the ReadMe with information about how to choose the column and customise the headers. 8 | * Generalise the logic on the GridFieldExcelExportButton to minimise code duplication between the XLSX, XLS and CSV action. 9 | * Update GridFieldExcelExportAction and GridFieldExcelExportButton so we can set the UseLabelsAsHeaders flag on the underlying ExcelDataFormatter. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Firebrand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silverstripe Excel Export module 2 | This Silverstripe module makes it easy to export a set of Silverstripe DataObjects to: 3 | * Excel 2007 (XLSX) 4 | * Excel 5 (XLS) 5 | * CSV 6 | 7 | This module is built by extending the standard [SilverStripe DataFormatter](http://api.silverstripe.org/3.1/class-DataFormatter.html). 8 | 9 | ## Requirements 10 | 11 | * [silverstripe/cms](https://github.com/silverstripe/silverstripe-cms) >=3.1 12 | * [phpoffice/phpexcel](https://github.com/PHPOffice/PHPExcel) >=1.8 13 | 14 | ## Suggestions 15 | * [silverstripe/restfulserver](https://github.com/silverstripe/silverstripe-restfulserver) >=3.1 16 | 17 | ## Installation 18 | 19 | Install the module through [composer](http://getcomposer.org): 20 | 21 | ```bash 22 | composer require firebrandhq/silverstripe-excel-export 23 | ``` 24 | 25 | ## Exporting your DataObjects 26 | There's 3 ways you can export your data to a spread sheet. 27 | 28 | ### Programmatically by calling the DataFormatter directly 29 | 3 DataFormatters are provided: 30 | * ExcelDataFormatter for XLSX 31 | * OldExcelDataFormatter for XLS 32 | * CsvDataFormatter for CSV 33 | 34 | You can manually instantiate them to convert a list of DataObjects or a single DataObject. 35 | 36 | ``` 37 | $formatter = new ExcelDataFormatter(); 38 | 39 | // Will return an Excel Spreadsheet as a string for a single user 40 | $filedata = $formatter->convertDataObject($user); 41 | 42 | // Will return an Excel Spreadsheet as a string for a list of user 43 | $filedata = $formatter->convertDataObjectSet(Member::get()); 44 | ``` 45 | 46 | `convertDataObjectSet()` and `convertDataObject()` will automatically set the _Content-Type_ HTTP header to an appropriate Mime Type. 47 | 48 | You can also retrieve the underlying _PHPExcel_ object and export your DataObject set to whatever format supported by _PHPExcel_. 49 | 50 | ``` 51 | // Get your Data 52 | $formatter = new ExcelDataFormatter(); 53 | $excel = $formatter->getPhpExcelObject(SiteTree::get()); 54 | 55 | // Set up a writer 56 | $writer = PHPExcel_IOFactory::createWriter($excel, 'HTML'); 57 | 58 | // Save the file somewhere on the server 59 | $writer->save('/tmp/sitetree_list.html'); 60 | 61 | // Output the results back to the browser 62 | $writer->save('php://output'); 63 | 64 | // Output the file to a variable 65 | ob_start(); 66 | $writer->save('php://output'); 67 | $fileData = ob_get_clean(); 68 | ``` 69 | 70 | ### Add the GridFieldExcelExportButton to a GridField 71 | The `GridFieldExcelExportButton` allows your CMS users to easily export the data from a GridField to a spreadsheet. 72 | 73 | ``` 74 | $rowEntryConfig = GridFieldConfig_RecordEditor::create(); 75 | $rowEntryConfig->addComponent(new GridFieldExcelExportButton()); 76 | $rowEntryDataGridField = new GridField( 77 | "ContentRow", 78 | "Content Row Entry", 79 | $this->ContentRow(), 80 | $rowEntryConfig 81 | ); 82 | $fields->addFieldToTab('Root.Main', $rowEntryDataGridField); 83 | ``` 84 | 85 | The above code snippet will display a split button allowing the user to export the GridField list to the format of their choice. 86 | 87 | Unlike the SilverStripe [GridFieldExportButton](http://api.silverstripe.org/3.1/class-GridFieldExportButton.html), the `GridFieldExcelExportButton` will export all the fields of the provided DataObjects ... not just the summary fields. 88 | 89 | You can also use the `GridFieldExcelExportAction` component. This button is added to each row and allows you to export individual records one at a time. Out of the box, `GridFieldExcelExportAction` will export to _xlsx_, but you can get it to export to _xls_ or _csv_ (e.g.: `new GridFieldExcelExportAction('csv')`). 90 | 91 | `GridFieldExcelExportAction` and `GridFieldExcelExportButton` can be used in conjunction if you want to give both options to your users. 92 | 93 | ### Call via the SilverStripe RestfulServer Module 94 | The [SilverStripe RestfulServer Module](https://github.com/silverstripe/silverstripe-restfulserver) allows you to turn any SilverStripe website into a RESTFul Server. 95 | 96 | If you use the _SilverStripe RestfulServer Module_ in conjunction with the _Silverstripe Excel Export module_, you'll be able to dynamically export any DataObject set just by entering the right URL in your browser. 97 | 98 | #### Access control 99 | Obviously, you don't want everyone to be able to download any data off your website. The SilverStripe RestfulServer Module will only return results for DataObject with the `$api_access` property set. 100 | 101 | ``` 102 | private static $api_access = true; 103 | ``` 104 | 105 | Additionally, access to individual DataObjects is controlled by the `canView` function. 106 | 107 | [Configuration the SilverStripe RestfulServer Module ](https://github.com/silverstripe/silverstripe-restfulserver#configuration) 108 | 109 | #### Getting to the data 110 | Exporting your data is just as easy as entering a URL. 111 | * Get a list of all Pages in Excel 2007: *http://localhost/api/v1/Page.xlsx* 112 | * Get a list of all Pages in Excel 5: *http://localhost/api/v1/Page.xls* 113 | * Get a list of all Pages in CSV: *http://localhost/api/v1/Page.csv* 114 | * Limit the list to 10 results: *http://localhost/api/v1/Page.csv?limit=10* 115 | * Return a single record: *http://localhost/api/v1/Page/37.xlsx* 116 | * Drill down into relationships: *http://localhost/api/v1/Tag/127/Articles.xlsx* 117 | 118 | [SilverStripe RestfulServer Module Supported operations](https://github.com/silverstripe/silverstripe-restfulserver#supported-operations) 119 | 120 | ## Customising the output 121 | 122 | There's 2 ways you can control the output: 123 | * Choose which fields to output ; 124 | * Choose to use field label instead of fields names in the headers. 125 | 126 | ### Choose which fields to output 127 | Because the `ExcelDataFormatter` extends [DataFormatter](http://api.silverstripe.org/3.3/class-DataFormatter.html), you can use methods like `setCustomFields()`, `setCustomAddFields()` or `setRemoveFields()` to control what fields will be present in the spread sheet. 128 | 129 | ``` 130 | $formatter = new ExcelDataFormatter(); 131 | 132 | // This formatter instead of returning every field of a DataObject, will only return 3 fields. 133 | $formatter->setCustomFields(['ID', 'Title', 'LastEdited']); 134 | 135 | // If youe DataObject has dynamic properties, you can reference them using setCustomAddFields(). 136 | $formatter->setCustomAddFields(['ChildrenCount']); 137 | ``` 138 | 139 | #### Defining a default column set 140 | You can customise the default column set that will be return for a specific DataObject class by defining a `getExcelExportFields()` method on your DataOject class. 141 | 142 | This `getExcelExportFields()` method should return an array of fields following the same format used by `DataObject::inheritedDatabaseFields()`: 143 | ``` 144 | return [ 145 | 'ID' => 'Int', 146 | 'Name' => 'Varchar', 147 | 'Address' => 'Text' 148 | ]; 149 | ``` 150 | 151 | You may also reference relationships in this array or dynamic properties: 152 | ``` 153 | return [ 154 | 'Owner.Name' => 'Varchar', 155 | 'Category.Title' => 'Varchar', 156 | 'ChildrenCount' => 'Int', 157 | ]; 158 | ``` 159 | 160 | This will also allow you to control the order the fields appear in the Spread Sheet. Note that ID will always be the first field and cannot be removed. 161 | 162 | This behavior can be overriden for specific instances of `ExcelDataFormatter` by calling the `setCustomFields()` method. 163 | 164 | ## Use field labels or field names as column headers 165 | Out of the box, the actual field names will be used as column header. (e.g.: `FirstName` rather than `First Name`). 166 | 167 | You can customise this behavior and use the Field Labels as define on your DataObject class instead. When generating the header row, `ExcelDataFormatter` will call the `fieldLabel()` method on your Data Object to decide what string to use in each header. 168 | 169 | ### Change the default for all `ExcelDataFormatter` 170 | In you YML config, you can use the following syntax to change the default headers. 171 | ``` 172 | ExcelDataFormatter: 173 | UseLabelsAsHeaders: true 174 | ``` 175 | 176 | ### Override the default for a specific instance 177 | You may change the default behavior for a specific instance. 178 | ``` 179 | $formatter->setUseLabelsAsHeaders(true); 180 | ``` 181 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | 6 | * @license MIT 7 | * @package silverstripe-excel-export 8 | */ 9 | 10 | define('EXCELEXPORT_DIR', ltrim(Director::makeRelative(realpath(__DIR__)), DIRECTORY_SEPARATOR)); 11 | -------------------------------------------------------------------------------- /code/CsvDataFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | * @license MIT 10 | * @package silverstripe-excel-export 11 | */ 12 | class CsvDataFormatter extends ExcelDataFormatter 13 | { 14 | 15 | /** 16 | * @inheritdoc 17 | */ 18 | public function supportedExtensions() 19 | { 20 | return array( 21 | 'csv', 22 | ); 23 | } 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | public function supportedMimeTypes() 29 | { 30 | return array( 31 | 'text/csv', 32 | ); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function convertDataObjectSet(SS_List $set) 39 | { 40 | $this->setHeader(); 41 | 42 | $excel = $this->getPhpExcelObject($set); 43 | 44 | $fileData = $this->getFileData($excel, 'CSV'); 45 | 46 | return $fileData; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /code/ExcelDataFormatter.php: -------------------------------------------------------------------------------- 1 | 12 | * @license MIT 13 | * @package silverstripe-excel-export 14 | */ 15 | class ExcelDataFormatter extends DataFormatter 16 | { 17 | 18 | 19 | private static $api_base = "api/v1/"; 20 | 21 | /** 22 | * Determined what we will use as headers for the spread sheet. 23 | * @var bool 24 | */ 25 | protected $useLabelsAsHeaders = null; 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function supportedExtensions() 31 | { 32 | return array( 33 | 'xlsx', 34 | ); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function supportedMimeTypes() 41 | { 42 | return array( 43 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 44 | ); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function convertDataObject(DataObjectInterface $do) 51 | { 52 | return $this->convertDataObjectSet(new ArrayList(array($do))); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function convertDataObjectSet(SS_List $set) 59 | { 60 | $this->setHeader(); 61 | 62 | $excel = $this->getPhpExcelObject($set); 63 | 64 | $fileData = $this->getFileData($excel, 'Excel2007'); 65 | 66 | return $fileData; 67 | } 68 | 69 | /** 70 | * Set the HTTP Content Type header to the appropriate Mime Type. 71 | */ 72 | protected function setHeader() 73 | { 74 | Controller::curr()->getResponse() 75 | ->addHeader("Content-Type", $this->supportedMimeTypes()[0]); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | protected function getFieldsForObj($obj) 82 | { 83 | $dbFields = array(); 84 | 85 | // if custom fields are specified, only select these 86 | if(is_array($this->customFields)) { 87 | foreach($this->customFields as $fieldName) { 88 | // @todo Possible security risk by making methods accessible - implement field-level security 89 | if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) { 90 | $dbFields[$fieldName] = $fieldName; 91 | } 92 | } 93 | } elseif ($obj->hasMethod('getExcelExportFields')) { 94 | $dbFields = $obj->getExcelExportFields(); 95 | } else { 96 | // by default, all database fields are selected 97 | $dbFields = $obj->inheritedDatabaseFields(); 98 | } 99 | 100 | if(is_array($this->customAddFields)) { 101 | foreach($this->customAddFields as $fieldName) { 102 | // @todo Possible security risk by making methods accessible - implement field-level security 103 | if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) { 104 | $dbFields[$fieldName] = $fieldName; 105 | } 106 | } 107 | } 108 | 109 | // Make sure our ID field is the first one. 110 | $dbFields = array('ID' => 'Int') + $dbFields; 111 | 112 | if(is_array($this->removeFields)) { 113 | $dbFields = array_diff_key($dbFields, array_combine($this->removeFields,$this->removeFields)); 114 | } 115 | 116 | return $dbFields; 117 | } 118 | 119 | /** 120 | * Generate a {@link PHPExcel} for the provided DataObject List 121 | * @param SS_List $set List of DataObjects 122 | * @return PHPExcel 123 | */ 124 | public function getPhpExcelObject(SS_List $set) 125 | { 126 | // Get the first object. We'll need it to know what type of objects we 127 | // are dealing with 128 | $first = $set->first(); 129 | 130 | // Get the Excel object 131 | $excel = $this->setupExcel($first); 132 | $sheet = $excel->setActiveSheetIndex(0); 133 | 134 | // Make sure we have at lease on item. If we don't, we'll be returning 135 | // an empty spreadsheet. 136 | if ($first) { 137 | // Set up the header row 138 | $fields = $this->getFieldsForObj($first); 139 | $this->headerRow($sheet, $fields, $first); 140 | 141 | // Add a new row for each DataObject 142 | foreach ($set as $item) { 143 | $this->addRow($sheet, $item, $fields); 144 | } 145 | 146 | // Freezing the first column and the header row 147 | $sheet->freezePane("B2"); 148 | 149 | // Auto sizing all the columns 150 | $col = sizeof($fields); 151 | for ($i = 0; $i < $col; $i++) { 152 | $sheet 153 | ->getColumnDimension( 154 | PHPExcel_Cell::stringFromColumnIndex($i) 155 | ) 156 | ->setAutoSize(true); 157 | } 158 | 159 | } 160 | 161 | return $excel; 162 | } 163 | 164 | /** 165 | * Initialize a new {@link PHPExcel} object based on the provided 166 | * {@link DataObjectInterface} interface. 167 | * @param DataObjectInterface $do 168 | * @return PHPExcel 169 | */ 170 | protected function setupExcel(DataObjectInterface $do) 171 | { 172 | // Try to get the current user 173 | $member = Member::currentUser(); 174 | $creator = $member ? $member->getName() : ''; 175 | 176 | // Get information about the current Model Class 177 | $singular = $do ? $do->i18n_singular_name() : ''; 178 | $plural = $do ? $do->i18n_plural_name() : ''; 179 | 180 | // Create the Spread sheet 181 | $excel = new PHPExcel(); 182 | 183 | $excel->getProperties() 184 | ->setCreator($creator) 185 | ->setTitle(_t( 186 | 'firebrandhq.EXCELEXPORT', 187 | '{singular} export', 188 | 'Title for the spread sheet export', 189 | array('singular' => $singular) 190 | )) 191 | ->setDescription(_t( 192 | 'firebrandhq.EXCELEXPORT', 193 | 'List of {plural} exported out of a SilverStripe website', 194 | 'Description for the spread sheet export', 195 | array('plural' => $plural) 196 | )); 197 | 198 | // Give a name to the sheet 199 | if ($plural) { 200 | $excel->getActiveSheet()->setTitle($plural); 201 | } 202 | 203 | return $excel; 204 | } 205 | 206 | /** 207 | * Add an header row to a {@link PHPExcel_Worksheet}. 208 | * @param PHPExcel_Worksheet $sheet 209 | * @param array $fields List of fields 210 | * @param DataObjectInterface $do 211 | * @return PHPExcel_Worksheet 212 | */ 213 | protected function headerRow(PHPExcel_Worksheet &$sheet, array $fields, DataObjectInterface $do) 214 | { 215 | // Counter 216 | $row = 1; 217 | $col = 0; 218 | 219 | $useLabelsAsHeaders = $this->getUseLabelsAsHeaders(); 220 | 221 | // Add each field to the first row 222 | foreach ($fields as $field => $type) { 223 | $header = $useLabelsAsHeaders ? $do->fieldLabel($field) : $field; 224 | $sheet->setCellValueByColumnAndRow($col, $row, $header); 225 | $col++; 226 | } 227 | 228 | // Get the last column 229 | $col--; 230 | $endcol = PHPExcel_Cell::stringFromColumnIndex($col); 231 | 232 | // Set Autofilters and Header row style 233 | $sheet->setAutoFilter("A1:{$endcol}1"); 234 | $sheet->getStyle("A1:{$endcol}1")->getFont()->setBold(true); 235 | 236 | 237 | return $sheet; 238 | } 239 | 240 | /** 241 | * Add a new row to a {@link PHPExcel_Worksheet} based of a 242 | * {@link DataObjectInterface} 243 | * @param PHPExcel_Worksheet $sheet 244 | * @param DataObjectInterface $item 245 | * @param array $fields List of fields to include 246 | * @return PHPExcel_Worksheet 247 | */ 248 | protected function addRow( 249 | PHPExcel_Worksheet &$sheet, 250 | DataObjectInterface $item, 251 | array $fields 252 | ) { 253 | $row = $sheet->getHighestRow() + 1; 254 | $col = 0; 255 | 256 | foreach ($fields as $field => $type) { 257 | if ($item->hasField($field) || $item->hasMethod("get{$field}")) { 258 | $value = $item->$field; 259 | } else { 260 | $viewer = SSViewer::fromString('$' . $field . '.RAW'); 261 | $value = $item->renderWith($viewer, true); 262 | } 263 | $sheet->setCellValueByColumnAndRow($col, $row, $value); 264 | $col++; 265 | } 266 | 267 | return $sheet; 268 | } 269 | 270 | /** 271 | * Generate a string representation of an {@link PHPExcel} spread sheet 272 | * suitable for output to the browser. 273 | * @param PHPExcel $excel 274 | * @param string $format Format to use when outputting the spreadsheet. 275 | * Must be compatible with the format expected by 276 | * {@link PHPExcel_IOFactory::createWriter}. 277 | * @return string 278 | */ 279 | protected function getFileData(PHPExcel $excel, $format) 280 | { 281 | $writer = PHPExcel_IOFactory::createWriter($excel, $format); 282 | ob_start(); 283 | $writer->save('php://output'); 284 | $fileData = ob_get_clean(); 285 | 286 | return $fileData; 287 | } 288 | 289 | /** 290 | * Accessor for UseLabelsAsHeaders. If this is `true`, the data formatter will call {@link DataObject::fieldLabel()} to pick the header strings. If it's set to false, it will use the raw field name. 291 | * 292 | * You can define this for a specific ExcelDataFormatter instance with `setUseLabelsAsHeaders`. You can set the default for all ExcelDataFormatter instance in your YML config file: 293 | * 294 | * ``` 295 | * ExcelDataFormatter: 296 | * UseLabelsAsHeaders: true 297 | * ``` 298 | * 299 | * Otherwise, the data formatter will default to false. 300 | * 301 | * @return bool 302 | */ 303 | public function getUseLabelsAsHeaders() 304 | { 305 | if ($this->useLabelsAsHeaders !== null) { 306 | return $this->useLabelsAsHeaders; 307 | } 308 | 309 | $useLabelsAsHeaders = static::config()->UseLabelsAsHeaders; 310 | if ($useLabelsAsHeaders !== null) { 311 | return $useLabelsAsHeaders; 312 | } 313 | 314 | return false; 315 | } 316 | 317 | /** 318 | * Setter for UseLabelsAsHeaders. If this is `true`, the data formatter will call {@link DataObject::fieldLabel()} to pick the header strings. If it's set to false, it will use the raw field name. 319 | * 320 | * If `$value` is `null`, the data formatter will fall back on whatevr the default is. 321 | * @param bool $value 322 | * @return ExcelDataFormatter 323 | */ 324 | public function setUseLabelsAsHeaders($value) 325 | { 326 | if ($value === null) { 327 | $this->useLabelsAsHeaders = null; 328 | } else { 329 | $this->useLabelsAsHeaders = (bool)$value; 330 | } 331 | return $this; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /code/GridFieldExcelExportAction.php: -------------------------------------------------------------------------------- 1 | exportType = $exportType; 30 | } 31 | 32 | /** 33 | * Add a column at the end of the grid field if need be 34 | * 35 | * @param GridField $gridField 36 | * @param array $columns 37 | */ 38 | public function augmentColumns($gridField, &$columns) { 39 | if(!in_array('Actions', $columns)) { 40 | $columns[] = 'Actions'; 41 | } 42 | } 43 | 44 | /** 45 | * Return any special attributes that will be used for FormField::create_tag() 46 | * 47 | * @param GridField $gridField 48 | * @param DataObject $record 49 | * @param string $columnName 50 | * @return array 51 | */ 52 | public function getColumnAttributes($gridField, $record, $columnName) { 53 | return array('class' => 'col-buttons'); 54 | } 55 | 56 | /** 57 | * Add the title 58 | * 59 | * @param GridField $gridField 60 | * @param string $columnName 61 | * @return array 62 | */ 63 | public function getColumnMetadata($gridField, $columnName) { 64 | if($columnName == 'Actions') { 65 | return array('title' => ''); 66 | } 67 | } 68 | 69 | /** 70 | * Which columns are handled by this component 71 | * 72 | * @param GridField $gridField 73 | * @return array 74 | */ 75 | public function getColumnsHandled($gridField) { 76 | return array('Actions'); 77 | } 78 | 79 | /** 80 | * Which GridField actions are this component handling 81 | * 82 | * @param GridField $gridField 83 | * @return array 84 | */ 85 | public function getActions($gridField) { 86 | return array('exportsingle'); 87 | } 88 | 89 | /** 90 | * Return the button to show at the end of the row 91 | * @param GridField $gridField 92 | * @param DataObject $record 93 | * @param string $columnName 94 | * @return string - the HTML for the column 95 | */ 96 | public function getColumnContent($gridField, $record, $columnName) { 97 | if(!$record->canView()) return; 98 | 99 | $field = GridField_FormAction::create($gridField, 'ExportSingle'.$record->ID, false, 100 | "exportsingle", array('RecordID' => $record->ID)) 101 | ->addExtraClass('gridfield-button-export-single no-ajax') 102 | ->setAttribute('title', _t('firebrandhq.EXCELEXPORT', "Export")) 103 | ->setAttribute('data-icon', 'download-csv'); 104 | return $field->Field(); 105 | } 106 | 107 | /** 108 | * Handle the actions and apply any changes to the GridField 109 | * 110 | * @param GridField $gridField 111 | * @param string $actionName 112 | * @param mixed $arguments 113 | * @param array $data - form data 114 | * @return void 115 | */ 116 | public function handleAction(GridField $gridField, $actionName, $arguments, $data) { 117 | if($actionName == 'exportsingle') { 118 | // Get the item 119 | $item = $gridField->getList()->byID($arguments['RecordID']); 120 | if(!$item) { 121 | return; 122 | } 123 | 124 | // Make sure th current user is authorised to view the item. 125 | if (!$item->canView()) { 126 | throw new ValidationException( 127 | _t('firebrandhq.EXCELEXPORT', "Can not view record"),0); 128 | } 129 | 130 | // Build a filename 131 | $filename = $item->i18n_singular_name() . ' - ID ' . $item->ID; 132 | $title = $item->getTitle(); 133 | if ($title) { 134 | $filename .= ' - ' . $title; 135 | } 136 | 137 | // Pick Converter 138 | switch ($this->exportType) { 139 | case 'xlsx': 140 | $formater = new ExcelDataFormatter(); 141 | break; 142 | case 'xls': 143 | $formater = new OldExcelDataFormatter(); 144 | break; 145 | case 'csv': 146 | $formater = new CsvDataFormatter(); 147 | break; 148 | default: 149 | user_error( 150 | "GridFieldExcelExportAction expects \$exportType to be either 'xlsx', 'xls' or 'csv'. " . 151 | "'{$this->exportType}' provided", 152 | E_USER_ERROR 153 | ); 154 | } 155 | 156 | // Set the header that will cause the browser to download and save the file. 157 | $this->setHeader($gridField, $this->exportType, $filename); 158 | 159 | // Export our Data Object 160 | $formater->setUseLabelsAsHeaders($this->useLabelsAsHeaders); 161 | $fileData = $formater->convertDataObject($item); 162 | 163 | return $fileData; 164 | } 165 | } 166 | 167 | /** 168 | * Helper function for building the right header to get the file downloaded. 169 | * @param GridField $gridField 170 | * @param string $ext Extension the file should have 171 | * @param string $filename Optional of the file (without the extension). Defaults to the grid field object type. 172 | */ 173 | protected function setHeader($gridField, $ext, $filename = '') 174 | { 175 | $do = singleton($gridField->getModelClass()); 176 | if (!$filename) { 177 | $filename = $do->i18n_plural_name(); 178 | } 179 | 180 | Controller::curr()->getResponse() 181 | ->addHeader( 182 | "Content-Disposition", 183 | 'attachment; filename="' . 184 | $filename . 185 | '.' . $ext . '"' 186 | ); 187 | } 188 | 189 | /** 190 | * Set the DataFormatter's UseFieldLabelsAsHeaders property 191 | * @param bool $value 192 | * @return GridFieldExcelExportButton 193 | */ 194 | public function setUseLabelsAsHeaders($value) 195 | { 196 | if ($value === null) { 197 | $this->useLabelsAsHeaders = null; 198 | } else { 199 | $this->useLabelsAsHeaders = (bool)$value; 200 | } 201 | return $this; 202 | } 203 | 204 | /** 205 | * Return the value that will be assigned to the DataFormatter's UseFieldLabelsAsHeaders property 206 | * 207 | * If null, will fallback on the default. 208 | * 209 | * @return bool|null 210 | */ 211 | public function getUseLabelsAsHeaders() 212 | { 213 | return $this->useLabelsAsHeaders; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /code/GridFieldExcelExportButton.php: -------------------------------------------------------------------------------- 1 | 12 | * @license MIT 13 | * @package silverstripe-excel-export 14 | */ 15 | class GridFieldExcelExportButton implements 16 | GridField_HTMLProvider, 17 | GridField_ActionProvider, 18 | GridField_URLHandler 19 | { 20 | 21 | /** 22 | * Whatever to override the default $useFieldLabelsAsHeaders value for the DataFormatter. 23 | * @var bool 24 | */ 25 | protected $useLabelsAsHeaders = null; 26 | 27 | /** 28 | * Fragment to write the button to 29 | */ 30 | protected $targetFragment; 31 | 32 | /** 33 | * Instanciate GridFieldExcelExportButton. 34 | * @param string $targetFragment 35 | */ 36 | public function __construct($targetFragment = "before") 37 | { 38 | $this->targetFragment = $targetFragment; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | * 44 | * Create the split button with all the export options. 45 | * 46 | * @param GridField $gridField 47 | * @return array 48 | */ 49 | public function getHTMLFragments($gridField) 50 | { 51 | // Set up the split button 52 | $splitButton = new SplitButton('Export', 'Export'); 53 | $splitButton->setAttribute('data-icon', 'download-csv'); 54 | 55 | // XLSX option 56 | $button = new GridField_FormAction( 57 | $gridField, 58 | 'xlsxexport', 59 | _t('firebrandhq.EXCELEXPORT', 'Export to Excel (XLSX)'), 60 | 'xlsxexport', 61 | null 62 | ); 63 | $button->addExtraClass('no-ajax'); 64 | $splitButton->push($button); 65 | 66 | // XLS option 67 | $button = new GridField_FormAction( 68 | $gridField, 69 | 'xlsexport', 70 | _t('firebrandhq.EXCELEXPORT', 'Export to Excel (XLS)'), 71 | 'xlsexport', 72 | null 73 | ); 74 | $button->addExtraClass('no-ajax'); 75 | $splitButton->push($button); 76 | 77 | // CSV option 78 | $button = new GridField_FormAction( 79 | $gridField, 80 | 'csvexport', 81 | _t('firebrandhq.EXCELEXPORT', 'Export to CSV'), 82 | 'csvexport', 83 | null 84 | ); 85 | $button->addExtraClass('no-ajax'); 86 | $splitButton->push($button); 87 | 88 | // Return the fragment 89 | return array( 90 | $this->targetFragment => 91 | $splitButton->Field() 92 | ); 93 | } 94 | 95 | /** 96 | * @inheritdoc 97 | */ 98 | public function getActions($gridField) 99 | { 100 | return array('xlsxexport', 'xlsexport', 'csvexport'); 101 | } 102 | 103 | /** 104 | * @inheritdoc 105 | */ 106 | public function handleAction( 107 | GridField $gridField, 108 | $actionName, 109 | $arguments, 110 | $data 111 | ) { 112 | if ($actionName == 'xlsxexport') { 113 | return $this->handleXlsx($gridField); 114 | } 115 | 116 | if ($actionName == 'xlsexport') { 117 | return $this->handleXls($gridField); 118 | } 119 | 120 | if ($actionName == 'csvexport') { 121 | return $this->handleCsv($gridField); 122 | } 123 | } 124 | 125 | /** 126 | * @inheritdoc 127 | */ 128 | public function getURLHandlers($gridField) 129 | { 130 | return array( 131 | 'xlsxexport' => 'handleXlsx', 132 | 'xlsexport' => 'handleXls', 133 | 'csvexport' => 'handleCsv', 134 | ); 135 | } 136 | 137 | /** 138 | * Action to export the GridField list to an Excel 2007 file. 139 | * @param GridField $gridField 140 | * @param SS_HTTPRequest $request 141 | * @return string 142 | */ 143 | public function handleXlsx(GridField $gridField, $request = null) 144 | { 145 | return $this->genericHandle('ExcelDataFormatter', 'xlsx', $gridField, $request); 146 | } 147 | 148 | /** 149 | * Action to export the GridField list to an Excel 5 file. 150 | * @param GridField $gridField 151 | * @param SS_HTTPRequest $request 152 | * @return string 153 | */ 154 | public function handleXls(GridField $gridField, $request = null) 155 | { 156 | return $this->genericHandle('OldExcelDataFormatter', 'xls', $gridField, $request); 157 | } 158 | 159 | /** 160 | * Action to export the GridField list to an CSV file. 161 | * @param GridField $gridField 162 | * @param SS_HTTPRequest $request 163 | * @return string 164 | */ 165 | public function handleCsv(GridField $gridField, $request = null) 166 | { 167 | return $this->genericHandle('CsvDataFormatter', 'csv', $gridField, $request); 168 | } 169 | 170 | /** 171 | * Generic Handle request that will return a Spread Sheet in the requested format 172 | * @param string $dataFormatterClass 173 | * @param string $ext 174 | * @param GridField $gridField 175 | * @param SS_HTTPRequest $request 176 | * @return string 177 | */ 178 | protected function genericHandle($dataFormatterClass, $ext, GridField $gridField, $request = null) 179 | { 180 | $items = $this->getItems($gridField); 181 | 182 | $this->setHeader($gridField, $ext); 183 | 184 | 185 | $formater = new $dataFormatterClass(); 186 | $formater->setUseLabelsAsHeaders($this->useLabelsAsHeaders); 187 | 188 | $fileData = $formater->convertDataObjectSet($items); 189 | 190 | return $fileData; 191 | } 192 | 193 | /** 194 | * Set the HTTP header to force a download and set the filename. 195 | * @param GridField $gridField 196 | * @param string $ext Extension to use in the filename. 197 | */ 198 | protected function setHeader($gridField, $ext) 199 | { 200 | $do = singleton($gridField->getModelClass()); 201 | 202 | Controller::curr()->getResponse() 203 | ->addHeader( 204 | "Content-Disposition", 205 | 'attachment; filename="' . 206 | $do->i18n_plural_name() . 207 | '.' . $ext . '"' 208 | ); 209 | } 210 | 211 | /** 212 | * Helper function to extract the item list out of the GridField. 213 | * @param GridField $gridField 214 | * @return SS_list 215 | */ 216 | protected function getItems(GridField $gridField) 217 | { 218 | $gridField->getConfig()->removeComponentsByType('GridFieldPaginator'); 219 | 220 | $items = $gridField->getManipulatedList(); 221 | 222 | foreach ($gridField->getConfig()->getComponents() as $component) { 223 | if ($component instanceof GridFieldFilterHeader || $component instanceof GridFieldSortableHeader) { 224 | $items = $component->getManipulatedData($gridField, $items); 225 | } 226 | } 227 | 228 | $arrayList = new ArrayList(); 229 | 230 | foreach ($items->limit(null) as $item) { 231 | if (!$item->hasMethod('canView') || $item->canView()) { 232 | $arrayList->add($item); 233 | } 234 | } 235 | 236 | return $arrayList; 237 | } 238 | 239 | /** 240 | * Set the DataFormatter's UseFieldLabelsAsHeaders property 241 | * @param bool $value 242 | * @return GridFieldExcelExportButton 243 | */ 244 | public function setUseLabelsAsHeaders($value) 245 | { 246 | if ($value === null) { 247 | $this->useLabelsAsHeaders = null; 248 | } else { 249 | $this->useLabelsAsHeaders = (bool)$value; 250 | } 251 | return $this; 252 | } 253 | 254 | /** 255 | * Return the value that will be assigned to the DataFormatter's UseFieldLabelsAsHeaders property 256 | * 257 | * If null, will fallback on the default. 258 | * 259 | * @return bool|null 260 | */ 261 | public function getUseLabelsAsHeaders() 262 | { 263 | return $this->useLabelsAsHeaders; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /code/OldExcelDataFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | * @license MIT 10 | * @package silverstripe-excel-export 11 | */ 12 | class OldExcelDataFormatter extends ExcelDataFormatter 13 | { 14 | 15 | /** 16 | * @inheritdoc 17 | */ 18 | public function supportedExtensions() 19 | { 20 | return array( 21 | 'xls', 22 | ); 23 | } 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | public function supportedMimeTypes() 29 | { 30 | return array( 31 | 'application/vnd.ms-excel', 32 | ); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function convertDataObjectSet(SS_List $set) 39 | { 40 | $this->setHeader(); 41 | 42 | $excel = $this->getPhpExcelObject($set); 43 | 44 | $fileData = $this->getFileData($excel, 'Excel5'); 45 | 46 | return $fileData; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /code/SplitButton.php: -------------------------------------------------------------------------------- 1 | 9 | * @license MIT 10 | * @package silverstripe-excel-export 11 | */ 12 | class SplitButton extends TabSet 13 | { 14 | 15 | /** 16 | * Underlying container to witch the buttons are goign to be added. 17 | * @var Tab 18 | */ 19 | protected $tab; 20 | 21 | /** 22 | * Create a new instance of SplitButton 23 | * @param string $name Form field name 24 | * @param string $title Title that will be displayed on the split button. 25 | * if not provided, the title will be guess from the `$name`. 26 | */ 27 | public function __construct($name, $title=null) { 28 | $args = func_get_args(); 29 | $name = array_shift($args); 30 | 31 | if ($args) { 32 | $title = array_shift($args); 33 | } 34 | 35 | // Guess the title if none provided 36 | if (!$title) { 37 | $title = self::name_to_label($name); 38 | } 39 | 40 | // Instanciate our undelying tab container 41 | $this->tab = new Tab( 42 | 'SplitButtonTab', 43 | $title 44 | ); 45 | 46 | //Call the parent consturctor 47 | parent::__construct($name, $this->tab); 48 | 49 | // Add the same class as the _more options_ link so we can piggy back 50 | // off that logic. 51 | $this->addExtraClass('ss-ui-action-tabset action-menus ss-ui-button'); 52 | 53 | // Add any provided button. 54 | if ($args) { 55 | foreach ($args as $button) { 56 | // Make sure we only add Form Fields to our tab. 57 | $isValidArg = 58 | (is_object($button) && 59 | !($button instanceof FormField)); 60 | if (!$isValidArg) { 61 | user_error( 62 | 'SplitButton::__construct(): Parameter not a valid FormField instance', 63 | E_USER_ERROR 64 | ); 65 | } 66 | 67 | $this->tab->push($button); 68 | } 69 | } 70 | 71 | // Define a custom spread sheet so we can style our button. 72 | Requirements::css(EXCELEXPORT_DIR . '/css/splitbutton.css'); 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function fieldByName($name) 79 | { 80 | return $this->tab->fieldByName(); 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function fieldPosition($field) 87 | { 88 | return $this->tab->fieldPosition($field); 89 | } 90 | 91 | /** 92 | * @inheritdoc 93 | */ 94 | public function getChildren() 95 | { 96 | return $this->tab->getChildren(); 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | public function setChildren($children) 103 | { 104 | return $this->tab->setChildren($children); 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | public function push(FormField $field) 111 | { 112 | return $this->tab->push($field); 113 | } 114 | 115 | /** 116 | * @inheritdoc 117 | */ 118 | public function insertBefore($field, $insertBefore) 119 | { 120 | return $this->tab->insertBefore($field, $insertBefore); 121 | } 122 | 123 | /** 124 | * @inheritdoc 125 | */ 126 | public function insertAfter($field, $insertBefore) 127 | { 128 | return $this->tab->insertAfter($field, $insertBefore); 129 | } 130 | 131 | /** 132 | * @inheritdoc 133 | */ 134 | public function removeByName($fieldName, $dataFieldOnly = false) 135 | { 136 | return $this->tab->removeByName($fieldName, $dataFieldOnly = false); 137 | } 138 | 139 | /** 140 | * @inheritdoc 141 | */ 142 | public function replaceField($fieldName, $newField) 143 | { 144 | return $this->tab->replaceField($fieldName, $newField); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebrandhq/silverstripe-excel-export", 3 | "description": "Silverstripe module offering DataFormatters to export DataObjects in Excel format.", 4 | "type": "silverstripe-vendormodule", 5 | "require": { 6 | "phpoffice/phpexcel": ">=1.8", 7 | "silverstripe/cms": ">=3.1" 8 | }, 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Firebrand", 13 | "email": "hello@firebrand.nz" 14 | } 15 | ], 16 | "suggest": { 17 | "silverstripe/restfulserver": "If you install the restfull server, you'll be able to dynamically generate SpreadSheet on the fly." 18 | }, 19 | "minimum-stability": "dev", 20 | "prefer-stable": true 21 | } 22 | -------------------------------------------------------------------------------- /css/splitbutton.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * silverstripe-excel-export 3 | * Copyright Firebrand 4 | * Licensed under MIT (https://github.com/firebrandhq/excel-export/blob/master/LICENSE) 5 | */ 6 | 7 | .cms .splitbutton.ss-ui-action-tabset.action-menus.ss-tabset { 8 | margin-top: 0; 9 | } 10 | 11 | .splitbutton.ui-button-text-only .ui-button-text { 12 | padding: 0; 13 | } 14 | .splitbutton.ui-button-text-icon-primary .ui-button-text { 15 | padding-top: 0; 16 | } 17 | 18 | .splitbutton.ui-tabs .ui-tabs-nav { 19 | padding-right: 0; 20 | } 21 | 22 | .cms .splitbutton.ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a { 23 | padding-right: 0; 24 | color: rgb(57, 57, 57); 25 | font-weight: bold; 26 | } 27 | 28 | .cms .splitbutton.ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel { 29 | margin-top: 32px; 30 | 31 | -moz-border-radius: 3px; 32 | -webkit-border-radius: 3px; 33 | border-radius: 3px; 34 | } 35 | -------------------------------------------------------------------------------- /templates/SplitButton.ss: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 | <% loop $Tabs %> 9 | <% if $Tabs %> 10 | $FieldHolder 11 | <% else %> 12 |
13 | <% loop $Fields %> 14 | $FieldHolder 15 | <% end_loop %> 16 |
17 | <% end_if %> 18 | <% end_loop %> 19 |
20 | --------------------------------------------------------------------------------