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..c578574 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 `getRequest()->getBody()`. **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..b3e1b71 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,13 @@ # 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 `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` - **Feature:** Wire `CacheException` into cache adapters (APCu, Redis), `ConfigException` into `Config`, `DatabaseException` into `PdoConnection` and `ConnectionManager` 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/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..c788513 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 `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 7369991..41dcd0d 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 `getRequest()->getBody()` (same as POST). See IETF `draft-ietf-httpbis-safe-method-w-body` and OpenAPI 3.2. ### Controller Parameters 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": { 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..67f0247 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 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 + * + * @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()); + } +}