CRUD Modules (BaseModule)
The Xe_Plugin\Modules\BaseModule abstract class provides an architecture for creating frontend CRUD (Create, Read, Update, Delete) controllers.
Overview
A module extending BaseModule automatically handles:
- Pagination and archive query handling.
- Multi-field metadata search queries using SQL posts_join, posts_where, and posts_groupby.
- Nonce and CSRF verification.
- Form validation and flash error messages via WordPress transients (_xe_plugin_alert, _xe_plugin_errors).
- Lifecycle state transitions: Save, Trash, Restore, and Permanent Delete.
- Post meta synchronization and safe redirects.
Scaffolding a Module
Use the CLI generator to scaffold a new module:
This creates src/Modules/Order.php.
Anatomy of a Module
namespace Xe_Plugin\Modules;
use Xe_Plugin\Modules\BaseModule;
use Xe_Plugin\Utils;
class Order extends BaseModule {
protected $nonce = '_xe_plugin_order_nonce';
protected $nonce_action = '_xe_plugin_order_action';
public $post_type = 'custom_order';
public $endpoint = 'orders';
public $per_page = 20;
public function __construct() {
parent::__construct();
}
public function save() {
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
return;
}
if ( ! $this->is_valid_request() ) {
set_transient( '_xe_plugin_alert', 'invalid_request', 30 );
return;
}
$id = absint( $_POST['id'] ?? 0 );
$is_new = ! $id;
$errors = [];
$customer_name = sanitize_text_field( $_POST['customer_name'] ?? '' );
if ( empty( $customer_name ) ) {
$errors['customer_name'] = esc_html__( 'Customer name is required', 'xe-plugin' );
}
$form_data = [
'id' => $id,
'customer_name' => $customer_name,
];
if ( ! empty( $errors ) ) {
set_transient( '_xe_plugin_form_data', $form_data, 30 );
set_transient( '_xe_plugin_errors', $errors, 30 );
return;
}
$post_data = [
'post_title' => $customer_name,
'post_type' => $this->post_type,
'post_status' => 'publish',
];
if ( ! $is_new ) {
$post_data['ID'] = $id;
}
$post_id = $id ? wp_update_post( $post_data, true ) : wp_insert_post( $post_data, true );
if ( ! is_wp_error( $post_id ) ) {
foreach ( $form_data as $key => $value ) {
update_post_meta( $post_id, '_' . $key, $value );
}
set_transient( '_xe_plugin_alert', $id ? 'updated' : 'saved', 30 );
wp_safe_redirect( _xe_plugin()->endpoints()->get_current_url( "edit/{$post_id}" ) );
exit;
}
}
}
BaseModule Methods
1. archive( array $args = [] ): \WP_Query
Fetches paginated posts based on current subpath action (trash or publish) and executes search queries if $_GET['search'] is present.
$module = new \Xe_Plugin\Modules\Order();
$query = $module->archive();
while ( $query->have_posts() ) {
$query->the_post();
// Render item...
}
wp_reset_postdata();
2. get( int $id ): ?\WP_Post
Safely retrieves a post ensuring it belongs to the module's $post_type.
3. actions()
Executes lifecycle actions (trash, restore, or delete) based on the URL action and ID:
- /orders/trash/42 → Trashes post #42.
- /orders/restore/42 → Restores post #42 to published.
- /orders/delete/42 → Permanently removes post #42 and its associated attachments.
4. Advanced Search Query Filters
BaseModule hooks into posts_join, posts_where, and posts_groupby during archive queries to enable deep searches across post titles, post metadata, user logins/emails, and custom ticket IDs.