Completed
Push — master ( ff0598...49e163 )
by Damian
07:44 queued 05:41
created

code/Report.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 SS_List of the search results
13
 *  {@link columns()}: Return information about the columns in this report.
14
 *  {@link parameterFields()}: Return a FieldList of the fields that can be used to filter this
15
 *  report.
16
 * 
17
 * If you wish to modify the report in more extreme ways, you could overload these methods instead.
18
 * 
19
 * {@link getReportField()}: Return a FormField in the place where your report's TableListField
20
 * usually appears.
21
 * {@link getCMSFields()}: Return the FieldList representing the complete right-hand area of the 
22
 * report, including the title, description, parameter fields, and results.
23
 * 
24
 * Showing reports to the user
25
 * ===========================
26
 * 
27
 * Right now, all subclasses of SS_Report will be shown in the ReportAdmin. In SS3 there is only
28
 * one place where reports can go, so this class is greatly simplifed from its version in SS2.
29
 * 
30
 * @package reports
31
 */
32
class SS_Report extends ViewableData
33
{
34
    /**
35
     * This is the title of the report,
36
     * used by the ReportAdmin templates.
37
     *
38
     * @var string
39
     */
40
    protected $title = '';
41
42
    /**
43
     * This is a description about what this
44
     * report does. Used by the ReportAdmin
45
     * templates.
46
     *
47
     * @var string
48
     */
49
    protected $description = '';
50
    
51
    /**
52
     * The class of object being managed by this report.
53
     * Set by overriding in your subclass.
54
     */
55
    protected $dataClass = 'SiteTree';
56
57
    /**
58
     * A field that specifies the sort order of this report
59
     * @var int
60
     */
61
    protected $sort = 0;
62
63
    /**
64
     * Reports which should not be collected and returned in get_reports
65
     * @var array
66
     */
67
    public static $excluded_reports = array(
68
        'SS_Report',
69
        'SS_ReportWrapper',
70
        'SideReportWrapper'
71
    );
72
    
73
    /**
74
     * Return the title of this report.
75
     * 
76
     * You have two ways of specifying the description:
77
     *  - overriding description(), which lets you support i18n 
78
     *  - defining the $description property
79
     */
80
    public function title()
81
    {
82
        return $this->title;
83
    }
84
    
85
    /**
86
     * Allows access to title as a property
87
     * 
88
     * @return string
89
     */
90
    public function getTitle()
91
    {
92
        return $this->title();
93
    }
94
    
95
    /**
96
     * Return the description of this report.
97
     * 
98
     * You have two ways of specifying the description:
99
     *  - overriding description(), which lets you support i18n 
100
     *  - defining the $description property
101
     */
102
    public function description()
103
    {
104
        return $this->description;
105
    }
106
    
107
    /**
108
     * Return the {@link SQLQuery} that provides your report data.
109
     */
110
    public function sourceQuery($params)
111
    {
112
        if ($this->hasMethod('sourceRecords')) {
113
            return $this->sourceRecords()->dataQuery();
114
        } else {
115
            user_error("Please override sourceQuery()/sourceRecords() and columns() or, if necessary, override getReportField()", E_USER_ERROR);
116
        }
117
    }
118
    
119
    /**
120
     * Return a SS_List records for this report.
121
     */
122
    public function records($params)
123
    {
124
        if ($this->hasMethod('sourceRecords')) {
125
            return $this->sourceRecords($params, null, null);
126
        } else {
127
            $query = $this->sourceQuery();
128
            $results = new ArrayList();
129
            foreach ($query->execute() as $data) {
130
                $class = $this->dataClass();
131
                $result = new $class($data);
132
                $results->push($result);
133
            }
134
            return $results;
135
        }
136
    }
137
138
    /**
139
     * Return the data class for this report
140
     */
141
    public function dataClass()
142
    {
143
        return $this->dataClass;
144
    }
145
146
147
    public function getLink($action = null)
148
    {
149
        return Controller::join_links(
150
            'admin/reports/',
151
            "$this->class",
152
            '/', // trailing slash needed if $action is null!
153
            "$action"
154
        );
155
    }
156
157
158
    /**
159
     * counts the number of objects returned
160
     * @param Array $params - any parameters for the sourceRecords
161
     * @return Int
162
     */
163
    public function getCount($params = array())
164
    {
165
        $sourceRecords = $this->sourceRecords($params, null, null);
166
        if (!$sourceRecords instanceof SS_List) {
167
            user_error($this->class."::sourceRecords does not return an SS_List", E_USER_NOTICE);
168
            return "-1";
169
        }
170
        return $sourceRecords->count();
171
    }
172
173
    /**
174
     * Exclude certain reports classes from the list of Reports in the CMS
175
     * @param $reportClass Can be either a string with the report classname or an array of reports classnames
176
     */
177
    public static function add_excluded_reports($reportClass)
178
    {
179
        if (is_array($reportClass)) {
180
            self::$excluded_reports = array_merge(self::$excluded_reports, $reportClass);
181
        } else {
182
            if (is_string($reportClass)) {
183
                //add to the excluded reports, so this report doesn't get used
184
                self::$excluded_reports[] = $reportClass;
185
            }
186
        }
187
    }
188
189
    /**
190
     * Return an array of excluded reports. That is, reports that will not be included in
191
     * the list of reports in report admin in the CMS.
192
     * @return array
193
     */
194
    public static function get_excluded_reports()
195
    {
196
        return self::$excluded_reports;
197
    }
198
199
    /**
200
     * Return the SS_Report objects making up the given list.
201
     * @return Array of SS_Report objects
202
     */
203
    public static function get_reports()
204
    {
205
        $reports = ClassInfo::subclassesFor(get_called_class());
206
207
        $reportsArray = array();
208
        if ($reports && count($reports) > 0) {
209
            //collect reports into array with an attribute for 'sort'
210
            foreach ($reports as $report) {
211
                if (in_array($report, self::$excluded_reports)) {
212
                    continue;
213
                }   //don't use the SS_Report superclass
214
                $reflectionClass = new ReflectionClass($report);
215
                if ($reflectionClass->isAbstract()) {
216
                    continue;
217
                }   //don't use abstract classes
218
219
                $reportObj = new $report;
220
                if (method_exists($reportObj, 'sort')) {
221
                    $reportObj->sort = $reportObj->sort();
222
                }  //use the sort method to specify the sort field
223
                $reportsArray[$report] = $reportObj;
224
            }
225
        }
226
227
        uasort($reportsArray, function ($a, $b) {
228
            if ($a->sort == $b->sort) {
229
                return 0;
230
            } else {
231
                return ($a->sort < $b->sort) ? -1 : 1;
232
            }
233
        });
234
235
        return $reportsArray;
236
    }
237
238
    /////////////////////// UI METHODS ///////////////////////
239
240
241
    /**
242
     * Returns a FieldList with which to create the CMS editing form.
243
     * You can use the extend() method of FieldList to create customised forms for your other
244
     * data objects.
245
     *
246
     * @uses getReportField() to render a table, or similar field for the report. This
247
     * method should be defined on the SS_Report subclasses.
248
     *
249
     * @return FieldList
250
     */
251
    public function getCMSFields()
252
    {
253
        $fields = new FieldList();
254
255
        if ($title = $this->title()) {
256
            $fields->push(new LiteralField('ReportTitle', "<h3>{$title}</h3>"));
257
        }
258
        
259
        if ($description = $this->description()) {
260
            $fields->push(new LiteralField('ReportDescription', "<p>" . $description . "</p>"));
261
        }
262
            
263
        // Add search fields is available
264
        if ($this->hasMethod('parameterFields') && $parameterFields = $this->parameterFields()) {
265
            foreach ($parameterFields as $field) {
266
                // Namespace fields for easier handling in form submissions
267
                $field->setName(sprintf('filters[%s]', $field->getName()));
268
                $field->addExtraClass('no-change-track'); // ignore in changetracker
269
                $fields->push($field);
270
            }
271
272
            // Add a search button
273
            $fields->push(new FormAction('updatereport', _t('GridField.Filter')));
274
        }
275
        
276
        $fields->push($this->getReportField());
277
278
        $this->extend('updateCMSFields', $fields);
279
        
280
        return $fields;
281
    }
282
    
283
    public function getCMSActions()
284
    {
285
        // getCMSActions() can be extended with updateCMSActions() on a extension
286
        $actions = new FieldList();
287
        $this->extend('updateCMSActions', $actions);
288
        return $actions;
289
    }
290
    
291
    /**
292
     * Return a field, such as a {@link GridField} that is
293
     * used to show and manipulate data relating to this report.
294
     * 
295
     * Generally, you should override {@link columns()} and {@link records()} to make your report,
296
     * but if they aren't sufficiently flexible, then you can override this method.
297
     *
298
     * @return FormField subclass
299
     */
300
    public function getReportField()
301
    {
302
        // TODO Remove coupling with global state
303
        $params = isset($_REQUEST['filters']) ? $_REQUEST['filters'] : array();
304
        $items = $this->sourceRecords($params, null, null);
305
306
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
307
            new GridFieldToolbarHeader(),
308
            new GridFieldSortableHeader(),
309
            new GridFieldDataColumns(),
310
            new GridFieldPaginator(),
311
            new GridFieldButtonRow('after'),
312
            new GridFieldPrintButton('buttons-after-left'),
313
            new GridFieldExportButton('buttons-after-left')
314
        );
315
        $gridField = new GridField('Report', $this->title(), $items, $gridFieldConfig);
316
        $columns = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
317
        $displayFields = array();
318
        $fieldCasting = array();
319
        $fieldFormatting = array();
320
321
        // Parse the column information
322
        foreach ($this->columns() as $source => $info) {
323
            if (is_string($info)) {
324
                $info = array('title' => $info);
325
            }
326
            
327
            if (isset($info['formatting'])) {
328
                $fieldFormatting[$source] = $info['formatting'];
329
            }
330
            if (isset($info['csvFormatting'])) {
331
                $csvFieldFormatting[$source] = $info['csvFormatting'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$csvFieldFormatting was never initialized. Although not strictly required by PHP, it is generally a good practice to add $csvFieldFormatting = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
332
            }
333
            if (isset($info['casting'])) {
334
                $fieldCasting[$source] = $info['casting'];
335
            }
336
337
            if (isset($info['link']) && $info['link']) {
338
                $link = singleton('CMSPageEditController')->Link('show');
339
                $fieldFormatting[$source] = '<a href=\"' . $link . '/$ID\">$value</a>';
340
            }
341
342
            $displayFields[$source] = isset($info['title']) ? $info['title'] : $source;
343
        }
344
        $columns->setDisplayFields($displayFields);
345
        $columns->setFieldCasting($fieldCasting);
346
        $columns->setFieldFormatting($fieldFormatting);
347
348
        return $gridField;
349
    }
350
    
351
    /**
352
     * @param Member $member
353
     * @return boolean
354
     */
355
    public function canView($member = null)
356
    {
357
        if (!$member && $member !== false) {
358
            $member = Member::currentUser();
359
        }
360
        
361
        return true;
362
    }
363
    
364
365
    /**
366
     * Return the name of this report, which
367
     * is used by the templates to render the
368
     * name of the report in the report tree,
369
     * the left hand pane inside ReportAdmin.
370
     *
371
     * @return string
372
     */
373
    public function TreeTitle()
374
    {
375
        return $this->title();
376
    }
377
}
378
379
/**
380
 * SS_ReportWrapper is a base class for creating report wappers.
381
 * 
382
 * Wrappers encapsulate an existing report to alter their behaviour - they are implementations of
383
 * the standard GoF decorator pattern.
384
 * 
385
 * This base class ensure that, by default, wrappers behave in the same way as the report that is
386
 * being wrapped.  You should override any methods that need to behave differently in your subclass
387
 * of SS_ReportWrapper.
388
 * 
389
 * It also makes calls to 2 empty methods that you can override {@link beforeQuery()} and
390
 * {@link afterQuery()}
391
 * 
392
 * @package reports
393
 */
