Creating Reports with JC_AS_Report_Controller
This tutorial will guide you through creating a new report in the system. We’ll use the TJ_Reading_Report
as an example to illustrate the process.
Primary Tasks
To create a new report, you need to implement these three components:
1. Create the Report Controller
Create a new controller class that extends JC_AS_Report_Controller
. Let’s break down each required part:
Class Structure and Dependencies
First, ensure the base class exists and set up the class structure:
if (!class_exists('JC_AS_Report_Controller')) {
return;
}
class TJ_Reading_Report_Controller extends JC_AS_Report_Controller {
public static $instance;
// ... methods will be defined here ...
}
TJ_Reading_Report_Controller::get_instance();
Constructor
The constructor sets up the essential attributes for the report system:
public function __construct() {
$this->report_type = 'tj-reading';
$this->action = 'tj-reading';
$this->base_action = 'tj_reading_report';
$this->supports_concurrent = false;
$this->per_step = 100;
$this->log_context['source'] = 'Reading Report';
parent::__construct();
}
Create File
This method handles the creation of the report file in the protected reports
directory:
protected function create_file(array $context) {
$upload_dir = wp_upload_dir();
$filename = sanitize_file_name('reading-report-' . date('Y-m-d-H:i:s') . '.csv');
$reports_dir = [
'path' => trailingslashit($upload_dir['basedir']) . 'reports' . $upload_dir['subdir'],
'url' => trailingslashit($upload_dir['baseurl']) . 'reports' . $upload_dir['subdir']
];
wp_mkdir_p($reports_dir['path']);
$file = [
'title' => $filename,
'name' => trailingslashit($reports_dir['path']) . $filename,
'url' => trailingslashit($reports_dir['url']) . $filename
];
$exists = file_exists($file['name']);
if (!$exists) {
$fp = fopen($file['name'], 'a');
fputcsv($fp, ['User ID', 'Feature ID', 'Feature Title', 'Type', 'Date/Time']);
fclose($fp);
}
return $file;
}
Setup Context
This method initializes the report context and sets up the file information:
protected function setup_context(JC_Async_Report &$report, $context = []) {
$file = $this->create_file($context);
$report->set_title("Reading Report");
$report->set_downloads(json_encode(['filename' => $file['name'], 'fileurl' => $file['url']]));
$report->set_context(json_encode($context));
}
Get Total Steps
This method calculates the total number of steps needed for the report:
protected function get_total_steps(JC_Async_Report &$report): int {
global $wpdb;
$total = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}jc_editorial_views"
);
return ceil($total / $this->per_step);
}
Fetch Data
This method retrieves a batch of data for processing, joining with the posts table to get feature titles:
protected function fetch_data(JC_Async_Report $report, int $step): array {
global $wpdb;
$offset = 0 == $step ? 0 : $this->per_step * $step;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT v.*, p.post_title as feature_title
FROM {$wpdb->prefix}jc_editorial_views v
LEFT JOIN {$wpdb->posts} p ON v.feature_id = p.ID
ORDER BY v.datetime DESC
LIMIT %d OFFSET %d",
$this->per_step,
$offset
),
ARRAY_A
);
}
Process Data
This method processes the fetched data and writes it to the output file:
protected function process_data(array $data, JC_Async_Report $report, int $step): bool {
$downloads = json_decode($report->get_downloads(), true);
if (!array_key_exists('filename', $downloads)) {
wc_get_logger()->warning("No download file" . print_r($downloads, true), $this->log_context());
return false;
}
$fp = fopen($downloads['filename'], 'a');
if (!$fp) {
return false;
}
foreach ($data as $row) {
fputcsv($fp, [
$row['user_id'],
$row['feature_id'],
$row['feature_title'],
$row['type'],
$row['datetime']
]);
}
fclose($fp);
return true;
}
Get Instance
This method implements the singleton pattern for the controller:
public static function get_instance() {
if (!isset(self::$instance) && !(self::$instance instanceof TJ_Reading_Report_Controller)) {
self::$instance = new TJ_Reading_Report_Controller();
}
return self::$instance;
}
2. Create the View Controller
Create a View Controller to handle the block interface:
<?php
class TJ_Reading_Report_VC {
protected $plugin_name;
protected $version;
public function __construct($plugin_name, $version) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
public function enqueue_scripts() {
// Register block editor script
wp_register_script(
'reading-report-vc',
plugin_dir_url(__FILE__) . 'js/reading-report-vc.js',
[
'wp-blocks',
'wp-i18n',
'wp-element',
'wp-components',
],
$this->version,
'all'
);
// Set up AJAX parameters
$protocol = isset($_SERVER["HTTPS"]) ? 'https://' : 'http://';
$params = array(
'ajaxurl' => admin_url('admin-ajax.php', $protocol),
'nonce' => wp_create_nonce('entitlements')
);
// Register and localize frontend script
wp_register_script(
'reading-report-vc-front',
plugin_dir_url(__FILE__) . 'js/reading-report-vc-front.js',
array('jquery', 'holdon'),
1,
true
);
wp_localize_script('reading-report-vc-front', 'jc_async_reports', $params);
}
public function register() {
register_block_type('journal-editorial/reading-report-vc', array(
'editor_script' => 'reading-report-vc',
'editor_style' => 'reading-report-vc',
'render_callback' => [$this, 'render'],
'attributes' => []
));
}
public function render($attr, $content) {
// Render the block interface
}
}
3. Create the JavaScript Files
Create two JavaScript files:
reading-report-vc.js
- The block editor script:{ (function() { const { registerBlockType } = wp.blocks; const { createElement } = wp.element; const { __ } = wp.i18n; registerBlockType('journal-editorial/reading-report-vc', { title: __('Reading Report VC'), category: __('common'), attributes: {}, edit: props => { const { attributes, setAttributes } = props; return createElement('div', {}, ['Reading Report VC']); }, save() { return null; } }); })(); }
reading-report-vc-front.js
- The frontend script:(function($) { 'use strict'; function generateReport(baseAction, $reports) { var options = { message: "Starting the report" }; HoldOn.open(options); var params = { action: baseAction + '_trigger', } $.post(jc_async_reports.ajaxurl, params, function(result) { var status = $(result).find('response_data').text(); if (status === 'started') { var template = $(result).find('supplemental template').text(); $reports.prepend(template); HoldOn.close(); } else { var message = $(result).find('supplemental message').text(); $reports.find('.notice').show().html(message); HoldOn.close(); } }); } $(function() { var baseAction = 'tj_reading_report'; var $reports = $('.journal-reading-report-vc .reports'); $('#generate-reading-report').on('click', function(e) { e.preventDefault(); generateReport(baseAction, $reports); }); }); })(jQuery);
4. Register the Components in Your Plugin Class
Add the following to your main plugin class:
/**
* Load dependent classes after required plugins are loaded
*/
public function load_dependant_classes() {
// Check if the required base class exists
if (class_exists('JC_AS_Report_Controller')) {
require_once plugin_dir_path(dirname(__FILE__)) . 'controllers/class-tj-reading-report-controller.php';
}
}
/**
* Define block hooks and register report components
*/
private function define_block_hooks() {
// Create instances of your components
$reading_report = new TJ_Reading_Report_VC($this->get_plugin_name(), $this->get_version());
$reading_report_controller = TJ_Reading_Report_Controller::get_instance();
// Register the View Controller hooks
$this->loader->add_action('init', $reading_report, 'enqueue_styles');
$this->loader->add_action('init', $reading_report, 'enqueue_scripts');
$this->loader->add_action('init', $reading_report, 'register');
// Register the controller's AJAX endpoints
$this->loader->add_action('wp_ajax_tj_reading_report', $reading_report_controller, 'generate_report');
$this->loader->add_action('wp_ajax_tj_reading_report_status', $reading_report_controller, 'get_report_status');
}
/**
* Initialize the plugin
*/
public function __construct() {
// ... other initialization code ...
// Load dependencies
$this->load_dependencies();
// Set up hooks for dependent classes
add_action('plugins_loaded', array($this, 'load_dependant_classes'), 99);
}
For more advanced dependency management, see Dependency Management in WordPress Plugins.
Supporting Information
The following sections provide additional context about how the report system works:
Key Concepts
Base Action
The base_action
is the primary identifier used for:
- Registering AJAX endpoints (e.g.,
wp_ajax_tj_reading_report
) - Identifying the report system in the database
- Naming the report type in the UI
- Example:
tj_reading_report
for reading reports
Action
The action
is used as:
- An identifier in failed AJAX responses
- A secondary identifier for the report type
- Example:
tj-reading
for reading report actions
Report Type
The report_type
is used to:
- Filter reports in the UI
- Group related reports together
- Example:
tj-reading
for reading reports
Report Generation Process
The JC_AS_Report_Controller
base class handles the overall report generation process through these steps:
- Initialization: Sets up the report context and creates the output file
- Step Calculation: Determines total steps based on data size
- Batch Processing: Fetches and processes data in batches
- Status Updates: Tracks progress and updates the UI
- Completion: Finalizes the report and makes it available for download
Constructor and Required Attributes
The constructor must set up these essential attributes:
public function __construct() {
// Report type used for UI filtering and grouping
$this->report_type = 'tj-reading';
// Action used in failed AJAX responses
$this->action = 'tj-reading';
// Base action used for AJAX endpoint registration
$this->base_action = 'tj_reading_report';
// Whether multiple reports can run simultaneously
$this->supports_concurrent = false;
// Number of records to process in each batch
$this->per_step = 100;
// Logging context for error tracking
$this->log_context['source'] = 'Reading Report';
parent::__construct();
}
Each attribute serves a specific purpose:
report_type
: Used to filter and group reports in the UIaction
: Used as an identifier in failed AJAX responsesbase_action
: Used to register AJAX endpointssupports_concurrent
: Controls whether multiple reports can run simultaneouslyper_step
: Determines the batch size for data processinglog_context
: Provides context for error logging
Best Practices
- Always validate and sanitize data before processing
- Use proper error handling and logging
- Implement proper security measures (nonces, capability checks)
- Consider adding filters for customizing the report output
- Add proper documentation for the report format and fields
Troubleshooting
If you encounter issues:
- Check the WordPress debug log for any errors
- Verify that the database table exists and has the correct structure
- Ensure all required capabilities are properly set
- Check that the upload directory is writable
- Verify that the AJAX endpoints are properly registered
Plugin Activation and Database Setup
When creating a new report system, you’ll need to set up the necessary database tables. Here’s how to implement the activation class:
/**
* Fired during plugin activation
*/
class Journal_Editorial_Activator {
private static $db_version = '2.2.0';
/**
* Main activation method
*/
public static function activate() {
self::create_tables();
}
/**
* Create required database tables
*/
public static function create_tables() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta(self::get_schema());
update_option('journal_editorial_db_version', self::$db_version);
}
/**
* Get database schema
*/
public static function get_schema() {
global $wpdb;
$collate = '';
if ($wpdb->has_cap('collation')) {
$collate = $wpdb->get_charset_collate();
}
$tables = "
CREATE TABLE {$wpdb->prefix}jc_editorial_views (
id bigint(20) NOT NULL auto_increment,
user_id bigint(20),
feature_id bigint(20),
type varchar(16),
datetime datetime,
PRIMARY KEY (id)
) $collate;";
return $tables;
}
/**
* Check and update database if needed
*/
public static function update_db_check() {
if (get_site_option('journal_editorial_db_version') < self::$db_version) {
self::create_tables();
}
}
}
Key Components
- Database Version Control:
- Maintain a version number for database schema
- Use
update_option
to track current version - Check version on updates with
update_db_check
- Table Creation:
- Use WordPress’s
dbDelta
function for safe table creation - Include proper character set and collation
- Define primary keys and indexes
- Use WordPress’s
- Activation Hooks:
- Register activation hook in main plugin file
- Call
activate()
method during plugin activation - Handle database updates during plugin updates
Usage in Main Plugin File
// Register activation hook
register_activation_hook(__FILE__, array('Journal_Editorial_Activator', 'activate'));
// Check for database updates
add_action('plugins_loaded', array('Journal_Editorial_Activator', 'update_db_check'));
Best Practices
- Version Control:
- Increment version number when changing schema
- Use semantic versioning for database versions
- Document schema changes in version history
- Data Safety:
- Use
dbDelta
for safe table creation/updates - Backup data before schema changes
- Test updates in development environment
- Use
- Performance:
- Add appropriate indexes
- Use proper data types
- Consider table partitioning for large datasets
- Security:
- Use WordPress table prefix
- Sanitize all database operations
- Implement proper capability checks
Troubleshooting
If you encounter database issues:
- Check WordPress debug log for SQL errors
- Verify table structure matches schema
- Ensure proper permissions for table creation
- Check for conflicts with other plugins
- Verify database version in options table