Development Guide
Repository Structure
Simple-WP-Helpdesk/
├── CHANGELOG.md
├── CLAUDE.md # AI assistant guidance
├── README.md
├── LICENSE
├── Makefile # Local PHP gate (lint/phpcs/phpstan/phpunit/semgrep); E2E via make e2e-docker
├── composer.json
├── docker-compose.test.yml # Docker test stack (WP + MySQL + MailHog)
├── docker/
│ ├── setup-test-wp.sh # Configures the Docker WP instance for E2E tests
│ └── mailhog-smtp.php # MU-plugin: routes wp_mail() through MailHog
├── docs/ # This documentation (GitHub Pages source)
├── testing/
│ ├── scripts/
│ │ ├── test_helpdesk_pw.py # Playwright E2E test suite (58 sections)
│ │ └── conftest.py # pytest fixtures and helpers
│ ├── pytest.ini
│ ├── requirements.txt
│ └── .env.example
└── simple-wp-helpdesk/
├── simple-wp-helpdesk.php # Bootstrap: constants, requires, lifecycle hooks
├── includes/
│ ├── helpers.php # Defaults, statuses, anti-spam, rate limiting
│ ├── class-installer.php # Activation, deactivation, uninstall, upgrade, CPT
│ ├── class-email.php # Template parsing, email sending, HTML wrapping
│ ├── class-ticket.php # File proxy, uploads, deletion, comment filters
│ └── class-cron.php # Auto-close, SLA check, retention (tickets + files)
├── admin/
│ ├── class-settings.php # Settings page render + save handler (8 tabs)
│ ├── class-ticket-editor.php # Meta boxes, save_post, conversation UI
│ ├── class-ticket-list.php # Columns, sorting, filters, bulk actions
│ ├── class-reporting.php # Reporting AJAX endpoints (status, resolution, trend, KPI)
│ └── class-reporting-ui.php # Reports submenu page render + Chart.js enqueue
├── frontend/
│ ├── class-shortcode.php # [submit_ticket] + [helpdesk_portal] shortcodes
│ └── class-portal.php # Client portal view
├── vendor/plugin-update-checker/ # GitHub auto-updater library
├── assets/
│ ├── swh-shared.css # Design tokens + shared components
│ ├── swh-admin.css # Admin-only styles
│ ├── swh-frontend.css # Frontend styles
│ ├── swh-admin.js # Admin JS (toast, canned responses, etc.)
│ └── swh-frontend.js # Frontend JS (form, portal interactions)
└── languages/
└── simple-wp-helpdesk.pot
The bootstrap file (simple-wp-helpdesk.php) is a thin loader — admin files are only loaded inside is_admin(). Constants: SWH_PLUGIN_DIR, SWH_PLUGIN_URL, SWH_PLUGIN_FILE.
Getting Started
git clone https://github.com/seanmousseau/Simple-WP-Helpdesk.git
cd Simple-WP-Helpdesk
composer install
No build step is required for the plugin itself. Drop the simple-wp-helpdesk/ folder into your WordPress wp-content/plugins/ directory and activate it from the WordPress dashboard.
Architecture Overview
No Custom Database Tables
The plugin uses only WordPress core data structures:
| Data | Storage | Key Meta |
|---|---|---|
| Tickets | helpdesk_ticket Custom Post Type | _ticket_uid, _ticket_token, _ticket_status, _ticket_priority, _ticket_email, _ticket_attachments, _ticket_csat, etc. |
| Replies & Notes | WP Comments (comment_type = 'helpdesk_reply') | _is_internal_note, _is_user_reply, _swh_reply_orignames |
| Settings | wp_options | All keys prefixed with swh_ |
| Canned responses | wp_options | swh_canned_responses (PHP serialized array, stored via update_option()) |
| Ticket templates | wp_options | swh_ticket_templates (PHP serialized array, stored via update_option()) |
Function & Class Naming
All public functions use the swh_ prefix; all classes use SWH_:
swh_activate()
swh_get_defaults()
swh_ticket_frontend()
swh_save_ticket_data()
Single Source of Truth for Defaults
swh_get_defaults() (includes/helpers.php) is the definitive list of every plugin option and its default value. It uses a static $defaults cache so it is built only once per request.
When adding a new option:
- Add it to
swh_get_defaults()with its default value — it is automatically registered by the upgrade routine, included in factory reset, and cleaned up on uninstall. - Add the field to the appropriate settings tab in
admin/class-settings.php. - Add it to the correct save block in
swh_handle_settings_save()(main form or Tools form — see below).
PSR-4 namespace convention (v3.7.0+)
New plugin classes use the SWH\ namespace, mapped to simple-wp-helpdesk/src/. Example: SWH\Email\Mailer → simple-wp-helpdesk/src/Email/Mailer.php.
This is additive — existing class-foo.php files loaded via require_once continue to work unchanged. v3.7 migrates one class as proof of concept (SWH\Email\Mailer); subsequent classes migrate organically across v4.x.
The runtime autoloader is shipped inside the plugin at simple-wp-helpdesk/vendor/autoload.php, generated from simple-wp-helpdesk/composer.json. The repo-root composer.json declares the same PSR-4 mapping so PHPUnit and PHPStan resolve the namespace from the dev vendor too.
After adding or moving a namespaced class, regenerate both autoloaders:
cd simple-wp-helpdesk && composer dump-autoload --no-dev
cd .. && composer dump-autoload
Commit the regenerated simple-wp-helpdesk/vendor/composer/autoload_*.php files.
Adding Features
New Option
// 1. includes/helpers.php — swh_get_defaults()
'swh_my_new_option' => 'default_value',
Then add the field to admin/class-settings.php and the corresponding save block.
New Email Template
Add both variants to swh_get_defaults():
'swh_my_event_sub' => 'Subject: {ticket_id}',
'swh_my_event_body' => 'Hello {name}, ...',
New Cron Job
// Register in swh_activate():
wp_schedule_event( time() + OFFSET_SECONDS, 'hourly', 'swh_my_event' );
// Clear in swh_deactivate():
wp_clear_scheduled_hook( 'swh_my_event' );
// Hook the handler:
add_action( 'swh_my_event', 'swh_process_my_event' );
Use a different offset from existing jobs (currently +0 min, +30 min, +60 min) to avoid simultaneous execution.
Settings Forms — Two Nonces
There are two separate forms on the settings page, each with its own nonce:
| Form | Nonce Action | Nonce Field | Owns |
|---|---|---|---|
| Main settings | swh_save_settings_action | swh_settings_nonce | Everything except Tools tab |
| Tools form | swh_save_tools_action | swh_tools_nonce | swh_retention_*, swh_delete_on_uninstall |
Never move swh_delete_on_uninstall or retention settings to the main form handler — they will reset silently.
Design Tokens
CSS custom properties are defined in swh-shared.css, loaded as a dependency of both swh-admin.css and swh-frontend.css. All tokens use the --swh- prefix.
Token Scales (v3.5.0)
Shadow
| Token | Value |
|---|---|
--swh-shadow-sm | 0 1px 2px rgba(0,0,0,0.08) |
--swh-shadow-md | 0 2px 6px rgba(0,0,0,0.12) |
--swh-shadow-lg | 0 4px 12px rgba(0,0,0,0.16) |
Z-index
| Token | Value | Used by |
|---|---|---|
--swh-z-base | 1 | General stacking |
--swh-z-dropdown | 100 | Dropdowns, popovers |
--swh-z-modal | 200 | Modal dialogs |
--swh-z-toast | 300 | Toast notifications |
Easing
| Token | Value |
|---|---|
--swh-ease-out | cubic-bezier(0,0,0.2,1) |
--swh-ease-in-out | cubic-bezier(0.4,0,0.2,1) |
Dark mode: token overrides live in a @media (prefers-color-scheme: dark) block in swh-shared.css, scoped to .swh-helpdesk-wrapper. This applies to the frontend only — do not add dark mode tokens to swh-admin.css (WordPress admin handles its own colour schemes).
Badge System
All status badges use a unified component defined in swh-shared.css.
Base class: .swh-badge — inline-block pill with padding, border-radius, and a hover transition. Modifier classes: .swh-badge-{slug} where slug = sanitize_title($status).
| Class | Used for |
|---|---|
.swh-badge-open | Open tickets |
.swh-badge-in-progress | In Progress tickets |
.swh-badge-resolved | Resolved tickets |
.swh-badge-closed | Closed tickets |
.swh-badge-sla-warn | SLA warning state |
.swh-badge-sla-breach | SLA breach state |
PHP pattern:
$status_slug = sanitize_title( $status );
echo '<span class="swh-badge swh-badge-' . esc_attr( $status_slug ) . '">'
. esc_html( $status ) . '</span>';
Inbound Email Webhook
The webhook endpoint is registered at POST /wp-json/swh/v1/inbound-email.
Authentication: Authorization: Bearer <swh_inbound_secret> header.
Payload fields:
| Field | Description |
|---|---|
sender | Sender email address (from accepted as fallback) |
subject | Email subject (must contain [TKT-XXXX]) |
body-plain | Message body (text accepted as fallback; lines beginning with > are stripped as quoted reply) |
The handler extracts the ticket ID from [TKT-XXXX] in the subject, verifies the sender matches _ticket_email via hash_equals(), strips quoted reply lines, and inserts a new reply comment.
Docker note: Apache strips Authorization headers before PHP. In Docker-based test environments, bypass HTTP entirely and call swh_handle_inbound_email() directly via wp eval.
Testing
The full test suite must pass before any PR is opened or release is cut.
make test-docker # full PHP gate inside Docker — preferred (no host PHP needed)
make e2e-docker # self-contained E2E: up → setup → Playwright → teardown
Individual tools:
| Command | Purpose |
|---|---|
make lint | PHP syntax check |
make phpcs | WordPress Coding Standards (zero errors) |
make phpstan | Static analysis level 9 |
make phpunit | Unit tests |
make semgrep | SAST security scan |
make coverage | PHPUnit + pcov → coverage.xml (Clover) |
MailHog email assertions: when MAILHOG_URL is set and WP_MODE=docker, expect_email() calls in the E2E suite assert email delivery via the MailHog API automatically.
Release Process
- Bump the version in
simple-wp-helpdesk.php:Version:in the plugin header commentdefine( 'SWH_VERSION', 'X.Y.Z' )
-
Update
CHANGELOG.md,simple-wp-helpdesk/readme.txt(stable tag + changelog section), and any relevantdocs/pages. -
Run the full gate —
make test-docker && make e2e-dockermust both exit 0. -
Close any GitHub issues addressed by the release.
-
Open a PR from
release/vX.Y.Ztomain. -
Merge to
main, then push the version tag:git tag vX.Y.Z && git push origin vX.Y.Zrelease.ymlfires automatically on the tag push — it buildssimple-wp-helpdesk.zipand creates the GitHub Release with the ZIP attached.