PR #83 added kind and expires_at to us_invites and the repository started writing them, but USC_VERSION stayed at 1.0.0-rc.2 — Plugin::boot() only re-runs Installer/dbDelta on a version mismatch, so upgraded sites never got the columns. Every invite insert then failed silently: nothing appeared under Pending Invites while the admin was still shown a registration link whose token hash was never stored. - Version / USC_VERSION -> 1.0.0-rc.3 (triggers dbDelta on next load). - InviteRepository::insert() returns 0 on failure instead of a stale insert_id, and the Invites page now shows an error notice instead of a dead link when creation fails (personal and group forms), including clearer validation messages. - CLAUDE.md: schema changes must bump the version. Closes #87 Co-Authored-By: Claude Fable 5 <[email protected]>
6.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
composer install # Install all dependencies
composer test # Run the full test suite (required after every change)
composer lint # PHPStan static analysis
composer cs # PHPCS coding standards check
composer cs:fix # Auto-fix coding standards
# Run a single test file
./vendor/bin/phpunit tests/Unit/Availability/AvailabilityRepositoryTest.php
# Run a single test by name
./vendor/bin/phpunit --filter testInsertCallsWpdbInsertAndReturnsId
Run composer test after every code change before considering a task complete.
Architecture
Plugin Bootstrap
unsupervised-schedular.php defines constants (USC_VERSION, USC_PLUGIN_DIR, USC_PLUGIN_URL), registers activation/deactivation hooks, then calls Plugin::boot() on plugins_loaded. No logic lives in the root file.
Directory Structure
src/ — All plugin PHP (PSR-4 namespace: Unsupervised\Schedular\)
Availability/ — Availability slots: value object, repository, controller, REST endpoint
Booking/ — Lessons/bookings: value object, repository, controller, REST endpoint, shortcode page
Auth/ — Roles, capabilities, login page
Plugin.php — Wires all components together on plugins_loaded
Installer.php — Creates DB tables and roles on activation
Schema.php — CREATE TABLE SQL for dbDelta
AdminMenu.php — Registers wp-admin menu pages
RestRegistrar.php — Registers all REST routes under us-scheduler/v1
ShortcodeRegistrar.php — Registers [us_booking] and [us_student_login] shortcodes
BlockRegistrar.php — Registers Gutenberg dynamic-block wrappers for the shortcodes
BlockPreview.php — Static editor-preview markup for the blocks
templates/ — PHP view files included by controllers/shortcodes
assets/ — CSS and JS (vanilla JS, no build step)
tests/Unit/ — PHPUnit unit tests (PSR-4: Unsupervised\Schedular\Tests\)
Availability/ — Tests for src/Availability/
Booking/ — Tests for src/Booking/
Auth/ — Tests for src/Auth/
docs/features/ — One markdown file per feature describing data model, API, and test locations
Code is organised package-by-domain (Availability, Booking, Auth). Each domain package contains everything related to that domain: value objects, repositories, controllers, REST endpoints, and shortcode pages. Cross-cutting wiring classes (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar, Schema) live directly under src/.
Data Storage
Two custom database tables (created via dbDelta on activation):
{prefix}us_availability— instructor availability windows{prefix}us_lessons— booked lessons
All database access goes through repository classes within their domain package. No direct $wpdb calls outside repositories.
Key Classes
| Class | Responsibility |
|---|---|
Plugin |
Wires all components together on plugins_loaded |
Installer |
Creates DB tables and roles on activation |
Schema |
CREATE TABLE SQL strings for dbDelta |
AdminMenu |
Registers wp-admin menu pages |
RestRegistrar |
Registers all REST routes under us-scheduler/v1 |
ShortcodeRegistrar |
Registers [us_booking] and [us_student_login] shortcodes |
BlockRegistrar |
Registers Gutenberg dynamic-block wrappers for the shortcodes |
BlockPreview |
Static editor-preview markup for the blocks |
Val |
Runtime coercion of untyped WP boundary values (wpdb rows, REST params, superglobals) |
Auth\RoleManager |
Registers us_instructor and us_student roles with custom caps |
Auth\LoginPage |
Renders front-end student login form |
Availability\AvailabilitySlot |
Immutable value object for a slot row |
Availability\AvailabilityRepository |
CRUD for availability slots |
Availability\AvailabilityController |
Instructor availability management page |
Availability\AvailabilityEndpoint |
REST handlers for availability CRUD |
Booking\Lesson |
Immutable value object for a lesson row |
Booking\BookingRepository |
CRUD for lesson bookings |
Booking\BookingEndpoint |
REST handlers for booking and status updates |
Booking\BookingPage |
Renders student booking UI shell (JS takes over) |
Booking\LessonController |
Admin and instructor lesson list pages |
REST API Namespace
All endpoints live under /wp-json/us-scheduler/v1/. Permissions are enforced via permission_callback using capability checks (manage_availability, book_lesson), never role name checks.
Testing Approach
Tests use Brain\Monkey to stub WordPress functions without a full WP installation, and Mockery to mock $wpdb and other dependencies.
All test classes extend tests/Unit/TestCase.php, which handles Monkey\setUp() / Monkey\tearDown() and stubs all WP translation/escape functions automatically.
Brain\Monkey API notes:
Functions\when('fn')->alias(fn() => ...)— stub with a closure (NOTreturnUsing())Functions\when('fn')->justReturn($val)— stub returning a fixed valueFunctions\expect('fn')->once()->with(...)— assert call count and arguments- Use
Functions\when()(notFunctions\expect()) when you need argument-routing (e.g.get_rolereturning different values per argument) to avoid chaining ambiguity - Mockery matchers (e.g.
\Mockery::type()) inside plain PHP arrays do not work withwith()— use\Mockery::on(fn($arr) => ...)or\Mockery::any()instead - When mocking
$wpdb, set$mock->prefix = 'wp_'explicitly — it is a public property, not a method
Adding a Feature
- If the feature touches
Schema.php, bump both theVersion:header andUSC_VERSIONinunsupervised-schedular.php.Plugin::boot()only re-runsInstaller/dbDeltawhen the storedus_schedular_versiondiffers, so a schema change without a version bump never reaches existing sites and inserts into new columns fail silently. - Write the feature doc in
docs/features/<feature-name>.md(data model, API, classes, test paths). - Create a domain package under
src/<Domain>/containing all classes for that feature. - Add template(s) under
templates/if needed. - Write unit tests under
tests/Unit/<Domain>/mirroring thesrc/<Domain>/structure. - Run
composer test— all tests must pass before the feature is complete.
CI
Gitea Actions (.gitea/workflows/ci.yml) runs on every push and pull request:
- lint — PHPCS WordPress coding standards
- static-analysis — PHPStan level 10
- test — PHPUnit on PHP 8.1, 8.2, 8.3
- no-debug — rejects commits with
var_dump,error_log, etc. insrc/