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 $urlData = explode('/', $_REQUEST['url']);
244 if (isset($_REQUEST['url']) && array_pop($urlData) == 'printall') {
245 $tlf->setTemplate('SSReportTableField');
246 }
247
248 if($fieldFormatting) $tlf->setFieldFormatting($fieldFormatting);
249 if($csvFieldFormatting) $tlf->setCSVFieldFormatting($csvFieldFormatting);
250 if($fieldCasting) $tlf->setFieldCasting($fieldCasting);
251
252 return $tlf;
253 }
254
255 /**
256 * @param Member $member
257 * @return boolean
258 */
259 function canView($member = null) {
260 if(!$member && $member !== FALSE) {
261 $member = Member::currentUser();
262 }
263
264 return true;
265 }
266
267
268 /**
269 * Return the name of this report, which
270 * is used by the templates to render the
271 * name of the report in the report tree,
272 * the left hand pane inside ReportAdmin.
273 *
274 * @return string
275 */
276 function TreeTitle() {
277 return $this->title();/* . ' (' . $this->count() . ')'; - this is too slow atm */
278 }
279
280 /**
281 * Return the ID of this Report class.
282 * Because it doesn't have a number, we
283 * use the class name as the ID.
284 *
285 * @return string
286 */
287 function ID() {
288 return $this->class;
289 }
290
291 /////////////////////////////////////////////////////////////////////////////////////////////
292
293 /**
294 * Register a report.
295 * @param $list The list to add the report to: "ReportAdmin" or "SideReports"
296 * @param $reportClass The class of the report to add.
297 * @param $priority The priority. Higher numbers will appear furhter up in the reports list.
298 * The default value is zero.
299 */
300 static function register($list, $reportClass, $priority = 0) {
301 if(strpos($reportClass, '(') === false && (!class_exists($reportClass) || !is_subclass_of($reportClass,'SS_Report'))) {
302 user_error("SS_Report::register(): '$reportClass' is not a subclass of SS_Report", E_USER_WARNING);
303 return;
304 }
305
306 self::$registered_reports[$list][$reportClass] = $priority;
307 }
308
309 /**
310 * Unregister a report, removing it from the list
311 */
312 static function unregister($list, $reportClass) {
313 unset(self::$registered_reports[$list][$reportClass]);
314 }
315
316 /**
317 * Return the SS_Report objects making up the given list.
318 * @return An array of SS_Report objects
319 */
320 static function get_reports($list) {
321 $output = array();
322 if(isset(self::$registered_reports[$list])) {
323 $listItems = self::$registered_reports[$list];
324
325 // Sort by priority, preserving internal order of items with the same priority
326 $groupedItems = array();
327 foreach($listItems as $k => $v) {
328 $groupedItems[$v][] = $k;
329 }
330 krsort($groupedItems);
331 $sortedListItems = call_user_func_array('array_merge', $groupedItems);
332
333 foreach($sortedListItems as $report) {
334 if(strpos($report,'(') === false) $reportObj = new $report;
335 else $reportObj = eval("return new $report;");
336
337 $output[$reportObj->ID()] = $reportObj;
338 }
339 }
340
341 return $output;
342 }
343
344 }
345
346 /**
347 * This is an object that can be used to dress up a more complex querying mechanism in the clothing
348 * of a SQLQuery object. This means that you can inject it into a TableListField.
349 *
350 * Use it like this:
351 *
352 * function sourceQuery($params) {
353 * return new SS_Report_FakeQuery($this, 'sourceRecords', $params)
354 * }
355 * function sourceRecords($params, $sort, $limit) {
356 * // Do some stuff
357 * // Return a DataObjectSet of actual objects.
358 * }
359 *
360 * This object is used by the default implementation of sourceQuery() on SS_Report, to make use of
361 * a sourceReords() method if one exists.
362 */
363 class SS_Report_FakeQuery extends SQLQuery {
364 public $orderby;
365 public $limit;
366
367 protected $obj, $method, $params;
368
369 function __construct($obj, $method, $params) {
370 $this->obj = $obj;
371 $this->method = $method;
372 $this->params = $params;
373 }
374
375 /**
376 * Provide a method that will return a list of columns that can be used to sort.
377 */
378 function setSortColumnMethod($sortColMethod) {
379 $this->sortColMethod = $sortColMethod;
380 }
381
382 function limit($limit) {
383 $this->limit = $limit;
384 }
385
386 function unlimitedRowCount($column = NULL) {
387 $source = $this->obj->{$this->method}($this->params, null, null);
388 return $source ? $source->Count() : 0;
389 }
390
391 function execute() {
392 $output = array();
393 $source = $this->obj->{$this->method}($this->params, $this->orderby, $this->limit);
394 if($source) foreach($source as $item) {
395 $mapItem = $item->toMap();
396 $mapItem['RecordClassName'] = get_class($item);
397 $output[] = $mapItem;
398 }
399 return $output;
400 }
401
402 function canSortBy($fieldName) {
403 $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
404 if($this->sortColMethod) {
405 $columns = $this->obj->{$this->sortColMethod}();
406 return in_array($fieldName, $columns);
407 } else {
408 return false;
409 }
410 }
411 }
412
413 /**
414 * SS_ReportWrapper is a base class for creating report wappers.
415 *
416 * Wrappers encapsulate an existing report to alter their behaviour - they are implementations of
417 * the standard GoF decorator pattern.
418 *
419 * This base class ensure that, by default, wrappers behave in the same way as the report that is
420 * being wrapped. You should override any methods that need to behave differently in your subclass
421 * of SS_ReportWrapper.
422 *
423 * It also makes calls to 2 empty methods that you can override {@link beforeQuery()} and
424 * {@link afterQuery()}
425 *
426 * @package cms
427 * @subpackage reports
428 */
429 abstract class SS_ReportWrapper extends SS_Report {
430 protected $baseReport;
431
432 function __construct($baseReport) {
433 $this->baseReport = is_string($baseReport) ? new $baseReport : $baseReport;
434 $this->dataClass = $this->baseReport->dataClass();
435 parent::__construct();
436 }
437
438 function ID() {
439 return get_class($this->baseReport) . '_' . get_class($this);
440 }
441
442 ///////////////////////////////////////////////////////////////////////////////////////////
443 // Filtering
444
445 function parameterFields() {
446 return $this->baseReport->parameterFields();
447 }
448
449 ///////////////////////////////////////////////////////////////////////////////////////////
450 // Columns
451
452 function columns() {
453 return $this->baseReport->columns();
454 }
455
456 ///////////////////////////////////////////////////////////////////////////////////////////
457 // Querying
458
459 /**
460 * Override this method to perform some actions prior to querying.
461 */
462 function beforeQuery($params) {
463 }
464
465 /**
466 * Override this method to perform some actions after querying.
467 */
468 function afterQuery() {
469 }
470
471 function sourceQuery($params) {
472 if($this->baseReport->hasMethod('sourceRecords')) {
473 // The default implementation will create a fake query from our sourceRecords() method
474 return parent::sourceQuery($params);
475
476 } else if($this->baseReport->hasMethod('sourceQuery')) {
477 $this->beforeQuery($params);
478 $query = $this->baseReport->sourceQuery($params);
479 $this->afterQuery();
480 return $query;
481
482 } else {
483 user_error("Please override sourceQuery()/sourceRecords() and columns() in your base report", E_USER_ERROR);
484 }
485
486 }
487
488 function sourceRecords($params = array(), $sort = null, $limit = null) {
489 $this->beforeQuery($params);
490 $records = $this->baseReport->sourceRecords($params, $sort, $limit);
491 $this->afterQuery();
492 return $records;
493 }
494
495
496 ///////////////////////////////////////////////////////////////////////////////////////////
497 // Pass-through
498
499 function title() {
500 return $this->baseReport->title();
501 }
502
503 function group() {
504 return $this->baseReport->hasMethod('group') ? $this->baseReport->group() : 'Group';
505 }
506
507 function sort() {
508 return $this->baseReport->hasMethod('sort') ? $this->baseReport->sort() : 0;
509 }
510
511 function description() {
512 return $this->baseReport->title();
513 }
514
515 function canView($member = NULL) {
516 return $this->baseReport->canView($member);
517 }
518
519 }
520
521 ?>
522