بنقرة واحدة
wpuf-backend-dev
// Add or modify WPUF backend PHP code following project conventions. Use when creating new classes, methods, hooks, REST controllers, or modifying existing backend code.
// Add or modify WPUF backend PHP code following project conventions. Use when creating new classes, methods, hooks, REST controllers, or modifying existing backend code.
Release a new version of WP User Frontend (free) to weDevsOfficial GitHub + wp.org via Appsero. One-command pipeline via `wpuf-release-free` alias. Trigger when user says "release wpuf", "ship wpuf X.Y.Z", "publish wpuf", "/wpuf-release", "wpuf-release-free".
Review WPUF code changes and pull requests for coding standards, security, and architectural compliance. Use when reviewing PRs, performing code audits, or checking code quality.
Add or modify WPUF frontend code (jQuery, vanilla JS, Vue legacy, Tailwind). Use when creating components, modifying build configuration, or working with frontend assets.
| name | wpuf-backend-dev |
| description | Add or modify WPUF backend PHP code following project conventions. Use when creating new classes, methods, hooks, REST controllers, or modifying existing backend code. |
This skill provides guidance for developing WP User Frontend backend PHP code according to project standards.
Invoke this skill before:
WeDevs\Wpuf\WeDevs\Wpuf\ maps to includes/WeDevs\Wpuf\Admin\Menu -> includes/Admin/Menu.phpLib/ directory (not namespaced via PSR-4)WeDevs\WpUtils\ContainerTrait, WeDevs\WpUtils\SingletonTrait from wedevs/wp-utilswpuf.php loads Composer autoloader (vendor/autoload.php)WPUF_VERSION, WPUF_FILE, WPUF_ROOT, WPUF_INCLUDES, WPUF_ASSET_URIWP_User_Frontend singleton via SingletonTrait — accessed globally via wpuf()includes() (loads critical files) and init_hooks()plugins_loaded: Appsero init, Free_Loader, Pro version check, upgrades, class instantiationinit: Textdomain loading, gateway manager, AJAX initializationWPUF uses a simple array-based container (not a full DI container like League):
// Registration (in bootstrap classes)
$this->container['service_name'] = new ServiceClass();
// Access via magic __get()
wpuf()->service_name->method();
Classes that use the container import WeDevs\WpUtils\ContainerTrait.
| Class | Role | Access |
|---|---|---|
WP_User_Frontend | Main singleton, top-level container | wpuf() |
Admin | Admin subsystem bootstrap (16+ services) | wpuf()->admin |
Frontend | Frontend subsystem bootstrap (7+ services) | wpuf()->frontend |
API | REST API bootstrap (2 controllers under Api/) | wpuf()->api |
Register in the appropriate bootstrap class constructor:
// In Admin.php for admin services:
$this->container['my_feature'] = new Admin\MyFeature();
// In Frontend.php for frontend services:
$this->container['my_feature'] = new Frontend\MyFeature();
// In WP_User_Frontend::instantiate() for global services:
$this->container['my_feature'] = new MyFeature();
snake_case (WordPress convention) — e.g., register_routes(), get_items()UPPER_SNAKE_CASEAdmin and Frontend classes instantiate their child services in the constructor:
namespace WeDevs\Wpuf;
use WeDevs\WpUtils\ContainerTrait;
class Admin {
use ContainerTrait;
public function __construct() {
$this->container['menu'] = new Admin\Menu();
$this->container['settings'] = new Admin\Admin_Settings();
// ... more services
add_action( 'admin_init', [ $this, 'some_hook' ] );
}
}
Free-only features load via Free_Loader when Pro is not active:
if ( ! class_exists( 'WP_User_Frontend_Pro' ) ) {
$this->container['free_feature'] = new Free\MyFeature();
}
Controllers extend WP_REST_Controller directly (no custom base class):
namespace WeDevs\Wpuf\Api;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
class MyController extends WP_REST_Controller {
protected $namespace = 'wpuf/v1';
protected $base = 'my-resource';
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->base, [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_items' ],
'permission_callback' => [ $this, 'permission_check' ],
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_item' ],
'permission_callback' => [ $this, 'permission_check' ],
],
]
);
}
public function permission_check() {
return current_user_can( wpuf_admin_role() );
}
}
wpuf/v1Api\Subscription, Api\FormList, AI\RestController (registered via AI_Manager)Add to API::__construct():
public function __construct() {
$this->subscription = new Subscription();
$this->form_list = new FormList();
$this->my_resource = new MyResource(); // Add new controller
add_action( 'rest_api_init', [ $this, 'init_api' ] );
}
The init_api() method iterates the container and calls register_routes() on each.
WPUF controllers return WP_REST_Response with a success flag:
// Success
return new WP_REST_Response( [
'success' => true,
'data' => $items,
] );
// Error
return new WP_REST_Response( [
'success' => false,
'message' => __( 'Something went wrong', 'wp-user-frontend' ),
] );
// Or use WP_Error for HTTP status codes
return new \WP_Error(
'wpuf_invalid_input',
__( 'Invalid input.', 'wp-user-frontend' ),
[ 'status' => 400 ]
);
Standard permission check uses wpuf_admin_role():
public function permission_check() {
return current_user_can( wpuf_admin_role() );
}
$value = apply_filters( 'wpuf_subscription_data', $data, $request );
$params = apply_filters( 'wpuf_ai_form_builder_localize_data', $localize_data );
do_action( 'wpuf_before_update_subscription_pack', $id, $request, $post_arr );
do_action( 'wpuf_after_update_subscription_pack_meta', $id, $request );
do_action( 'wpuf_loaded' );
$value = wpuf_get_option( 'key', 'wpuf_option_group', 'default' );
Text domain: wp-user-frontend — used for ALL translatable strings in free version. wpuf-pro for Pro strings.
| Function | Usage |
|---|---|
__( 'Text', 'wp-user-frontend' ) | Return translated string |
_e( 'Text', 'wp-user-frontend' ) | Echo translated string |
esc_html__( 'Text', 'wp-user-frontend' ) | Return translated + HTML-escaped |
esc_html_e( 'Text', 'wp-user-frontend' ) | Echo translated + HTML-escaped |
esc_attr__( 'Text', 'wp-user-frontend' ) | Return translated + attribute-escaped |
_n( 'single', 'plural', $count, 'wp-user-frontend' ) | Pluralization |
_x( 'Text', 'context', 'wp-user-frontend' ) | Context-aware translation |
Always add /* translators: */ comments before sprintf() with placeholders:
/* translators: %1$s: opening tag, %2$s: shortcode, %3$s: closing tag */
__( '%1$sThis post contains %2$s shortcode%3$s', 'wp-user-frontend' )
Use sprintf() for dynamic content — never concatenate translated strings:
// CORRECT
sprintf( __( 'Welcome, %s!', 'wp-user-frontend' ), $name )
// WRONG
__( 'Welcome, ', 'wp-user-frontend' ) . $name
WordPress-Core + WordPress (via phpcs.xml.dist)in_array() strict mode: required (enforced as error)wp-user-frontend, wpuf-proMANDATORY: Apply every rule below before submitting code. These are the top causes of review rejections.
NEVER use == or !=. Always use === and !==.
// ❌ WRONG — will be rejected
if ( $value == 'yes' ) {}
if ( $status != false ) {}
if ( 0 == $count ) {}
// ✅ CORRECT
if ( $value === 'yes' ) {}
if ( $status !== false ) {}
if ( 0 === $count ) {}
ALWAYS pass true as the third argument. This is enforced as an error by PHPCS.
// ❌ WRONG — PHPCS error, review rejection
if ( in_array( $value, $allowed ) ) {}
in_array( $type, $list )
// ✅ CORRECT
if ( in_array( $value, $allowed, true ) ) {}
in_array( $type, $list, true )
Also applies to: array_search() — always pass strict true.
Every $_POST, $_GET, $_REQUEST, $_SERVER access MUST be sanitized with wp_unslash() first.
// ❌ WRONG — missing wp_unslash
$title = sanitize_text_field( $_POST['title'] );
$id = intval( $_GET['id'] );
// ✅ CORRECT
$title = sanitize_text_field( wp_unslash( $_POST['title'] ?? '' ) );
$id = intval( wp_unslash( $_GET['id'] ?? 0 ) );
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
$url = esc_url_raw( wp_unslash( $_POST['redirect'] ?? '' ) );
Type-specific sanitization:
| Data type | Function |
|---|---|
| Text | sanitize_text_field( wp_unslash( ... ) ) |
sanitize_email( wp_unslash( ... ) ) | |
| URL | esc_url_raw( wp_unslash( ... ) ) |
| Integer | intval( wp_unslash( ... ) ) or absint() |
| HTML content | wp_kses_post( wp_unslash( ... ) ) |
| Textarea | sanitize_textarea_field( wp_unslash( ... ) ) |
| Key/slug | sanitize_key( wp_unslash( ... ) ) |
ALL output MUST be escaped. No exceptions.
// ❌ WRONG — raw output
echo $title;
echo $url;
<input value="<?php echo $value; ?>">
// ✅ CORRECT
echo esc_html( $title );
echo esc_url( $url );
<input value="<?php echo esc_attr( $value ); ?>">
Escaping function reference:
| Context | Function |
|---|---|
| HTML text | esc_html() |
| HTML attribute | esc_attr() |
| URL (href, src) | esc_url() |
| JavaScript string | esc_js() |
| Rich HTML | wp_kses_post() |
| Translated text output | esc_html__(), esc_html_e(), esc_attr__() |
NEVER concatenate variables into SQL. Always use $wpdb->prepare().
// ❌ WRONG — SQL injection risk
$wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wpuf_transaction WHERE user_id = $user_id" );
$wpdb->get_results( "SELECT * FROM $table ORDER BY $orderby $order LIMIT $offset, $limit" );
// ✅ CORRECT
$wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpuf_transaction WHERE user_id = %d",
$user_id
)
);
Even for ORDER BY / LIMIT — use prepare or allowlist:
$allowed_orderby = [ 'created_at', 'amount', 'status' ];
$orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at';
$order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
$wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpuf_transaction ORDER BY {$orderby} {$order} LIMIT %d, %d",
$args['offset'],
$args['number']
)
);
Every form submission and AJAX handler MUST verify a nonce.
// Form submissions
if (
! isset( $_POST['wpuf_nonce'] )
|| ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['wpuf_nonce'] ) ), 'wpuf_action' )
) {
wp_die( esc_html__( 'Security check failed', 'wp-user-frontend' ) );
}
// AJAX handlers
check_ajax_referer( 'wpuf_nonce', 'nonce' );
Every sensitive operation MUST verify user capabilities.
// Admin operations
if ( ! current_user_can( wpuf_admin_role() ) ) {
wp_die( esc_html__( 'Permission denied', 'wp-user-frontend' ) );
}
// REST endpoints — NEVER use __return_true for sensitive data
'permission_callback' => function() {
return current_user_can( wpuf_admin_role() );
}
NEVER use camelCase for PHP methods or functions.
// ❌ WRONG — review rejection
public function getStates() {}
public function verifyResponse() {}
public function catbuildTree() {}
// ✅ CORRECT
public function get_states() {}
public function verify_response() {}
public function build_category_tree() {}
Every public/protected method MUST have a PHPDoc with @since.
// ❌ WRONG — missing docblock
public function process_form( $form_id ) {}
// ✅ CORRECT
/**
* Process form submission.
*
* @since WPUF_SINCE
*
* @param int $form_id Form ID.
*
* @return bool
*/
public function process_form( $form_id ) {}
Use WPUF_SINCE placeholder — never hardcode a version number.
All user-facing strings MUST use translation functions. All sprintf() with placeholders MUST have translator comments.
// ❌ WRONG — missing translator comment
sprintf( __( 'Hello %s, you have %d posts', 'wp-user-frontend' ), $name, $count );
// ✅ CORRECT
/* translators: %1$s: user name, %2$d: post count */
sprintf( __( 'Hello %1$s, you have %2$d posts', 'wp-user-frontend' ), $name, $count );
Text domain rules:
'wp-user-frontend' — NEVER use 'wpuf''wpuf-pro'sprintf() with a single __() call// ❌ WRONG — missing spaces
if($condition){
if (!empty($items))
in_array($val,$list,true)
// ✅ CORRECT — spaces inside parentheses, after !
if ( $condition ) {
if ( ! empty( $items ) )
in_array( $val, $list, true )
All hooks MUST be prefixed with wpuf_ and use snake_case.
// ❌ WRONG
do_action( 'form_before_render', $form_id );
apply_filters( 'formFields', $fields );
// ✅ CORRECT
do_action( 'wpuf_form_before_render', $form_id );
apply_filters( 'wpuf_form_fields', $fields, $form_id );
Run this mental checklist on every line you write:
== replaced with ===? Every != with !==?in_array() and array_search() has true as third arg?$_POST/$_GET/$_REQUEST/$_SERVER wrapped in wp_unslash() + sanitize?echo/output uses esc_html(), esc_attr(), esc_url(), or wp_kses_post()?$wpdb->prepare() for dynamic values?current_user_can()?snake_case? No camelCase?@since WPUF_SINCE docblock?sprintf() with __() have /* translators: */ comment?'wp-user-frontend' (not 'wpuf')?wpuf_?( $var ) not ($var)?! $var not !$var?wpuf.php — Main plugin file, singleton bootstrapwpuf-functions.php — Global utility functionsincludes/Admin.php — Admin subsystem (16+ services)includes/Frontend.php — Frontend subsystem (7+ services)includes/API.php — REST API bootstrapincludes/Api/Subscription.php — Subscription REST controllerincludes/Api/FormList.php — Form list REST controllerincludes/AI/RestController.php — AI form builder REST controller (registered via AI_Manager)includes/Assets.php — Script/style registrationincludes/Free/Free_Loader.php — Free-only features (conditional on Pro absence)includes/Integrations.php — Third-party integrations (Dokan, WC Vendors, ACF, n8n)includes/AI_Manager.php — AI form builder features