When I develop the custom plugin, I prefer to build the custom plugin with Object-Oriented Programming(OOP) to avoid the conflict from other plugins or the themes. Plus my code is cleaner and more organized. Today I will share how to create the report plugin with OOP.
Here what the plugin does:

The plugin will display the data regarding the criteria. I use the datatable plugin for the data table with search, pagination and column sort features. The datatable plugin has more features you can use. The datepicker plugin I use the jquery-ui plugin.
Reports plugin structure
- mct-reports
- classes
- css
- images (this is for jquery-ui)
- datepicker-bootstrap3.css
- jquery-ui.min.css
- mct-reports.css (my custom style for the reports)
- js
- jquery-ui.min.js
- mct-reports.js
- languages
- mct-reports-th_TH.mo
- mct-reports-th_TH.po
- lib
- DataTables (this folder you can download from DataTables site)
- mct-reports.php (main plugin file)
mct-reports.php
This is a main plugin file. We define the constants and include our classes.
<?php
/*
Plugin Name: Reports
Description:
Version: 1.0
Author: <a href="http://applerinquest.com" target="_BLANK">AppleRinquest</a>
Text Domain: mct-reports
Domain Path: /languages/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// declare the constant
define('MCT_REPORTS_PLUGIN_PATH', dirname(__FILE__)); // this constant uses in the class files
define('MCT_REPORTS_PLUGIN_URL' , WP_PLUGIN_URL . '/mct-reports/' ); // this constant uses in enqueue file and style
define('MCT_REPORTS_LANG_PATH', basename( dirname( __FILE__ ) ));
// our custom classes
require_once( dirname(__FILE__) . '/classes/class-translation.php' );
require_once( dirname(__FILE__) . '/classes/class-reports.php' );
class-reports.php
<?php
// If this file is called directly, abort.
if (!defined('ABSPATH')) die("No cheating!");
if(!class_exists('Mtc_Reports')) {
class Mtc_Reports
{
/**
* Start up
*/
public function __construct() {
// add admin menu
add_action( 'admin_menu', array( $this, 'reports_menu' ) );
// add scripts and style on the backend
add_action( 'admin_enqueue_scripts', array( $this,'reports_admin_scripts') );
add_action( 'wp_ajax_get_job_applications', array( $this, 'get_job_applications') );
}
/**
* add reports menu
*/
public function reports_menu() {
// add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position );
add_menu_page(__('Reports','mct-reports'), __('Reports','mct-reports'), 'edit_others_posts', 'mct-reports', array( $this, 'reports_section') );
}
/**
* add jquery ui datepicker widget
*/
public function reports_admin_scripts()
{
// add the script and style on our plugin only
if (isset($_GET['page']) && $_GET['page'] === 'mct-reports') {
// # load style
// load datepicker plugin style
// @link https://gist.github.com/miwahall/7028640
wp_enqueue_style('mct-reports-bootstrap-style', MCT_REPORTS_PLUGIN_URL . 'css/datepicker-bootstrap3.css' );
// load datatable plugin style
wp_enqueue_style('mct-reports-datatable-style', MCT_REPORTS_PLUGIN_URL . 'lib/DataTables/datatables.min.css' );
// load custom style
wp_enqueue_style('mct-reports-style', MCT_REPORTS_PLUGIN_URL . 'css/mct-reports.css' );
// # load scripts
// datepicker plugin
wp_enqueue_script( 'mct-reports-datepicker' , MCT_REPORTS_PLUGIN_URL . 'js/jquery-ui.min.js', array('jquery') ,true);
wp_enqueue_script( 'mct-reports' , MCT_REPORTS_PLUGIN_URL . 'js/mct-reports.js', array('jquery','mct-reports-datepicker') ,true);
wp_localize_script( 'mct-reports', 'mctReportsAjax', array(
'ajaxurl' => admin_url('admin-ajax.php')
));
// datatable plugin
wp_enqueue_script( 'mct-reports-datatable' , MCT_REPORTS_PLUGIN_URL . 'lib/DataTables/datatables.min.js', array('jquery') ,true);
}
}
/**
* display the reports
*/
public function reports_section()
{
// make sure the users have the access permision
if (!current_user_can('edit_others_posts')) {
wp_die('Unauthorized user');
}
?>
<div class="wrap">
<!-- Title -->
<h1><?php echo __('Reports', 'mct-reports') ?></h1>
<br>
<div>
<!-- render a criteria -->
<table id="report_criteria" class="form-table">
<tr>
<th>
<?php echo __('Time period', 'mct-reports') ?>
</th>
<td id="report_date">
<!-- default date is last 7 days until today -->
<input type="text" id="txtStartDate" name="txtStartDate" value="<?php echo date('d/m/Y', strtotime('-7 days')); ?>">
<?php echo __(' to ','mct-reports'); ?>
<input type="text" id="txtEndDate" name="txtEndDate" value="<?php echo date('d/m/Y'); ?>">
<div class="help-block with-errors" style="display:none;"><?php echo __('Please choose the time period','mct-reports') ?></div>
</td>
</tr>
<tr>
<th>
<?php echo __('Select a report','mct-reports') ?>
</th>
<td>
<select id="reports_selector" name="reports_selector">
<option value="total" selected><?php echo __('Total job applications','mct-reports') ?></option>
<option value="total_by_location"><?php echo __('Total job applications by locations','mct-reports') ?></option>
<option value="total_by_positionType"><?php echo __('Total job applications by position type','mct-reports') ?></option>
<option value="total_by_specialty"><?php echo __('Total job applications by specialty','mct-reports') ?></option>
</select>
</td>
</tr>
<tr>
<td style="padding-left:0px;">
<button class="btn-add button-primary" id="btn_report" name="btn_report"><?php echo __('Show report','mct-reports') ?></button>
</td>
</tr>
</table>
</div>
</div>
<!-- end div.wrap -->
<br>
<div class="wrap">
<!-- render a table -->
<!-- @link https://datatables.net/examples/data_sources/dom.html -->
<table id="mct_report" class="table table-striped table-bordered" style="width:100%; display:none;">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Position Applied</th>
<th>Salary Expectation</th>
<th>Email</th>
<th>Date</th>
</tr>
</thead>
<!-- the body section will inject by ajax -->
<tfoot>
<tr>
<th>#</th>
<th>Name</th>
<th>Position Applied</th>
<th>Salary Expectation</th>
<th>Email</th>
<th>Date</th>
</tr>
</tfoot>
</table>
</div>
<!-- end div.wrap -->
<div class="wrap">
<!-- @link https://datatables.net/examples/advanced_init/row_grouping.html -->
<table id="mct_report_grouping" class="table table-striped table-bordered" style="width:100%; display:none;">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Position Applied</th>
<th>Salary Expectation</th>
<th>Email</th>
<th>Date</th>
<th>Localtion</th>
<th>Position Type</th>
<th>Specialty</th>
</tr>
</thead>
<!-- the body section will inject by ajax -->
<tfoot>
<tr>
<th>#</th>
<th>Name</th>
<th>Position Applied</th>
<th>Salary Expectation</th>
<th>Email</th>
<th>Date</th>
<th>Localtion</th>
<th>Position Type</th>
<th>Specialty</th>
</tr>
</tfoot>
</table>
</div>
<!-- end div.wrap -->
<?php
}
/**
* get data from table
*/
public function get_job_applications()
{
global $wpdb;
$table_name = $wpdb->prefix . 'job_applications'; // do not forget about tables prefix
$status = 'active';
$orderby = 'date_created, firstname';
$order = 'asc';
// # the parameters from ajax are $_POST['start_date'] and $_POST['end_date']
// set the query condition and convert date format for using in query
$start_date = strtr( $_POST['start_date'], '/', '-' );
$start_date = strtotime($start_date);
$start_date = date('Y-m-d',$start_date);
$end_date = strtr( $_POST['end_date'], '/', '-' );
$end_date = strtotime($end_date);
$end_date = date('Y-m-d',$end_date);
// get current user id
$user_id = get_current_user_id();
// get user role
$user_meta = get_userdata( $user_id );
$user_roles = $user_meta->roles;
// query data from the custom table
$query_result = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table_name WHERE status = '$status' AND date_created BETWEEN '$start_date' AND '$end_date' ORDER BY %s %s", array( $orderby, $order ) )
, OBJECT);
// add post meta into the query result so we can use these values in ajax call
foreach( $query_result as $value ) {
// get post meta
$job_localtion_id = get_post_meta( $value->id_job_opp, 'job_location', true );
$position_type_id = get_post_meta( $value->id_job_opp, 'job_type_position', true );
$specialty_id = get_post_meta( $value->id_job_opp, 'job_specialty', true );
// get post title from custom post type
$job_location = get_the_title( $job_localtion_id );
// get term name by ID
$position_type = get_term_by( 'id' , $position_type_id, 'job_types_position' )->name;
$specialty = get_term_by( 'id' , $specialty_id, 'job_specialty' )->name;
// add 3 additional columns for row grouping table
$value->job_location = ( trim($job_location)==='' ? 'No Location' : $job_location );
$value->position_type = ( trim($position_type)==='' ? 'No Position Type' : $position_type );
$value->specialty = ( trim($specialty)==='' ? 'No Specialty' : $specialty );
}
// echo as json object to ajax calling
echo json_encode($query_result);
// # MUST add wp_die() here to avoid the 0 number that appends to the result json object and send back to ajax calling
wp_die();
}
} // class ends
// # initiate class on in the WP backend
if( is_admin() ) {
$mtc_reports = new Mtc_Reports;
}
} // class_exists() ends
Below is a filter form when you call reports_section function.

