Skip to content

Reporting Subsystem

Jeremy Booker edited this page May 5, 2015 · 3 revisions

History

In the beginning, HMS had only a rudimentary reporting system. It showed a list of reports, you clicked a report name, the report ran in a popup window, and when you closed that window the report data was gone forever. These old reports were all lumped into various functions inside one gigantic class file (1500+ lines) called HMS_Reports. They were almost entirely procedural, and not very maintainable.

The newer reporting classes improve upon this basic design with the following goals:

  • Store a copy of the output from each report.
  • Save a copy of the output in multiple formats, whichever formats make sense.
  • Allow the user to easily find/view/download output from previous report runs.
  • Allow the user to run a report now, run it in the background (doesn't lock session), or schedule the report for a specific time.
  • Provide for some reports which are always scheduled to run at particular times.
  • Break reports up into their own folders / classes.
  • Enforce good Model/View/Controller pattern.
  • Enforce some proper interfaces for individual report implementation.

Conventions

There are several 'conventions' that each report is expected to adhere to.

No matter the output format (view), the report output must include the following elements:

  • Report Title
  • Report execution date/time
  • The user name of the person who scheduled/executed the report
  • The term (if applicable) for which the report was run.
  • The term for which a report is run is common to many reports, but not all, so each individual report is responsible for providing the term.
  • When listing a Residence Hall, the max occupancy of the hall is listed beside the hall name.

For HTML & PDF Views:

  • Grid lines and/or row-highlighting where appropriate (html/pdf views only).

For CSV views:

  • Place the report execution date/time, user name, and term as the last fields on header (first) row of the output.

Major Classes Involved:

Report Logic

  • Report - Abstract base class all reports extend from.
  • ReportController - Base controller class. Provides a lot of default behavior.
  • ReportFactory - Contains static methods used for creating and loading Report and ReportController objects.
  • ReportInterfaces - Defines all the various interfaces a Report/ReportController can implement.

Views

  • ReportView - Base view class, must be extended
  • ReportHtmlView - Base HTML class, must be extended
  • (Individual report HTML views extend ReportHtmlView)
  • ReportCsvView - Provides default CSV export
  • ReportPdfView - Base PDF view class, must be extended
  • ReportPdfViewFromHtml - Provides default html->behavior.

Execution

  • ExecReportSyncCommand - Runs a report synchronously with the browser, hangs the user's session.
  • ScheduleReportCommand - Schedules a report to run at a given time (can be now, or anytime in the future). Runs in the background, doesn't hang the user's session.
  • ReportRunner - A Pulse class which schedules itself to be run once a minute. Executes any reports scheduled by the SheduleReportCommand.
  • CancelReportCommand - Cancels a report that was scheduled with the ScheduleReportCommand.

Management Interfaces

  • ListReportsCommand / ListReportsView - Shows the list of available reports
  • ReportMenuItemView - Represents a report item in the list of reports.
  • ShowReportDetailCommand / ReportDetailView - Shows historical results of a report and lets the user schedule & execute the report.
  • ReportSetupView - Setup dialog for providing report parameters when the report will be run asynchronously.

Creating A New Report

This section is a walk-through on how to create a new report. We'll use the ''Single Gender vs. Co-ed Preference'' report as the example we'll construct.

Create the Directory

Start be creating a new directory inside the /class/report/ directory. The directory name must match the report's model class name, which we'll create later. A good deal of code relies on this naming convention. We'll create the directory: /class/report/SingleGenderVCoedPref/

Create the Model Class

Create the model class called /class/report/SingleGenderVCoedPref/SingleGenderVCoedPref.php:

<?php

class SingleGenderVsCoedPref extends Report {
    
    const friendlyName = 'Single Gender Vs Coed Preference';
    const shortName = 'SingleGenderVsCoedPref';
    
    // Member variable
    //TODO
    
    public function __construct($id = 0)
    {
        parent::__construct($id);
        
        // Initialize member variables
        //TODO
    }
    
    public function execute()
    {
        // TODO
    }
    
    /****************************
    * Accessor/Mutator Methods *
    ****************************/
    //TODO
}
?>

Note that this class must extend the Report class, and that required implementing the execute() method. The friendlyName and shortName constants are required, and must use those names. The ReportController class looks for these (there's not really a better way to do this in PHP....).

Create the Controller Class

Create the report's controller class called /class/report/SingleGenderVCoedPref/SingleGenderVsCoedPrefController.php:

<?php

class SingleGenderVsCoedPrefController extends ReportController implements iSyncReport {
    public function setParams(Array $params)
    {
        //TODO
    }
    
    public function getParams()
    {
        //TODO
    }
}
?>

Most of the heavy lifting is done for in the the parent ReportController class, so for now this class can be left empty. We'll need to add to it later. Notice that the Controller also implements the iSyncReport interface. This interface will allow the report to be 'Run now' (more on that later).

At this point, you should be able to see the new report in the list of reports (ListReportsCommand / ListReportsView), and view the details of the report (ShowReportDetailCommand / ReportDetailView). Running the report with the "Run now" link will cause a record to be placed in the 'Archived Results' table, but there won't be any results to view (since we haven't created any views). So, the next step is to create at least one view for this report.

Create a View

One of the simplest views to create is a HTML view. The default implementation in the ReportController class will look for a HTML view with a class name of the format <reportName>HtmlView.php. So, start by creating a class called /class/report/SingleGenderVCoedPref/SingleGenderVsCoedPrefHtmlView.php:

<?php

class SingleGenderVsCoedPrefHtmlView extends ReportHtmlView {

    protected function render()
    {
        parent::render();
        
        //TODO Use accessor methods on $this->report to get report data and
        // plug it into template variables in $this->tpl[]
        
        return PHPWS_Template::process($this->tpl, 'hms', 'admin/reports/SingleGenderVsCoedPref.tpl');
    }
}

?>

Notice that this HTML view class extends ReportHtmlView. This parent class provides template variables for the report name, execution date, and who executed (or scheduled) the report. It also provides an interface and functionality for turning the HTML snippets this View returns into a full-fledged HTML document, so it can then be converted to a PDF or used as a stand-alone web page.

Next, we need to create the corresponding template admin/reports/SingleGenderVsCoedPref.tpl:

<h1>{NAME} - {TERM}</h1>

Executed on: {EXEC_DATE} by {EXEC_USER}<br />

<!-- TODO: insert table of report results here -->

Report results go here...

Again the {NAME}, {EXEC_DATE}, and {EXEC_USER} tags are supplied by the ReportHtmlView::render() method.

For the final step, we need to tell the User Interface that this report supports an HTML view by implementing the iHtmlReportView interface on the Report's controller class. To do that, change the class signature of SingleGenderVsCoedPrefController to be:

class SingleGenderVsCoedPrefController extends ReportController implements iSyncReport, iHtmlReportView {

The iHtmlReportView interface requires that any implementing class has two methods: getHtmlView and saveHtmlOutput(ReportHtmlView $htmlView). A default implementation of these methods is provided in the ReportController parent class. We could override these methods in our SingleGenderVsCoedPrefController class if necessary, but the default implementation is good enough for this report.

Controlling Execution Methods

Run Now

This method allows the user to execute a report immediately and see its results as soon as the report is complete. The user's browser session will hang while the report is running, preventing the user from accessing any other pages on the server with that session. This is permissible for reports which generally can be completed in no more than one minute. If the report usually takes longer than a minute, then this method should not be allowed. This is mostly to prevent the user from accidentally clicking "run now" and hanging their session on a report that will be running for 20 minutes. It's better to just disable 'run now' and force the user to use 'run in background'.

To enable this method, the report's controller class must implement the {{{iSyncReport}}} interface.

Run in Background

This method allows the user to execute a report separately from their session. The report is scheduled for the current time ("right now") using the ScheduleReportCommand. The ReportRunner command is then expected to be invoked by Pulse, where it will find the scheduled report and execute it within a minute or two of it being scheduled.

This is mostly useful for long-running reports where the use might want output as soon as possible, but doesn't want to cause their browser session to hang while waiting for the report to run.

To enable this method, the report's controller class must implement the iAsyncReport.

Schedule Run

This methods allows the user to schedule a report to be executed at any arbitrary point in the future. The report is scheduled for the selected date/time using the ScheduleReportCommand. The same ReportRunner command as is used for the 'background' method is expected to execute the scheduled report once its scheduled execution time passes.

To enable this method, the report's controller class must implement the iSchedReport interface.

Passing in Parameters

The above example report isn't of much practical use as we've left it. The report logic in SingleGenderVsCoedPref::execute() needs more information before it can generate the report we want. Specifically, it needs to know in which term to analyze the housing applications.

When a report is run synchronously, the 'selected term' in the user's session is always passed in. This is done as a convention, simply because the term is used so often. However, for running reports in the background or at a scheduled time, there is no user session or form submission to take parameters from. Instead, the parameters must be inputted and saved when the report is scheduled. When the report is ready to be executed, the saved parameters need to be loaded and passed to the report before execution starts. To accomplish this, the iAsyncReport interface (and consequently the iSchedReport interface) require the report's controller class to implement the setParams(Array $params) and getParams() methods. These methods allow the report controller to map input parameters to/from an associative array for the report object that the controller is managing.

Adding these methods to the controller class give us:

<?php

class SingleGenderVsCoedPrefController extends ReportController implements iSyncReport, iHtmlReportView {
    
    public function setParams(Array $params)
    {
        $this->report->setTerm($params['term']);
    }
    
    public function getParams()
    {
        $params = array();
    
        $params['term'] = $this->report->getTerm();
    
        return $params;
    }
}

?>

NB: A 'configuration object' could be passed around instead, where each report would need another class (implementing an interface) to handle creating/using the configuration object. An array was used as a compromise for simplicity. Note that parameters are always received from the web browser in an associate array ($_REQUEST, etc) anyway, so it's really just a matter of where we map between an array and an object.

Next we need to add a term member variable and the corresponding getTerm() and setTerm methods (used above) to the actual report class. To the SingleGenderVsCoedPref class add:

private $term;

and

public function setTerm($term){
    $this->term = $term;
}
    
public function getTerm(){
    return $this->term;
}

Now we can adjust the HTML view to show the term as intended by adding the following line to SingleGenderVsCoedPrefHtmlView::render() method, just after the call to parent::render():

$this->tpl['TERM'] = Term::toString($this->report->getTerm());

Views

The above sections provided an overview of using report views with an HTML view. This section gives more details on each of the possible views.

Each report can implement one or more of the available views. To implement a view, the report controller class implements one of the available view interfaces and supplies a corresponding class.

The report object, after having its execute() method called, is passed to each view. The generated data should be stored inside the report object. Each implemented view is then responsible for pulling the data out of the report as desired (using accessor methods) and formatting that view's output. Once the report is run and the views are rendered, the generated data is lost. Only the output of each view is saved.

HTML Views

An HTML view lets the user see the output of the report directly in the browser. To create an HTML view for a report, simply extend the ReportHtmlView class, override the render() method (being sure to call parent::render()), and return a string of HTML. The returned string of HTML should not be a fully-formed HTML document, just a snippet. The report's controller class must implement the iHtmlReportView interface.

Your render() method should use the accessor methods of the report object (available in $this->report) to populate the $this->tpl array of template variables, render those variables using a template, and then return the rendered HTML.

The ReportHtmlView class provides several pieces of important functionality. First, it provides a default set of template tags that should be common to every report:

  • NAME - The friendly title of the report.
  • EXEC_DATE - The date the report was run.
  • EXEC_USER - The user who requested the report.

It also provides the getWrappedHtml() method, which is used in the automatic HTML->PDF conversion process.

PDF Views

A PDF view lets the report generate a PDF document for the user to download and print. There are two ways to create a PDF view:

  • Automatic PDF conversion from a ReportHtmlView object.
  • Custom PDF generation

If a report controller implements the iHtmlReportView, then the report can also implement the iPdfReportView interface to get a PDF view "for free". The ReportController class provides the HTML->PDF functionality through the ReportPdfViewFromHtml class. This class uses the WebKitToPDF project to do the actual conversion.

The second way to generate a PDF view is though a custom PDF generation class. To do this, you will need to:

  • Create a <reportName>PdfView class which extends ReportPdfView.
  • Override the ReportController->getPdfView() method to return a properly configured instance of the above PdfView class.

CSV Views

A CSV view lets the user generate a CSV (comma separated values) file for download, which can then be imported into a spreadsheet application. To create a CSV view, the report controller class must implement the iCsvReportView interface. This allows the ReportController class to provide default behavior for csv export through the ReportCsvView class.

The ReportCsvView class requires the report class (not the controller) to implement the iCSVReport interface. This interface requires that your report implement two methods named getCsvColumnsArray() and getCsvRowsArray(). The first method returns an Array of column names which becomes the header row of the CSV file. The latter method must return a two-dimensional array containing the rows and columns of data.

Showing the Output

Once the report is run, the views are executed, and the output stored... we then need to be able to actually look at that output. There are several commands available for this:

  • ShowReportHtmlCommand - (Default) Renders a report's HTML to the page.
  • ShowReportPdfCommand - Attempts to open a PDF embedded in the browser.
  • ShowReportCsvCommand - Attempts to download the .csv file to the browser.

If a report is invoked synchronously (i.e. 'run now'), once it finishes running, the user's browser is automatically redirect to the default view for that report. The command to run when a report finishes can be changed to be any command you'd like by overriding Report::getDefaultOutputViewCmd(). The default is to use ShowReportHtmlCommand. This is also the command run by default when the user clicks the link showing the relative time the report was last executed.

Descriptions

The description shown on the Detail View is provided by a template file. The should give a user-friendly description of what the report is going to do and what information its output contains. Any HTML (including images, etc) can be included. By default, the ReportController class looks for the report's description is a file named: /templates/admin/reports/<reportName>Desc.tpl.