diff --git a/CLAUDE.md b/CLAUDE.md index ad8ce3d..6065365 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,100 +4,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands -```bash -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/`. +### Code organisation +**Code is organised package-by-domain.** Each domain package under `src//` 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 +Custom database tables are created via `dbDelta` on activation; `Schema.php` holds the SQL. 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](https://brain-wp.github.io/BrainMonkey/) 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 (NOT `returnUsing()`) -- `Functions\when('fn')->justReturn($val)` — stub returning a fixed value -- `Functions\expect('fn')->once()->with(...)` — assert call count and arguments -- Use `Functions\when()` (not `Functions\expect()`) when you need argument-routing (e.g. `get_role` returning different values per argument) to avoid chaining ambiguity -- Mockery matchers (e.g. `\Mockery::type()`) inside plain PHP arrays do not work with `with()` — 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 +Tests stub WordPress with Brain\Monkey rather than booting a real WP install. The setup and the Brain\Monkey/Mockery API gotchas are in `tests/CLAUDE.md`. ### Adding a Feature 0. **If the feature touches `Schema.php`, bump both the `Version:` header and `USC_VERSION` in `unsupervised-schedular.php`.** `Plugin::boot()` only re-runs `Installer`/`dbDelta` when the stored `us_schedular_version` differs, so a schema change without a version bump never reaches existing sites and inserts into new columns fail silently. @@ -106,10 +29,3 @@ All test classes extend `tests/Unit/TestCase.php`, which handles `Monkey\setUp() 3. Add template(s) under `templates/` if needed. 4. Write unit tests under `tests/Unit//` mirroring the `src//` structure. 5. 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. in `src/` diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md new file mode 100644 index 0000000..9ec5d83 --- /dev/null +++ b/tests/CLAUDE.md @@ -0,0 +1,14 @@ +# Writing tests + +Tests use [Brain\Monkey](https://brain-wp.github.io/BrainMonkey/) 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 (NOT `returnUsing()`) +- `Functions\when('fn')->justReturn($val)` — stub returning a fixed value +- `Functions\expect('fn')->once()->with(...)` — assert call count and arguments +- Use `Functions\when()` (not `Functions\expect()`) when you need argument-routing (e.g. `get_role` returning different values per argument) to avoid chaining ambiguity +- Mockery matchers (e.g. `\Mockery::type()`) inside plain PHP arrays do not work with `with()` — 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