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:

  1. 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;
             }
         });
     })();
    }
    
  2. 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:

  1. Initialization: Sets up the report context and creates the output file
  2. Step Calculation: Determines total steps based on data size
  3. Batch Processing: Fetches and processes data in batches
  4. Status Updates: Tracks progress and updates the UI
  5. 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 UI
  • action: Used as an identifier in failed AJAX responses
  • base_action: Used to register AJAX endpoints
  • supports_concurrent: Controls whether multiple reports can run simultaneously
  • per_step: Determines the batch size for data processing
  • log_context: Provides context for error logging

Best Practices

  1. Always validate and sanitize data before processing
  2. Use proper error handling and logging
  3. Implement proper security measures (nonces, capability checks)
  4. Consider adding filters for customizing the report output
  5. Add proper documentation for the report format and fields

Troubleshooting

If you encounter issues:

  1. Check the WordPress debug log for any errors
  2. Verify that the database table exists and has the correct structure
  3. Ensure all required capabilities are properly set
  4. Check that the upload directory is writable
  5. 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

  1. 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
  2. Table Creation:
    • Use WordPress’s dbDelta function for safe table creation
    • Include proper character set and collation
    • Define primary keys and indexes
  3. 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

  1. Version Control:
    • Increment version number when changing schema
    • Use semantic versioning for database versions
    • Document schema changes in version history
  2. Data Safety:
    • Use dbDelta for safe table creation/updates
    • Backup data before schema changes
    • Test updates in development environment
  3. Performance:
    • Add appropriate indexes
    • Use proper data types
    • Consider table partitioning for large datasets
  4. Security:
    • Use WordPress table prefix
    • Sanitize all database operations
    • Implement proper capability checks

Troubleshooting

If you encounter database issues:

  1. Check WordPress debug log for SQL errors
  2. Verify table structure matches schema
  3. Ensure proper permissions for table creation
  4. Check for conflicts with other plugins
  5. Verify database version in options table