|
| 1 | +--- |
| 2 | +name: scaffold-feature |
| 3 | +description: Scaffold a new SeQura integration-core feature end-to-end following repo conventions — Domain service/contracts/models, a store-scoped DataAccess repository + ORM entity, the BootstrapComponent registration block, and (when API-exposed) a facade controller with Request/Response DTOs plus a facade method. Use when adding a new persisted and/or API-exposed feature under src/BusinessLogic/Domain/<Feature>. |
| 4 | +--- |
| 5 | + |
| 6 | +# Scaffold an integration-core feature |
| 7 | + |
| 8 | +This repo adds a feature in a fixed, convention-heavy shape spread across three to |
| 9 | +four places. This skill generates that skeleton so nothing is forgotten and every |
| 10 | +file matches the existing style. **`CountryConfiguration` is the canonical reference** |
| 11 | +— read those files when a template detail is unclear. |
| 12 | + |
| 13 | +## Before you start |
| 14 | + |
| 15 | +1. Get the feature name in `PascalCase` (e.g. `ShippingRules`) and decide: |
| 16 | + - **Persisted?** → needs a `DataAccess/<Feature>` repository + ORM entity. |
| 17 | + - **API-exposed?** → needs a controller under a facade (`AdminAPI`, `CheckoutAPI`, |
| 18 | + `WebhookAPI`, or `ConfigurationWebhookAPI`) + a facade method. |
| 19 | + - **Talks to SeQura's HTTP API?** → also needs a `ProxyContracts/` interface and a |
| 20 | + `SeQuraAPI` proxy (see `OrderProxy`). Out of scope for the basic skeleton — flag it. |
| 21 | +2. Hard constraints (these are non-negotiable in `src/`): |
| 22 | + - **PHP 7.2 syntax only.** No arrow fns, no `?->`, no named args, no enums, no |
| 23 | + constructor promotion, no union/typed-property syntax newer than 7.2. |
| 24 | + - **PSR-12** (`.phpcs.xml.dist`) and **PHPStan level 6** clean. |
| 25 | + - Namespaces are PSR-4: `SeQura\Core\BusinessLogic\...` → `src/BusinessLogic/...`. |
| 26 | + - Every class/interface/method gets a docblock matching the surrounding style. |
| 27 | + |
| 28 | +## Layers to generate |
| 29 | + |
| 30 | +For feature `<Feature>` with a domain model `<Model>`: |
| 31 | + |
| 32 | +### 1. Domain (`src/BusinessLogic/Domain/<Feature>/`) |
| 33 | +- `Models/<Model>.php` — plain immutable-ish model: constructor + getters, no framework deps. |
| 34 | +- `RepositoryContracts/<Feature>RepositoryInterface.php` — the persistence contract. |
| 35 | + Docblocks say **"for current store context"** — the store scoping is implicit, never a param. |
| 36 | +- `Services/<Feature>Service.php` — business logic. Depends on the **`...RepositoryInterface`** |
| 37 | + (and other `*Interface`/`*Service` collaborators), **never** on a concrete repository, |
| 38 | + the ORM, or `HttpClient`. |
| 39 | +- `Exceptions/*.php` — domain exceptions. If the error must surface to an API caller as a |
| 40 | + translatable message, extend the translatable base (see `Domain/Translations`) so |
| 41 | + `ErrorHandlingAspect` can turn it into a `TranslatableErrorResponse`. |
| 42 | + |
| 43 | +### 2. DataAccess (`src/BusinessLogic/DataAccess/<Feature>/`) — only if persisted |
| 44 | +- `Entities/<Feature>.php` — `extends Entity`. Must declare `public const CLASS_NAME = __CLASS__;`, |
| 45 | + a `protected $storeId;`, and implement `inflate()`, `toArray()`, `getConfig()`. `getConfig()` |
| 46 | + returns an `EntityConfiguration` whose `IndexMap` adds at least `addStringIndex('storeId')`. |
| 47 | +- `Repositories/<Feature>Repository.php` — `implements <Feature>RepositoryInterface`. |
| 48 | + Constructor takes `(RepositoryInterface $repository, StoreContext $storeContext)`. **Every** |
| 49 | + read/write filters by `storeContext->getStoreId()` via a `QueryFilter` and stamps `storeId` |
| 50 | + on save/update. This is the multistore contract — never skip it. |
| 51 | + |
| 52 | +### 3. BootstrapComponent registration (`src/BusinessLogic/BootstrapComponent.php`) |
| 53 | +Add a `ServiceRegister::registerService(...)` lazy-callable block in the matching `init*()` method: |
| 54 | +- repository interface → concrete repo in **`initRepositories()`** (resolve the ORM repo with |
| 55 | + `RepositoryRegistry::getRepository(<Feature>::getClassName())` and inject `StoreContext`). |
| 56 | +- service → in **`initServices()`** (inject the repo interface + collaborators via `ServiceRegister::getService(...)`). |
| 57 | +- controller → in **`initControllers()`** (inject the service(s)). |
| 58 | +- Also ensure the ORM **entity class is registered** wherever the entity list lives |
| 59 | + (host platform / `RepositoryRegistry`); the core references it via `getClassName()`. |
| 60 | +- If the feature emits/handles events or webhook topics, wire `initEvents()` / `initTopicHandlers()` too. |
| 61 | + |
| 62 | +### 4. API facade (only if API-exposed) — e.g. `src/BusinessLogic/AdminAPI/<Feature>/` |
| 63 | +- `<Feature>Controller.php` — **returns `Response` objects and NEVER throws** to the caller. |
| 64 | + Each method returns a typed `*Response`; mutating methods take a typed `*Request`. |
| 65 | +- `Requests/*.php` — `extends Request`, expose `transformToDomainModel()`. |
| 66 | +- `Responses/*.php` — `extends Response`, implement `toArray()`. |
| 67 | +- Add a facade method on the facade class (e.g. `AdminAPI::<feature>(string $storeId): Aspects`) |
| 68 | + returning `Aspects::run(new ErrorHandlingAspect())->andRun(new StoreContextAspect($storeId)) |
| 69 | + ->beforeEachMethodOfService(<Feature>Controller::class)`. |
| 70 | + |
| 71 | +### 5. Tests (`tests/BusinessLogic/<Feature>/`) |
| 72 | +Mirror an existing controller/service test (see `tests/BusinessLogic/AdminAPI/CountryConfiguration/`). |
| 73 | +Register the ORM entity in the test bootstrap so the in-memory repository can resolve it. |
| 74 | + |
| 75 | +## Verify (the project gate — required before calling it done) |
| 76 | + |
| 77 | +The Docker `php` service must be up (`./setup.sh`). Then, per `CLAUDE.md`: |
| 78 | +```bash |
| 79 | +./bin/php-syntax-check --php=7.2 # 7.2 syntax sanity on new files |
| 80 | +./bin/phpcbf && ./bin/phpcs # PSR-12 |
| 81 | +./bin/phpstan # level 6 over src/ |
| 82 | +./bin/phpunit # full suite (bin/phpunit ignores args) |
| 83 | +``` |
| 84 | +Single test while iterating: |
| 85 | +`docker compose exec php vendor/bin/phpunit --configuration phpunit.xml --filter <TestName>` |
| 86 | + |
| 87 | +## Output discipline |
| 88 | + |
| 89 | +Generate only the files the feature actually needs — skip DataAccess if not persisted, |
| 90 | +skip the facade if not API-exposed. Don't invent configurability or extra methods beyond |
| 91 | +what was asked (CLAUDE.md: simplicity first, surgical changes). After generating, list every |
| 92 | +file created/edited and confirm the registration block was added to `BootstrapComponent`. |
0 commit comments