Dokan Market Guard
Private state-segmented cannabis marketplace with ACF integration
by Al Amin Ahamed · github.com/mralaminahamed/dokan-market-guard · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/mralaminahamed/dokan-market-guard/archive/refs/heads/trunk.zipDokan Market Guard — Developer Guide
A closed, state-segmented marketplace: only signed-in members see it, and each member only sees what may lawfully be sold to them where they are.
What it is
A cannabis marketplace is not a shop with a login on it. Two rules apply at once and neither is optional: the catalogue may not be public, and what may be sold depends on the buyer's state. A plugin that only did the first would leak a Colorado product to a Texas buyer; one that only did the second would leave the whole catalogue indexable.
So the plugin closes the marketplace and segments it, in the same pass. Anonymous
visitors are redirected away rather than shown a login wall over the content;
noindex goes on the pages that must not be indexed; products are filtered by
state at the query, not in the template; and purchase is blocked at
woocommerce_is_purchasable, which is the last gate before money moves.
Vendors carry a state too, set at registration and editable on the user profile, so a vendor's catalogue is constrained by where they are as well as where the buyer is.
Features
Access control
- Anonymous visitors redirected off restricted pages at
template_redirect noindexon the pages that must not be indexed- Purchase blocked at
woocommerce_is_purchasable, not merely hidden - Age verification, checked on
wpbefore anything renders
State segmentation
- Product queries filtered by the viewer's state through
woocommerce_product_query - Vendor categories restricted through
dokan_product_category_args - Vendor state captured at
dokan_new_seller_createdand on the Dokan registration form - The same field editable on the WordPress user profile, both own and others'
Custom fields
- ACF field groups registered on
acf/init - Fields injected into the Dokan vendor product form
- Server-side validation, exposed over AJAX for the form to call
- The whole feature is behind an option —
dokan_market_guard_custom_fields_enabled
Requirements
- WordPress 6.0+
- WooCommerce, Dokan Lite and Advanced Custom Fields — all three declared in
Requires Plugins, so WordPress blocks activation until they are present - PHP 7.4+
Installation
git clone https://github.com/mralaminahamed/dokan-market-guard.git
cd dokan-market-guard
composer install
yarn install && yarn build
Development
composer phpcs # WordPress coding standards
composer phpcbf # auto-fix what it can
composer phpstan # static analysis
composer test # PHPUnit — unit and integration
Architecture
dokan-market-guard.php entry point, constants, dependency guard
class-dokan-market-guard.php singleton; init_services() wires everything
includes/
├── Core/
│ ├── AccessControl.php who may see the marketplace at all
│ ├── StateSegmentation.php what they may see, given where they are
│ └── CustomFields.php ACF groups and the vendor product form
├── Dashboard/ProductSettings.php the vendor-facing product settings
├── REST/
│ ├── Registry.php registers the controllers
│ ├── Settings_Controller.php
│ ├── Vendors_Controller.php
│ └── Analytics_Controller.php
├── Abstracts/REST_Controller.php
├── Frontend/Hooks.php
├── Admin/Menu.php
├── Assets.php
└── Utils/
├── Helper.php
└── Validator.php
tests/
├── php/Unit/ Utils\Helper
└── php/Integration/ AccessControl
The React admin talks to the REST controllers under REST/, all of which extend
Abstracts\REST_Controller — so permission checks and response shaping are
written once rather than three times.
Static services, one init() each
Every service is a static class with an init() that registers its own hooks,
and init_services() calls them in order. There is no container and no instance
state, because none of these services has any: they are hook registrations
grouped by the question they answer. AccessControl decides whether,
StateSegmentation decides what, CustomFields decides which fields.
Where the gates are, and why there
| Gate | Hook | Why there |
|---|---|---|
| Redirect anonymous visitors | template_redirect |
Before any template renders, so nothing leaks in markup |
noindex |
wp_head |
The only place a meta tag can go |
| Filter products by state | woocommerce_product_query |
At the query — a template-level filter still runs the query and still counts the rows |
| Block purchase | woocommerce_is_purchasable |
The last gate before money moves; survives a direct add-to-cart URL |
| Age verification | wp |
Early enough to redirect, late enough to know the request |
Filtering at the query rather than the template is the load-bearing choice. Pagination, counts and REST all read the query, so a template-level filter would show "24 products" and render nine.
Data
No custom tables. Vendor state is user meta; the feature switches are options
(dokan_market_guard_custom_fields_enabled among them); field definitions belong
to ACF.
License
GPL-2.0-or-later. See LICENSE.