| 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 to includes/
- 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/:
protected $services = [
'my_service' => \WeDevs\Dokan\MyDomain\Manager::class,
];
protected $services = [
\WeDevs\Dokan\MyDomain\Hooks::class,
];
Accessing Services
dokan()->order->get( $order_id );
dokan()->vendor->get( $vendor_id );
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-in check_permission() checking manage_woocommerce capability.
DokanBaseVendorController — For vendor endpoints. Includes VendorAuthorizable trait for store access checks.
DokanBaseController — For general endpoints that don't fit the above.
Note: Some older controllers extend WP_REST_Controller directly (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;
protected $rest_base = 'my-resource';
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' ],
],
]
);
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:
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:
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 count
X-WP-TotalPages — Total page count
Link: <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 {
} catch ( Exception $e ) {
return $this->send_response_error( $e );
}
}
}
Permission Callbacks
Admin controllers inherit check_permission() from DokanBaseAdminController.
Vendor controllers use VendorAuthorizable trait methods:
$this->can_access_vendor_store( $store_id );
$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:
$args = apply_filters( 'dokan_rest_get_my_resource_args', $args, $request );
'args' => apply_filters( 'dokan_rest_api_my_resource_collection_params', $this->get_collection_params() ),
return apply_filters( 'dokan_rest_prepare_my_resource_object', $response, $item, $request );
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 with format_collection_response() pagination
includes/REST/DokanBaseAdminController.php — Admin base (dokan/v1/admin, check_permission())
includes/REST/DokanBaseVendorController.php — Vendor base (VendorAuthorizable trait)
includes/REST/DokanBaseCustomerController.php — Customer base
includes/REST/Manager.php — Controller registration & dokan_rest_api_class_map filter
includes/Traits/RESTResponseError.php — Exception-to-WP_Error trait
includes/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:
__( '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:
sprintf( __( 'Account Name: %s', 'dokan-lite' ), $payment['ac_name'] )
__( '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:
date_i18n( wc_date_format(), strtotime( $date_string ) );
dokan_get_translated_days( 'monday' );
Escaping in Templates
In templates, always escape translated output:
<?php esc_html_e( 'Payment Methods', 'dokan-lite' ); ?>
<input placeholder="<?php esc_attr_e( 'Search...', 'dokan-lite' ); ?>">
POT File Generation
npm run makepot
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 (via phpcs.xml.dist)
- PHP compatibility: 7.4+
- Strict comparisons: enforced as errors
in_array() strict mode: required
- Text domain:
dokan-lite for all __(), _e(), esc_html__(), etc.
- Custom sanitization:
wc_clean, wc_esc_json, dokan_sanitize_phone_number are registered
- Yoda conditions: not enforced
Key Reference Files
dokan.php — Main plugin file, container bootstrap
dokan-class.php — WeDevs_Dokan singleton
includes/DependencyManagement/Providers/ServiceProvider.php — Main service registration
includes/DependencyManagement/Providers/CommonServiceProvider.php — Hookable class registration
includes/REST/DokanBaseController.php — Base REST controller
includes/REST/Manager.php — REST API management & controller map
includes/Contracts/Hookable.php — Hookable interface
includes/functions.php — Core utility functions (3,290 lines)