394
abstract class SS_ReportWrapper extends SS_Report
395
{
396
    protected $baseReport;
397
398
    public function __construct($baseReport)
399
    {
400
        $this->baseReport = is_string($baseReport) ? new $baseReport : $baseReport;
401
        $this->dataClass = $this->baseReport->dataClass();
402
        parent::__construct();
403
    }
404
405
    public function ID()
406
    {
407
        return get_class($this->baseReport) . '_' . get_class($this);
408
    }
409
410
    ///////////////////////////////////////////////////////////////////////////////////////////
411
    // Filtering
412
413
    public function parameterFields()
414
    {
415
        return $this->baseReport->parameterFields();
416
    }
417
418
    ///////////////////////////////////////////////////////////////////////////////////////////
419
    // Columns
420
421
    public function columns()
422
    {
423
        return $this->baseReport->columns();
424
    }
425
426
    ///////////////////////////////////////////////////////////////////////////////////////////
427
    // Querying
428
429
    /**
430
     * Override this method to perform some actions prior to querying.
431
     */
432
    public function beforeQuery($params)
433
    {
434
    }
435
436
    /**
437
     * Override this method to perform some actions after querying.
438
     */
439
    public function afterQuery()
440
    {
441
    }
442
443
    public function sourceQuery($params)
444
    {
445
        if ($this->baseReport->hasMethod('sourceRecords')) {
446
            // The default implementation will create a fake query from our sourceRecords() method
447
            return parent::sourceQuery($params);
448
        } elseif ($this->baseReport->hasMethod('sourceQuery')) {
449
            $this->beforeQuery($params);
450
            $query = $this->baseReport->sourceQuery($params);
451
            $this->afterQuery();
452
            return $query;
453
        } else {
454
            user_error("Please override sourceQuery()/sourceRecords() and columns() in your base report", E_USER_ERROR);
455
        }
456
    }
457
458
    public function sourceRecords($params = array(), $sort = null, $limit = null)
459
    {
460
        $this->beforeQuery($params);
461
        $records = $this->baseReport->sourceRecords($params, $sort, $limit);
462
        $this->afterQuery();
463
        return $records;
464
    }
465
466
467
    ///////////////////////////////////////////////////////////////////////////////////////////
468
    // Pass-through
469
470
    public function title()
471
    {
472
        return $this->baseReport->title();
473
    }
474
    
475
    public function group()
476
    {
477
        return $this->baseReport->hasMethod('group') ? $this->baseReport->group() : 'Group';
478
    }
479
    
480
    public function sort()
481
    {
482
        return $this->baseReport->hasMethod('sort') ? $this->baseReport->sort() : 0;
483
    }
484
485
    public function description()
486
    {
487
        return $this->baseReport->description();
488
    }
489
490
    public function canView($member = null)
491
    {
492
        return $this->baseReport->canView($member);
493
    }
494
}
495