Version: current

      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

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -