Compare commits

..

1 Commits

Author SHA1 Message Date
2fb2ca392d Restructure src/ and tests/ from package-by-type to package-by-domain
All checks were successful
CI / Coding Standards (push) Successful in 43s
CI / PHPStan (push) Successful in 52s
CI / Tests (PHP 8.1) (push) Successful in 47s
CI / Tests (PHP 8.2) (push) Successful in 49s
CI / Tests (PHP 8.3) (push) Successful in 37s
CI / No Debug Code (push) Successful in 2s
All classes are now organised by domain (Availability, Booking, Auth).
Each domain package contains its value object, repository, admin controller,
REST endpoint, and any shortcode pages under a matching sub-namespace.
Cross-cutting wiring (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar,
Schema) lives at src/ root. Tests mirror the domain structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 16:37:30 -03:00
26 changed files with 108 additions and 83 deletions

View File

@@ -13,7 +13,7 @@ composer cs # PHPCS coding standards check
composer cs:fix # Auto-fix coding standards composer cs:fix # Auto-fix coding standards
# Run a single test file # Run a single test file
./vendor/bin/phpunit tests/Unit/Data/AvailabilityRepositoryTest.php ./vendor/bin/phpunit tests/Unit/Availability/AvailabilityRepositoryTest.php
# Run a single test by name # Run a single test by name
./vendor/bin/phpunit --filter testInsertCallsWpdbInsertAndReturnsId ./vendor/bin/phpunit --filter testInsertCallsWpdbInsertAndReturnsId
@@ -29,18 +29,32 @@ composer cs:fix # Auto-fix coding standards
### Directory Structure ### Directory Structure
``` ```
src/ — All plugin PHP (PSR-4 namespace: Unsupervised\Schedular\) 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
templates/ — PHP view files included by controllers/shortcodes templates/ — PHP view files included by controllers/shortcodes
assets/ — CSS and JS (vanilla JS, no build step) assets/ — CSS and JS (vanilla JS, no build step)
tests/Unit/ — PHPUnit unit tests (PSR-4: Unsupervised\Schedular\Tests\) tests/Unit/ — PHPUnit unit tests (PSR-4: Unsupervised\Schedular\Tests\)
docs/features/— One markdown file per feature describing data model, API, and test locations 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 ### Data Storage
Two custom database tables (created via `dbDelta` on activation): Two custom database tables (created via `dbDelta` on activation):
- `{prefix}us_availability` — instructor availability windows - `{prefix}us_availability` — instructor availability windows
- `{prefix}us_lessons` — booked lessons - `{prefix}us_lessons` — booked lessons
All database access goes through repository classes in `src/Data/`. No direct `$wpdb` calls outside repositories. All database access goes through repository classes within their domain package. No direct `$wpdb` calls outside repositories.
### Key Classes ### Key Classes
@@ -48,20 +62,21 @@ All database access goes through repository classes in `src/Data/`. No direct `$
|---|---| |---|---|
| `Plugin` | Wires all components together on `plugins_loaded` | | `Plugin` | Wires all components together on `plugins_loaded` |
| `Installer` | Creates DB tables and roles on activation | | `Installer` | Creates DB tables and roles on activation |
| `Roles\RoleManager` | Registers `us_instructor` and `us_student` roles with custom caps | | `Schema` | CREATE TABLE SQL strings for dbDelta |
| `Data\AvailabilityRepository` | CRUD for availability slots | | `AdminMenu` | Registers wp-admin menu pages |
| `Data\BookingRepository` | CRUD for lesson bookings | | `RestRegistrar` | Registers all REST routes under `us-scheduler/v1` |
| `Model\AvailabilitySlot` | Immutable value object for a slot row | | `ShortcodeRegistrar` | Registers `[us_booking]` and `[us_student_login]` shortcodes |
| `Model\Lesson` | Immutable value object for a lesson row | | `Auth\RoleManager` | Registers `us_instructor` and `us_student` roles with custom caps |
| `Admin\AdminMenu` | Registers wp-admin menu pages | | `Auth\LoginPage` | Renders front-end student login form |
| `Admin\AvailabilityController` | Instructor availability management page | | `Availability\AvailabilitySlot` | Immutable value object for a slot row |
| `Admin\LessonController` | Admin and instructor lesson list pages | | `Availability\AvailabilityRepository` | CRUD for availability slots |
| `Api\RestRegistrar` | Registers all REST routes under `us-scheduler/v1` | | `Availability\AvailabilityController` | Instructor availability management page |
| `Api\AvailabilityEndpoint` | REST handlers for availability CRUD | | `Availability\AvailabilityEndpoint` | REST handlers for availability CRUD |
| `Api\BookingEndpoint` | REST handlers for booking and status updates | | `Booking\Lesson` | Immutable value object for a lesson row |
| `Frontend\ShortcodeRegistrar` | Registers `[us_booking]` and `[us_student_login]` shortcodes | | `Booking\BookingRepository` | CRUD for lesson bookings |
| `Frontend\BookingPage` | Renders student booking UI shell (JS takes over) | | `Booking\BookingEndpoint` | REST handlers for booking and status updates |
| `Frontend\LoginPage` | Renders front-end student login form | | `Booking\BookingPage` | Renders student booking UI shell (JS takes over) |
| `Booking\LessonController` | Admin and instructor lesson list pages |
### REST API Namespace ### 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. 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.
@@ -81,9 +96,9 @@ All test classes extend `tests/Unit/TestCase.php`, which handles `Monkey\setUp()
### Adding a Feature ### Adding a Feature
1. Write the feature doc in `docs/features/<feature-name>.md` (data model, API, classes, test paths). 1. Write the feature doc in `docs/features/<feature-name>.md` (data model, API, classes, test paths).
2. Implement the classes under `src/`. 2. Create a domain package under `src/<Domain>/` containing all classes for that feature.
3. Add template(s) under `templates/` if needed. 3. Add template(s) under `templates/` if needed.
4. Write unit tests under `tests/Unit/` mirroring the `src/` directory structure. 4. Write unit tests under `tests/Unit/<Domain>/` mirroring the `src/<Domain>/` structure.
5. Run `composer test` — all tests must pass before the feature is complete. 5. Run `composer test` — all tests must pass before the feature is complete.
### CI ### CI

View File

@@ -2,3 +2,4 @@
- [Project: unsupervised-schedular](project_schedular.md) — WordPress lesson scheduling plugin; initial scaffold created March 2026 - [Project: unsupervised-schedular](project_schedular.md) — WordPress lesson scheduling plugin; initial scaffold created March 2026
- [Feedback: Brain\Monkey testing patterns](feedback_brainmonkey.md) — specific API quirks discovered during test setup - [Feedback: Brain\Monkey testing patterns](feedback_brainmonkey.md) — specific API quirks discovered during test setup
- [Feedback: Package-by-domain architecture](feedback_architecture.md) — all new code must be organised by domain, never by type

View File

@@ -0,0 +1,16 @@
---
name: Package-by-domain architecture
description: New features must be organised as domain packages, not by type (no Admin/, Api/, Data/ etc.)
type: feedback
---
Always organise code package-by-domain, not package-by-type.
**Why:** User explicitly requested the restructure on 2026-03-30. Package-by-type (Admin/, Api/, Data/, Frontend/, Model/, Roles/) was the initial scaffold but was replaced before any real features shipped.
**How to apply:**
- New domain → new directory under `src/<Domain>/` (e.g. `src/Availability/`, `src/Booking/`, `src/Auth/`)
- All classes for a domain (value objects, repositories, admin controllers, REST endpoints, shortcode pages) live inside that directory under the sub-namespace `Unsupervised\Schedular\<Domain>\`
- Cross-cutting wiring that glues domains together (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar, Schema) lives at `src/` root with namespace `Unsupervised\Schedular\`
- Tests mirror the domain structure: `tests/Unit/<Domain>/`
- Never create `src/Admin/`, `src/Api/`, `src/Data/`, `src/Frontend/`, `src/Model/`, or `src/Roles/` directories

View File

@@ -15,5 +15,6 @@ WordPress plugin for instructor/student lesson scheduling. Full scaffold created
- REST API (`us-scheduler/v1`) for all front-end interactions; templates are minimal shell divs, JS (vanilla) takes over - REST API (`us-scheduler/v1`) for all front-end interactions; templates are minimal shell divs, JS (vanilla) takes over
- Instructors use wp-admin login; students use front-end `[us_student_login]` shortcode calling `wp_signon()` - Instructors use wp-admin login; students use front-end `[us_student_login]` shortcode calling `wp_signon()`
- PSR-4 namespace `Unsupervised\Schedular\` from `src/` - PSR-4 namespace `Unsupervised\Schedular\` from `src/`
- **Package-by-domain architecture** (restructured 2026-03-30): `src/Availability/`, `src/Booking/`, `src/Auth/` — each domain contains its value object, repository, controller, REST endpoint, and any shortcode pages. Cross-cutting wiring (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar, Schema) lives at `src/` root. Tests mirror the domain structure under `tests/Unit/<Domain>/`.
**How to apply:** When adding features, follow the docs/features/ + src/ + tests/Unit/ pattern. Always run `composer test` after changes. **How to apply:** When adding features, create a domain package under `src/<Domain>/` with all related classes, mirror it in `tests/Unit/<Domain>/`, write a feature doc in `docs/features/`, then run `composer test`.

View File

@@ -1,11 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Admin; namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Availability\AvailabilityController;
use Unsupervised\Schedular\Data\BookingRepository; use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Roles\RoleManager; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Booking\BookingRepository;
use Unsupervised\Schedular\Booking\LessonController;
class AdminMenu { class AdminMenu {

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Frontend; namespace Unsupervised\Schedular\Auth;
class LoginPage { class LoginPage {

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Roles; namespace Unsupervised\Schedular\Auth;
class RoleManager { class RoleManager {

View File

@@ -1,11 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Admin; namespace Unsupervised\Schedular\Availability;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Model\AvailabilitySlot;
use Unsupervised\Schedular\Roles\RoleManager;
class AvailabilityController { class AvailabilityController {

View File

@@ -1,11 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Api; namespace Unsupervised\Schedular\Availability;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Model\AvailabilitySlot;
use Unsupervised\Schedular\Roles\RoleManager;
class AvailabilityEndpoint { class AvailabilityEndpoint {

View File

@@ -1,9 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Data; namespace Unsupervised\Schedular\Availability;
use Unsupervised\Schedular\Model\AvailabilitySlot;
class AvailabilityRepository { class AvailabilityRepository {

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Model; namespace Unsupervised\Schedular\Availability;
class AvailabilitySlot { class AvailabilitySlot {

View File

@@ -1,12 +1,10 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Api; namespace Unsupervised\Schedular\Booking;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Data\BookingRepository; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Model\Lesson;
use Unsupervised\Schedular\Roles\RoleManager;
class BookingEndpoint { class BookingEndpoint {

View File

@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Frontend; namespace Unsupervised\Schedular\Booking;
use Unsupervised\Schedular\Roles\RoleManager; use Unsupervised\Schedular\Auth\RoleManager;
class BookingPage { class BookingPage {

View File

@@ -1,9 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Data; namespace Unsupervised\Schedular\Booking;
use Unsupervised\Schedular\Model\Lesson;
class BookingRepository { class BookingRepository {

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Model; namespace Unsupervised\Schedular\Booking;
class Lesson { class Lesson {

View File

@@ -1,10 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Admin; namespace Unsupervised\Schedular\Booking;
use Unsupervised\Schedular\Data\BookingRepository; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Roles\RoleManager;
class LessonController { class LessonController {

View File

@@ -3,8 +3,7 @@ declare(strict_types=1);
namespace Unsupervised\Schedular; namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Data\Schema; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Roles\RoleManager;
class Installer { class Installer {

View File

@@ -3,12 +3,9 @@ declare(strict_types=1);
namespace Unsupervised\Schedular; namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Admin\AdminMenu; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Api\RestRegistrar; use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Booking\BookingRepository;
use Unsupervised\Schedular\Data\BookingRepository;
use Unsupervised\Schedular\Frontend\ShortcodeRegistrar;
use Unsupervised\Schedular\Roles\RoleManager;
class Plugin { class Plugin {

View File

@@ -1,10 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Api; namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Availability\AvailabilityEndpoint;
use Unsupervised\Schedular\Data\BookingRepository; use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Booking\BookingEndpoint;
use Unsupervised\Schedular\Booking\BookingRepository;
class RestRegistrar { class RestRegistrar {

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Data; namespace Unsupervised\Schedular;
class Schema { class Schema {

View File

@@ -1,7 +1,10 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Frontend; namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Booking\BookingPage;
class ShortcodeRegistrar { class ShortcodeRegistrar {

View File

@@ -1,10 +1,10 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Roles; namespace Unsupervised\Schedular\Tests\Unit\Auth;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use Unsupervised\Schedular\Roles\RoleManager; use Unsupervised\Schedular\Auth\RoleManager;
use Unsupervised\Schedular\Tests\Unit\TestCase; use Unsupervised\Schedular\Tests\Unit\TestCase;
class RoleManagerTest extends TestCase class RoleManagerTest extends TestCase

View File

@@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Data; namespace Unsupervised\Schedular\Tests\Unit\Availability;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use Mockery; use Mockery;
use Unsupervised\Schedular\Data\AvailabilityRepository; use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Model\AvailabilitySlot; use Unsupervised\Schedular\Availability\AvailabilitySlot;
use Unsupervised\Schedular\Tests\Unit\TestCase; use Unsupervised\Schedular\Tests\Unit\TestCase;
class AvailabilityRepositoryTest extends TestCase class AvailabilityRepositoryTest extends TestCase

View File

@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Model; namespace Unsupervised\Schedular\Tests\Unit\Availability;
use Unsupervised\Schedular\Model\AvailabilitySlot; use Unsupervised\Schedular\Availability\AvailabilitySlot;
use Unsupervised\Schedular\Tests\Unit\TestCase; use Unsupervised\Schedular\Tests\Unit\TestCase;
class AvailabilitySlotTest extends TestCase class AvailabilitySlotTest extends TestCase

View File

@@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Data; namespace Unsupervised\Schedular\Tests\Unit\Booking;
use Brain\Monkey\Functions; use Brain\Monkey\Functions;
use Mockery; use Mockery;
use Unsupervised\Schedular\Data\BookingRepository; use Unsupervised\Schedular\Booking\BookingRepository;
use Unsupervised\Schedular\Model\Lesson; use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\Tests\Unit\TestCase; use Unsupervised\Schedular\Tests\Unit\TestCase;
class BookingRepositoryTest extends TestCase class BookingRepositoryTest extends TestCase

View File

@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Model; namespace Unsupervised\Schedular\Tests\Unit\Booking;
use Unsupervised\Schedular\Model\Lesson; use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\Tests\Unit\TestCase; use Unsupervised\Schedular\Tests\Unit\TestCase;
class LessonTest extends TestCase class LessonTest extends TestCase