1 <?php
2 /**
3 * Base "abstract" class creating reports on your data.
4 *
5 * Creating reports
6 * ================
7 *
8 * Creating a new report is a matter overloading a few key methods
9 *
10 * {@link title()}: Return the title - i18n is your responsibility
11 * {@link description()}: Return the description - i18n is your responsibility
12 * {@link sourceQuery()}: Return a DataObjectSet of the search results
13 * {@link columns()}: Return information about the columns in this report.
14 * {@link parameterFields()}: Return a FieldSet of the fields that can be used to filter this
15 * report.
16 *
17 * If you can't express your report as a query, you can implement the this method instead:
18 *
19 * // Return an array of fields that can be used to sort the data
20 * public function sourceRecords($params, $sort, $limit) { ... }
21 *
22 * The $sort value will be set to the corresponding key of the columns() array. If you wish to
23 * make only a subset of the columns sortable, then you can override `sortColumns()` to return a
24 * subset of the array keys.
25 *
26 * Note that this implementation is less efficient and should only be used when necessary.
27 *
28 * If you wish to modify the report in more extreme ways, you could overload these methods instead.
29 *
30 * {@link getReportField()}: Return a FormField in the place where your report's TableListField
31 * usually appears.
32 * {@link getCMSFields()}: Return the FieldSet representing the complete right-hand area of the
33 * report, including the title, description, parameter fields, and results.
34 *
35 * Showing reports to the user
36 * ===========================
37 *
38 * Right now, all subclasses of SS_Report will be shown in the ReportAdmin. However, we are planning
39 * on adding an explicit registration mechanism, so that you can decide which reports go in the
40 * report admin, and which go elsewhere (such as the side panel in the CMS).
41 *
42 * @package cms
43 * @subpackage reports
44 */
45 class SS_Report extends ViewableData {
46 /**
47 * Report registry populated by {@link SS_Report::register()}
48 */
49 private static $registered_reports = array();
50
51 /**
52 * This is the title of the report,
53 * used by the ReportAdmin templates.
54 *
55 * @var string
56 */
57 protected $title = '';
58
59 /**
60 * This is a description about what this
61 * report does. Used by the ReportAdmin
62 * templates.
63 *
64 * @var string
65 */
66 protected $description = '';
67
68 /**
69 * The class of object being managed by this report.
70 * Set by overriding in your subclass.
71 */
72 protected $dataClass = 'SiteTree';
73
74 /**
75 * Return the title of this report.
76 *
77 * You have two ways of specifying the description:
78 * - overriding description(), which lets you support i18n
79 * - defining the $description property
80 */
81 function title() {
82 return $this->title;
83 }
84
85 /**
86 * Return the description of this report.
87 *
88 * You have two ways of specifying the description:
89 * - overriding description(), which lets you support i18n
90 * - defining the $description property
91 */
92 function description() {
93 return $this->description;
94 }
95
96 /**
97 * Return a FieldSet specifying the search criteria for this report.
98 *
99 * Override this method to define search criteria.
100 */
101 function parameterFields() {
102 return null;
103 }
104
105 /**
106 * Return the {@link SQLQuery} that provides your report data.
107 */
108 function sourceQuery($params) {
109 if($this->hasMethod('sourceRecords')) {
110 $query = new SS_Report_FakeQuery($this, 'sourceRecords', $params);
111 $query->setSortColumnMethod('sortColumns');
112 return $query;
113 } else {
114 user_error("Please override sourceQuery()/sourceRecords() and columns() or, if necessary, override getReportField()", E_USER_ERROR);
115 }
116 }
117
118 /**
119 * Return a DataObjectSet records for this report.
120 */
121 function records($params) {
122 if($this->hasMethod('sourceRecords')) return $this->sourceRecords($params, null, null);
123 else {
124 $query = $this->sourceQuery();
125 return singleton($this->dataClass())->buildDataObjectSet($query->execute(), "DataObjectSet", $query);
126 }
127 }
128
129 /**
130 * Return an map of columns for your report.
131 * - The map keys will be the source columns for your report (in TableListField dot syntax)
132 * - The values can either be a string (the column title), or a map containing the following
133 * column parameters:
134 * - title: The column title
135 * - formatting: A formatting string passed to {@link TableListField::setFieldFormatting()}
136 */
137 function columns() {
138 user_error("Please override sourceQuery() and columns() or, if necessary, override getReportField()", E_USER_ERROR);
139 }
140
141 function sortColumns() {
142 return array_keys($this->columns());
143 }
144
145 /**
146 * Return the number of records in this report with no filters applied.
147 */
148 function count() {
149 return (int)$this->sourceQuery(array())->unlimitedRowCount();
150 }
151
152 /**
153 * Return the data class for this report
154 */
155 function dataClass() {
156 return $this->dataClass;
157 }
158
159
160 /**
161 * Returns a FieldSet with which to create the CMS editing form.
162 * You can use the extend() method of FieldSet to create customised forms for your other
163 * data objects.
164 *
165 * @uses getReportField() to render a table, or similar field for the report. This
166 * method should be defined on the SS_Report subclasses.
167 *
168 * @return FieldSet
169 */
170 function getCMSFields() {
171 $fields = new FieldSet(
172 new LiteralField(
173 'ReportTitle',
174 "<h3>{$this->title()}</h3>"
175 )
176 );
177
178 if($this->description) $fields->push(
179 new LiteralField('ReportDescription', "<p>{$this->description}</p>"));
180
181 // Add search fields is available
182 if($params = $this->parameterFields()) {
183 $filters = new FieldGroup('Filters');
184 foreach($params as $param) {
185 if ($param instanceof HiddenField) $fields->push($param);
186 else $filters->push($param);
187 }
188 $fields->push($filters);
189
190 // Add a search button
191 $fields->push(new FormAction('updatereport', _t('SS_Report.FilterAction', 'Filter')));
192 }
193
194 $fields->push($this->getReportField());
195
196 $this->extend('updateCMSFields', $fields);
197
198 return $fields;
199 }
200
201 function getCMSActions() {
202 // getCMSActions() can be extended with updateCMSActions() on a decorator
203 $actions = new FieldSet();
204 $this->extend('updateCMSActions', $actions);
205 return $actions;
206 }
207
208 /**
209 * Return a field, such as a {@link ComplexTableField} that is
210 * used to show and manipulate data relating to this report.
211 *
212 * Generally, you should override {@link columns()} and {@link records()} to make your report,
213 * but if they aren't sufficiently flexible, then you can override this method.
214 *
215 * @return FormField subclass
216 */
217 function getReportField() {
218 $columnTitles = array();
219 $fieldFormatting = array();
220 $csvFieldFormatting = array();
221 $fieldCasting = array();
222
223 // Parse the column information
224 foreach($this->columns() as $source => $info) {
225 if(is_string($info)) $info = array('title' => $info);
226
227 if(isset($info['formatting'])) $fieldFormatting[$source] = $info['formatting'];
228 if(isset($info['csvFormatting'])) $csvFieldFormatting[$source] = $info['csvFormatting'];
229 if(isset($info['casting'])) $fieldCasting[$source] = $info['casting'];
230 $columnTitles[$source] = isset($info['title']) ? $info['title'] : $source;
231 }
232
233 // To do: implement pagination
234 $query = $this->sourceQuery($_REQUEST);
235
236 $tlf = new TableListField('ReportContent', $this->dataClass(), $columnTitles);
237 $tlf->setCustomQuery($query);
238 $tlf->setShowPagination(true);
239 $tlf->setPageSize(50);
240 $tlf->setPermissions(array('export', 'print'));
241
242 // Hack to figure out if we are printing
243 if (isset($_REQUEST['url']) && array_pop(explode('/', $_REQUEST['url'])) == 'printall') {
244 $tlf->setTemplate('SSReportTableField');
245 }
246
247 if($fieldFormatting) $tlf->setFieldFormatting($fieldFormatting);
248 if($csvFieldFormatting) $tlf->setCSVFieldFormatting($csvFieldFormatting);
249 if($fieldCasting) $tlf->setFieldCasting($fieldCasting);
250
251 return $tlf;
252 }
253
254 /**
255 * @param Member $member
256 * @return boolean
257 */
258 function canView($member = null) {
259 if(!$member && $member !== FALSE) {
260 $member = Member::currentUser();
261 }
262
263 return true;
264 }
265
266
267 /**
268 * Return the name of this report, which
269 * is used by the templates to render the
270 * name of the report in the report tree,
271 * the left hand pane inside ReportAdmin.
272 *
273 * @return string
274 */
275 function TreeTitle() {
276 return $this->title();/* . ' (' . $this->count() . ')'; - this is too slow atm */
277 }
278
279 /**
280 * Return the ID of this Report class.
281 * Because it doesn't have a number, we
282 * use the class name as the ID.
283 *
284 * @return string
285 */
286 function ID() {
287 return $this->class;
288 }
289
290 /////////////////////////////////////////////////////////////////////////////////////////////
291
292 /**
293 * Register a report.
294 * @param $list The list to add the report to: "ReportAdmin" or "SideReports"
295 * @param $reportClass The class of the report to add.
296 * @param $priority The priority. Higher numbers will appear furhter up in the reports list.
297 * The default value is zero.
298 */
299 static function register($list, $reportClass, $priority = 0) {
300 if(strpos($reportClass, '(') === false && (!class_exists($reportClass) || !is_subclass_of($reportClass,'SS_Report'))) {
301 user_error("SS_Report::register(): '$reportClass' is not a subclass of SS_Report", E_USER_WARNING);
302 return;
303 }
304
305 self::$registered_reports[$list][$reportClass] = $priority;
306 }
307
308 /**
309 * Unregister a report, removing it from the list
310 */
311 static function unregister($list, $reportClass) {
312 unset(self::$registered_reports[$list][$reportClass]);
313 }
314
315 /**
316 * Return the SS_Report objects making up the given list.
317 * @return An array of SS_Report objects
318 */
319 static function get_reports($list) {
320 $output = array();
321 if(isset(self::$registered_reports[$list])) {
322 $listItems = self::$registered_reports[$list];
323
324 // Sort by priority, preserving internal order of items with the same priority
325 $groupedItems = array();
326 foreach($listItems as $k => $v) {
327 $groupedItems[$v][] = $k;
328 }
329 krsort($groupedItems);
330 $sortedListItems = call_user_func_array('array_merge', $groupedItems);
331
332 foreach($sortedListItems as $report) {
333 if(strpos($report,'(') === false) $reportObj = new $report;
334 else $reportObj = eval("return new $report;");
335
336 $output[$reportObj->ID()] = $reportObj;
337 }
338 }
339
340 return $output;
341 }
342
343 }
344
345 /**
346 * This is an object that can be used to dress up a more complex querying mechanism in the clothing
347 * of a SQLQuery object. This means that you can inject it into a TableListField.
348 *
349 * Use it like this:
350 *
351 * function sourceQuery($params) {
352 * return new SS_Report_FakeQuery($this, 'sourceRecords', $params)
353 * }
354 * function sourceRecords($params, $sort, $limit) {
355 * // Do some stuff
356 * // Return a DataObjectSet of actual objects.
357 * }
358 *
359 * This object is used by the default implementation of sourceQuery() on SS_Report, to make use of
360 * a sourceReords() method if one exists.
361 */
362 class SS_Report_FakeQuery extends SQLQuery {
363 public $orderby;
364 public $limit;
365
366 protected $obj, $method, $params;
367
368 function __construct($obj, $method, $params) {
369 $this->obj = $obj;
370 $this->method = $method;
371 $this->params = $params;
372 }
373
374 /**
375 * Provide a method that will return a list of columns that can be used to sort.
376 */
377 function setSortColumnMethod($sortColMethod) {
378 $this->sortColMethod = $sortColMethod;
379 }
380
381 function limit($limit) {
382 $this->limit = $limit;
383 }
384
385 function unlimitedRowCount() {
386 $source = $this->obj->{$this->method}($this->params, null, null);
387 return $source ? $source->Count() : 0;
388 }
389
390 function execute() {
391 $output = array();
392 $source = $this->obj->{$this->method}($this->params, $this->orderby, $this->limit);
393 if($source) foreach($source as $item) {
394 $mapItem = $item->toMap();
395 $mapItem['RecordClassName'] = get_class($item);
396 $output[] = $mapItem;
397 }
398 return $output;
399 }
400
401 function canSortBy($fieldName) {
402 $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
403 if($this->sortColMethod) {
404 $columns = $this->obj->{$this->sortColMethod}();
405 return in_array($fieldName, $columns);
406 } else {
407 return false;
408 }
409 }
410 }
411
412 /**
413 * SS_ReportWrapper is a base class for creating report wappers.
414 *
415 * Wrappers encapsulate an existing report to alter their behaviour - they are implementations of
416 * the standard GoF decorator pattern.
417 *
418 * This base class ensure that, by default, wrappers behave in the same way as the report that is
419 * being wrapped. You should override any methods that need to behave differently in your subclass
420 * of SS_ReportWrapper.
421 *
422 * It also makes calls to 2 empty methods that you can override {@link beforeQuery()} and
423 * {@link afterQuery()}
424 *
425 * @package cms
426 * @subpackage reports
427 */
428 abstract class SS_ReportWrapper extends SS_Report {
429 protected $baseReport;
430
431 function __construct($baseReport) {
432 $this->baseReport = is_string($baseReport) ? new $baseReport : $baseReport;
433 $this->dataClass = $this->baseReport->dataClass();
434 parent::__construct();
435 }
436
437 function ID() {
438 return get_class($this->baseReport) . '_' . get_class($this);
439 }
440
441 ///////////////////////////////////////////////////////////////////////////////////////////
442 // Filtering
443
444 function parameterFields() {
445 return $this->baseReport->parameterFields();
446 }
447
448 ///////////////////////////////////////////////////////////////////////////////////////////
449 // Columns
450
451 function columns() {
452 return $this->baseReport->columns();
453 }
454
455 ///////////////////////////////////////////////////////////////////////////////////////////
456 // Querying
457
458 /**
459 * Override this method to perform some actions prior to querying.
460 */
461 function beforeQuery($params) {
462 }
463
464 /**
465 * Override this method to perform some actions after querying.
466 */
467 function afterQuery() {
468 }
469
470 function sourceQuery($params) {
471 if($this->baseReport->hasMethod('sourceRecords')) {
472 // The default implementation will create a fake query from our sourceRecords() method
473 return parent::sourceQuery($params);
474
475 } else if($this->baseReport->hasMethod('sourceQuery')) {
476 $this->beforeQuery($params);
477 $query = $this->baseReport->sourceQuery($params);
478 $this->afterQuery();
479 return $query;
480
481 } else {
482 user_error("Please override sourceQuery()/sourceRecords() and columns() in your base report", E_USER_ERROR);
483 }
484
485 }
486
487 function sourceRecords($params = array(), $sort = null, $limit = null) {
488 $this->beforeQuery($params);
489 $records = $this->baseReport->sourceRecords($params, $sort, $limit);
490 $this->afterQuery();
491 return $records;
492 }
493
494
495 ///////////////////////////////////////////////////////////////////////////////////////////
496 // Pass-through
497
498 function title() {
499 return $this->baseReport->title();
500 }
501
502 function group() {
503 return $this->baseReport->hasMethod('group') ? $this->baseReport->group() : 'Group';
504 }
505
506 function sort() {
507 return $this->baseReport->hasMethod('sort') ? $this->baseReport->sort() : 0;
508 }
509
510 function description() {
511 return $this->baseReport->title();
512 }
513
514 function canView() {
515 return $this->baseReport->canView();
516 }
517
518 }
519
520 ?>
521