From 18d99b45032146a4579a2cc6f0810a7828dd5a69 Mon Sep 17 00:00:00 2001 From: Kim Sandell Date: Tue, 30 Jun 2026 09:18:21 +0200 Subject: [PATCH 1/4] feat: add HTTP QUERY method support Add first-class handleQUERY() handler to the base Controller (dispatch case + default 405) and ControllerInterface, and include QUERY in RouteGroup's default method set. QUERY is safe and idempotent like GET but carries a request body (IETF draft-ietf-httpbis-safe-method-w-body, OpenAPI 3.2). Add tests/Core/ControllerTest.php covering the default 405 and override paths. Update all docs and AI instruction files enumerating HTTP methods. Bump VERSION 0.0.41 -> 0.0.42 with CHANGELOG entry. --- .claude/claude-instructions.md | 2 +- .claude/llm-instructions.md | 6 ++- .github/copilot-instructions.md | 1 + VERSION | 2 +- changelog.md | 3 ++ .../Architecture-Overview.md | 3 +- doc/Getting-Started/Project-Structure.md | 2 +- doc/Reference/API-Reference.md | 3 ++ doc/User-Guide/Routing.md | 3 ++ readme.md | 2 +- src/Core/Controller.php | 17 ++++++ src/Core/ControllerInterface.php | 11 ++++ src/Core/RouteGroup.php | 2 +- tests/Core/ControllerTest.php | 53 +++++++++++++++++++ 14 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 tests/Core/ControllerTest.php diff --git a/.claude/claude-instructions.md b/.claude/claude-instructions.md index 4b75679..c7cc777 100644 --- a/.claude/claude-instructions.md +++ b/.claude/claude-instructions.md @@ -223,7 +223,7 @@ class UserController extends Controller ``` **Key points:** -- Each HTTP method gets a handler: `handleGET()`, `handlePOST()`, `handlePUT()`, `handleDELETE()`, `handlePATCH()`, `handleHEAD()`, `handleOPTIONS()` +- Each HTTP method gets a handler: `handleGET()`, `handlePOST()`, `handlePUT()`, `handleDELETE()`, `handlePATCH()`, `handleHEAD()`, `handleOPTIONS()`, `handleQUERY()` - URL parameters are passed as `$args` array - Always return `ResponseInterface` - Use global helpers (`response()`, `responseJson()`) for response construction diff --git a/.claude/llm-instructions.md b/.claude/llm-instructions.md index faddda3..6856e6a 100644 --- a/.claude/llm-instructions.md +++ b/.claude/llm-instructions.md @@ -97,7 +97,7 @@ Routes are **never** defined in PHP. Instead, use JSON files: - `groups`: organize routes by prefix with optional middleware - `routes`: root-level routes - URL parameters use `:paramName` syntax (e.g., `/:id`, `/:slug`) -- `methods` array specifies HTTP verbs (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) +- `methods` array specifies HTTP verbs (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, QUERY) See `doc/Routing.md` for full specification. @@ -201,7 +201,9 @@ class UserController extends Controller } ``` -**Handler methods:** `handleGET()`, `handlePOST()`, `handlePUT()`, `handleDELETE()`, `handlePATCH()`, `handleHEAD()`, `handleOPTIONS()` +**Handler methods:** `handleGET()`, `handlePOST()`, `handlePUT()`, `handleDELETE()`, `handlePATCH()`, `handleHEAD()`, `handleOPTIONS()`, `handleQUERY()` + +> `handleQUERY()` handles the QUERY verb — safe + idempotent like GET but with a request body. Read it via `$this->body` / `decodeJsonBody()`. **Conventions:** - `$args` contains URL parameters as array keys (e.g., `['id' => '123']`) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 03c167b..3def07a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -62,6 +62,7 @@ doc/ # Feature documentation ```php public function handleGET(array $args): ResponseInterface { } public function handlePOST(array $args): ResponseInterface { } + public function handleQUERY(array $args): ResponseInterface { } // safe+idempotent like GET, with body private function validateInput(array $data): bool { } ``` - **Variables** — camelCase diff --git a/VERSION b/VERSION index f9bcd8b..6fbcf53 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.41 \ No newline at end of file +0.0.42 \ No newline at end of file diff --git a/changelog.md b/changelog.md index 142cea4..259d1c4 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,9 @@ # Changelog SPIN Framework Changelog +## 0.0.42 +- **Feature:** HTTP `QUERY` method support — controllers gain a first-class `handleQUERY()` handler and `QUERY` is in the default route method set. QUERY is safe + idempotent like GET but carries a request body (read via `$this->body` / `decodeJsonBody()`). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. + ## 0.0.40 - **Feature:** Add domain exception classes: `CacheException`, `ConfigException`, `DatabaseException`, `MiddlewareException` — all extending `SpinException` - **Feature:** Wire `CacheException` into cache adapters (APCu, Redis), `ConfigException` into `Config`, `DatabaseException` into `PdoConnection` and `ConnectionManager` diff --git a/doc/Contributor-Guide/Architecture-Overview.md b/doc/Contributor-Guide/Architecture-Overview.md index bb34b22..764271f 100644 --- a/doc/Contributor-Guide/Architecture-Overview.md +++ b/doc/Contributor-Guide/Architecture-Overview.md @@ -100,7 +100,8 @@ abstract class Controller public function handlePOST(array $args): ResponseInterface public function handlePUT(array $args): ResponseInterface public function handleDELETE(array $args): ResponseInterface - // ... other HTTP methods + public function handleQUERY(array $args): ResponseInterface + // ... other HTTP methods (PATCH, HEAD, OPTIONS) } ``` diff --git a/doc/Getting-Started/Project-Structure.md b/doc/Getting-Started/Project-Structure.md index a4aaffa..4caac52 100644 --- a/doc/Getting-Started/Project-Structure.md +++ b/doc/Getting-Started/Project-Structure.md @@ -325,7 +325,7 @@ Never commit `.env` to version control. Add `.env` to `.gitignore`. | Config files | `config{-env}.json` | `config.json`, `config-dev.json` | | Route files | `routes{-env}.json` | `routes.json`, `routes-dev.json` | | Classes | `{Name}` (PascalCase) | `TaskService`, `SessionManager` | -| Methods | `handle{METHOD}` | `handleGET`, `handlePOST` | +| Methods | `handle{METHOD}` | `handleGET`, `handlePOST`, `handleQUERY` | | Namespaces | `App\{Area}` | `App\Controllers`, `App\Middlewares` | ## PSR-4 Autoloading diff --git a/doc/Reference/API-Reference.md b/doc/Reference/API-Reference.md index 6ee7f50..8adc4af 100644 --- a/doc/Reference/API-Reference.md +++ b/doc/Reference/API-Reference.md @@ -64,6 +64,9 @@ Base controller class providing HTTP method handlers and convenient access to fr | `handleDELETE()` | `public function handleDELETE(array $args): ResponseInterface` | Handle DELETE requests | | `handleHEAD()` | `public function handleHEAD(array $args): ResponseInterface` | Handle HEAD requests | | `handleOPTIONS()` | `public function handleOPTIONS(array $args): ResponseInterface` | Handle OPTIONS requests | +| `handleQUERY()` | `public function handleQUERY(array $args): ResponseInterface` | Handle QUERY requests (safe + idempotent like GET, with a request body) | + +> **QUERY** carries a request body — read it via `$this->body` / `decodeJsonBody()`. See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. **Usage Example:** diff --git a/doc/User-Guide/Routing.md b/doc/User-Guide/Routing.md index 7369991..979e2c2 100644 --- a/doc/User-Guide/Routing.md +++ b/doc/User-Guide/Routing.md @@ -190,6 +190,9 @@ SPIN controllers use specific method names to handle different HTTP requests: - `handlePUT(array $args)` - Handles PUT requests - `handleDELETE(array $args)` - Handles DELETE requests - `handlePATCH(array $args)` - Handles PATCH requests +- `handleQUERY(array $args)` - Handles QUERY requests + +> **QUERY** is safe and idempotent like GET but carries a request body. Read it via `$this->body` / `decodeJsonBody()` (same as POST). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. ### Controller Parameters diff --git a/readme.md b/readme.md index 540fb07..c58d1e3 100644 --- a/readme.md +++ b/readme.md @@ -194,7 +194,7 @@ class AuthMiddleware extends Middleware ### 🛣️ JSON-Based Routing - **Route Groups** - Organize routes with shared middleware and prefixes -- **HTTP Method Support** - Full support for GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS +- **HTTP Method Support** - Full support for GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, QUERY - **Dynamic Parameters** - Capture URL parameters with `{paramName}` syntax - **Middleware Integration** - Apply middleware at common, group, or route level diff --git a/src/Core/Controller.php b/src/Core/Controller.php index 0219492..86113c3 100644 --- a/src/Core/Controller.php +++ b/src/Core/Controller.php @@ -67,6 +67,7 @@ public function handle(array $args) case "DELETE" : return $this->handleDELETE($args); case "HEAD" : return $this->handleHEAD($args); case "OPTIONS": return $this->handleOPTIONS($args); + case "QUERY" : return $this->handleQUERY($args); default : return $this->handleCUSTOM($args); } } @@ -155,6 +156,22 @@ public function handleOPTIONS(array $args) return \response('',405); } + /** + * Handle QUERY request + * + * QUERY is safe and idempotent like GET but carries a request body. Read the + * body via $this->body / decodeJsonBody() (same as POST). See IETF + * draft-ietf-httpbis-safe-method-w-body and OpenAPI 3.2. + * + * @param array $args Path variable arguments as name=value pairs + * + * @return Response Value returned by $app->run() + */ + public function handleQUERY(array $args) + { + return \response('',405); + } + /** * Handle custom request * diff --git a/src/Core/ControllerInterface.php b/src/Core/ControllerInterface.php index 7c46727..0b1d26d 100644 --- a/src/Core/ControllerInterface.php +++ b/src/Core/ControllerInterface.php @@ -100,6 +100,17 @@ public function handleHEAD(array $args); */ public function handleOPTIONS(array $args); + /** + * Handle QUERY request + * + * QUERY is safe and idempotent like GET but carries a request body. + * + * @param array $args Path variable arguments as name=value pairs + * + * @return bool Value returned by $app->run() + */ + public function handleQUERY(array $args); + /** * Handle custom request * diff --git a/src/Core/RouteGroup.php b/src/Core/RouteGroup.php index 6e3aa2b..57998f6 100644 --- a/src/Core/RouteGroup.php +++ b/src/Core/RouteGroup.php @@ -72,7 +72,7 @@ public function __construct(array $definition) # Method extraction # Default to ALL - $methods = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS']; + $methods = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS','QUERY']; if (isset($route['methods'])) { $methods = $route['methods']; diff --git a/tests/Core/ControllerTest.php b/tests/Core/ControllerTest.php new file mode 100644 index 0000000..bc3960c --- /dev/null +++ b/tests/Core/ControllerTest.php @@ -0,0 +1,53 @@ +handleQUERY([]); + + $this->assertSame(405, $response->getStatusCode()); + } + + /** + * A controller overriding handleQUERY() returns its own response — proving + * QUERY is a first-class, dispatchable verb. + */ + public function testHandleQueryCanBeOverridden(): void + { + $controller = new class extends Controller { + public function handleQUERY(array $args) + { + return \response('ok', 200); + } + }; + + $response = $controller->handleQUERY([]); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('ok', (string) $response->getBody()); + } +} From 7ecaab66356f4e0dc51d5f28bab2eb250f45b69d Mon Sep 17 00:00:00 2001 From: Kim Sandell Date: Tue, 30 Jun 2026 09:20:16 +0200 Subject: [PATCH 2/4] release: bump composer.json and package.json to 0.0.42 Sync the two manifests missed in the QUERY release commit. Per .claude/commands/release.md the version lives in four places: VERSION, composer.json, package.json, changelog.md. src/Application.php carries no framework version (it reads the consuming app's version.json). --- composer.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 28e25d7..77fd237 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "celarius/spin-framework", "description": "A super lightweight PHP UI/REST Framework", - "version": "0.0.41", + "version": "0.0.42", "keywords": [ "php8", "php-framework", diff --git a/package.json b/package.json index 5961af8..ef40717 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "spin-framework", "title": "Spin Framework", - "version": "0.0.41", + "version": "0.0.42", "homepage": "https://github.com/Celarius/spin-framework", "description": "A super lightweight PHP UI/REST Framework", "author": { From 8a961d833352485ff1fba4e5c9490cd9aecc2243 Mon Sep 17 00:00:00 2001 From: Kim Sandell Date: Tue, 30 Jun 2026 09:25:08 +0200 Subject: [PATCH 3/4] docs: reference getRequest()->getBody() for QUERY body, not app-level helpers Code review found the QUERY docs/docblocks referenced $this->body / decodeJsonBody(), which are app-level (skeleton) constructs not present on the framework base Controller. Point to the PSR-7 getRequest()->getBody() instead. --- .claude/llm-instructions.md | 2 +- doc/Reference/API-Reference.md | 2 +- doc/User-Guide/Routing.md | 2 +- src/Core/Controller.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/llm-instructions.md b/.claude/llm-instructions.md index 6856e6a..c578574 100644 --- a/.claude/llm-instructions.md +++ b/.claude/llm-instructions.md @@ -203,7 +203,7 @@ class UserController extends Controller **Handler methods:** `handleGET()`, `handlePOST()`, `handlePUT()`, `handleDELETE()`, `handlePATCH()`, `handleHEAD()`, `handleOPTIONS()`, `handleQUERY()` -> `handleQUERY()` handles the QUERY verb — safe + idempotent like GET but with a request body. Read it via `$this->body` / `decodeJsonBody()`. +> `handleQUERY()` handles the QUERY verb — safe + idempotent like GET but with a request body. Read it via `getRequest()->getBody()`. **Conventions:** - `$args` contains URL parameters as array keys (e.g., `['id' => '123']`) diff --git a/doc/Reference/API-Reference.md b/doc/Reference/API-Reference.md index 8adc4af..c788513 100644 --- a/doc/Reference/API-Reference.md +++ b/doc/Reference/API-Reference.md @@ -66,7 +66,7 @@ Base controller class providing HTTP method handlers and convenient access to fr | `handleOPTIONS()` | `public function handleOPTIONS(array $args): ResponseInterface` | Handle OPTIONS requests | | `handleQUERY()` | `public function handleQUERY(array $args): ResponseInterface` | Handle QUERY requests (safe + idempotent like GET, with a request body) | -> **QUERY** carries a request body — read it via `$this->body` / `decodeJsonBody()`. See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. +> **QUERY** carries a request body — read it via `getRequest()->getBody()`. See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. **Usage Example:** diff --git a/doc/User-Guide/Routing.md b/doc/User-Guide/Routing.md index 979e2c2..41dcd0d 100644 --- a/doc/User-Guide/Routing.md +++ b/doc/User-Guide/Routing.md @@ -192,7 +192,7 @@ SPIN controllers use specific method names to handle different HTTP requests: - `handlePATCH(array $args)` - Handles PATCH requests - `handleQUERY(array $args)` - Handles QUERY requests -> **QUERY** is safe and idempotent like GET but carries a request body. Read it via `$this->body` / `decodeJsonBody()` (same as POST). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. +> **QUERY** is safe and idempotent like GET but carries a request body. Read it via `getRequest()->getBody()` (same as POST). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. ### Controller Parameters diff --git a/src/Core/Controller.php b/src/Core/Controller.php index 86113c3..67f0247 100644 --- a/src/Core/Controller.php +++ b/src/Core/Controller.php @@ -160,7 +160,7 @@ public function handleOPTIONS(array $args) * Handle QUERY request * * QUERY is safe and idempotent like GET but carries a request body. Read the - * body via $this->body / decodeJsonBody() (same as POST). See IETF + * body via getRequest()->getBody() (same as POST). See IETF * draft-ietf-httpbis-safe-method-w-body and OpenAPI 3.2. * * @param array $args Path variable arguments as name=value pairs From 6a22e6e065fcb05154808fa73793a67dbc3b6f31 Mon Sep 17 00:00:00 2001 From: Kim Sandell Date: Tue, 30 Jun 2026 09:30:04 +0200 Subject: [PATCH 4/4] docs: backfill missing 0.0.41 changelog section 0.0.41 bumped the version (Redis integer round-trip fix, #78) but never added a changelog entry, leaving a gap between 0.0.42 and 0.0.40. Restore the trace. --- changelog.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 259d1c4..b3e1b71 100644 --- a/changelog.md +++ b/changelog.md @@ -3,7 +3,11 @@ SPIN Framework Changelog ## 0.0.42 -- **Feature:** HTTP `QUERY` method support — controllers gain a first-class `handleQUERY()` handler and `QUERY` is in the default route method set. QUERY is safe + idempotent like GET but carries a request body (read via `$this->body` / `decodeJsonBody()`). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. +- **Feature:** HTTP `QUERY` method support — controllers gain a first-class `handleQUERY()` handler and `QUERY` is in the default route method set. QUERY is safe + idempotent like GET but carries a request body (read via `getRequest()->getBody()`). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. + +## 0.0.41 +- **Fix:** Redis adapter `get()` returned `false` for integer values stored via `set()` — Predis always returns strings, so the `is_int()` fast-path never matched and the raw integer was passed to `unserialize()`. `get()` now detects raw integers via `filter_var()` and uses an explicit null check, so falsy stored values are no longer mistaken for cache misses (#78) +- **Test:** Add `testValueRoundTrip` data-provider regression tests to the Redis and APCu adapter suites ## 0.0.40 - **Feature:** Add domain exception classes: `CacheException`, `ConfigException`, `DatabaseException`, `MiddlewareException` — all extending `SpinException`