Skip to content

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:

php xe make:module Order

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.