name: dokan-backend-dev description: Add or modify Dokan backend PHP code following project conventions. Use when creating new classes, methods, hooks, REST controllers, or modifying existing backend code. Invoke before writing PHP unit tests.
Dokan Backend Development
This skill provides guidance for developing Dokan Lite backend PHP code according to project standards.
When to Use This Skill
Invoke this skill before:
- Writing new PHP unit tests
- Creating new PHP classes or services
- Modifying existing backend PHP code
- Adding hooks, filters, or REST endpoints
Namespace & File Structure
- Root namespace:
WeDevs\Dokan\ - PSR-4 autoloading:
WeDevs\Dokan\maps toincludes/ - File path follows namespace:
WeDevs\Dokan\Order\Manager→includes/Order/Manager.php - Third-party (Mozart):
WeDevs\Dokan\ThirdParty\Packages\→lib/packages/
Class Conventions
Method & Property Naming
- Methods:
snake_case(WordPress convention) — e.g.,register_routes(),get_stores() - Properties: typed (PHP 7.4+) — e.g.,
protected bool $should_adjust_refund = true; - Constants:
UPPER_SNAKE_CASE
Manager Pattern
Most subsystems use a Manager class as the primary facade:
namespace WeDevs\Dokan\Order;
class Manager {
public function all( $args = [] ) { ... }
public function get( $id ) { ... }
public function create( $args ) { ... }
}
Access via: dokan()->order->all()
Hookable Interface
Classes that register WordPress hooks should implement Hookable:
namespace WeDevs\Dokan\Product;
use WeDevs\Dokan\Contracts\Hookable;
class Hooks implements Hookable {
public function register_hooks(): void {
add_action( 'save_post_product', [ $this, 'handle_product_save' ], 10, 2 );
add_filter( 'dokan_product_listing_args', [ $this, 'filter_listing_args' ] );
}
}
Classes implementing Hookable are auto-registered in CommonServiceProvider — their hooks load automatically.
Dependency Injection
Uses League Container v4 (namespaced under WeDevs\Dokan\ThirdParty\Packages\League\Container).
Registering Services
Add to the appropriate ServiceProvider in includes/DependencyManagement/Providers/:
// In ServiceProvider.php (main) for core services:
protected $services = [
'my_service' => \WeDevs\Dokan\MyDomain\Manager::class,
];
// In CommonServiceProvider.php for Hookable classes:
protected $services = [
\WeDevs\Dokan\MyDomain\Hooks::class,
];
Accessing Services
// Via magic getter (most common)
dokan()->order->get( $order_id );
dokan()->vendor->get( $vendor_id );
// Via container directly
dokan()->get_container()->get( 'order' );
Base Service Provider Helper
Use share_with_implements_tags() to auto-tag services by their interfaces:
$this->share_with_implements_tags( MyService::class );
REST API Controllers
Controller Hierarchy
WP_REST_Controller (WordPress core)
└── DokanBaseController (dokan/v1)
├── DokanBaseAdminController (dokan/v1/admin) — admin-only endpoints
├── DokanBaseVendorController (dokan/v1) — vendor endpoints (uses VendorAuthorizable trait)
└── DokanBaseCustomerController (dokan/v1) — customer endpoints
Choose the appropriate base class:
DokanBaseAdminController— For admin-only endpoints (dokan/v1/admin/*). Has built-incheck_permission()checkingmanage_woocommercecapability.DokanBaseVendorController— For vendor endpoints. IncludesVendorAuthorizabletrait for store access checks.DokanBaseController— For general endpoints that don't fit the above.
Note: Some older controllers extend
WP_REST_Controllerdirectly (e.g.,StoreController,WithdrawController). New controllers should extend one of the Dokan base classes.
Full Controller Example
namespace WeDevs\Dokan\REST;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WeDevs\Dokan\Traits\RESTResponseError;
class MyResourceController extends DokanBaseAdminController {
use RESTResponseError;
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'my-resource';
/**
* Register routes.
*
* @return void
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_items' ],
'args' => array_merge(
$this->get_collection_params(),
[
'status' => [
'description' => __( 'Filter by status.', 'dokan-lite' ),
'type' => 'string',
'enum' => [ 'active', 'inactive' ],
'default' => 'active',
],
]
),
'permission_callback' => [ $this, 'check_permission' ],
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_item' ],
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
'permission_callback' => [ $this, 'check_permission' ],
],
'schema' => [ $this, 'get_item_schema' ],
]
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
'args' => [
'id' => [
'description' => __( 'Unique identifier for the object.', 'dokan-lite' ),
'type' => 'integer',
],
],
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_item' ],
'permission_callback' => [ $this, 'check_permission' ],
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'update_item' ],
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
'permission_callback' => [ $this, 'check_permission' ],
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [ $this, 'delete_item' ],
'permission_callback' => [ $this, 'check_permission' ],
],
]
);
// Batch endpoint
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/batch', [
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'batch_items' ],
'permission_callback' => [ $this, 'check_permission' ],
'args' => $this->get_public_batch_schema()['properties'],
],
'schema' => [ $this, 'get_public_batch_schema' ],
]
);
}
}
Prepare Item for Response
Every controller must implement prepare_item_for_response(). This method transforms the internal data model into the REST API response shape, adds HATEOAS links, and applies an extensibility filter:
/**
* Prepare a single item for response.
*
* @param MyModel $item Data object.
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response
*/
public function prepare_item_for_response( $item, $request ) {
$data = [
'id' => absint( $item->get_id() ),
'title' => $item->get_title(),
'status' => $item->get_status(),
'amount' => floatval( $item->get_amount() ),
'created' => mysql_to_rfc3339( $item->get_date() ),
];
$data = apply_filters( 'dokan_rest_prepare_my_resource_data', $data, $item, $request );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
return apply_filters( 'dokan_rest_prepare_my_resource_object', $response, $item, $request );
}
Prepare Links (HATEOAS)
Provide self and collection links for discoverability:
/**
* Prepare links for the request.
*
* @param MyModel $item Object data.
* @param WP_REST_Request $request Request object.
*
* @return array Links for the given item.
*/
protected function prepare_links( $item, $request ) {
return [
'self' => [
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $item->get_id() ) ),
],
'collection' => [
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
],
];
}
Collection Response with Pagination
Use format_collection_response() (inherited from DokanBaseController) to add pagination headers:
public function get_items( $request ) {
$args = [
'number' => (int) $request['per_page'],
'offset' => (int) ( $request['page'] - 1 ) * $request['per_page'],
];
$items = $this->get_my_items( $args );
$total_items = $this->get_my_items_count( $args );
$data = [];
foreach ( $items as $item ) {
$item_data = $this->prepare_item_for_response( $item, $request );
$data[] = $this->prepare_response_for_collection( $item_data );
}
$response = rest_ensure_response( $data );
$response = $this->format_collection_response( $response, $request, $total_items );
return $response;
}
format_collection_response() sets these headers automatically:
X-WP-Total— Total item countX-WP-TotalPages— Total page countLink: <url>; rel="prev"/Link: <url>; rel="next"— Pagination links
Item Schema
Define get_item_schema() to enable automatic argument validation via get_endpoint_args_for_item_schema():
public function get_item_schema() {
$schema = [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'my-resource',
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Unique identifier for the object.', 'dokan-lite' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'title' => [
'description' => __( 'Resource title.', 'dokan-lite' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'status' => [
'description' => __( 'Resource status.', 'dokan-lite' ),
'type' => 'string',
'enum' => [ 'active', 'inactive' ],
'context' => [ 'view', 'edit' ],
'default' => 'active',
],
'amount' => [
'description' => __( 'Amount value.', 'dokan-lite' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'required' => true,
],
],
];
return $this->add_additional_fields_schema( $schema );
}
Error Handling
Use WP_Error with descriptive error codes and HTTP status:
return new WP_Error(
'dokan_rest_resource_not_found',
__( 'Resource not found.', 'dokan-lite' ),
[ 'status' => 404 ]
);
For exception-based error handling, use the RESTResponseError trait:
use WeDevs\Dokan\Traits\RESTResponseError;
class MyController extends DokanBaseController {
use RESTResponseError;
public function create_item( $request ) {
try {
// ... business logic that may throw DokanException
} catch ( Exception $e ) {
return $this->send_response_error( $e );
}
}
}
Permission Callbacks
Admin controllers inherit check_permission() from DokanBaseAdminController.
Vendor controllers use VendorAuthorizable trait methods:
// Check if current user can access a vendor's store
$this->can_access_vendor_store( $store_id );
// Resolve vendor ID (handles vendor staff → vendor mapping)
$store_id = $this->get_vendor_id_for_user( $requested_id );
Custom permission checks — use WordPress capabilities:
public function get_items_permissions_check( $request ) {
return current_user_can( 'dokan_manage_withdraw' );
}
Route Argument Validation
Define args inline with type, enum, required, default, sanitize_callback, validate_callback:
'args' => [
'id' => [
'description' => __( 'Unique identifier for the object.', 'dokan-lite' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => [ $this, 'validate_resource_id' ],
],
],
Extensibility via Filters
Controllers should apply filters at key extension points:
// Filter collection query args
$args = apply_filters( 'dokan_rest_get_my_resource_args', $args, $request );
// Filter collection endpoint params
'args' => apply_filters( 'dokan_rest_api_my_resource_collection_params', $this->get_collection_params() ),
// Filter prepared response
return apply_filters( 'dokan_rest_prepare_my_resource_object', $response, $item, $request );
// Action after create/update
do_action( 'dokan_rest_insert_my_resource', $item, $request, $creating );
Registering a Controller
Add via the dokan_rest_api_class_map filter in REST\Manager:
add_filter( 'dokan_rest_api_class_map', function ( $class_map ) {
$class_map[ DOKAN_DIR . '/includes/REST/MyResourceController.php' ] = '\WeDevs\Dokan\REST\MyResourceController';
return $class_map;
} );
API Versioning
Multiple versions exist side-by-side: OrderController.php (v1), OrderControllerV2.php, OrderControllerV3.php. Namespace changes accordingly (dokan/v1, dokan/v2, dokan/v3).
Key REST Reference Files
includes/REST/DokanBaseController.php— Base controller withformat_collection_response()paginationincludes/REST/DokanBaseAdminController.php— Admin base (dokan/v1/admin,check_permission())includes/REST/DokanBaseVendorController.php— Vendor base (VendorAuthorizabletrait)includes/REST/DokanBaseCustomerController.php— Customer baseincludes/REST/Manager.php— Controller registration &dokan_rest_api_class_mapfilterincludes/Traits/RESTResponseError.php— Exception-to-WP_Error traitincludes/Traits/VendorAuthorizable.php— Vendor store access authorization
Extensibility Patterns
Filters
$value = apply_filters( 'dokan_get_vendor_orders', $orders, $args );
$params = apply_filters( 'dokan_rest_api_store_collection_params', $this->get_store_collection_params() );
Actions
do_action( 'dokan_rest_insert_product_object', $product, $request, true );
do_action( 'dokan_new_seller_created', $vendor_id, $data );
Plugin Options
$value = dokan_get_option( 'key', 'dokan_option_group', 'default' );
Localization / Translation (PHP)
Text domain: dokan-lite — used for ALL translatable strings in Lite.
Translation Functions
| Function | Usage |
|---|---|
__( 'Text', 'dokan-lite' ) |
Return translated string |
_e( 'Text', 'dokan-lite' ) |
Echo translated string |
esc_html__( 'Text', 'dokan-lite' ) |
Return translated + HTML-escaped |
esc_html_e( 'Text', 'dokan-lite' ) |
Echo translated + HTML-escaped |
esc_attr__( 'Text', 'dokan-lite' ) |
Return translated + attribute-escaped |
esc_attr_e( 'Text', 'dokan-lite' ) |
Echo translated + attribute-escaped |
_n( 'single', 'plural', $count, 'dokan-lite' ) |
Pluralization |
_x( 'Text', 'context', 'dokan-lite' ) |
Context-aware translation |
_nx( 'single', 'plural', $count, 'context', 'dokan-lite' ) |
Context-aware pluralization |
Translator Comments
Always add /* translators: */ comments before sprintf() with placeholders:
/* translators: 1: Required PHP version 2: Running PHP version */
__( 'Minimum PHP version required is %1$s. You are running %2$s.', 'dokan-lite' )
String Formatting
Use sprintf() for dynamic content — never concatenate translated strings:
// CORRECT
sprintf( __( 'Account Name: %s', 'dokan-lite' ), $payment['ac_name'] )
// WRONG — don't concatenate
__( 'Account Name: ', 'dokan-lite' ) . $payment['ac_name']
Pluralization
sprintf(
_n( '%d vendor approved.', '%d vendors approved.', $count, 'dokan-lite' ),
$count
)
Date/Time Formatting
Always use locale-aware functions:
// WordPress locale-aware date
date_i18n( wc_date_format(), strtotime( $date_string ) );
// Translated day names
dokan_get_translated_days( 'monday' );
Escaping in Templates
In templates, always escape translated output:
// In template files
<?php esc_html_e( 'Payment Methods', 'dokan-lite' ); ?>
<input placeholder="<?php esc_attr_e( 'Search...', 'dokan-lite' ); ?>">
POT File Generation
npm run makepot # Generates languages/dokan-lite.pot
Textdomain Loading
Handled in dokan-class.php via load_plugin_textdomain() on woocommerce_loaded hook. No manual setup needed.
Coding Standards
- PHPCS ruleset:
WordPress-Extra+WordPress(viaphpcs.xml.dist) - PHP compatibility: 7.4+
- Strict comparisons: enforced as errors
in_array()strict mode: required- Text domain:
dokan-litefor all__(),_e(),esc_html__(), etc. - Custom sanitization:
wc_clean,wc_esc_json,dokan_sanitize_phone_numberare registered - Yoda conditions: not enforced
Key Reference Files
dokan.php— Main plugin file, container bootstrapdokan-class.php—WeDevs_Dokansingletonincludes/DependencyManagement/Providers/ServiceProvider.php— Main service registrationincludes/DependencyManagement/Providers/CommonServiceProvider.php— Hookable class registrationincludes/REST/DokanBaseController.php— Base REST controllerincludes/REST/Manager.php— REST API management & controller mapincludes/Contracts/Hookable.php— Hookable interfaceincludes/functions.php— Core utility functions (3,290 lines)