class-translation.php
This class is used for translation.
<?php
// If this file is called directly, abort.
if (!defined('ABSPATH')) die("No cheating!");
if(!class_exists('Mtc_Reports_Translation'))
{
class Mtc_Reports_Translation
{
public function __construct() {
// translation
add_action( 'init', array( $this, 'mct_reports_load_textdomain' ) );
}
public function mct_reports_load_textdomain() {
load_plugin_textdomain( 'mct-reports', false, MCT_REPORTS_LANG_PATH . '/languages' );
}
}
// initialize class
if( is_admin() ) {
$mtc_reports_translation = new Mtc_Reports_Translation;
}
}
mct-reports.js
This JavaScript file, it handles the click event from the filter form and then fetches the data following the filter form. Fetching data by Ajax. Once Ajax gets the response back, we will destroy the Datatable object and then we reinitiate the Datatable by mct_reports and mct_reports_grouping functions.
jQuery(document).ready(function ($) {
// ------- START Report button is triggered --------- //
$('#btn_report').on('click', function (e) {
var $period = $('#report_date');
var $txtStartDate = $('#txtStartDate');
var $txtEndDate = $('#txtEndDate');
var $reports_selector = $('#reports_selector');
var $mct_report = $('#mct_report');
var $mct_report_grouping = $('#mct_report_grouping');
// # getting db via PHP function
// we pass the javascript value to data attribute. this way, get_job_application function(PHP) can use these value. In PHP, you can access the value by $_POST.
$.ajax({
method: 'post',
url: mctReportsAjax.ajaxurl,
cache: false,
dataType: 'json',
data: {
action: 'get_job_applications',
start_date: $txtStartDate.val().trim(),
end_date: $txtEndDate.val().trim(),
},
beforeSend: function () {
// # hide the datatable tables
$mct_report.hide();
$mct_report_grouping.hide();
// # destroy the database tables object then we can initialize when the criteria changes without loading new page
$mct_report.dataTable().fnDestroy();
$mct_report_grouping.dataTable().fnDestroy();
},
success: function (response) {
// # check which report users want to view
switch ($reports_selector.val()) {
case 'total_by_location':
// console.log('total_by_location');
// show the report table
$mct_report_grouping.show();
// initial the report row grouping
mct_reports_grouping(6, response);
break;
case 'total_by_positionType':
// console.log('total_by_positionType');
// show the report table
$mct_report_grouping.show();
// initial the report row grouping
mct_reports_grouping(7, response);
break;
case 'total_by_specialty':
// console.log('total_by_specialty');
// show the report table
$mct_report_grouping.show();
// initial the report row grouping
mct_reports_grouping(8, response);
break;
default:
// show the report table
$mct_report.show();
// initial the report
mct_reports(response);
break;
}
},
error: function (xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
alert(err.Message);
}
});
});
// ------- END Report button is triggered --------- //
});
The sample code above shows you how to write the plugin in an OOP way. Basically, it is the same PHP class. One thing you have to do in order to call the functions within the class is passing $this in the array() just like the code below.
<?php
// call the function in a class
add_action( 'init', array( $this, 'mct_reports_load_textdomain' ) );
?>
<?php
// call the function outside a class
add_action( 'init', 'mct_reports_load_textdomain' );
?>
If you don’t know how to write the code in OOP. You may start with something like the video tutorial.
mct_reports function
/**
* The datatables in bootstrap style. Create the data set array for datatable source data and initialize the datatable plugin
*
* @link https://datatables.net/examples/styling/bootstrap.html
*
* @param array object data from WP query function
*/
function mct_reports(response) {
// # create data set for datatable plugin
var dataReport = []; // array type
$.each(response, function (idx, item) {
// add each data array into the dataReport variable
dataReport.push([(idx + 1).toString(), item.firstname + ' ' + item.middlename + ' ' + item.lastname, item.position_appiled, item.salary_expect, item.email, item.date_created]);
}); // loop ends
// # initial datatable plugin
$('#mct_report').DataTable({
data: dataReport,
});
}
mct_reports_grouping function
/**
* Row grouping
* @link https://datatables.net/examples/advanced_init/row_grouping.html
*/
function mct_reports_grouping(groupColumnParam, response) {
// # which column(<td>) you want to group. start index with 0 for the first column
var groupColumn = groupColumnParam;
// # create data set for datatable plugin
var dataReport = []; // array type
$.each(response, function (idx, item) {
// add each data array into the dataReport variable
dataReport.push(['',
item.firstname + ' ' + item.middlename + ' ' + item.lastname,
item.position_appiled,
item.salary_expect,
item.email,
item.date_created,
item.job_location,
item.position_type,
item.specialty
]);
}); // loop ends
var table = $('#mct_report_grouping').DataTable({
"data": dataReport,
"columnDefs": [{
"visible": false,
"targets": groupColumn
}],
"order": [
[groupColumn, 'asc']
],
"displayLength": 25,
"drawCallback": function (settings) {
var api = this.api();
var rows = api.rows({
page: 'current'
}).nodes();
var last = null;
api.column(groupColumn, {
page: 'current'
}).data().each(function (group, i) {
if (last !== group) {
$(rows).eq(i).before(
// colspan with total columns in the table except group column
'<tr class="group"><td colspan="8">' + group + '</td></tr>'
);
last = group;
}
});
}
});
Yep, that’s it for today. I hope you enjoy!