From b2a29e48d411c7acd5514468a2d26cfbb0dc6206 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:15:36 -0300 Subject: [PATCH 01/14] feat: complete v2.1.0 deprecation cycle, fix response auto-emit, PSR-14 events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This bundles the work accumulated for the v2.1.0 pre-release, validated together via the full CI suite (1067 tests, 0 failures): Response emission model (bug fix): - Response::json()/text()/html() no longer auto-emit. Emission now happens exactly once, at Application::run() — the only place that's guaranteed to run after the full middleware pipeline (including post-handler middleware) has finished. Previously, auto-emit inside the handler sent bytes before post-handler middleware could act on the response, and every request logged a spurious "body already sent" warning from the redundant emit() call in run(). - Application::run() now guards its emit() call with $response->isSent(). - set_exception_handler (registered in configureBasicErrorHandling() and configureErrorHandling()) previously discarded handleException()'s return value, relying entirely on the auto-emit side effect to get the error response to the client. Now emits explicitly — otherwise removing auto-emit would have silently swallowed error responses for exceptions that escape the handle()/run() flow entirely (e.g. bootstrap errors). - disableAutoEmit() kept as a no-op for backward compatibility with existing callers. v2.1.0 deprecation cycle (docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md, ITEM-001 through ITEM-008, all completed 2026-05-29): - Core\Container, Request::getIp(), LoadShedder, Performance\RateLimitMiddleware and Str::startsWith/endsWith/contains marked @deprecated with trigger_error(E_USER_DEPRECATED), scheduled for removal in v3.0.0. - Providers\Logger replaced by Logging\PsrLogger (PSR-3); Providers\Logger kept as a deprecated shim. Dead Logging\Logger/FileHandler/ LogHandlerInterface removed. - Providers\EventDispatcher and Providers\ListenerProvider deprecated in favor of Events\EventDispatcher (now PSR-14 compliant) and Events\ListenerProvider. Other fixes captured in tasks/ (2026-05-29): Request body-parsing JSON array fallback, HeaderRequest missing strict_types, Validator required false-negative on integer zero, Psr7Pool header-iteration mutation on reset, static cached-input leaking across requests, missing CustomHeaderCollection import in Request. Housekeeping: react/http moved from require to suggest (only needed by the pivotphp/reactphp extension); obsolete v1.1.4 examples removed; tests/Services/* merged into tests/Http/*; ContainerTestSimple removed (superseded by ContainerTest). --- CLAUDE.md | 162 ++-- README.md | 74 +- composer.json | 2 +- .../technical/DEPRECATION_AND_REMOVAL_PLAN.md | 530 +++++++++++ docs/technical/INCONSISTENCIES_REPORT.md | 709 +++++++++++++++ examples/01-basics/hello-world.php | 20 +- examples/02-routing/route-parameters-v114.php | 742 --------------- .../03-middleware/custom-middleware-v114.php | 844 ------------------ examples/04-api/rest-api-modernized-v114.php | 791 ---------------- examples/04-api/rest-api-v114.php | 738 --------------- examples/05-performance/high-performance.php | 14 +- examples/07-advanced/array-callables-v114.php | 513 ----------- examples/07-advanced/performance-v1.1.3.php | 427 --------- .../json-pool-demo-v114.php | 537 ----------- .../enhanced-errors-v114.php | 555 ------------ examples/README.md | 87 +- phpstan.neon | 6 +- src/Core/Application.php | 160 ++-- src/Core/Container.php | 7 +- src/Events/EventDispatcher.php | 60 +- src/Events/ListenerProvider.php | 112 +++ .../GlobalsToServerRequestAdapter.php | 10 +- src/Http/Adapters/Psr7PoolAdapter.php | 2 +- src/Http/CustomHeaderCollection.php | 83 ++ src/Http/HeaderRequest.php | 20 +- src/Http/Pool/Psr7Pool.php | 20 +- src/Http/Psr7/Adapters/HeaderPoolAdapter.php | 6 +- src/Http/Psr7/ServerRequest.php | 2 +- src/Http/Request.php | 159 +--- src/Http/Response.php | 45 +- src/Logging/FileHandler.php | 57 -- src/Logging/LogHandlerInterface.php | 16 - src/Logging/Logger.php | 171 ---- src/Logging/PsrLogger.php | 148 +++ .../Http/ApiDocumentationMiddleware.php | 65 +- src/Middleware/LoadShedder.php | 22 +- src/Middleware/MiddlewareStack.php | 34 +- .../Performance/RateLimitMiddleware.php | 18 +- src/Middleware/RateLimiter.php | 2 +- src/Providers/EventDispatcher.php | 6 + src/Providers/EventServiceProvider.php | 8 +- src/Providers/ExtensionManager.php | 4 +- src/Providers/ListenerProvider.php | 10 + src/Providers/Logger.php | 3 + src/Providers/LoggingServiceProvider.php | 3 +- src/Support/HookManager.php | 2 +- src/Support/Str.php | 11 + src/Utils/Arr.php | 6 +- src/Utils/Utils.php | 12 +- src/Validation/Validator.php | 8 +- src/aliases-performance-tools.php | 4 +- src/aliases.php | 80 +- ...ire-renomeacao-sem-retrocompatibilidade.md | 88 ++ ...-05-29-headerrequest-falta-strict-types.md | 65 ++ ...parsebody-logic-bug-json-array-fallback.md | 105 +++ ...psr7pool-reset-header-iteration-mutacao.md | 76 ++ ...srlogger-nome-hardcoded-express-php-log.md | 61 ++ ...ing-use-import-custom-header-collection.md | 66 ++ ...ic-cached-input-isolacao-entre-requests.md | 98 ++ ...or-required-false-negative-integer-zero.md | 81 ++ tests/Core/ContainerTest.php | 6 + tests/Core/ContainerTestSimple.php | 70 -- tests/Core/RateLimitMiddlewareTestPsr15.php | 2 +- tests/Http/HeaderRequestTest.php | 237 +++++ tests/Http/RequestTest.php | 156 +++- tests/Http/ResponseTest.php | 232 +++++ tests/Middleware/Core/BaseMiddlewareTest.php | 2 +- .../Performance/RateLimitMiddlewareTest.php | 13 + tests/Middleware/RateLimiterTest.php | 2 +- tests/Middleware/SimpleLoadShedderTest.php | 10 + tests/Services/HeaderRequestTest.php | 255 ------ tests/Services/RequestTest.php | 173 ---- tests/Services/ResponseTest.php | 248 ----- tests/Support/HttpTestCase.php | 36 + tests/Support/StrTest.php | 13 + 75 files changed, 3491 insertions(+), 6731 deletions(-) create mode 100644 docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md create mode 100644 docs/technical/INCONSISTENCIES_REPORT.md delete mode 100644 examples/02-routing/route-parameters-v114.php delete mode 100644 examples/03-middleware/custom-middleware-v114.php delete mode 100644 examples/04-api/rest-api-modernized-v114.php delete mode 100644 examples/04-api/rest-api-v114.php delete mode 100644 examples/07-advanced/array-callables-v114.php delete mode 100644 examples/07-advanced/performance-v1.1.3.php delete mode 100644 examples/08-json-optimization/json-pool-demo-v114.php delete mode 100644 examples/09-error-handling/enhanced-errors-v114.php create mode 100644 src/Events/ListenerProvider.php create mode 100644 src/Http/CustomHeaderCollection.php delete mode 100644 src/Logging/FileHandler.php delete mode 100644 src/Logging/LogHandlerInterface.php delete mode 100644 src/Logging/Logger.php create mode 100644 src/Logging/PsrLogger.php create mode 100644 tasks/2026-05-29-event-dispatcher-fire-renomeacao-sem-retrocompatibilidade.md create mode 100644 tasks/2026-05-29-headerrequest-falta-strict-types.md create mode 100644 tasks/2026-05-29-parsebody-logic-bug-json-array-fallback.md create mode 100644 tasks/2026-05-29-psr7pool-reset-header-iteration-mutacao.md create mode 100644 tasks/2026-05-29-psrlogger-nome-hardcoded-express-php-log.md create mode 100644 tasks/2026-05-29-request-missing-use-import-custom-header-collection.md create mode 100644 tasks/2026-05-29-static-cached-input-isolacao-entre-requests.md create mode 100644 tasks/2026-05-29-validator-required-false-negative-integer-zero.md delete mode 100644 tests/Core/ContainerTestSimple.php delete mode 100644 tests/Services/HeaderRequestTest.php delete mode 100644 tests/Services/RequestTest.php delete mode 100644 tests/Services/ResponseTest.php create mode 100644 tests/Support/HttpTestCase.php diff --git a/CLAUDE.md b/CLAUDE.md index 12e7e2c..0bb7a96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,19 +88,23 @@ composer docker:test-quality # All versions + extended quality metrics ### Running Examples ```bash -composer examples:basic # Basic framework usage -composer examples:auth # Authentication example -composer examples:middleware # Middleware example +composer examples:hello-world # Hello World (01-basics) +composer examples:basic-routes # Basic CRUD routes (01-basics) +composer examples:jwt-auth # JWT authentication (06-security) +composer examples:array-callables # Array callable syntax (07-advanced) +composer examples:performance # Performance mode (05-performance) +composer examples:rest-api # Complete REST API (04-api) ``` -### v1.2.0 Simplicity Edition Features +### v2.0.0 Features ```php // Array callable support (PHP 8.4+ compatible) $app->get('/users', [UserController::class, 'index']); $app->post('/users', [$controller, 'store']); // Router methods now accept callable|array union types -// NOVO v1.2.0: Documentação OpenAPI/Swagger Automática +// v2.0.0: Documentação OpenAPI/Swagger Automática +// Gera paths a partir das rotas registradas (sem parsing de PHPDoc) use PivotPHP\Core\Middleware\Http\ApiDocumentationMiddleware; $app->use(new ApiDocumentationMiddleware([ @@ -109,14 +113,7 @@ $app->use(new ApiDocumentationMiddleware([ 'base_url' => 'http://localhost:8080' ])); -// Suas rotas com documentação PHPDoc $app->get('/users', function($req, $res) { - /** - * @summary List all users - * @description Returns a list of all users in the system - * @tags Users - * @response 200 array List of users - */ return $res->json(['users' => User::all()]); }); @@ -128,28 +125,24 @@ $app->get('/users', function($req, $res) { // Response pool reuse: 0% → 99.9% // Framework throughput: 20,400 → 44,092 ops/sec -// Organized middleware structure (v1.1.2) +// Organized middleware structure use PivotPHP\Core\Middleware\Security\CsrfMiddleware; use PivotPHP\Core\Middleware\Performance\RateLimitMiddleware; use PivotPHP\Core\Middleware\Http\CorsMiddleware; -// Backward compatibility maintained via aliases -use PivotPHP\Core\Http\Psr15\Middleware\CsrfMiddleware; // Still works -use PivotPHP\Core\Support\Arr; // Still works, now points to Utils\Arr - // Consolidated utilities use PivotPHP\Core\Utils\Arr; $result = Arr::get($array, 'nested.key', 'default'); $shuffled = Arr::shuffle($array); // Preserves keys -// JSON optimization (v1.1.1 feature, maintained) +// JSON optimization use PivotPHP\Core\Json\Pool\JsonBufferPool; $json = JsonBufferPool::encodeWithPool($data); $stats = JsonBufferPool::getStatistics(); -// High-performance mode (v1.1.0 feature, maintained) -use PivotPHP\Core\Performance\HighPerformanceMode; -HighPerformanceMode::enable(HighPerformanceMode::PROFILE_HIGH); +// Performance mode (simplified class — v2.0.0 promotes PerformanceMode as default) +use PivotPHP\Core\Performance\PerformanceMode; +PerformanceMode::enable(PerformanceMode::PROFILE_PRODUCTION); ``` ## Code Architecture @@ -159,10 +152,12 @@ HighPerformanceMode::enable(HighPerformanceMode::PROFILE_HIGH); pivotphp-core/ ├── src/ # Framework source code │ ├── Core/ # Application core, container, services -│ ├── Http/ # HTTP layer (Request, Response, PSR-7) +│ ├── Events/ # Event system: EventDispatcher (PSR-14), ListenerProvider +│ ├── Http/ # HTTP layer (Request, Response, PSR-7, CustomHeaderCollection) +│ ├── Logging/ # PSR-3 logging: PsrLogger │ ├── Routing/ # Router and route management │ ├── Middleware/ # Middleware system (Security, Performance, HTTP, Core) -│ ├── Providers/ # Service providers +│ ├── Providers/ # Service providers (Container ativo; demais classes em depreciacao v2.1.0) │ ├── Performance/ # Performance optimization components │ ├── Json/ # JSON optimization and pooling │ ├── Utils/ # Utility classes @@ -185,8 +180,8 @@ pivotphp-core/ ### Core Framework Structure - **Service Provider Pattern**: All major components are registered via service providers in `src/Providers/` - **PSR Standards**: Strict PSR-7 (HTTP messages), PSR-15 (middleware), PSR-12 (coding style) compliance -- **Container**: Dependency injection container at the heart of the framework (`src/Core/Container.php`) -- **Event-Driven**: Event dispatcher with hooks system for extensibility +- **Container**: Dependency injection container ativo e `src/Providers/Container.php` (PSR-11). `src/Core/Container.php` esta depreciado desde v2.1.0 e sera removido em v3.0.0. +- **Event-Driven**: Event dispatcher PSR-14 em `src/Events/EventDispatcher.php` com hooks system para extensibilidade. `src/Providers/EventDispatcher.php` depreciado em v2.1.0. ### Key Components 1. **Application Core** (`src/Core/Application.php`): Main application class that bootstraps the framework @@ -199,26 +194,23 @@ pivotphp-core/ 3. **Middleware Pipeline** (`src/Middleware/`): PSR-15 compliant middleware system organized by responsibility - **Security**: `src/Middleware/Security/` - AuthMiddleware, CsrfMiddleware, XssMiddleware, SecurityHeadersMiddleware - - **Performance**: `src/Middleware/Performance/` - CacheMiddleware, RateLimitMiddleware - - **HTTP**: `src/Middleware/Http/` - CorsMiddleware, ErrorMiddleware + - **Performance**: `src/Middleware/Performance/` - CacheMiddleware, RateLimitMiddleware (depreciado v2.1.0; usar `RateLimiter`) + - **HTTP**: `src/Middleware/Http/` - CorsMiddleware, ErrorMiddleware, ApiDocumentationMiddleware - **Core**: `src/Middleware/Core/` - BaseMiddleware, MiddlewareInterface - - **Advanced**: LoadShedder, TrafficClassifier + - **Advanced**: LoadShedder (depreciado v2.1.0; usar `RateLimiter`), TrafficClassifier 4. **HTTP Layer** (`src/Http/`): PSR-7 hybrid implementation - Express.js style API with PSR-7 compliance - Object pooling via `OptimizedHttpFactory` and `DynamicPoolManager` 5. **Performance Components**: - - **JSON Optimization**: `JsonBufferPool`, `JsonBuffer` (v1.1.1) - - **Pool Management**: `DynamicPoolManager` (consolidated in v1.1.2) - - **Memory Management**: `MemoryManager` - - **Performance Monitoring**: `PerformanceMonitor` (unified in v1.1.2) - - **Distributed Coordination**: `DistributedPoolManager` + - **JSON Optimization**: `JsonBufferPool`, `JsonBuffer` + - **Pool Management**: `PoolManager` in `Http/Pool/` + - **Performance Monitoring**: `PerformanceMonitor` in `Performance/` + - **Performance Mode**: `PerformanceMode` (simplified default, in `Performance/`) -### v1.1.4 Major Improvements & v1.1.2 Architectural Foundation -v1.1.4 delivers major performance breakthroughs built on the v1.1.2 consolidated architecture: +### v2.0.0 Middleware Organization -#### Middleware Organization ``` src/Middleware/ ├── Security/ # Security-focused middlewares @@ -228,30 +220,23 @@ src/Middleware/ │ └── XssMiddleware.php ├── Performance/ # Performance-focused middlewares │ ├── CacheMiddleware.php -│ └── RateLimitMiddleware.php +│ └── RateLimitMiddleware.php # @deprecated v2.1.0 — usar RateLimiter ├── Http/ # HTTP protocol middlewares +│ ├── ApiDocumentationMiddleware.php │ ├── CorsMiddleware.php │ └── ErrorMiddleware.php └── Core/ # Base middleware infrastructure ├── BaseMiddleware.php └── MiddlewareInterface.php +# LoadShedder.php (raiz Middleware/) — @deprecated v2.1.0 — usar RateLimiter ``` -#### v1.1.4 Performance Optimizations -- **Object Pool Crisis Fixed**: Revolutionized pool reuse from 0% to 100% (Request) and 99.9% (Response) +#### v2.0.0 Key Characteristics +- **Object Pool**: Pool reuse 100% (Request) and 99.9% (Response) - **Array Callable Support**: Full PHP 8.4+ compatibility with `callable|array` union types in Router -- **Framework Performance**: +116% improvement (20,400 → 44,092 ops/sec) -- **Test Suite Stabilization**: 100% PSR-12 compliance, PHPUnit 10 compatibility, zero violations - -#### v1.1.2 Foundation (Eliminated Duplications) -- **Support/Arr.php**: Removed, consolidated into `Utils/Arr.php` -- **PerformanceMonitor**: Consolidated from multiple locations into `Performance/PerformanceMonitor.php` -- **DynamicPool**: Unified as `DynamicPoolManager` in `Http/Pool/` - -#### Backward Compatibility -- **12 automatic aliases** maintain 100% compatibility with existing code -- Old namespace imports continue working transparently -- Migration to new structure is optional but recommended +- **Framework Performance**: +116% improvement (20,400 → 44,092 ops/sec) maintained from v1.1.4 +- **Legacy Cleanup**: 18% code reduction — eliminated deprecated classes and legacy namespaces +- **PerformanceMode** replaces `HighPerformanceMode` as the default simplified class ### Request/Response Hybrid Design The framework uses a hybrid approach for PSR-7 compatibility: @@ -261,12 +246,11 @@ The framework uses a hybrid approach for PSR-7 compatibility: ### Testing Approach - Tests organized by domain in `tests/` directory (see phpunit.xml for test suites) -- Three main test suites: Core Tests, Security Tests, and full Express PHP Test Suite +- Test suites: Core, Security, Performance, Integration, Stress, Unit - Each major component has its own test suite - Integration tests verify component interaction -- **v1.1.2 Achievement**: 100% test success rate (430/430 tests passing) +- **v2.0.0**: All 5,548 tests passing (100% success rate) - Enhanced test maintainability with constants instead of hardcoded values -- Comprehensive stress testing in `tests/Stress/` - JSON optimization tests in `tests/Json/Pool/` ### Code Style Requirements @@ -277,17 +261,17 @@ The framework uses a hybrid approach for PSR-7 compatibility: - All new code must include proper type declarations ### Performance Considerations -- Framework optimized for high throughput (48,323 ops/sec average in v1.1.2) -- v1.1.0 achieves 25x faster Request/Response creation with pooling -- v1.1.1 provides automatic JSON optimization with 161K ops/sec (small), 17K ops/sec (medium), 1.7K ops/sec (large) -- v1.1.2 maintains performance while reducing codebase size by 3.1% +- Framework optimized for high throughput (44,092 ops/sec in v2.0.0) +- Object pool reuse 100% (Request) and 99.9% (Response) with lazy loading +- JSON optimization with automatic pooling threshold (256 bytes) +- v2.0.0 reduced codebase by 18% compared to v1.2.0 while maintaining performance - Benchmark any performance-critical changes using `composer benchmark` - Avoid unnecessary object creation in hot paths - Use lazy loading for optional dependencies ## Route Handler Syntax -PivotPHP Core supports the following route handler syntaxes (v1.1.4 adds full array callable support): +PivotPHP Core supports the following route handler syntaxes: ### ✅ Supported Syntaxes ```php @@ -296,7 +280,7 @@ $app->get('/users', function($req, $res) { return $res->json(['users' => []]); }); -// Array callable with class (NEW: Enhanced in v1.1.4) +// Array callable with class $app->get('/users', [UserController::class, 'index']); // Static/Instance method $app->post('/users', [$controller, 'store']); // Instance method $app->put('/users/:id', [UserController::class, 'update']); // With parameters @@ -314,11 +298,9 @@ $app->get('/users', 'getUsersHandler'); $app->get('/users', 'UserController@index'); // TypeError! ``` -**v1.1.4 Improvements**: Router methods now use `callable|array` union types for PHP 8.4+ strict typing compatibility. +**Important**: Router methods use `callable|array` union types for PHP 8.4+ strict typing compatibility. Strings in the format `Controller@method` are not considered callable by PHP and will result in a TypeError. -**Important**: The framework validates that all handlers are `callable`. Strings in the format `Controller@method` are not considered callable by PHP and will result in a TypeError. - -**Migration**: Replace `'Controller@method'` with `[Controller::class, 'method']` in all documentation examples. +**Migration**: Replace `'Controller@method'` with `[Controller::class, 'method']` in all code. ## Development Workflow @@ -328,16 +310,16 @@ $app->get('/users', 'UserController@index'); // TypeError! 4. Code style must comply with PSR-12 5. For releases, use `./scripts/release/prepare_release.sh` followed by `./scripts/release/release.sh` -### Array Callable Testing (v1.1.4) +### Array Callable Testing When implementing array callable routes, verify compatibility: ```bash # Test array callable functionality -vendor/bin/phpunit tests/Unit/Routing/RouterArrayCallableTest.php +vendor/bin/phpunit tests/Unit/Routing/ArrayCallableTest.php vendor/bin/phpunit tests/Integration/Routing/ArrayCallableIntegrationTest.php # Test parameter routing with array callables -vendor/bin/phpunit tests/Examples/ParameterRoutingExampleTest.php +vendor/bin/phpunit tests/Unit/Routing/ParameterRoutingTest.php ``` ### Debugging and Troubleshooting @@ -355,9 +337,6 @@ vendor/bin/phpunit tests/Middleware/Security/ --testdox composer benchmark:simple # Quick performance check vendor/bin/phpunit tests/Performance/ --group performance -# Memory usage analysis -vendor/bin/phpunit tests/Performance/MemoryManagerTest.php - # JSON pool debugging vendor/bin/phpunit tests/Json/Pool/ --testdox ``` @@ -379,21 +358,15 @@ namespace PivotPHP\Core\Middleware\Http; use PivotPHP\Core\Middleware\Core\BaseMiddleware; ``` -### JSON Optimization System (v1.1.1) +### JSON Optimization System -The framework includes a sophisticated JSON pooling system that dramatically improves performance for JSON operations: +The framework includes a JSON pooling system that improves performance for JSON operations: #### Automatic Optimization -- **Smart Detection**: Automatically uses pooling for datasets that benefit (arrays 10+ elements, objects 5+ properties, strings >1KB) +- **Smart Threshold**: Automatically uses pooling for data above 256 bytes - **Transparent Fallback**: Small data uses traditional `json_encode()` for optimal performance - **Zero Configuration**: Works out-of-the-box with existing code -#### Performance Characteristics -- **Throughput**: 161K ops/sec (small), 17K ops/sec (medium), 1.7K ops/sec (large) in Docker testing -- **Reuse Rate**: 100% buffer reuse in high-frequency scenarios -- **Memory Efficiency**: Significant reduction in garbage collection pressure -- **Scalability**: Adaptive pool sizing based on usage patterns - #### Manual Control ```php // Direct pool usage @@ -401,14 +374,9 @@ $json = JsonBufferPool::encodeWithPool($data); // Configuration for production workloads JsonBufferPool::configure([ + 'threshold_bytes' => 256, // Use pool only for data > 256 bytes 'max_pool_size' => 200, 'default_capacity' => 8192, - 'size_categories' => [ - 'small' => 2048, // 2KB - 'medium' => 8192, // 8KB - 'large' => 32768, // 32KB - 'xlarge' => 131072 // 128KB - ] ]); // Real-time monitoring @@ -418,13 +386,13 @@ $stats = JsonBufferPool::getStatistics(); ## Current Version Status -- **Current Version**: 1.2.0 (Simplicity Edition - Simplicidade sobre Otimização Prematura) +- **Current Version**: 2.0.0 (Legacy Cleanup Edition - Simplicity through Elimination) - **Release Date**: 2025-07-21 (Quality & Maintainability Release) - **Previous Versions**: 1.1.4 (Developer Experience), 1.1.3 (Performance Breakthrough), 1.1.2 (Consolidation), 1.1.1 (JSON Optimization), 1.1.0 (High-Performance) - **Tests Status**: 684 CI tests + 131 integration tests (100% success rate), architectural simplification - **Performance**: +116% framework improvement (20,400 → 44,092 ops/sec), 100% object pool reuse - **Code Quality**: PHPStan Level 9, PSR-12 100% compliant, **zero IDE warnings**, enhanced readability -- **Architecture**: Simple classes as core defaults, Legacy namespace for complex classes, automatic OpenAPI/Swagger documentation +- **Architecture**: Simple classes as core defaults, deprecated complex classes removed, automatic OpenAPI/Swagger documentation - **Compatibility**: 100% backward compatible via automatic aliases - **Key Features**: ApiDocumentationMiddleware for automatic OpenAPI/Swagger generation, simplified core classes, enhanced developer experience @@ -446,9 +414,9 @@ composer cs:fix # Auto-fix code style - The event system allows for deep customization without modifying core code - Documentation updates should be made in the `/docs` directory when adding features -### v1.2.0 Key Changes +### v2.0.0 Key Changes - **🎯 Simplicity Edition**: Simple classes promoted to core defaults (PerformanceMode, LoadShedder, MemoryManager, etc.) -- **🏗️ Legacy Architecture**: Complex classes moved to `src/Legacy/` namespace for backward compatibility +- **🏗️ Legacy Cleanup**: Deprecated complex classes and legacy aliases removed (breaking change vs v1.x) - **📖 Automatic OpenAPI/Swagger Documentation**: New `ApiDocumentationMiddleware` for automatic documentation generation - **🔄 100% Backward Compatibility**: All existing code continues to work via automatic aliases - **⚡ Performance Maintained**: All v1.1.4 performance improvements preserved @@ -457,7 +425,7 @@ composer cs:fix # Auto-fix code style Following the "Simplicidade sobre Otimização Prematura" principle: - **✅ Simple Classes as Core**: `PerformanceMode`, `LoadShedder`, `MemoryManager`, `PoolManager`, etc. are now the default implementations -- **✅ Legacy Namespace**: Complex classes moved to `src/Legacy/` for those who need advanced features +- **✅ Clean Removal**: Deprecated and complex classes removed — codebase reduced by 18% - **✅ Automatic Documentation**: `ApiDocumentationMiddleware` provides automatic OpenAPI/Swagger generation - **✅ Zero Breaking Changes**: All existing code continues to work without modification through aliases - **✅ Clean Architecture**: Focused on essential functionality without unnecessary complexity @@ -465,11 +433,11 @@ Following the "Simplicidade sobre Otimização Prematura" principle: **Key Principle**: "Simplicidade sobre Otimização Prematura" - Simple, correct code over complex "optimized" code. #### 📖 **Automatic OpenAPI/Swagger Documentation** -The v1.2.0 introduces `ApiDocumentationMiddleware` that automatically: -- Generates OpenAPI 3.0.0 specification from all routes +The v2.0.0 introduces `ApiDocumentationMiddleware` that automatically: +- Generates OpenAPI 3.0.0 specification from all registered routes - Provides `/docs` endpoint with JSON OpenAPI - Provides `/swagger` endpoint with Swagger UI interface -- Parses PHPDoc comments for route metadata +- Generates basic path entries from route method and path (no PHPDoc parsing) - Requires zero configuration to work ```php @@ -480,7 +448,7 @@ $app->use(new ApiDocumentationMiddleware([ ])); ``` -### Architectural Foundation (v1.1.2+) -- Organized middleware structure while maintaining full backward compatibility -- All performance optimizations from v1.1.1 and v1.1.0 are preserved and enhanced -- Migration to new namespace structure is recommended but optional +### Architectural Foundation (v2.0.0) +- Organized middleware structure with Security, Performance, Http, and Core namespaces +- 18% code reduction — legacy aliases and deprecated classes removed +- All performance optimizations from v1.1.4 are preserved diff --git a/README.md b/README.md index 7393825..9b55ee5 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ echo "Operações: {$stats['total_operations']}\n"; ### 🔍 Enhanced Error Diagnostics -PivotPHP v1.2.0 mantém **ContextualException** para diagnósticos avançados de erros: +PivotPHP v2.0.0 mantém **ContextualException** para diagnósticos avançados de erros: #### ⚡ Sistema de Erro Inteligente @@ -395,7 +395,7 @@ ContextualException::configure([ ]); ``` -#### ✨ Recursos v1.2.0 +#### ✨ Recursos v2.0.0 - ✅ **Erro IDs Únicos** - Rastreamento facilitado para debugging - ✅ **Sugestões Inteligentes** - Orientações específicas para resolver problemas @@ -409,14 +409,14 @@ ContextualException::configure([ - [JsonBufferPool Optimization Guide](docs/technical/json/BUFFER_POOL_OPTIMIZATION.md) - [Enhanced Error Diagnostics](docs/technical/error-handling/CONTEXTUAL_EXCEPTION_GUIDE.md) -### 📖 Documentação OpenAPI/Swagger Automática (v1.2.0+) +### 📖 Documentação OpenAPI/Swagger Automática (v2.0.0+) -O PivotPHP v1.2.0+ inclui **middleware automático** para geração de documentação OpenAPI/Swagger: +O PivotPHP v2.0.0+ inclui **middleware automático** para geração de documentação OpenAPI/Swagger: ```php use PivotPHP\Core\Middleware\Http\ApiDocumentationMiddleware; -// ✅ NOVO v1.2.0+: Documentação automática em 3 linhas! +// v2.0.0: Documentação automática em 3 linhas! $app = new Application(); // Adicionar middleware de documentação automática @@ -426,26 +426,12 @@ $app->use(new ApiDocumentationMiddleware([ 'base_url' => 'http://localhost:8080' ])); -// Suas rotas com documentação PHPDoc +// Registrar rotas normalmente $app->get('/users', function($req, $res) { - /** - * @summary List all users - * @description Returns a list of all users in the system - * @tags Users - * @response 200 array List of users - */ return $res->json(['users' => User::all()]); }); $app->get('/users/:id', function($req, $res) { - /** - * @summary Get user by ID - * @description Returns a single user by their ID - * @tags Users - * @param int id User ID - * @response 200 object User object - * @response 404 object User not found - */ $userId = $req->param('id'); return $res->json(['user' => User::find($userId)]); }); @@ -457,14 +443,15 @@ $app->get('/users/:id', function($req, $res) { #### 🎯 Recursos do Middleware de Documentação -- ✅ **Geração automática** de OpenAPI 3.0.0 de todas as rotas +- ✅ **Geração automática** de OpenAPI 3.0.0 de todas as rotas registradas - ✅ **Interface Swagger UI** integrada (zero configuração) -- ✅ **Parsing de PHPDoc** para metadados das rotas - ✅ **Endpoints automáticos** `/docs` e `/swagger` - ✅ **Configuração flexível** de paths e URLs - ✅ **Zero dependências** externas - ✅ **Compatibilidade total** com todas as rotas +> **Nota**: O middleware gera paths básicos (método HTTP + caminho) a partir das rotas registradas. Metadados adicionais (descrições, tags, parâmetros) devem ser configurados manualmente no objeto OpenAPI retornado, ou via extensão futura. + #### 📝 Exemplo Completo Veja o exemplo funcional em [`examples/api_documentation_example.php`](examples/api_documentation_example.php): @@ -608,16 +595,16 @@ O PivotPHP oferece suporte duplo para PSR-7, permitindo uso com projetos moderno ### Verificar versão atual ```bash -php scripts/switch-psr7-version.php --check +php scripts/utils/switch-psr7-version.php --check ``` ### Alternar entre versões ```bash # Mudar para PSR-7 v1.x (compatível com ReactPHP) -php scripts/switch-psr7-version.php 1 +php scripts/utils/switch-psr7-version.php 1 # Mudar para PSR-7 v2.x (padrão moderno) -php scripts/switch-psr7-version.php 2 +php scripts/utils/switch-psr7-version.php 2 ``` ### Após alternar versões @@ -633,28 +620,28 @@ Veja a [documentação completa sobre PSR-7](docs/technical/compatibility/psr7-d --- -## 🏗️ Arquitetura v1.2.0 (Simplicity Edition) +## 🏗️ Arquitetura v2.0.0 (Legacy Cleanup Edition) -O PivotPHP v1.2.0 simplifica a arquitetura seguindo o princípio "Simplicidade sobre Otimização Prematura", **priorizando facilidade de uso para provas de conceito**: +O PivotPHP v2.0.0 simplifica a arquitetura seguindo o princípio "Simplicidade sobre Otimização Prematura", **priorizando facilidade de uso para provas de conceito**: -### 🎯 Recursos v1.2.0 +### 🎯 Recursos v2.0.0 #### 🚀 Array Callables Nativos ```php -// ✅ MANTIDO v1.2.0: Suporte nativo a array callables +// Suporte nativo a array callables $app->get('/users', [UserController::class, 'index']); $app->post('/users', [$userController, 'store']); -// ✅ Validação automática de métodos +// Validação automática de métodos // Se método for privado/protegido, erro claro com sugestão -// ✅ Integração total com IDE +// Integração total com IDE // Autocomplete, refactoring, jump-to-definition ``` #### 🧠 JsonBufferPool Inteligente ```php -// ✅ Sistema com threshold de 256 bytes +// Sistema com threshold de 256 bytes // Dados pequenos: json_encode() direto (performance máxima) // Dados grandes: pooling automático (otimização máxima) @@ -663,7 +650,7 @@ $response = $res->json($anyData); // Sempre otimizado! #### 🔍 Enhanced Error Diagnostics ```php -// ✅ ContextualException com sugestões inteligentes +// ContextualException com sugestões inteligentes // Contexto rico, categorização automática, logging integrado try { @@ -673,10 +660,6 @@ try { } ``` -## 🏗️ Arquitetura v1.2.0 (Simplified Foundation) - -O PivotPHP v1.2.0 simplifica a arquitetura v1.1.x, eliminando complexidade desnecessária: - ### 🎯 Estrutura de Middlewares Organizada ``` src/Middleware/ @@ -693,25 +676,24 @@ src/Middleware/ └── ErrorMiddleware.php ``` -### ✅ Melhorias da v1.2.0 (Foco em Simplicidade) +### ✅ Melhorias da v2.0.0 (Legacy Cleanup) +- **🧹 18% code reduction** - 11,871 linhas removidas, código limpo e direto - **🎯 Orientado a Protótipos** - Arquitetura simplificada para desenvolvimento rápido -- **📚 Documentação Didática** - Exemplos práticos e guias de aprendizado - **🔧 Setup Mínimo** - Configuração zero para começar imediatamente - **💡 Conceitos Claros** - Estrutura lógica e intuitiva para estudos -- **🛡️ Qualidade Educacional** - PHPStan Level 9, 100% testes passando para aprendizado +- **🛡️ Qualidade** - PHPStan Level 9, PSR-12 100%, todos os testes passando -### 🔄 Migração para v1.2.0 +### 🔄 Migração para v2.0.0 ```php -// Imports antigos (ainda funcionam via aliases) -use PivotPHP\Core\Http\Psr15\Middleware\CorsMiddleware; -use PivotPHP\Core\Support\Arr; +// Imports v1.x (não funcionam mais — aliases removidos na v2.0.0) +use PivotPHP\Core\Http\Psr15\Middleware\CorsMiddleware; // REMOVIDO -// Imports recomendados (nova estrutura) +// Imports corretos (namespace atual) use PivotPHP\Core\Middleware\Http\CorsMiddleware; use PivotPHP\Core\Utils\Arr; ``` -Veja o [Overview Estrutural](docs/releases/FRAMEWORK_OVERVIEW_v1.2.0.md) para detalhes completos. +Veja o [Overview Estrutural v2.0.0](docs/releases/FRAMEWORK_OVERVIEW_v2.0.0.md) para detalhes completos. --- diff --git a/composer.json b/composer.json index 9eb43bf..8893746 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,6 @@ "psr/log": "^3.0", "psr/cache": "^2.0|^3.0", "psr/simple-cache": "^2.0|^3.0", - "react/http": "^1.9", "pivotphp/core-routing": "^1.0" }, "require-dev": { @@ -68,6 +67,7 @@ "ext-apcu": "For caching middleware and performance optimization", "pivotphp/performance-tools": "Separate package with advanced pooling, caching, and middleware compilation (v2.2.0+)", "pivotphp/cycle-orm": "Database ORM integration for PivotPHP", + "react/http": "Required for async/ReactPHP integration (pivotphp/reactphp extension)", "pivotphp/reactphp": "Async runtime extension for continuous execution" }, "autoload": { diff --git a/docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md b/docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md new file mode 100644 index 0000000..a3e3a5f --- /dev/null +++ b/docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md @@ -0,0 +1,530 @@ +# Deprecation and Removal Plan + +**Version:** 2.0.0 → 3.0.0 +**Date:** 2026-05-29 +**Status:** Active + +## Overview + +This document establishes the official deprecation and removal schedule for identified dead code, duplicates, and design violations in PivotPHP Core. The plan follows a two-version cycle: + +- **v2.1.0** — Deprecation announced: `@deprecated` annotations added, `trigger_error(E_USER_DEPRECATED)` calls inserted, documentation updated. +- **v3.0.0** — Breaking removal: deprecated code deleted, tests updated, aliases removed. + +No item is removed without first completing a full deprecation cycle with at least one minor release between announcement and removal. + +--- + +## Summary Table + +| ID | Item | File | Type | Deprecated in | Removed in | Impact | Progresso | +|---|---|---|---|---|---|---|---| +| ITEM-001 | `PivotPHP\Core\Core\Container` | `src/Core/Container.php` | Class | v2.1.0 | v3.0.0 | 1 test file | `@deprecated` + `trigger_error` em `getInstance()` aplicados. Aguardando v3.0.0. | +| ITEM-002 | `Request::getIp()` | `src/Http/Request.php:994` | Method | v2.1.0 | v3.0.0 | 1 src file (RateLimiter) | `@deprecated` ja existia; `trigger_error` adicionado; `RateLimiter.php:68` atualizado para `ip()`. | +| ITEM-003 | `PivotPHP\Core\Middleware\LoadShedder` | `src/Middleware/LoadShedder.php` | Class | v2.1.0 | v3.0.0 | 1 src alias, 1 test file | `@deprecated` + `trigger_error` em `__construct()` e `handle()` aplicados. | +| ITEM-004 | `PivotPHP\Core\Middleware\Performance\RateLimitMiddleware` | `src/Middleware/Performance/RateLimitMiddleware.php` | Class | v2.1.0 | v3.0.0 | 1 validation script, 2 test files | `@deprecated` + `trigger_error` aplicados; FQN errado em `RateLimitMiddlewareTestPsr15.php` corrigido. | +| ITEM-005 | `Str::startsWith/endsWith/contains` | `src/Support/Str.php:80-99` | 3 Methods | v2.1.0 | v3.0.0 | 1 test file (6 assertions) | `@deprecated` + `trigger_error` nos 3 metodos aplicados. | +| ITEM-006 | `PivotPHP\Core\Providers\Logger` | `src/Providers/Logger.php` | Class | v2.1.0 | v3.0.0 | 1 src file (LoggingServiceProvider) | `PsrLogger` criado em `src/Logging/`; `LoggingServiceProvider` atualizado; `@deprecated` + `trigger_error` em `Providers\Logger` aplicados; `Logging\Logger.php` morto removido. | +| ITEM-007 | `PivotPHP\Core\Providers\EventDispatcher` | `src/Providers/EventDispatcher.php` | Class | v2.1.0 | v3.0.0 | `Providers/` como namespace incorreto | `@deprecated` aplicado em `Providers\EventDispatcher`; `Events\EventDispatcher` atualizado para PSR-14 e absorve responsabilidade. | +| ITEM-008 | `PivotPHP\Core\Providers\ListenerProvider` | `src/Providers/ListenerProvider.php` | Class | v2.1.0 | v3.0.0 | `Providers/` como namespace incorreto | `@deprecated` aplicado; `Events\ListenerProvider` e o substituto canonico. | + +--- + +## Items + +--- + +### [ITEM-001] Class: `PivotPHP\Core\Core\Container` + +| Property | Value | +|---|---| +| File | `src/Core/Container.php` | +| Lines | 1–478 | +| Substitute | `PivotPHP\Core\Providers\Container` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | + +**Progresso (2026-05-29)** + +- `@deprecated v2.1.0 Use \PivotPHP\Core\Providers\Container instead.` adicionado na classe. +- `trigger_error(...)` adicionado em `getInstance()`. +- Aguardando v3.0.0 para remocao do arquivo e migracao dos testes. + +**Problem** + +`Core\Container` is a singleton IoC container with reflection-based autowiring, tagging, and `call()`. It is never instantiated by `Application`. The application imports and instantiates `PivotPHP\Core\Providers\Container` (PSR-11 compliant, the actual production container). `Core\Container` is dead code with an incompatible API surface. + +Key differences: +- `Core\Container`: `getInstance()`, `make()`, `call()`, `tag()`, `tagged()`, `bound()` — singleton, private constructor. +- `Providers\Container`: `get()`, `has()`, `bind()`, `singleton()`, `instance()`, `alias()` — PSR-11, public constructor. + +**References found** + +``` +# src/ — zero production references +# tests/: +tests/Core/ContainerTest.php — tests the dead container (573 lines, testing code that has no effect on production) +``` + +**Migration for users** + +1. Replace `use PivotPHP\Core\Core\Container` with `use PivotPHP\Core\Providers\Container`. +2. Replace `Container::getInstance()` with `new Container()` or `$app->make(ContainerInterface::class)`. +3. `make()` with autowiring → use service providers with explicit bindings (`$container->bind(...)`). +4. `tag()` / `tagged()` → no equivalent; use named bindings. +5. `call()` → no equivalent; resolve dependencies manually. + +**Actions for v2.1.0** + +- Add class-level `@deprecated v2.1.0 Use \PivotPHP\Core\Providers\Container instead.` +- Add `trigger_error(...)` in `getInstance()`. +- Suppress deprecation in `tests/Core/ContainerTest.php` during transition. + +**Actions for v3.0.0** + +- Delete `src/Core/Container.php`. +- Delete `tests/Core/ContainerTest.php` (or migrate assertions to test `Providers\Container`). +- No aliases exist in `aliases.php` or `aliases-performance-tools.php`. + +--- + +### [ITEM-002] Method: `Request::getIp()` + +| Property | Value | +|---|---| +| File | `src/Http/Request.php` | +| Lines | 990–999 | +| Substitute | `Request::ip()` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (presente desde v2.0.0) | + +**Progresso (2026-05-29)** + +- `@deprecated` ja existia desde v2.0.0. +- `trigger_error('Request::getIp() is deprecated. Use Request::ip() instead.', E_USER_DEPRECATED)` adicionado no corpo do metodo. +- `src/Middleware/RateLimiter.php:68` atualizado: `$request->getIp()` substituido por `$request->ip()`. + +**Problem** + +`getIp()` delegates to `ip()` since v2.0.0. The historic implementation read `HTTP_X_FORWARDED_FOR` without validating IP ranges, making it spoofable for rate-limiting and access-control. `ip()` applies `FILTER_VALIDATE_IP` with `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE`. Both methods now return the same value, but `getIp()` carries the misleading historic name. + +**References found** + +``` +src/Middleware/RateLimiter.php:68 return $request->getIp(); ← must be fixed in v2.1.0 +src/Http/Request.php:994 @deprecated annotation (already present) +``` + +**Migration for users** + +```php +// Before: +$ip = $request->getIp(); + +// After: +$ip = $request->ip(); +``` + +**Actions for v2.1.0** + +- Add `trigger_error('Request::getIp() is deprecated. Use Request::ip() instead.', E_USER_DEPRECATED)` inside the method body. +- Fix `src/Middleware/RateLimiter.php:68`: change `$request->getIp()` to `$request->ip()`. + +**Actions for v3.0.0** + +- Delete the `getIp()` method from `src/Http/Request.php`. +- Verify `RateLimiter.php` was already updated (ITEM-003 dependency). + +--- + +### [ITEM-003] Class: `PivotPHP\Core\Middleware\LoadShedder` + +| Property | Value | +|---|---| +| File | `src/Middleware/LoadShedder.php` | +| Lines | 1–150 | +| Substitute | `PivotPHP\Core\Middleware\RateLimiter` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | + +**Progresso (2026-05-29)** + +- `@deprecated v2.1.0 Use \PivotPHP\Core\Middleware\RateLimiter instead.` adicionado na classe. +- `trigger_error(...)` adicionado em `__construct()` e `handle()`. +- Alias `'load-shedder'` em `Application::$middlewareAliases` marcado com comentario `// @deprecated v2.1.0 — removed in v3.0.0`. + +**Problem** + +`LoadShedder` is a per-IP rate limiter with a sliding window in process memory. It is a feature-incomplete subset of `RateLimiter`: + +- Tracks a `$requestCounts` array keyed by `"{ip}:{timestamp}"` — grows indefinitely in long-running servers (memory leak, O(n) lookup per request). +- Strategy constants (`STRATEGY_PRIORITY`, `STRATEGY_CONSERVATIVE`, etc.) exist but are never read — single algorithm only. +- Does not implement PSR-15 `MiddlewareInterface`. +- Is registered in `Application::$middlewareAliases` as `'load-shedder'`. + +**References found** + +``` +src/Core/Application.php:103 'load-shedder' => \PivotPHP\Core\Middleware\LoadShedder::class, +tests/Middleware/SimpleLoadShedderTest.php — 1 test file +``` + +**Migration for users** + +```php +// Before: +$app->use(new LoadShedder(100, 60)); + +// After (RateLimiter — equivalent): +use PivotPHP\Core\Middleware\RateLimiter; +$limiter = new RateLimiter([ + 'strategy' => RateLimiter::STRATEGY_SLIDING_WINDOW, + 'max_requests' => 100, + 'window_size' => 60, +]); +$app->use(fn($req, $res, $next) => $limiter->handle($req, $res, $next)); +``` + +**Actions for v2.1.0** + +- Add class-level `@deprecated v2.1.0 Use \PivotPHP\Core\Middleware\RateLimiter instead.` +- Add `trigger_error(...)` in `__construct()` and `handle()`. +- Mark `'load-shedder'` alias in `Application::$middlewareAliases` with comment: `// @deprecated v2.1.0 — removed in v3.0.0`. + +**Actions for v3.0.0** + +- Delete `src/Middleware/LoadShedder.php`. +- Remove `'load-shedder'` entry from `Application::$middlewareAliases`. +- Delete `tests/Middleware/SimpleLoadShedderTest.php`. +- No aliases in `aliases.php` or `aliases-performance-tools.php`. + +--- + +### [ITEM-004] Class: `PivotPHP\Core\Middleware\Performance\RateLimitMiddleware` + +| Property | Value | +|---|---| +| File | `src/Middleware/Performance/RateLimitMiddleware.php` | +| Lines | 1–95 | +| Substitute | `PivotPHP\Core\Middleware\RateLimiter` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | + +**Progresso (2026-05-29)** + +- `@deprecated v2.1.0 Use \PivotPHP\Core\Middleware\RateLimiter. This class uses $_SESSION which violates HTTP statelessness.` adicionado na classe. +- `trigger_error(...)` adicionado em `__construct()`. +- Bug corrigido: `tests/Core/RateLimitMiddlewareTestPsr15.php:6` — FQN errado `PivotPHP\Core\Http\Psr15\Middleware\RateLimitMiddleware` substituido pelo FQN correto `PivotPHP\Core\Middleware\Performance\RateLimitMiddleware`. + +**Problem** + +`RateLimitMiddleware` implements PSR-15 `MiddlewareInterface` but uses PHP sessions (`$_SESSION`) as its storage backend, violating HTTP statelessness: + +- Calls `session_start()` on every request. +- Stores per-IP timestamps in `$_SESSION['rate_limit'][$clientIp][]`. +- Incompatible with ReactPHP, Swoole, CLI testing, and stateless API contexts. + +Additionally, `tests/Core/RateLimitMiddlewareTestPsr15.php:6` imports the **wrong FQN** `PivotPHP\Core\Http\Psr15\Middleware\RateLimitMiddleware` (namespace does not exist) — a latent bug introduced when the namespace was reorganized. + +**References found** + +``` +src/Middleware/Performance/RateLimitMiddleware.php:38-81 session_start() + $_SESSION (production) +scripts/validation/validate_project.php:209 'RateLimitMiddleware' reference +tests/Middleware/Performance/RateLimitMiddlewareTest.php correct FQN +tests/Core/RateLimitMiddlewareTestPsr15.php:6 WRONG FQN — latent bug +``` + +**Migration for users** + +```php +// Before: +use PivotPHP\Core\Middleware\Performance\RateLimitMiddleware; +$app->use(new RateLimitMiddleware(['max' => 100, 'windowMs' => 900000])); + +// After: +use PivotPHP\Core\Middleware\RateLimiter; +$limiter = new RateLimiter([ + 'strategy' => RateLimiter::STRATEGY_SLIDING_WINDOW, + 'max_requests' => 100, + 'window_size' => 900, // windowMs / 1000 +]); +$app->use(fn($req, $res, $next) => $limiter->handle($req, $res, $next)); +``` + +**Actions for v2.1.0** + +- Add `@deprecated v2.1.0 Use \PivotPHP\Core\Middleware\RateLimiter. This class uses $_SESSION which violates HTTP statelessness.` +- Add `trigger_error(...)` in `__construct()`. +- **Fix latent bug** in `tests/Core/RateLimitMiddlewareTestPsr15.php:6`: update import to `PivotPHP\Core\Middleware\Performance\RateLimitMiddleware`. + +**Actions for v3.0.0** + +- Delete `src/Middleware/Performance/RateLimitMiddleware.php`. +- Delete `tests/Middleware/Performance/RateLimitMiddlewareTest.php`. +- Delete `tests/Core/RateLimitMiddlewareTestPsr15.php`. +- Update `scripts/validation/validate_project.php:209` to remove the class reference. +- Audit `"ext-session"` in `composer.json`: keep if `CsrfMiddleware` or `Utils.php` still use `session_start()`. + +--- + +### [ITEM-005] Methods: `Str::startsWith()`, `Str::endsWith()`, `Str::contains()` + +| Property | Value | +|---|---| +| File | `src/Support/Str.php` | +| Lines | `startsWith`: 80–83 / `endsWith`: 88–91 / `contains`: 96–99 | +| Substitute | `str_starts_with()`, `str_ends_with()`, `str_contains()` (PHP 8.0+) | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | +| Note | The `Str` **class** is NOT deprecated — only these 3 methods | + +**Progresso (2026-05-29)** + +- `@deprecated v2.1.0 Use native str_starts_with() instead.` (e equivalentes) adicionados nos 3 metodos. +- `trigger_error(...)` adicionado no corpo de cada um dos 3 metodos. + +**Problem** + +PHP 8.0 (November 2020) introduced `str_starts_with()`, `str_ends_with()`, and `str_contains()` as native functions. PivotPHP Core requires PHP >= 8.1, so these functions are always available. The wrapper methods duplicate functionality already in the language with no added value. + +Zero callers exist in `src/`. The framework itself already uses the native PHP 8 functions throughout `src/Http/`, `src/Middleware/`, etc. + +**References found** + +``` +tests/Support/StrTest.php:55-68 6 assertions across 3 methods (only callers) +# src/ — zero production references +``` + +**Migration for users** + +```php +// Before: +use PivotPHP\Core\Support\Str; +Str::startsWith($url, '/api'); +Str::endsWith($filename, '.php'); +Str::contains($message, 'error'); + +// After (native PHP 8.0+): +str_starts_with($url, '/api'); +str_ends_with($filename, '.php'); +str_contains($message, 'error'); +``` + +**Actions for v2.1.0** + +- Add `@deprecated v2.1.0 Use native str_starts_with() instead.` to each method's PHPDoc. +- Add `trigger_error(...)` at the start of each method body. +- Mark the 6 test assertions in `tests/Support/StrTest.php` with `@group deprecated` or suppress the warning. + +**Actions for v3.0.0** + +- Remove the three methods from `src/Support/Str.php`. +- Remove the corresponding test assertions from `tests/Support/StrTest.php`. + +--- + +### [ITEM-006] Class: `PivotPHP\Core\Providers\Logger` + +| Property | Value | +|---|---| +| File | `src/Providers/Logger.php` | +| Lines | 1–148 | +| Substitute | `PivotPHP\Core\Logging\PsrLogger` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | + +**Progresso (2026-05-29)** + +- `src/Logging/PsrLogger.php` criado com a implementacao migrada de `Providers\Logger` e namespace `PivotPHP\Core\Logging`. +- `LoggingServiceProvider` atualizado para usar `new \PivotPHP\Core\Logging\PsrLogger($logPath)`. +- `@deprecated v2.1.0 Use \PivotPHP\Core\Logging\PsrLogger instead.` adicionado em `Providers\Logger`. +- `trigger_error(...)` adicionado em `Providers\Logger::__construct()`. +- `src/Logging/Logger.php` (codigo morto, sem referencias) removido. + +**Problem** + +Two logger classes exist: + +1. **`Providers\Logger`** (148 lines, extends PSR-3 `AbstractLogger`) — active; used by `LoggingServiceProvider`. Wrongly placed under `Providers/` (not a service provider). +2. **`Logging\Logger`** (171 lines, does NOT implement PSR-3) — dead code; never referenced anywhere in `src/`, tests, or examples. + +`Providers\Logger` is the active implementation but has a namespace placement violation: it is a concrete service implementation living under `Providers/`. It must be moved to `Logging/`. + +**References found** + +``` +src/Providers/LoggingServiceProvider.php:24 return new \PivotPHP\Core\Providers\Logger($logPath); +# Logging\Logger — zero references anywhere (fully dead) +``` + +**Migration for users** + +```php +// Before (direct instantiation): +use PivotPHP\Core\Providers\Logger; +$logger = new Logger('/path/to/app.log'); + +// After — resolve via container (preferred): +use Psr\Log\LoggerInterface; +$logger = $app->make(LoggerInterface::class); + +// Or — direct instantiation in v2.1.0–v2.x: +use PivotPHP\Core\Logging\PsrLogger; +$logger = new PsrLogger('/path/to/app.log'); +``` + +**Actions for v2.1.0** + +1. Create `src/Logging/PsrLogger.php` (copy implementation from `Providers\Logger`, change namespace to `PivotPHP\Core\Logging`). +2. Update `LoggingServiceProvider` to use `new \PivotPHP\Core\Logging\PsrLogger($logPath)`. +3. Add `@deprecated v2.1.0 Use \PivotPHP\Core\Logging\PsrLogger instead.` to `Providers\Logger`. +4. Add `trigger_error(...)` in `Providers\Logger::__construct()`. + +**Actions for v3.0.0** + +- Delete `src/Providers/Logger.php`. +- Delete `src/Logging/Logger.php` (dead handler-based logger — never used). +- Evaluate removing `src/Logging/FileHandler.php` and `src/Logging/LogHandlerInterface.php` if no remaining callers. + +--- + +--- + +### [ITEM-007] Class: `PivotPHP\Core\Providers\EventDispatcher` + +| Property | Value | +|---|---| +| File | `src/Providers/EventDispatcher.php` | +| Substitute | `PivotPHP\Core\Events\EventDispatcher` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | +| Impact | Codigo interno; nenhuma referencia externa identificada | + +**Contexto** + +`Providers\EventDispatcher` era uma implementacao de dispatcher de eventos posicionada no namespace incorreto. Foi depreciada e `Events\EventDispatcher` foi atualizado para implementar PSR-14 (`EventDispatcherInterface`) e absorver integralmente a responsabilidade de despacho de eventos. + +**Migration for users** + +```php +// Before: +use PivotPHP\Core\Providers\EventDispatcher; + +// After: +use PivotPHP\Core\Events\EventDispatcher; +``` + +**Progresso (2026-05-29)** + +- `@deprecated v2.1.0 Use \PivotPHP\Core\Events\EventDispatcher instead.` adicionado em `Providers\EventDispatcher`. +- `Events\EventDispatcher` atualizado para PSR-14 (`Psr\EventDispatcher\EventDispatcherInterface`). +- Aguardando v3.0.0 para remocao de `src/Providers/EventDispatcher.php`. + +**Actions for v3.0.0** + +- Deletar `src/Providers/EventDispatcher.php`. +- Verificar referencias remanescentes em `Application` e service providers. + +--- + +### [ITEM-008] Class: `PivotPHP\Core\Providers\ListenerProvider` + +| Property | Value | +|---|---| +| File | `src/Providers/ListenerProvider.php` | +| Substitute | `PivotPHP\Core\Events\ListenerProvider` | +| Deprecation | v2.1.0 | +| Removal | v3.0.0 | +| Has `@deprecated`? | **Sim** (adicionado na refatoracao 2026-05-29) | +| Impact | Codigo interno; nenhuma referencia externa identificada | + +**Contexto** + +`Providers\ListenerProvider` foi depreciada junto com `Providers\EventDispatcher` como parte da reorganizacao do namespace `Events/`. `Events\ListenerProvider` e o substituto canonico e implementa `Psr\EventDispatcher\ListenerProviderInterface`. + +**Migration for users** + +```php +// Before: +use PivotPHP\Core\Providers\ListenerProvider; + +// After: +use PivotPHP\Core\Events\ListenerProvider; +``` + +**Progresso (2026-05-29)** + +- `@deprecated v2.1.0 Use \PivotPHP\Core\Events\ListenerProvider instead.` adicionado em `Providers\ListenerProvider`. +- `Events\ListenerProvider` confirmado como substituto em `src/Events/ListenerProvider.php`. +- Aguardando v3.0.0 para remocao de `src/Providers/ListenerProvider.php`. + +**Actions for v3.0.0** + +- Deletar `src/Providers/ListenerProvider.php`. +- Verificar referencias remanescentes em service providers e `Application`. + +--- + +## Cross-Cutting Concerns + +### Aliases files + +- **`src/aliases.php`** — Contains routing aliases only. None of the 6 deprecated items referenced. No changes needed. +- **`src/aliases-performance-tools.php`** — Contains performance pool aliases. None of the 6 items referenced. The file itself is marked `@deprecated 2.2.0` and follows its own removal schedule. + +### `composer.json` dependency audit + +`"ext-session": "*"` is required by: +- `RateLimitMiddleware` (ITEM-004 — being removed) +- `CsrfMiddleware` (retained) +- `Utils.php` (retained) + +After removing ITEM-004, `ext-session` remains required due to `CsrfMiddleware` and `Utils`. Do NOT remove it in v3.0.0 without a separate audit. + +### `Application::$middlewareAliases` + +```php +protected array $middlewareAliases = [ + 'load-shedder' => \PivotPHP\Core\Middleware\LoadShedder::class, // ITEM-003 — remove in v3.0.0 + 'rate-limiter' => \PivotPHP\Core\Middleware\RateLimiter::class, // retained +]; +``` + +--- + +## Implementation Sequence + +### v2.1.0 (Deprecation) + +Execute in this order to respect cross-dependencies: + +1. **ITEM-006** — Criar `Logging\PsrLogger`, atualizar `LoggingServiceProvider`, deprecar `Providers\Logger`. **Concluido (2026-05-29).** +2. **ITEM-007** — Deprecar `Providers\EventDispatcher`; atualizar `Events\EventDispatcher` para PSR-14. **Concluido (2026-05-29).** +3. **ITEM-008** — Deprecar `Providers\ListenerProvider`; confirmar `Events\ListenerProvider` como substituto. **Concluido (2026-05-29).** +4. **ITEM-005** — Adicionar `@deprecated` + `trigger_error()` nos tres metodos `Str`. **Concluido (2026-05-29).** +5. **ITEM-004** — Deprecar `RateLimitMiddleware`. Corrigir FQN errado em `RateLimitMiddlewareTestPsr15.php`. **Concluido (2026-05-29).** +6. **ITEM-003** — Deprecar `LoadShedder`. Marcar alias `'load-shedder'` como deprecated em `Application`. **Concluido (2026-05-29).** +7. **ITEM-002** — Adicionar `trigger_error()` em `getIp()`. Corrigir `RateLimiter.php:68`. **Concluido (2026-05-29).** +8. **ITEM-001** — Deprecar `Core\Container` (menor risco — zero chamadores em producao). **Concluido (2026-05-29).** + +### v3.0.0 (Removal) + +Execute in reverse dependency order: + +1. **ITEM-002** — Remover `getIp()` (confirmar que `RateLimiter.php` ja foi atualizado). +2. **ITEM-004** — Remover `RateLimitMiddleware`. Atualizar arquivos de teste e script de validacao. +3. **ITEM-003** — Remover `LoadShedder`. Remover `'load-shedder'` de `Application::$middlewareAliases`. +4. **ITEM-005** — Remover os tres metodos de `Str`. Atualizar arquivo de teste. +5. **ITEM-006** — Remover `Providers\Logger`. Avaliar remocao de `FileHandler` e `LogHandlerInterface`. +6. **ITEM-007** — Remover `Providers\EventDispatcher`. +7. **ITEM-008** — Remover `Providers\ListenerProvider`. +8. **ITEM-001** — Remover `Core\Container`. Deletar `tests/Core/ContainerTest.php`. diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md new file mode 100644 index 0000000..4705b6d --- /dev/null +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -0,0 +1,709 @@ +# Relatório de Inconsistencias Arquiteturais e de Qualidade — PivotPHP Core v2.0.0 + +**Data do Relatório:** 2026-05-29 +**Versao analisada:** 2.0.0 (Legacy Cleanup Edition) +**Responsavel:** Agentes especializados de analise arquitetural e qualidade de codigo + +--- + +## Sumario Executivo + +| Severidade | Quantidade | Impacto Principal | +|------------|:----------:|----------------------------------------------------------| +| Critico | 3 | Seguranca, corretude funcional, container morto | +| Alto | 7 | Bugs fatais em dead code, spoofing de IP, violacao PSR | +| Medio | 11 | Duplicacao, acoplamento, crescimento ilimitado de memoria| +| Baixo | 5 | Legibilidade, duplicacao de funcoes nativas | +| **Total** | **26** | | + +--- + +## Indice + +- [Secao 1 — Inconsistencias Criticas](#secao-1--inconsistencias-criticas) +- [Secao 2 — Inconsistencias de Impacto Alto](#secao-2--inconsistencias-de-impacto-alto) +- [Secao 3 — Inconsistencias de Impacto Medio](#secao-3--inconsistencias-de-impacto-medio) +- [Secao 4 — Inconsistencias de Impacto Baixo](#secao-4--inconsistencias-de-impacto-baixo) +- [Status de Correcao](#status-de-correcao) +- [Priorizacao por Sprint](#priorizacao-por-sprint) + +--- + +## Secao 1 — Inconsistencias Criticas + +### C-01 — Dois Containers IoC incompativeis coexistem no projeto + +**Descricao:** +Existem duas classes `Container` com responsabilidades identicas, em namespaces distintos, sem qualquer integracao entre si. + +- `src/Core/Container.php` — construtor privado, padrao singleton, resolucao por Reflection. Nunca instanciado pela `Application`. +- `src/Providers/Container.php` — implementa `Psr\Container\ContainerInterface` (PSR-11), instanciado diretamente pela `Application` na linha 138. + +Os testes em `tests/Core/ContainerTest.php` cobrem exclusivamente `Core\Container`, ou seja, cobrem codigo morto que nao e executado em producao. + +**Arquivos afetados:** + +| Arquivo | Situacao | +|---------------------------------------|---------------------| +| `src/Core/Container.php` | Container morto (478 linhas) | +| `src/Providers/Container.php` | Container ativo (167 linhas) | +| `src/Core/Application.php` linha 138 | Instancia `Providers\Container` | +| `tests/Core/ContainerTest.php` | Cobre container morto | + +**Impacto tecnico:** +- Cobertura de testes falsa: os testes passam mas nao validam o comportamento real do sistema. +- Risco de manutencao: alteracoes em `Providers\Container` nao sao detectadas pelos testes existentes. +- Violacao do principio de fonte unica de verdade para o container. + +**Recomendacao:** +1. Remover `src/Core/Container.php` ou documenta-lo explicitamente como utilitario sem relacao com a `Application`. +2. Mover os testes para cobrir `Providers\Container`. +3. Definir um unico container como padrao em toda a documentacao. + +--- + +### C-02 — Leitura dupla de `php://input` ignora cache e pode esvaziar o body PSR-7 + +**Descricao:** +O metodo `getCachedInput()` (linha 97) foi introduzido para evitar multiplas leituras do stream `php://input`, que e destruido apos a primeira leitura em PHP. Porem, o metodo `parseBody()` (linha 988) realiza uma segunda leitura direta com `file_get_contents('php://input')`, ignorando completamente o cache. + +```php +// src/Http/Request.php:97 — cache correto +private function getCachedInput(): string +{ + $input = @file_get_contents('php://input'); // primeira leitura + ... +} + +// src/Http/Request.php:988 — leitura direta que ignora o cache +private function parseBody(): void +{ + $input = file_get_contents('php://input'); // stream ja pode estar vazio + ... +} +``` + +O body PSR-7 e populado via `getCachedInput()` (linha 181), enquanto o body Express.js e populado por `parseBody()`. Se `getCachedInput()` for chamado primeiro, a segunda leitura em `parseBody()` retornara string vazia. + +**Arquivos afetados:** + +| Arquivo | Linhas | +|--------------------------------|---------------| +| `src/Http/Request.php` | 97-109, 988 | + +**Impacto tecnico:** +- Body da requisicao pode ficar vazio dependendo da ordem de acesso entre API Express.js e PSR-7. +- Comportamento nao determinista dificulta depuracao. +- Falhas silenciosas em endpoints POST/PUT/PATCH. + +**Recomendacao:** +Substituir `file_get_contents('php://input')` na linha 988 por `$this->getCachedInput()`. + +--- + +### C-03 — `Psr7Pool::resetServerRequest()` descarta `$headers` e `$serverParams` + +**Descricao:** +O metodo `resetServerRequest()` em `src/Http/Pool/Psr7Pool.php` recebe `$headers` e `$serverParams` como parametros mas nao os aplica ao objeto reutilizado do pool. Apenas `method`, `uri`, `body` e `protocolVersion` sao resetados. + +```php +// src/Http/Pool/Psr7Pool.php:236-250 +private static function resetServerRequest( + ServerRequestInterface $request, + string $method, + UriInterface $uri, + StreamInterface $body, + array $headers, // recebido mas ignorado + string $version, + array $serverParams // recebido mas ignorado +): ServerRequestInterface { + return $request + ->withMethod($method) + ->withUri($uri) + ->withBody($body) + ->withProtocolVersion($version); + // $headers e $serverParams nunca sao aplicados +} +``` + +**Arquivos afetados:** + +| Arquivo | Linhas | +|----------------------------------|-----------| +| `src/Http/Pool/Psr7Pool.php` | 236-250 | + +**Impacto tecnico (seguranca):** +- Requests reutilizados do pool carregam headers da requisicao anterior (ex.: `Authorization`, `Cookie`, `X-User-Id`). +- `$serverParams` da requisicao anterior pode vazar para a requisicao corrente. +- Vulnerabilidade de vazamento de dados entre requisicoes em ambientes de alta concorrencia (Swoole, ReactPHP, FrankenPHP). + +**Recomendacao:** +Aplicar `withoutHeader()` para limpar todos os headers existentes antes de aplicar os novos, e aplicar `$serverParams` via metodo `withServerParams()`. + +--- + +## Secao 2 — Inconsistencias de Impacto Alto + +### A-01 — Tres implementacoes de rate limiting incompativeis + +**Descricao:** +O projeto contem tres mecanismos de controle de taxa de requisicoes com designs mutuamente incompativeis: + +| Componente | Mecanismo de estado | Interface | +|-----------------------------------------------|--------------------------|-----------------| +| `src/Middleware/LoadShedder.php` | Array em memoria (`$requestCounts`) | Closure/callable | +| `src/Middleware/RateLimiter.php` | Nao identificado | Propria | +| `src/Middleware/Performance/RateLimitMiddleware.php` | `$_SESSION` (PHP) | PSR-15 | + +`RateLimitMiddleware` inicia sessao PHP (`session_start()`) dentro de um middleware PSR-15, violando o principio de statelessness HTTP e tornando o componente incompativel com proxies, load balancers e servidores asincronos. + +**Arquivos afetados:** + +| Arquivo | Linhas | +|------------------------------------------------------|----------| +| `src/Middleware/Performance/RateLimitMiddleware.php` | 38-64 | +| `src/Middleware/LoadShedder.php` | Geral | +| `src/Middleware/RateLimiter.php` | Geral | + +**Impacto tecnico:** +- `RateLimitMiddleware` falha em ambientes sem sessao PHP (APIs REST puras, Swoole). +- Tres implementacoes sem composicao ou hierarquia criam ambiguidade para desenvolvedores. +- Impossibilidade de usar as tres juntas sem conflito de estado. + +**Recomendacao:** +Consolidar em uma unica interface com implementacoes intercambiaveis (storage em memoria, Redis, sessao). Extrair a logica de estado para um `RateLimitStorage` injetavel. + +--- + +### A-02 — `MiddlewareStack::warmupCommonPipelines()` chama metodo inexistente + +**Descricao:** +O metodo `warmupCommonPipelines()` em `MiddlewareStack.php` contem closures que chamam `$resp->setHeader()`. Este metodo nao existe na classe `Response`. O metodo correto e `header()`. + +```php +// src/Middleware/MiddlewareStack.php:283 +function ($req, $resp, $next) { + $resp->setHeader('Access-Control-Allow-Origin', '*'); // metodo inexistente + return $next($req, $resp); +} +``` + +O metodo correto em `src/Http/Response.php` e `header(string $name, string $value): self` (linha 172). + +**Arquivos afetados:** + +| Arquivo | Linhas | +|----------------------------------------|-------------| +| `src/Middleware/MiddlewareStack.php` | 283, 289, 295, 296 | +| `src/Http/Response.php` | 172 (metodo correto) | + +**Impacto tecnico:** +- Fatal error (`Call to undefined method`) ao chamar `warmupCommonPipelines()` em producao. +- Dead code com bug fatal que nao e coberto por testes. + +**Recomendacao:** +Substituir todas as ocorrencias de `setHeader(` por `header(` no metodo `warmupCommonPipelines()` e adicionar testes cobrindo o warmup. + +--- + +### A-03 — Dois metodos de obtencao de IP com logicas incompativeis + +**Descricao:** +A classe `Request` expoe dois metodos publicos para obtencao do IP do cliente com comportamentos distintos: + +```php +// src/Http/Request.php:419 — valida com FILTER_FLAG_NO_PRIV_RANGE +public function ip(): string +{ + // Rejeita IPs privados (192.168.x.x, 10.x.x.x, etc.) + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return $ip; + } +} + +// src/Http/Request.php:1085 — sem validacao alguma +public function getIp(): string +{ + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + return trim($ips[0]); // retorna sem validar + } +} +``` + +**Arquivos afetados:** + +| Arquivo | Linhas | +|--------------------------|--------------| +| `src/Http/Request.php` | 419, 1085 | + +**Impacto tecnico (seguranca):** +- `getIp()` e vulneravel a IP spoofing via header `X-Forwarded-For` sem qualquer validacao. +- Codigo de autenticacao ou auditoria que usa `getIp()` pode registrar IPs forjados. +- Inconsistencia silenciosa: o desenvolvedor nao tem indicacao de qual metodo e seguro. + +**Recomendacao:** +Deprecar `getIp()` e unificar no metodo `ip()`. Documentar explicitamente o comportamento de validacao. + +--- + +### A-04 — `ApiDocumentationMiddleware` viola PSR-15 instanciando `Response` Express.js + +**Descricao:** +`ApiDocumentationMiddleware` implementa `MiddlewareInterface` (PSR-15) mas instancia diretamente `PivotPHP\Core\Http\Response` (classe Express.js do framework) em vez de usar `ResponseInterface` via factory. + +```php +// src/Middleware/Http/ApiDocumentationMiddleware.php:78, 137, 193 +$response = new Response(); // Response Express.js, nao PSR-7 +``` + +A assinatura do metodo `process()` retorna `ResponseInterface`, mas o objeto criado e a implementacao Express.js especifica do framework. + +**Arquivos afetados:** + +| Arquivo | Linhas | +|------------------------------------------------------|---------------| +| `src/Middleware/Http/ApiDocumentationMiddleware.php` | 8, 78, 137, 193 | + +**Impacto tecnico:** +- Acoplamento duro ao `Response` Express.js impede uso do middleware com outros stacks PSR-15. +- Viola o contrato de desacoplamento esperado em componentes PSR. + +**Recomendacao:** +Injetar um `ResponseFactoryInterface` via construtor e substituir `new Response()` por `$this->responseFactory->createResponse()`. + +--- + +### A-05 — Bug no `Validator`: inteiro zero falha validacao incorretamente + +**Descricao:** +A validacao de inteiros no `Validator` usa o resultado de `filter_var()` diretamente em contexto booleano. O valor `0` e um inteiro valido mas `filter_var(0, FILTER_VALIDATE_INT)` retorna `0`, que e avaliado como `false` em PHP. + +```php +// src/Validation/Validator.php:125 +if (!filter_var($value, FILTER_VALIDATE_INT)) { + // $value = 0 entra aqui incorretamente + $errors[] = "$field must be an integer"; +} +``` + +**Arquivos afetados:** + +| Arquivo | Linha | +|-----------------------------------|-------| +| `src/Validation/Validator.php` | 125 | + +**Impacto tecnico:** +- Falso positivo: o valor `0` e rejeitado como "nao e inteiro". +- APIs que aceitam IDs ou quantidades zero retornam erros de validacao incorretos. + +**Recomendacao:** +Substituir por comparacao estrita: +```php +if (filter_var($value, FILTER_VALIDATE_INT) === false) { +``` + +--- + +### A-06 — Estado estatico em `MiddlewareStack` incompativel com servidores asincronos + +**Descricao:** +`MiddlewareStack` possui cinco propriedades estaticas que persistem entre requisicoes no mesmo processo PHP: + +```php +// src/Middleware/MiddlewareStack.php:30-53 +private static array $compiledPipelines = []; +private static array $stats = []; +private static array $groupMiddlewares = []; +private static ?MiddlewarePipelineCompilerInterface $compiler = null; +private static ?SerializationCacheInterface $serializationCache = null; +``` + +**Arquivos afetados:** + +| Arquivo | Linhas | +|----------------------------------------|---------| +| `src/Middleware/MiddlewareStack.php` | 30-53 | + +**Impacto tecnico:** +- Em Swoole, ReactPHP ou FrankenPHP, o estado de uma requisicao vaza para a proxima dentro do mesmo worker. +- Pipeline compilado para uma rota pode ser incorretamente reutilizado em outra rota. +- Impossibilidade de isolar contextos de requisicao sem reset manual. + +**Recomendacao:** +Documentar explicitamente a incompatibilidade com servidores asincronos. Para suporte asincrono, converter propriedades estaticas para instancia ou usar contexto por corrotina. + +--- + +### A-07 — `Response.json()` com auto-emit oculto exige dois flags para desativar + +**Descricao:** +O metodo `json()` em `Response` emite automaticamente a saida HTTP como efeito colateral. Para desativa-lo sao necessarios dois flags independentes: `$testMode` (linha 65) e `$disableAutoEmit` (linha 75). A documentacao nao menciona esse comportamento. + +```php +// src/Http/Response.php:290 +if (!$this->testMode && !$this->disableAutoEmit) { + $this->emit(); // efeito colateral oculto +} +``` + +**Arquivos afetados:** + +| Arquivo | Linhas | +|--------------------------|----------------| +| `src/Http/Response.php` | 65, 75, 290, 315, 340 | + +**Impacto tecnico:** +- Comportamento surpresa para desenvolvedores que esperam `json()` apenas serializar dados. +- Dificuldade de teste unitario sem ativar flags especificos. +- Headers podem ser enviados prematuramente em fluxos de middleware complexos. + +**Recomendacao:** +Separar `json()` (serializa e configura headers) de `emit()` (envia a resposta). Tornar o auto-emit opt-in, nao opt-out. + +--- + +## Secao 3 — Inconsistencias de Impacto Medio + +### M-01 — Dois `PoolManager` com designs opostos + +**Descricao:** +Existem dois `PoolManager` com arquiteturas opostas: + +| Arquivo | Design | Caracteristica | +|--------------------------------------|-------------|------------------------------| +| `src/Http/Pool/PoolManager.php` | Instancia | Singleton opcional, pools nomeados genericos | +| `src/Http/Psr7/Pool/PoolManager.php` | Estatico | Todos os metodos estaticos, pools especificos PSR-7 | + +**Arquivos afetados:** `src/Http/Pool/PoolManager.php`, `src/Http/Psr7/Pool/PoolManager.php` + +**Impacto tecnico:** Ambiguidade na escolha do pool. Impossivel substituir um pelo outro sem alteracoes de chamada. + +**Recomendacao:** Unificar em uma unica interface `PoolManagerInterface` com implementacoes distintas para pools genericos e PSR-7. + +--- + +### M-02 — Conversao camelCase duplicada em 6 locais + +**Descricao:** +A logica de conversao de string para camelCase e replicada inline em pelo menos 6 pontos do codigo sem uso de funcao utilitaria centralizada. + +**Arquivos afetados:** + +| Arquivo | Descricao | +|----------------------------------|----------------------------------------| +| `src/Http/Request.php` | Linhas 530-533, 547-550, 572-576, 593-597 (dentro da classe anonima) | +| `src/Http/HeaderRequest.php` | Conversao de headers | +| `src/Utils/Utils.php` | `camelCase()` (linha 252) | +| `src/Support/Str.php` | `camelCase()` (linha 13) | + +**Impacto tecnico:** Edge cases resolvidos de forma diferente em cada local. Bug corrigido em um ponto nao e propagado aos demais. + +**Recomendacao:** Centralizar toda conversao camelCase em `Str::camelCase()` e substituir as implementacoes inline. + +--- + +### M-03 — `Utils` e `Str` duplicam camel/snake/kebab com resultados distintos em edge cases + +**Descricao:** +Ambas as classes oferecem metodos de conversao de case com implementacoes diferentes: + +| Metodo | `src/Utils/Utils.php` | `src/Support/Str.php` | +|-----------------|----------------------|----------------------| +| camelCase | linha 252 | linha 13 | +| snakeCase | linha 263 | ausente (tem `snake`) | +| kebabCase | linha 275 | linha 42 (`kebab`) | + +Implementacoes distintas podem produzir resultados diferentes para strings com numeros, underscores duplos ou caracteres especiais. + +**Arquivos afetados:** `src/Utils/Utils.php`, `src/Support/Str.php` + +**Recomendacao:** Definir `Str` como fonte unica de verdade. Deprecar metodos duplicados em `Utils` com redirecionamento para `Str`. + +--- + +### M-04 — `Application::use()` sem type hint e com complexidade ciclomatica elevada + +**Descricao:** +O metodo `use()` (linha 411) nao possui type hint no parametro `$middleware`, impossibilitando analise estatica e autocomplemento de IDEs. + +```php +// src/Core/Application.php:411 +public function use($middleware): self // sem type hint +``` + +O metodo possui complexidade ciclomatica aproximada de 6 (3 blocos `if/elseif/else` aninhados com verificacoes multiplas). + +**Arquivos afetados:** `src/Core/Application.php` linha 411 + +**Recomendacao:** Adicionar union type `string|callable|object` e extrair cada branch em metodo privado. + +--- + +### M-05 — `LoadShedder::$requestCounts` cresce indefinidamente + +**Descricao:** +O array `$requestCounts` em `LoadShedder` (linha 34) acumula uma entrada por IP de cliente. A limpeza via `array_filter` remove apenas entradas antigas por janela de tempo, mas nao limita o tamanho total do array. Em cenarios de alto volume com muitos IPs distintos, o array cresce sem limite. + +Alem disso, a busca pelo cliente usa `array_keys($this->requestCounts)` (linha 143), que e O(n) linear. + +**Arquivos afetados:** `src/Middleware/LoadShedder.php` linhas 34, 76, 84, 143 + +**Impacto tecnico:** Vazamento de memoria em producao com trafego diversificado. + +**Recomendacao:** Adicionar limite maximo de entradas com politica LRU ou usar estrutura de dados com expiracão automatica. + +--- + +### M-06 — Namespace `Providers/` contem implementacoes, nao providers + +**Descricao:** +O diretorio `src/Providers/` contem classes que sao implementacoes diretas de servicos: + +| Arquivo | Tipo real | +|----------------------------------|--------------------------| +| `src/Providers/Container.php` | Implementacao PSR-11 | +| `src/Providers/Logger.php` | Implementacao PSR-3 | +| `src/Providers/EventDispatcher.php` | Implementacao de evento | + +Providers de servico tipicamente registram servicos no container — estas classes sao os proprios servicos. + +**Recomendacao:** Mover para namespaces semanticamente corretos (`Core\Container`, `Logging\Logger`) ou criar providers separados que registrem estas implementacoes. + +--- + +### M-07 — `ExtensionManager` acoplado diretamente a `Application` + +**Descricao:** +`ExtensionManager` recebe `Application` via construtor (linha 51) e armazena referencia direta (linha 46). `Application` provavelmente instancia `ExtensionManager`, criando dependencia ciclica implicita. + +```php +// src/Providers/ExtensionManager.php:46, 51 +private Application $app; +public function __construct(Application $app) +``` + +**Arquivos afetados:** `src/Providers/ExtensionManager.php` linhas 46-51 + +**Recomendacao:** Introduzir interface `ApplicationInterface` e injetar a interface em vez da classe concreta. + +--- + +### M-08 — `GlobalsToServerRequestAdapter::createUploadedFile()` sem verificacao de arquivo + +**Descricao:** +O metodo `createUploadedFile()` (linha 154) cria um `Stream` a partir de `$file['tmp_name']` sem verificar se o arquivo temporario existe, se o upload teve erro (`UPLOAD_ERR_OK`), ou se `tmp_name` nao esta vazio. + +```php +// src/Http/Adapters/GlobalsToServerRequestAdapter.php:156 +$stream = Stream::createFromFile($file['tmp_name']); // sem verificacao de existencia +``` + +**Arquivos afetados:** `src/Http/Adapters/GlobalsToServerRequestAdapter.php` linhas 154-165 + +**Impacto tecnico:** Excecao ou comportamento indefinido ao processar uploads com erro (ex.: `UPLOAD_ERR_NO_FILE`). + +**Recomendacao:** Verificar `$file['error'] === UPLOAD_ERR_OK` antes de criar o stream e tratar os casos de erro conforme a especificacao PSR-7. + +--- + +### M-09 — Dois `Logger.php` em namespaces distintos + +**Descricao:** +Existem duas implementacoes de logger: + +| Arquivo | Namespace | Extends/Implements | +|----------------------------------|-------------------------------|-------------------------| +| `src/Providers/Logger.php` | `PivotPHP\Core\Providers` | `Psr\Log\AbstractLogger` | +| `src/Logging/Logger.php` | `PivotPHP\Core\Logging` | Propria | + +**Recomendacao:** Manter apenas `Logging\Logger` (namespace semanticamente correto) e remover ou deprecar `Providers\Logger`. + +--- + +### M-10 — Classe anonima de 90 linhas em `setHeaders()` com zero testabilidade + +**Descricao:** +O metodo `setHeaders()` em `Request` (linha 509) define uma classe anonima de aproximadamente 90 linhas que estende `HeaderRequest`. Esta classe anonima reimplementa a logica de conversao camelCase (duplicacao do M-02) e nao pode ser testada isoladamente. + +**Arquivos afetados:** `src/Http/Request.php` linhas 512-601 + +**Recomendacao:** Extrair para uma classe nomeada interna `CustomHeaderRequest` ou equivalente. + +--- + +### M-11 — `Validator.php` sem `declare(strict_types=1)` + +**Descricao:** +`src/Validation/Validator.php` e o unico arquivo do projeto sem `declare(strict_types=1)`. Isso permite coercao implicita de tipos em chamadas de metodo, podendo mascarar erros de tipo. + +**Arquivos afetados:** `src/Validation/Validator.php` (linha 1) + +**Recomendacao:** Adicionar `declare(strict_types=1)` e verificar se algum teste passa argumentos do tipo errado que seriam rejeitados com strict types. + +--- + +## Secao 4 — Inconsistencias de Impacto Baixo + +### B-01 — `Application` com aproximadamente 1229 linhas (God Class) + +**Descricao:** +`src/Core/Application.php` acumula responsabilidades de bootstrap, roteamento, middleware, configuracao, tratamento de erros e logging. Com 1229 linhas, viola o principio de responsabilidade unica. + +**Arquivos afetados:** `src/Core/Application.php` + +**Recomendacao:** Extrair responsabilidades em servicos dedicados: `BootstrapService`, `ErrorHandler`, `MiddlewareRegistry`. + +--- + +### B-02 — `Str::startsWith/endsWith/contains` reimplementam funcoes nativas PHP 8.0+ + +**Descricao:** +PHP 8.0 introduziu `str_starts_with()`, `str_ends_with()` e `str_contains()`. Os metodos em `Str` reimplementam o mesmo comportamento sem adicionar valor. + +```php +// src/Support/Str.php:80, 88, 96 +public static function startsWith(string $haystack, string $needle): bool +public static function endsWith(string $haystack, string $needle): bool +public static function contains(string $haystack, string $needle): bool +``` + +**Arquivos afetados:** `src/Support/Str.php` linhas 80-100 + +**Recomendacao:** Deprecar os metodos e redirecionar internamente para as funcoes nativas. O framework ja requer PHP 8.1+. + +--- + +### B-03 — `Arr::only()` e `Arr::except()` sem type hint em `$keys` + +**Descricao:** +Ambos os metodos usam `$keys` sem type hint, com logica manual de normalizacao via `func_get_args()`: + +```php +// src/Utils/Arr.php:162, 188 +public static function only(array $array, $keys): array +public static function except(array $array, $keys): array +``` + +**Arquivos afetados:** `src/Utils/Arr.php` linhas 162, 188 + +**Recomendacao:** Declarar `array|string $keys` como tipo e simplificar a normalizacao interna. + +--- + +### B-04 — Dois metodos de error handling com logica duplicada em `Application.php` + +**Descricao:** +`Application` possui pelo menos dois pontos de tratamento de excecao (linhas 323, 350 e 631) que chamam `handleException()`, mas com contextos e fluxos de retorno distintos. A logica de determinacao de status HTTP (`$e instanceof HttpException`) esta duplicada. + +**Arquivos afetados:** `src/Core/Application.php` linhas 323, 350, 631, 772-811 + +**Recomendacao:** Centralizar toda logica de tratamento em `handleException()` e garantir que todos os pontos de captura usem o mesmo fluxo. + +--- + +### B-05 — Magic strings HTTP hardcoded em `handleException()` ignorando mapa em `Response::error()` + +**Descricao:** +`handleException()` hardcoda strings de mensagem HTTP: + +```php +// src/Core/Application.php:806 +'message' => $statusCode === 404 ? 'Not Found' : 'Internal Server Error', +``` + +`Response::error()` (linha 405) ja possui logica de mensagens de erro padrao que poderia ser reutilizada. + +**Arquivos afetados:** `src/Core/Application.php` linha 806, `src/Http/Response.php` linha 405 + +**Recomendacao:** Delegar a mensagem de erro ao metodo `Response::error()` ou a um mapa centralizado de status HTTP. + +--- + +## Status de Correcao + +Ultima atualizacao: 2026-05-29 + +| ID | Descricao Resumida | Severidade | Status | +|------|-------------------------------------------------|------------|---------------------------------------------| +| C-01 | Dois Containers IoC incompativeis | Critico | Depreciado (`@deprecated` + `trigger_error` em `getInstance()`; remocao v3.0.0) | +| C-02 | Leitura dupla de `php://input` | Critico | Corrigido (`parseBody()` usa `getCachedInput()`) | +| C-03 | `resetServerRequest()` ignora headers/params | Critico | Corrigido (headers aplicados e limpos; `serverParams` aplicados) | +| A-01 | Tres implementacoes de rate limiting | Alto | Depreciado (`LoadShedder` e `RateLimitMiddleware` com `@deprecated` + `trigger_error`; remocao v3.0.0) | +| A-02 | `warmupCommonPipelines()` chama metodo invalido | Alto | Corrigido (metodo `warmupCommonPipelines()` removido) | +| A-03 | Dois metodos de IP com logicas distintas | Alto | Depreciado (`getIp()` delega para `ip()`; `@deprecated` + `trigger_error`; remocao v3.0.0) | +| A-04 | `ApiDocumentationMiddleware` viola PSR-15 | Alto | Corrigido (usa `Psr7Response` + `Stream` puros; sem instanciar `Response` Express.js) | +| A-05 | Bug Validator: inteiro zero falha validacao | Alto | Corrigido (comparacao estrita `=== false`) | +| A-06 | Estado estatico em `MiddlewareStack` | Alto | Pendente Sprint 4 | +| A-07 | `Response.json()` com auto-emit oculto | Alto | Parcialmente Corrigido (`setTestMode()` sincroniza `$disableAutoEmit`; condicoes simplificadas; extracao de `ResponseEmitter` pendente Sprint 4) | +| M-01 | Dois `PoolManager` com designs opostos | Medio | Pendente Sprint 4 | +| M-02 | Conversao camelCase duplicada em 6 locais | Medio | Corrigido (`HeaderRequest::headerToCamel()` centralizado; `CustomHeaderCollection` herda) | +| M-03 | `Utils` e `Str` duplicam case conversion | Medio | Corrigido (`Utils::camelCase/snake/kebab` delegam para `Str`) | +| M-04 | `Application::use()` sem type hint | Medio | Corrigido (decomposto em `resolveClassMiddleware()` e `wrapObjectMiddleware()`) | +| M-05 | `LoadShedder::$requestCounts` cresce sem limite | Medio | Depreciado (classe depreciada; remocao v3.0.0) | +| M-06 | Namespace `Providers/` contem implementacoes | Medio | Parcialmente Corrigido (`EventDispatcher` e `ListenerProvider` movidos para `Events/`; `Providers/` depreciados; `Container` pendente Sprint 4) | +| M-07 | `ExtensionManager` acoplado a `Application` | Medio | Pendente Sprint 4 | +| M-08 | `createUploadedFile()` sem verificacao de erro | Medio | Corrigido (verificacao de `file_exists` adicionada) | +| M-09 | Dois `Logger.php` em namespaces distintos | Medio | Corrigido (`Logging/PsrLogger.php` criado; `Providers/Logger.php` depreciado; `Logging/Logger.php` morto removido) | +| M-10 | Classe anonima de 90 linhas sem testabilidade | Medio | Corrigido (extraida para `CustomHeaderCollection` em `src/Http/`) | +| M-11 | `Validator.php` sem `strict_types` | Medio | Corrigido (`declare(strict_types=1)` adicionado) | +| B-01 | `Application` God Class (1229 linhas) | Baixo | Pendente Sprint 4 | +| B-02 | `Str` reimplementa funcoes nativas PHP 8.0+ | Baixo | Depreciado (`@deprecated` + `trigger_error` nos 3 metodos; remocao v3.0.0) | +| B-03 | `Arr::only/except` sem type hint em `$keys` | Baixo | Corrigido (type hint `array|string` adicionado) | +| B-04 | Error handling duplicado em `Application` | Baixo | Pendente Sprint 4 | +| B-05 | Magic strings HTTP em `handleException()` | Baixo | Pendente Sprint 4 | + +--- + +## Priorizacao por Sprint + +### Sprint 1 — Criticos e Seguranca (semana 1-2) + +Foco: corretude funcional e seguranca. Nenhum item de Sprint 1 deve ir para producao sem correcao. + +| ID | Item | Esforco estimado | +|------|---------------------------------------------------|------------------| +| C-03 | Corrigir `resetServerRequest()` — vazamento de headers entre requests | Baixo | +| C-02 | Corrigir leitura dupla de `php://input` em `parseBody()` | Baixo | +| A-05 | Corrigir bug Validator para inteiro zero | Baixo | +| A-02 | Corrigir `warmupCommonPipelines()` — metodo invalido `setHeader` | Baixo | +| A-03 | Deprecar `getIp()` e documentar vulnerabilidade a spoofing | Medio | +| C-01 | Definir container canonico e corrigir cobertura de testes | Medio | + +### Sprint 2 — Qualidade Arquitetural (semana 3-4) + +Foco: eliminar duplicacao, consolidar responsabilidades e corrigir violacoes de contrato. + +| ID | Item | Esforco estimado | +|------|---------------------------------------------------|------------------| +| A-01 | Consolidar tres implementacoes de rate limiting | Alto | +| A-04 | Corrigir `ApiDocumentationMiddleware` — injetar `ResponseFactoryInterface` | Medio | +| A-07 | Separar `json()` de `emit()` em Response | Medio | +| M-02 | Centralizar conversao camelCase em `Str` | Medio | +| M-03 | Deprecar duplicatas em `Utils`, unificar em `Str` | Baixo | +| M-08 | Adicionar verificacao de erro de upload em `createUploadedFile()` | Baixo | +| M-11 | Adicionar `strict_types` em `Validator.php` | Baixo | +| M-09 | Remover `Providers\Logger` duplicado | Baixo | + +### Sprint 3 — Divida Tecnica e Refatoracao (semana 5-6) + +Foco: estrutura de longo prazo, testabilidade e compatibilidade com servidores asincronos. + +| ID | Item | Esforco estimado | +|------|---------------------------------------------------|------------------| +| A-06 | Documentar incompatibilidade estatica com Swoole/ReactPHP | Baixo | +| M-05 | Adicionar limite de tamanho em `LoadShedder::$requestCounts` | Medio | +| M-01 | Unificar `PoolManager` com interface comum | Alto | +| M-10 | Extrair classe anonima de `setHeaders()` para classe nomeada | Medio | +| M-04 | Adicionar type hint e reduzir complexidade em `Application::use()` | Medio | +| M-06 | Reorganizar namespace `Providers/` | Alto | +| M-07 | Introduzir interface para desacoplar `ExtensionManager` | Medio | +| B-01 | Decompor `Application` — extrair `ErrorHandler`, `MiddlewareRegistry` | Alto | +| B-02 | Deprecar `Str::startsWith/endsWith/contains` | Baixo | +| B-03 | Adicionar type hints em `Arr::only/except` | Baixo | +| B-04 | Centralizar error handling em `Application` | Medio | +| B-05 | Eliminar magic strings HTTP de `handleException()` | Baixo | + +--- + +*Documento gerado em 2026-05-29. Ultima revisao de status: 2026-05-29 (pos-refatoracao Sprints 1-3). Revisao recomendada apos cada release.* diff --git a/examples/01-basics/hello-world.php b/examples/01-basics/hello-world.php index 6d79924..aa70a91 100644 --- a/examples/01-basics/hello-world.php +++ b/examples/01-basics/hello-world.php @@ -1,12 +1,12 @@ 'PivotPHP Core', 'version' => Application::VERSION, 'style' => 'Express.js for PHP', - 'features_v120' => [ + 'features_v200' => [ 'simplified_architecture' => 'Simplicidade sobre Otimização Prematura ✅', 'array_callables' => 'Native support maintained ✅', 'json_optimization' => 'Intelligent threshold maintained ✅', @@ -69,7 +69,7 @@ public function features($req, $res) ]); return $res->json([ - 'framework' => 'PivotPHP Core v1.2.0', + 'framework' => 'PivotPHP Core v2.0.0', 'optimization_note' => 'Large data - automatic pooling activated', 'features' => $features, 'pool_stats' => JsonBufferPool::getStatistics() @@ -80,7 +80,7 @@ public function features($req, $res) // Criar aplicação $app = new Application(); -// ✅ MANTIDO v1.2.0: Array callables nativos +// ✅ MANTIDO v2.0.0: Array callables nativos $controller = new HelloController(); $app->get('/', [$controller, 'index']); @@ -89,7 +89,7 @@ public function features($req, $res) // Rota com closure (ainda suportada) $app->get('/text', function ($req, $res) { - return $res->send('Hello from PivotPHP v1.2.0! 🚀'); + return $res->send('Hello from PivotPHP v2.0.0! 🚀'); }); // Health check com demonstração de threshold diff --git a/examples/02-routing/route-parameters-v114.php b/examples/02-routing/route-parameters-v114.php deleted file mode 100644 index e4658af..0000000 --- a/examples/02-routing/route-parameters-v114.php +++ /dev/null @@ -1,742 +0,0 @@ - 'PivotPHP v1.1.4+ - Route Parameters Examples', - 'description' => 'Demonstrações modernizadas de parâmetros de rota com novos recursos', - 'features_v114' => [ - 'array_callables' => 'Controllers organizados com array callables ✅', - 'json_optimization' => 'JsonBufferPool automático baseado no tamanho ✅', - 'enhanced_errors' => 'Validação contextual de parâmetros ✅', - 'performance_monitoring' => 'Estatísticas em tempo real ✅' - ], - 'examples' => [ - 'Basic Parameters' => [ - 'GET /users/:id' => 'Parâmetro básico com validação', - 'GET /posts/:year/:category' => 'Múltiplos parâmetros', - 'GET /api/users/:userId/posts/:postId/comments' => 'Parâmetros aninhados' - ], - 'Query Parameters' => [ - 'GET /search?q=term&page=1' => 'Query strings com validação', - 'GET /filter?category=tech&sort=date&order=desc' => 'Filtros complexos' - ], - 'Mixed Parameters' => [ - 'GET /posts/:category?page=1&limit=10' => 'Route + Query params', - 'GET /users/:id/posts?status=published' => 'Aninhados + Query' - ], - 'Wildcard Parameters' => [ - 'GET /files/*' => 'Captura de caminhos completos', - 'GET /browse/:category/*' => 'Wildcards com parâmetros' - ] - ], - 'parameter_methods' => [ - '$req->param(name)' => 'Obter parâmetro de rota', - '$req->get(name, default)' => 'Obter query parameter', - '$req->query()' => 'Todos os query parameters', - '$req->params()' => 'Todos os parâmetros de rota' - ], - 'v114_improvements' => [ - 'contextual_validation' => 'ContextualException para parâmetros inválidos', - 'automatic_optimization' => 'JsonBufferPool decide automaticamente', - 'controller_organization' => 'Array callables para melhor estrutura', - 'performance_tracking' => 'Monitoramento integrado de performance' - ] - ]; - - return $res->json($documentation); - } -} - -class UserController -{ - private array $users; - - public function __construct() - { - $this->users = [ - 1 => ['id' => 1, 'name' => 'João Silva', 'email' => 'joao@example.com'], - 2 => ['id' => 2, 'name' => 'Maria Santos', 'email' => 'maria@example.com'], - 3 => ['id' => 3, 'name' => 'Pedro Costa', 'email' => 'pedro@example.com'] - ]; - } - - public function show($req, $res) - { - $id = $req->param('id'); - - // ✅ NOVO v1.1.4+: Enhanced parameter validation - if (!is_numeric($id)) { - throw ContextualException::parameterError( - 'id', - 'numeric user ID', - $id, - '/users/:id' - ); - } - - $id = (int) $id; - - if (!isset($this->users[$id])) { - throw ContextualException::parameterError( - 'id', - 'existing user ID', - $id, - '/users/:id' - ); - } - - $user = $this->users[$id]; - - // Enrich user data - $user['profile'] = [ - 'bio' => "Biografia do usuário {$id}", - 'location' => 'São Paulo, Brasil', - 'joined' => '2024-01-15', - 'posts_count' => rand(5, 50), - 'followers' => rand(100, 1000) - ]; - - $response = [ - 'user' => $user, - 'route_params' => $req->params(), - 'extracted_id' => $id, - 'id_type' => gettype($id), - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($user), - 'data_size' => strlen(json_encode($user)) . ' bytes', - 'strategy' => 'Single user - optimized for speed' - ] - ]; - - return $res->json($response); - } -} - -class PostController -{ - public function byYearAndCategory($req, $res) - { - $year = $req->param('year'); - $category = $req->param('category'); - - // ✅ NOVO v1.1.4+: Enhanced validation for year parameter - if (!is_numeric($year) || $year < 2000 || $year > 2030) { - throw ContextualException::parameterError( - 'year', - 'valid year (2000-2030)', - $year, - '/posts/:year/:category' - ); - } - - // Validate category - $validCategories = ['technology', 'science', 'business', 'lifestyle', 'programming']; - if (!in_array($category, $validCategories)) { - throw new ContextualException( - 400, - 'Invalid category parameter', - [ - 'parameter' => 'category', - 'received_value' => $category, - 'valid_categories' => $validCategories, - 'route_pattern' => '/posts/:year/:category' - ], - [ - 'Use one of the valid categories: ' . implode(', ', $validCategories), - 'Check the spelling of the category name', - 'Categories are case-sensitive' - ], - 'PARAMETER_VALIDATION' - ); - } - - // Generate posts for demonstration - $posts = array_fill(0, rand(3, 8), [ - 'id' => rand(1, 1000), - 'title' => "Post sobre {$category} em {$year}", - 'category' => $category, - 'year' => (int) $year, - 'content' => 'Conteúdo detalhado do post sobre ' . $category, - 'published_at' => "{$year}-" . sprintf('%02d', rand(1, 12)) . "-" . sprintf('%02d', rand(1, 28)), - 'author' => ['Autor A', 'Autor B', 'Autor C'][rand(0, 2)], - 'views' => rand(100, 10000), - 'likes' => rand(10, 500) - ]); - - $response = [ - 'posts' => $posts, - 'filters' => [ - 'year' => (int) $year, - 'category' => $category - ], - 'route_params' => $req->params(), - 'total_posts' => count($posts), - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($posts), - 'data_size' => $this->estimateDataSize($posts), - 'performance_note' => 'Large dataset automatically uses buffer pooling' - ], - 'pool_stats' => JsonBufferPool::getStatistics() - ]; - - return $res->json($response); - } - - private function estimateDataSize(array $data): string - { - $size = strlen(json_encode($data)); - if ($size < 1024) return $size . ' bytes'; - if ($size < 1024 * 1024) return round($size / 1024, 1) . ' KB'; - return round($size / (1024 * 1024), 1) . ' MB'; - } -} - -class SearchController -{ - public function search($req, $res) - { - // Parâmetros obrigatórios - $query = $req->get('q'); - - if (!$query) { - throw new ContextualException( - 400, - 'Search query parameter is required', - [ - 'missing_parameter' => 'q', - 'received_params' => $req->query(), - 'endpoint' => '/search' - ], - [ - 'Add ?q=your-search-term to the URL', - 'Example: /search?q=php&category=tech&page=1', - 'Query parameter "q" cannot be empty' - ], - 'MISSING_PARAMETER' - ); - } - - // Parâmetros opcionais com defaults e validação - $page = max(1, (int) $req->get('page', 1)); - $limit = max(1, min(100, (int) $req->get('limit', 10))); - $category = $req->get('category', 'all'); - $sort = $req->get('sort', 'relevance'); - $order = $req->get('order', 'desc'); - - // Validar sort parameter - $validSorts = ['relevance', 'date', 'title', 'author']; - if (!in_array($sort, $validSorts)) { - $sort = 'relevance'; // Fallback silencioso - } - - // Parâmetros de filtro avançado - $dateFrom = $req->get('date_from'); - $dateTo = $req->get('date_to'); - $author = $req->get('author'); - $tags = $req->get('tags'); - - // Processar tags se fornecidas - $tagsArray = $tags ? array_map('trim', explode(',', $tags)) : []; - - // Simular resultados de busca baseados nos parâmetros - $results = array_fill(0, min($limit, rand(3, 15)), [ - 'id' => rand(1, 1000), - 'title' => "Tutorial de {$query}", - 'category' => $category !== 'all' ? $category : ['technology', 'programming', 'science'][rand(0, 2)], - 'author' => $author ?: ['João Silva', 'Maria Santos', 'Pedro Costa'][rand(0, 2)], - 'published_at' => date('Y-m-d', strtotime('-' . rand(1, 365) . ' days')), - 'relevance_score' => round(rand(70, 100) + rand(0, 99) / 100, 2), - 'excerpt' => "Trecho do conteúdo sobre {$query}...", - 'tags' => !empty($tagsArray) ? array_slice($tagsArray, 0, 3) : ['tag1', 'tag2'] - ]); - - $response = [ - 'results' => $results, - 'search_params' => [ - 'query' => $query, - 'page' => $page, - 'limit' => $limit, - 'category' => $category, - 'sort' => $sort, - 'order' => $order - ], - 'filters' => [ - 'date_from' => $dateFrom, - 'date_to' => $dateTo, - 'author' => $author, - 'tags' => $tagsArray - ], - 'pagination' => [ - 'current_page' => $page, - 'per_page' => $limit, - 'total_results' => rand(50, 500), - 'total_pages' => rand(5, 50), - 'has_next' => $page < rand(5, 10), - 'has_prev' => $page > 1 - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($results), - 'query_complexity' => 'medium', - 'response_strategy' => 'Automatic optimization based on result count' - ], - 'all_query_params' => $req->query() - ]; - - return $res->json($response); - } -} - -class CommentController -{ - public function byUserAndPost($req, $res) - { - $userId = $req->param('userId'); - $postId = $req->param('postId'); - - // ✅ NOVO v1.1.4+: Enhanced nested parameter validation - if (!is_numeric($userId)) { - throw ContextualException::parameterError( - 'userId', - 'numeric user ID', - $userId, - '/api/users/:userId/posts/:postId/comments' - ); - } - - if (!is_numeric($postId)) { - throw ContextualException::parameterError( - 'postId', - 'numeric post ID', - $postId, - '/api/users/:userId/posts/:postId/comments' - ); - } - - $userId = (int) $userId; - $postId = (int) $postId; - - // Query parameters para paginação - $page = max(1, (int) $req->get('page', 1)); - $limit = max(1, min(50, (int) $req->get('limit', 5))); - $status = $req->get('status', 'approved'); - - // Validar status - $validStatuses = ['approved', 'pending', 'rejected', 'all']; - if (!in_array($status, $validStatuses)) { - throw new ContextualException( - 400, - 'Invalid status parameter', - [ - 'parameter' => 'status', - 'received_value' => $status, - 'valid_statuses' => $validStatuses, - 'endpoint' => '/api/users/:userId/posts/:postId/comments' - ], - [ - 'Use one of: ' . implode(', ', $validStatuses), - 'Status parameter is case-sensitive', - 'Default status is "approved"' - ], - 'PARAMETER_VALIDATION' - ); - } - - // Simular comentários - $comments = array_fill(0, rand(2, 10), [ - 'id' => rand(1, 1000), - 'user_id' => $userId, - 'post_id' => $postId, - 'author' => ['Ana Costa', 'Carlos Lima', 'Lucia Ferreira', 'Roberto Silva'][rand(0, 3)], - 'content' => 'Comentário interessante sobre o post. Muito informativo e bem escrito.', - 'status' => $status === 'all' ? ['approved', 'pending'][rand(0, 1)] : $status, - 'likes' => rand(0, 50), - 'created_at' => date('Y-m-d H:i:s', strtotime('-' . rand(1, 30) . ' days')), - 'updated_at' => date('Y-m-d H:i:s', strtotime('-' . rand(0, 5) . ' days')) - ]); - - $response = [ - 'comments' => $comments, - 'context' => [ - 'user_id' => $userId, - 'post_id' => $postId, - 'status_filter' => $status - ], - 'pagination' => [ - 'page' => $page, - 'limit' => $limit, - 'total' => count($comments) - ], - 'route_hierarchy' => [ - 'user' => "/api/users/{$userId}", - 'post' => "/api/users/{$userId}/posts/{$postId}", - 'comments' => "/api/users/{$userId}/posts/{$postId}/comments" - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($comments), - 'nested_params' => 'Successfully validated', - 'performance_note' => 'Nested routes with automatic optimization' - ] - ]; - - return $res->json($response); - } -} - -class FileController -{ - public function handleWildcard($req, $res) - { - $path = $req->param('*'); // Captura tudo após /files/ - - if (empty($path)) { - throw new ContextualException( - 400, - 'File path is required', - [ - 'wildcard_param' => '*', - 'captured_value' => $path, - 'route_pattern' => '/files/*' - ], - [ - 'Provide a file path after /files/', - 'Example: /files/documents/report.pdf', - 'Wildcard parameter cannot be empty' - ], - 'WILDCARD_PARAMETER' - ); - } - - // Analisar o caminho - $pathParts = explode('/', trim($path, '/')); - $filename = end($pathParts); - $directory = implode('/', array_slice($pathParts, 0, -1)); - $extension = pathinfo($filename, PATHINFO_EXTENSION); - - $fileInfo = [ - 'full_path' => $path, - 'directory' => $directory ?: 'root', - 'filename' => $filename, - 'extension' => $extension, - 'path_parts' => $pathParts, - 'depth' => count($pathParts), - 'file_type' => $this->getFileType($extension), - 'estimated_size' => rand(1024, 1024 * 1024) . ' bytes' - ]; - - $response = [ - 'file_info' => $fileInfo, - 'wildcard_info' => [ - 'pattern' => '/files/*', - 'captured' => $path, - 'description' => 'Wildcard captura todo o resto da URL' - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($fileInfo), - 'wildcard_handling' => 'Enhanced with contextual validation' - ] - ]; - - return $res->json($response); - } - - private function getFileType(string $extension): string - { - $types = [ - 'pdf' => 'document', - 'doc' => 'document', 'docx' => 'document', - 'jpg' => 'image', 'jpeg' => 'image', 'png' => 'image', 'gif' => 'image', - 'mp4' => 'video', 'avi' => 'video', 'mov' => 'video', - 'mp3' => 'audio', 'wav' => 'audio', - 'zip' => 'archive', 'rar' => 'archive', - 'txt' => 'text', 'md' => 'text' - ]; - - return $types[strtolower($extension)] ?? 'unknown'; - } -} - -// =============================================== -// MIDDLEWARE v1.1.4+ -// =============================================== - -class RouteMiddleware -{ - public static function parameterLogger($req, $res, $next) - { - $routeParams = $req->params(); - $queryParams = $req->query(); - - error_log("Route Params: " . json_encode($routeParams)); - error_log("Query Params: " . json_encode($queryParams)); - - $res->header('X-Route-Params', json_encode($routeParams)); - $res->header('X-Query-Params', json_encode($queryParams)); - - return $next($req, $res); - } - - public static function performanceTracker($req, $res, $next) - { - $start = microtime(true); - $memoryBefore = memory_get_usage(true); - - $response = $next($req, $res); - - $duration = round((microtime(true) - $start) * 1000, 2); - $memoryUsed = memory_get_usage(true) - $memoryBefore; - - $res->header('X-Response-Time', $duration . 'ms'); - $res->header('X-Memory-Used', round($memoryUsed / 1024, 2) . 'KB'); - $res->header('X-Optimization-Active', 'JsonBufferPool-v1.1.4+'); - - return $response; - } -} - -// =============================================== -// APPLICATION SETUP v1.1.4+ -// =============================================== - -$app = new Application(); - -// ✅ Apply middleware using array callables -$app->use([RouteMiddleware::class, 'parameterLogger']); -$app->use([RouteMiddleware::class, 'performanceTracker']); - -// ✅ Initialize controllers -$routeController = new RouteParamsController(); -$userController = new UserController(); -$postController = new PostController(); -$searchController = new SearchController(); -$commentController = new CommentController(); -$fileController = new FileController(); - -// =============================================== -// ROUTES with Array Callables v1.1.4+ -// =============================================== - -// ✅ Main documentation (Array Callable) -$app->get('/', [$routeController, 'index']); - -// ✅ Basic parameter routes (Array Callables) -$app->get('/users/:id', [$userController, 'show']); -$app->get('/posts/:year/:category', [$postController, 'byYearAndCategory']); - -// ✅ Query parameter routes (Array Callables) -$app->get('/search', [$searchController, 'search']); - -// ✅ Nested parameter routes (Array Callables) -$app->get('/api/users/:userId/posts/:postId/comments', [$commentController, 'byUserAndPost']); - -// ✅ Wildcard routes (Array Callables) -$app->get('/files/*', [$fileController, 'handleWildcard']); - -// Advanced parameter demo with mixed types -$app->get('/reports/:type/:year', function($req, $res) { - $type = $req->param('type'); - $year = $req->param('year'); - - // Validate parameters - if (!is_numeric($year) || $year < 2020 || $year > 2030) { - throw ContextualException::parameterError( - 'year', - 'valid year (2020-2030)', - $year, - '/reports/:type/:year' - ); - } - - $validTypes = ['sales', 'financial', 'operational', 'marketing']; - if (!in_array($type, $validTypes)) { - throw new ContextualException( - 400, - 'Invalid report type', - [ - 'parameter' => 'type', - 'received_value' => $type, - 'valid_types' => $validTypes - ], - [ - 'Use one of: ' . implode(', ', $validTypes), - 'Report types are case-sensitive' - ], - 'PARAMETER_VALIDATION' - ); - } - - // Query parameters para customização - $format = $req->get('format', 'json'); - $detailed = $req->get('detailed', 'false') === 'true'; - $department = $req->get('department'); - $months = $req->get('months'); - - $monthsArray = $months ? array_map('intval', explode(',', $months)) : range(1, 12); - - // Simular dados do relatório - $reportData = [ - 'type' => $type, - 'year' => (int) $year, - 'months_included' => $monthsArray, - 'department' => $department, - 'summary' => [ - 'total_records' => rand(1000, 5000), - 'average_per_month' => rand(80, 400), - 'peak_month' => ['Janeiro', 'Dezembro', 'Julho'][rand(0, 2)] - ] - ]; - - if ($detailed) { - $reportData['detailed_data'] = [ - 'monthly_breakdown' => array_map(function ($month) { - return [ - 'month' => $month, - 'value' => rand(50, 500), - 'growth' => rand(-10, 25) . '%' - ]; - }, $monthsArray) - ]; - } - - $response = [ - 'report' => $reportData, - 'parameters' => [ - 'route' => [ - 'type' => $type, - 'year' => (int) $year - ], - 'query' => [ - 'format' => $format, - 'detailed' => $detailed, - 'department' => $department, - 'months' => $months - ] - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($reportData), - 'response_strategy' => 'Mixed parameters with automatic optimization' - ], - 'metadata' => [ - 'generated_at' => date('c'), - 'format' => $format, - 'request_uri' => $req->uri() - ] - ]; - - // Retornar em formato diferente se solicitado - if ($format === 'csv') { - $res->header('Content-Type', 'text/csv'); - return $res->send("type,year,total_records\n{$type},{$year},{$reportData['summary']['total_records']}"); - } - - return $res->json($response); -}); - -// Comprehensive parameter demonstration -$app->get('/demo/:category/:id', function($req, $res) { - $category = $req->param('category'); - $id = $req->param('id'); - - // Enhanced parameter info with v1.1.4+ features - $response = [ - 'demonstration' => 'Todos os tipos de parâmetros v1.1.4+', - 'route_parameters' => [ - 'all_params' => $req->params(), - 'category' => $category, - 'id' => $id, - 'parameter_types' => [ - 'category' => gettype($category), - 'id' => gettype($id) - ] - ], - 'query_parameters' => [ - 'all_query' => $req->query(), - 'specific_examples' => [ - 'page' => $req->get('page'), - 'limit' => $req->get('limit', 10), - 'sort' => $req->get('sort') - ], - 'query_count' => count($req->query()) - ], - 'request_info' => [ - 'method' => $req->method(), - 'uri' => $req->uri(), - 'full_url' => $req->header('Host') . $req->uri(), - 'user_agent' => $req->header('User-Agent') - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($req->params()), - 'performance_note' => 'Demonstration endpoint with automatic optimization', - 'pool_stats' => JsonBufferPool::getStatistics() - ], - 'tips' => [ - 'basic_test' => '/demo/technology/123?page=2&limit=20&sort=date', - 'advanced_test' => '/demo/programming/456?page=1&limit=5&sort=title&detailed=true', - 'error_test' => 'Try invalid parameters to see enhanced error diagnostics' - ] - ]; - - return $res->json($response); -}); - -// Performance stats endpoint -$app->get('/performance-stats', function($req, $res) { - $stats = JsonBufferPool::getStatistics(); - - return $res->json([ - 'title' => 'Route Parameters Performance Stats v1.1.4+', - 'json_pool_stats' => $stats, - 'memory_usage' => [ - 'current_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2) - ], - 'optimization_benefits' => [ - 'automatic_threshold' => '256 bytes - system decides when to use pooling', - 'route_optimization' => 'Complex route responses use buffer pooling', - 'parameter_validation' => 'Enhanced error diagnostics prevent issues', - 'controller_organization' => 'Array callables improve code maintainability' - ], - 'timestamp' => date('c') - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/03-middleware/custom-middleware-v114.php b/examples/03-middleware/custom-middleware-v114.php deleted file mode 100644 index d5aa0ad..0000000 --- a/examples/03-middleware/custom-middleware-v114.php +++ /dev/null @@ -1,844 +0,0 @@ - 'PivotPHP v1.1.4+ - Custom Middleware Examples', - 'description' => 'Demonstrações de middleware personalizados modernizados', - 'features_v114' => [ - 'array_callable_middleware' => 'Middleware organizados em classes ✅', - 'json_optimization' => 'JsonBufferPool automático ✅', - 'enhanced_error_handling' => 'ContextualException com diagnósticos ✅', - 'performance_monitoring' => 'Tracking integrado de performance ✅' - ], - 'middleware_examples' => [ - 'RequestLogger' => 'Log de todas as requisições com contexto', - 'ResponseTimer' => 'Medição de tempo de resposta', - 'ApiKeyValidator' => 'Validação de chave de API com contexto', - 'ContentNegotiation' => 'Negociação de conteúdo (JSON/XML)', - 'InputValidator' => 'Validação contextual de dados de entrada', - 'RequestTransformer' => 'Transformação de dados da requisição', - 'ResponseModifier' => 'Modificação de resposta antes do envio', - 'ErrorHandler' => 'Tratamento contextual de erros' - ], - 'test_endpoints' => [ - 'GET /' => 'Esta página com logs', - 'POST /api/users' => 'Criação com validação contextual', - 'GET /protected' => 'Rota protegida por API key', - 'GET /api/data' => 'Negociação de conteúdo', - 'POST /validate' => 'Validação de entrada com diagnósticos', - 'GET /transform' => 'Transformação de dados', - 'GET /error-demo' => 'Demonstração de erro contextual', - 'GET /performance-stats' => 'Estatísticas de performance v1.1.4+' - ], - 'migration_from_old_version' => [ - 'before' => 'function($req, $res, $next) { ... }', - 'after' => '[MiddlewareClass::class, \'method\']', - 'benefits' => 'Better organization, IDE support, enhanced errors' - ] - ]; - - return $res->json($documentation); - } -} - -// =============================================== -// MIDDLEWARE CLASSES v1.1.4+ (Array Callables) -// =============================================== - -class RequestLogger -{ - public static function log($req, $res, $next) - { - $startTime = microtime(true); - $method = $req->method(); - $uri = $req->uri(); - $ip = $req->ip(); - $userAgent = $req->header('User-Agent') ?? 'Unknown'; - $requestId = uniqid('req_', true); - - // ✅ NOVO v1.1.4+: Enhanced logging with context - error_log("🔍 [{$requestId}] [{$method}] {$uri} - IP: {$ip} - UA: " . substr($userAgent, 0, 50)); - - // Adicionar dados de log ao request - $req->logData = [ - 'request_id' => $requestId, - 'start_time' => $startTime, - 'method' => $method, - 'uri' => $uri, - 'ip' => $ip, - 'user_agent' => $userAgent - ]; - - // Add request ID header - $res->header('X-Request-ID', $requestId); - - // Continuar para próximo middleware - $response = $next($req, $res); - - // Log pós-processamento com contexto - $endTime = microtime(true); - $duration = round(($endTime - $startTime) * 1000, 2); - $memoryUsed = round(memory_get_usage(true) / 1024 / 1024, 2); - - error_log("✅ [{$requestId}] [{$method}] {$uri} - {$duration}ms - {$memoryUsed}MB"); - - return $response; - } -} - -class ResponseTimer -{ - public static function time($req, $res, $next) - { - $startTime = microtime(true); - $memoryBefore = memory_get_usage(true); - - // Executar próximo middleware - $response = $next($req, $res); - - // Calcular métricas e adicionar headers - $endTime = microtime(true); - $duration = round(($endTime - $startTime) * 1000, 2); - $memoryUsed = memory_get_usage(true) - $memoryBefore; - - $res->header('X-Response-Time', $duration . 'ms'); - $res->header('X-Memory-Used', round($memoryUsed / 1024, 2) . 'KB'); - $res->header('X-Processed-At', date('c')); - $res->header('X-JsonPool-Active', 'v1.1.4+'); - - return $response; - } -} - -class ApiKeyValidator -{ - public static function validate($req, $res, $next) - { - $apiKey = $req->header('Authorization'); - - // ✅ NOVO v1.1.4+: Enhanced validation with contextual errors - if (!$apiKey) { - throw new ContextualException( - 401, - 'API Key is required for this endpoint', - [ - 'endpoint' => $req->uri(), - 'method' => $req->method(), - 'required_header' => 'Authorization', - 'middleware' => 'ApiKeyValidator' - ], - [ - 'Add Authorization header: "Authorization: Bearer "', - 'Valid tokens for testing: valid-token, admin-token, user-token', - 'Check API documentation for authentication requirements' - ], - 'AUTHENTICATION' - ); - } - - // Verificar formato Bearer - if (!str_starts_with($apiKey, 'Bearer ')) { - throw new ContextualException( - 401, - 'Invalid API Key format', - [ - 'provided_format' => substr($apiKey, 0, 20) . '...', - 'expected_format' => 'Bearer ', - 'endpoint' => $req->uri(), - 'middleware' => 'ApiKeyValidator' - ], - [ - 'Use Bearer token format: "Authorization: Bearer "', - 'Example: "Authorization: Bearer valid-token"', - 'Ensure there is a space after "Bearer"' - ], - 'AUTHENTICATION' - ); - } - - $token = substr($apiKey, 7); - - // Validar token com contexto - $validTokens = [ - 'valid-token' => ['id' => 1, 'name' => 'Usuario Teste', 'role' => 'user'], - 'admin-token' => ['id' => 2, 'name' => 'Admin User', 'role' => 'admin'], - 'user-token' => ['id' => 3, 'name' => 'Regular User', 'role' => 'user'] - ]; - - if (!isset($validTokens[$token])) { - throw new ContextualException( - 403, - 'Invalid or expired API Key', - [ - 'provided_token' => $token, - 'token_length' => strlen($token), - 'endpoint' => $req->uri(), - 'middleware' => 'ApiKeyValidator', - 'valid_token_count' => count($validTokens) - ], - [ - 'Use a valid test token: valid-token, admin-token, or user-token', - 'Check if your token has expired', - 'Verify token spelling and format', - 'Contact support if you need a new API key' - ], - 'AUTHORIZATION' - ); - } - - // Adicionar informações do usuário ao request - $req->authenticatedUser = $validTokens[$token]; - $req->apiToken = $token; - - return $next($req, $res); - } -} - -class ContentNegotiation -{ - public static function negotiate($req, $res, $next) - { - $acceptHeader = $req->header('Accept') ?? 'application/json'; - - // Determinar formato preferido - $preferredFormat = 'json'; // default - - if (strpos($acceptHeader, 'application/xml') !== false) { - $preferredFormat = 'xml'; - } elseif (strpos($acceptHeader, 'text/csv') !== false) { - $preferredFormat = 'csv'; - } elseif (strpos($acceptHeader, 'text/plain') !== false) { - $preferredFormat = 'text'; - } - - // Adicionar informações ao request - $req->preferredFormat = $preferredFormat; - $req->acceptHeader = $acceptHeader; - - // Executar próximo middleware - $response = $next($req, $res); - - // ✅ NOVO v1.1.4+: JsonBufferPool aware content negotiation - if (isset($req->responseData) && $preferredFormat !== 'json') { - $data = $req->responseData; - - switch ($preferredFormat) { - case 'xml': - $xml = self::arrayToXml($data); - $res->header('Content-Type', 'application/xml'); - return $res->send($xml); - - case 'csv': - $csv = self::arrayToCsv($data); - $res->header('Content-Type', 'text/csv'); - return $res->send($csv); - - case 'text': - $text = self::arrayToText($data); - $res->header('Content-Type', 'text/plain'); - return $res->send($text); - } - } - - return $response; - } - - private static function arrayToXml(array $data): string - { - $xml = '' . "\n\n"; - foreach ($data as $key => $value) { - if (is_array($value)) { - $xml .= " <{$key}>\n"; - foreach ($value as $subKey => $subValue) { - $xml .= " <{$subKey}>" . htmlspecialchars($subValue) . "\n"; - } - $xml .= " \n"; - } else { - $xml .= " <{$key}>" . htmlspecialchars($value) . "\n"; - } - } - $xml .= ''; - return $xml; - } - - private static function arrayToCsv(array $data): string - { - $flatData = []; - array_walk_recursive($data, function($value, $key) use (&$flatData) { - $flatData[$key] = $value; - }); - - $csv = implode(',', array_keys($flatData)) . "\n"; - $csv .= implode(',', array_values($flatData)); - return $csv; - } - - private static function arrayToText(array $data): string - { - $text = ''; - array_walk_recursive($data, function($value, $key) use (&$text) { - $text .= "{$key}: {$value}\n"; - }); - return trim($text); - } -} - -class InputValidator -{ - public static function create(array $rules): callable - { - return function ($req, $res, $next) use ($rules) { - $data = $req->getBodyAsStdClass(); - $errors = []; - $warnings = []; - - foreach ($rules as $field => $rule) { - $value = $data->$field ?? null; - - // Required validation - if (isset($rule['required']) && $rule['required'] && empty($value)) { - $errors[$field][] = "Campo {$field} é obrigatório"; - continue; - } - - if (!empty($value)) { - // Type validation with enhanced diagnostics - if (isset($rule['type'])) { - switch ($rule['type']) { - case 'string': - if (!is_string($value)) { - $errors[$field][] = "Campo {$field} deve ser string, recebido: " . gettype($value); - } - break; - case 'number': - if (!is_numeric($value)) { - $errors[$field][] = "Campo {$field} deve ser numérico, recebido: " . gettype($value); - } - break; - case 'email': - if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { - $errors[$field][] = "Campo {$field} deve ser um email válido, recebido: {$value}"; - } - break; - } - } - - // Length validation - if (isset($rule['min_length']) && strlen($value) < $rule['min_length']) { - $errors[$field][] = "Campo {$field} deve ter pelo menos {$rule['min_length']} caracteres (atual: " . strlen($value) . ")"; - } - - if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) { - $errors[$field][] = "Campo {$field} deve ter no máximo {$rule['max_length']} caracteres (atual: " . strlen($value) . ")"; - } - - // Pattern validation - if (isset($rule['pattern']) && !preg_match($rule['pattern'], $value)) { - $errors[$field][] = "Campo {$field} não atende ao padrão exigido"; - } - } - } - - // ✅ NOVO v1.1.4+: Enhanced validation errors with context - if (!empty($errors)) { - throw new ContextualException( - 422, - 'Data validation failed', - [ - 'validation_errors' => $errors, - 'rules_applied' => $rules, - 'received_fields' => array_keys((array)$data), - 'required_fields' => array_keys(array_filter($rules, fn($rule) => $rule['required'] ?? false)), - 'endpoint' => $req->uri(), - 'middleware' => 'InputValidator' - ], - [ - 'Check all required fields are provided', - 'Verify data types match the expected format', - 'Ensure field lengths are within specified limits', - 'Validate email format if email fields are used', - 'Review API documentation for exact field requirements' - ], - 'VALIDATION' - ); - } - - // Adicionar dados validados ao request - $req->validatedData = $data; - - return $next($req, $res); - }; - } -} - -class RequestTransformer -{ - public static function transform($req, $res, $next) - { - $body = $req->getBodyAsStdClass(); - $transformations = []; - - // Transformações automáticas com log - if (isset($body->email)) { - $original = $body->email; - $body->email = strtolower(trim($body->email)); - $transformations['email'] = ['from' => $original, 'to' => $body->email]; - } - - if (isset($body->name)) { - $original = $body->name; - $body->name = ucwords(strtolower(trim($body->name))); - $transformations['name'] = ['from' => $original, 'to' => $body->name]; - } - - if (isset($body->phone)) { - $original = $body->phone; - $body->phone = preg_replace('/[^0-9]/', '', $body->phone); - $transformations['phone'] = ['from' => $original, 'to' => $body->phone]; - } - - // Adicionar campos automáticos com tracking - $body->transformed_at = date('c'); - $body->ip_address = $req->ip(); - $body->user_agent = $req->header('User-Agent'); - - // ✅ NOVO v1.1.4+: Enhanced transformation tracking - $req->transformedData = $body; - $req->transformationLog = $transformations; - - return $next($req, $res); - } -} - -class ResponseModifier -{ - public static function modify($req, $res, $next) - { - // Executar próximo middleware - $response = $next($req, $res); - - // ✅ NOVO v1.1.4+: Enhanced headers with optimization info - $res->header('X-API-Version', '1.1.4+'); - $res->header('X-Framework', 'PivotPHP'); - $res->header('X-Features', 'array-callables,json-optimization,enhanced-errors'); - $res->header('X-JsonPool-Threshold', '256-bytes'); - - // Add performance metrics if available - if (isset($req->logData)) { - $res->header('X-Request-ID', $req->logData['request_id']); - } - - return $response; - } -} - -class ErrorHandler -{ - public static function handle($req, $res, $next) - { - try { - return $next($req, $res); - } catch (ContextualException $e) { - // ✅ NOVO v1.1.4+: Enhanced contextual error handling - $requestId = $req->logData['request_id'] ?? uniqid('err_', true); - - error_log("❌ ContextualException [{$requestId}]: {$e->getMessage()}"); - error_log("📍 Context: " . json_encode($e->getContext())); - - $errorResponse = [ - 'error' => true, - 'message' => $e->getMessage(), - 'category' => $e->getCategory(), - 'context' => $e->getContext(), - 'suggestions' => $e->getSuggestions(), - 'debug_info' => $e->getDebugInfo(), - 'request_id' => $requestId, - 'timestamp' => date('c'), - 'middleware' => 'ErrorHandler v1.1.4+' - ]; - - return $res->status($e->getStatusCode())->json($errorResponse); - - } catch (Exception $e) { - // Standard exception handling - $requestId = $req->logData['request_id'] ?? uniqid('err_', true); - - error_log("❌ Exception [{$requestId}]: {$e->getMessage()}"); - - return $res->status(500)->json([ - 'error' => true, - 'message' => 'Internal Server Error', - 'exception_class' => get_class($e), - 'original_message' => $e->getMessage(), - 'request_id' => $requestId, - 'timestamp' => date('c'), - 'middleware' => 'ErrorHandler v1.1.4+', - 'suggestion' => 'Check server logs for detailed error information' - ]); - } - } -} - -// =============================================== -// API CONTROLLERS v1.1.4+ -// =============================================== - -class ApiController -{ - public function createUser($req, $res) - { - $userData = $req->validatedData; - - // Simulate user creation - $user = [ - 'id' => rand(1000, 9999), - 'name' => $userData->name, - 'email' => $userData->email, - 'created_at' => date('c'), - 'status' => 'active' - ]; - - $response = [ - 'message' => 'Usuário criado com sucesso', - 'user' => $user, - 'validation' => [ - 'validated_data' => $userData, - 'middleware_applied' => ['InputValidator v1.1.4+'] - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($user), - 'response_strategy' => 'User creation with automatic optimization' - ] - ]; - - return $res->status(201)->json($response); - } - - public function getData($req, $res) - { - $data = [ - 'id' => 123, - 'name' => 'Produto Exemplo', - 'price' => '99.90', - 'category' => 'electronics', - 'features' => [ - 'waterproof' => true, - 'wireless' => true, - 'warranty' => '2 years' - ], - 'specifications' => [ - 'weight' => '250g', - 'dimensions' => '10x5x2cm', - 'color' => 'black' - ] - ]; - - // Armazenar dados para possível transformação - $req->responseData = $data; - - $response = [ - 'data' => $data, - 'format_info' => [ - 'preferred_format' => $req->preferredFormat, - 'accept_header' => $req->acceptHeader, - 'available_formats' => ['json', 'xml', 'csv', 'text'] - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($data), - 'content_negotiation' => 'Enhanced with JsonBufferPool awareness' - ] - ]; - - return $res->json($response); - } - - public function protectedEndpoint($req, $res) - { - return $res->json([ - 'message' => 'Acesso autorizado com sucesso!', - 'authenticated_user' => $req->authenticatedUser, - 'api_token' => $req->apiToken, - 'middleware_applied' => [ - 'RequestLogger v1.1.4+', - 'ResponseTimer v1.1.4+', - 'ApiKeyValidator v1.1.4+' - ], - 'security_info' => [ - 'user_role' => $req->authenticatedUser['role'], - 'token_validation' => 'passed', - 'access_level' => 'authorized' - ] - ]); - } - - public function transformData($req, $res) - { - return $res->json([ - 'message' => 'Dados transformados com sucesso', - 'original_data' => $req->getBodyAsStdClass(), - 'transformed_data' => $req->transformedData, - 'transformation_log' => $req->transformationLog ?? [], - 'transformations_applied' => [ - 'email' => 'lowercase + trim', - 'name' => 'title case + trim', - 'phone' => 'numbers only', - 'auto_fields' => ['transformed_at', 'ip_address', 'user_agent'] - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($req->transformedData), - 'transformation_strategy' => 'Enhanced with detailed logging' - ] - ]); - } - - public function performanceStats($req, $res) - { - $stats = JsonBufferPool::getStatistics(); - - return $res->json([ - 'title' => 'Middleware Performance Stats v1.1.4+', - 'framework_version' => Application::VERSION, - 'json_pool_stats' => $stats, - 'memory_usage' => [ - 'current_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2) - ], - 'middleware_improvements' => [ - 'contextual_errors' => 'Enhanced diagnostics with suggestions', - 'automatic_optimization' => 'JsonBufferPool threshold-based pooling', - 'organized_structure' => 'Array callables for better maintainability', - 'performance_tracking' => 'Integrated monitoring and metrics' - ], - 'timestamp' => date('c') - ]); - } - - public function errorDemo($req, $res) - { - // Simulate different types of errors for demonstration - $errorType = $req->get('type', 'contextual'); - - switch ($errorType) { - case 'contextual': - throw new ContextualException( - 500, - 'Demonstração de erro contextual v1.1.4+', - [ - 'error_type' => 'demonstration', - 'endpoint' => '/error-demo', - 'middleware_stack' => ['ErrorHandler', 'ResponseModifier'], - 'request_details' => [ - 'method' => $req->method(), - 'uri' => $req->uri(), - 'ip' => $req->ip() - ] - ], - [ - 'Este é um erro de demonstração para mostrar o ErrorHandler', - 'Erros contextuais fornecem informações detalhadas', - 'Suggestions ajudam desenvolvedores a resolver problemas', - 'Try different error types: ?type=standard' - ], - 'DEMONSTRATION' - ); - - case 'standard': - throw new Exception('Este é um erro padrão para demonstração do middleware ErrorHandler'); - - default: - return $res->json([ - 'message' => 'Erro demo endpoint', - 'available_types' => ['contextual', 'standard'], - 'example' => '/error-demo?type=contextual' - ]); - } - } -} - -// =============================================== -// APPLICATION SETUP v1.1.4+ -// =============================================== - -$app = new Application(); - -// ✅ Apply middleware using array callables -$app->use([RequestLogger::class, 'log']); -$app->use([ResponseTimer::class, 'time']); -$app->use([ErrorHandler::class, 'handle']); -$app->use([ResponseModifier::class, 'modify']); - -// ✅ Initialize controllers -$middlewareController = new MiddlewareController(); -$apiController = new ApiController(); - -// =============================================== -// ROUTES with Array Callables v1.1.4+ -// =============================================== - -// ✅ Main documentation (Array Callable) -$app->get('/', [$middlewareController, 'index']); - -// ✅ Protected route (Array Callable + Middleware) -$app->get('/protected', [ApiKeyValidator::class, 'validate'], [$apiController, 'protectedEndpoint']); - -// ✅ Content negotiation route (Array Callable + Middleware) -$app->get('/api/data', [ContentNegotiation::class, 'negotiate'], [$apiController, 'getData']); - -// ✅ User creation with validation (Array Callable + Middleware) -$app->post('/api/users', - InputValidator::create([ - 'name' => [ - 'required' => true, - 'type' => 'string', - 'min_length' => 2, - 'max_length' => 50 - ], - 'email' => [ - 'required' => true, - 'type' => 'email' - ], - 'age' => [ - 'type' => 'number' - ] - ]), - [$apiController, 'createUser'] -); - -// ✅ Data transformation route (Array Callable + Middleware) -$app->post('/transform', [RequestTransformer::class, 'transform'], [$apiController, 'transformData']); - -// ✅ Validation demonstration (Array Callable + Middleware) -$app->post('/validate', - InputValidator::create([ - 'name' => ['required' => true, 'type' => 'string', 'min_length' => 2], - 'email' => ['required' => true, 'type' => 'email'], - 'phone' => ['type' => 'string', 'pattern' => '/^[0-9+\-\s()]+$/'] - ]), - function($req, $res) { - return $res->json([ - 'message' => 'Validation passed successfully!', - 'validated_data' => $req->validatedData, - 'validation_middleware' => 'InputValidator v1.1.4+', - 'note' => 'Enhanced validation with contextual error diagnostics' - ]); - } -); - -// ✅ Error demonstration (Array Callable) -$app->get('/error-demo', [$apiController, 'errorDemo']); - -// ✅ Performance stats (Array Callable) -$app->get('/performance-stats', [$apiController, 'performanceStats']); - -// Complex middleware stack demonstration -$app->post('/complex', - [ApiKeyValidator::class, 'validate'], - [ContentNegotiation::class, 'negotiate'], - [RequestTransformer::class, 'transform'], - InputValidator::create([ - 'title' => ['required' => true, 'min_length' => 5], - 'content' => ['required' => true, 'min_length' => 10], - 'category' => ['required' => true, 'type' => 'string'] - ]), - function ($req, $res) { - $response = [ - 'message' => 'Processado por stack completo de middleware v1.1.4+', - 'middleware_stack' => [ - 'RequestLogger::log', - 'ResponseTimer::time', - 'ErrorHandler::handle', - 'ResponseModifier::modify', - 'ApiKeyValidator::validate', - 'ContentNegotiation::negotiate', - 'RequestTransformer::transform', - 'InputValidator::create' - ], - 'results' => [ - 'authenticated_user' => $req->authenticatedUser, - 'validated_data' => $req->validatedData, - 'transformed_data' => $req->transformedData, - 'preferred_format' => $req->preferredFormat, - 'transformation_log' => $req->transformationLog ?? [] - ], - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($req->validatedData), - 'complex_stack' => 'All middleware enhanced with v1.1.4+ features' - ] - ]; - - return $res->json($response); - } -); - -// Migration comparison endpoint -$app->get('/migration-comparison', function($req, $res) { - return $res->json([ - 'title' => 'Middleware Migration: v1.1.3 → v1.1.4+', - 'old_approach' => [ - 'middleware_definition' => 'function($req, $res, $next) { ... }', - 'error_handling' => 'Basic try-catch with generic messages', - 'organization' => 'Inline functions scattered throughout code', - 'validation' => 'Manual validation with basic error messages' - ], - 'new_approach_v114' => [ - 'middleware_definition' => '[MiddlewareClass::class, \'method\']', - 'error_handling' => 'ContextualException with detailed diagnostics', - 'organization' => 'Organized classes with static methods', - 'validation' => 'Enhanced validation with contextual error messages' - ], - 'benefits' => [ - 'better_organization' => 'Middleware organized in logical classes', - 'enhanced_errors' => 'Detailed error context and suggestions', - 'ide_support' => 'Full autocomplete and refactoring capabilities', - 'automatic_optimization' => 'JsonBufferPool integration', - 'better_debugging' => 'Request tracking and performance metrics' - ], - 'migration_effort' => 'Moderate - restructure middleware into classes with array callables', - 'optimization_v114' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling([]), - 'migration_demo' => 'Live demonstration of v1.1.4+ features' - ] - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/04-api/rest-api-modernized-v114.php b/examples/04-api/rest-api-modernized-v114.php deleted file mode 100644 index 6ccd6c5..0000000 --- a/examples/04-api/rest-api-modernized-v114.php +++ /dev/null @@ -1,791 +0,0 @@ -products = [ - 1 => [ - 'id' => 1, - 'name' => 'iPhone 15 Pro', - 'description' => 'Smartphone Apple com chip A17 Pro', - 'price' => 8999.99, - 'category' => 'electronics', - 'stock' => 25, - 'sku' => 'IPHONE15PRO-256', - 'tags' => ['smartphone', 'apple', 'premium'], - 'status' => 'active', - 'created_at' => '2024-01-15T10:30:00Z', - 'updated_at' => '2024-01-15T10:30:00Z' - ], - 2 => [ - 'id' => 2, - 'name' => 'MacBook Pro M3', - 'description' => 'Laptop profissional com chip M3', - 'price' => 15999.99, - 'category' => 'electronics', - 'stock' => 12, - 'sku' => 'MACBOOK-M3-512', - 'tags' => ['laptop', 'apple', 'professional'], - 'status' => 'active', - 'created_at' => '2024-01-20T14:45:00Z', - 'updated_at' => '2024-01-20T14:45:00Z' - ], - 3 => [ - 'id' => 3, - 'name' => 'Clean Code', - 'description' => 'Livro sobre código limpo por Robert Martin', - 'price' => 89.90, - 'category' => 'books', - 'stock' => 50, - 'sku' => 'BOOK-CLEANCODE', - 'tags' => ['programming', 'development', 'bestseller'], - 'status' => 'active', - 'created_at' => '2024-01-10T09:15:00Z', - 'updated_at' => '2024-01-10T09:15:00Z' - ] - ]; - } - - public function index($req, $res) - { - // Query parameters - $page = max(1, (int) $req->get('page', 1)); - $limit = max(1, min(100, (int) $req->get('limit', 10))); - $sort = $req->get('sort', 'id'); - $order = strtolower($req->get('order', 'asc')) === 'desc' ? 'desc' : 'asc'; - - // Filters - $filters = [ - 'category' => $req->get('category'), - 'status' => $req->get('status', 'active'), - 'min_price' => $req->get('min_price'), - 'max_price' => $req->get('max_price'), - 'search' => $req->get('search') - ]; - - // Apply filters - $filteredProducts = $this->filterProducts($filters); - - // Sort products - $sortableFields = ['id', 'name', 'price', 'created_at', 'stock']; - if (in_array($sort, $sortableFields)) { - uasort($filteredProducts, function($a, $b) use ($sort, $order) { - $result = $a[$sort] <=> $b[$sort]; - return $order === 'desc' ? -$result : $result; - }); - } - - // Paginate results - $result = $this->paginateResults($filteredProducts, $page, $limit); - - // ✅ NOVO v1.1.4+: Add optimization info - $result['optimization_v114'] = [ - 'json_pooling' => JsonBufferPool::shouldUsePooling($result) ? 'active' : 'direct_encode', - 'data_size' => $this->estimateDataSize($result), - 'performance_note' => 'Automatic optimization based on data size' - ]; - - // Add filter info to response - $result['filters'] = array_filter($filters); - $result['sort'] = ['field' => $sort, 'order' => $order]; - - // Set pagination headers - $res->header('X-Total-Count', (string)$result['pagination']['total']); - $res->header('X-Page', (string)$page); - $res->header('X-Per-Page', (string)$limit); - - return $res->json($result); - } - - public function show($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - // ✅ NOVO v1.1.4+: Enhanced error diagnostics - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $product = $this->products[$id]; - - // Add related information - $response = [ - 'data' => $product, - 'meta' => [ - 'retrieved_at' => date('c'), - 'optimization' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($product), - 'strategy' => 'Single product - optimized for speed' - ], - 'links' => [ - 'self' => "/api/v1/products/{$id}", - 'update' => "/api/v1/products/{$id}", - 'delete' => "/api/v1/products/{$id}", - 'category' => "/api/v1/categories/{$product['category']}" - ] - ] - ]; - - return $res->json($response); - } - - public function store($req, $res) - { - $body = $req->getBodyAsStdClass(); - - // Validate input - $errors = $this->validateProduct($body); - - // Check for duplicate SKU - if (!empty($body->sku)) { - foreach ($this->products as $product) { - if ($product['sku'] === $body->sku) { - $errors['sku'] = 'SKU já existe'; - break; - } - } - } - - if (!empty($errors)) { - // ✅ NOVO v1.1.4+: Enhanced validation errors - return $res->status(422)->json([ - 'error' => [ - 'code' => 'VALIDATION_ERROR', - 'message' => 'Dados de entrada inválidos', - 'details' => $errors, - 'context' => [ - 'endpoint' => 'POST /api/v1/products', - 'received_fields' => array_keys((array)$body), - 'required_fields' => ['name', 'price', 'category'] - ], - 'suggestions' => [ - 'Verifique se todos os campos obrigatórios estão presentes', - 'Confirme que o preço é um número positivo', - 'Verifique se a categoria existe' - ] - ] - ]); - } - - // Create product - $product = [ - 'id' => $this->nextId++, - 'name' => trim($body->name), - 'description' => trim($body->description ?? ''), - 'price' => (float) $body->price, - 'category' => $body->category, - 'stock' => (int) ($body->stock ?? 0), - 'sku' => trim($body->sku ?? ''), - 'tags' => $body->tags ?? [], - 'status' => $body->status ?? 'active', - 'created_at' => date('c'), - 'updated_at' => date('c') - ]; - - $this->products[$product['id']] = $product; - - $response = [ - 'data' => $product, - 'meta' => [ - 'created_at' => $product['created_at'], - 'optimization' => [ - 'json_encoding' => 'Optimized with JsonBufferPool v1.1.4+', - 'performance_gain' => 'Automatic based on response size' - ], - 'links' => [ - 'self' => "/api/v1/products/{$product['id']}", - 'update' => "/api/v1/products/{$product['id']}", - 'delete' => "/api/v1/products/{$product['id']}" - ] - ] - ]; - - return $res->status(201)->json($response); - } - - public function update($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $body = $req->getBodyAsStdClass(); - $errors = $this->validateProduct($body); - - if (!empty($errors)) { - return $res->status(422)->json([ - 'error' => [ - 'code' => 'VALIDATION_ERROR', - 'message' => 'Dados de entrada inválidos', - 'details' => $errors - ] - ]); - } - - // Update product (full replacement) - $originalProduct = $this->products[$id]; - $this->products[$id] = [ - 'id' => $id, - 'name' => trim($body->name), - 'description' => trim($body->description ?? ''), - 'price' => (float) $body->price, - 'category' => $body->category, - 'stock' => (int) ($body->stock ?? 0), - 'sku' => trim($body->sku ?? ''), - 'tags' => $body->tags ?? [], - 'status' => $body->status ?? 'active', - 'created_at' => $originalProduct['created_at'], - 'updated_at' => date('c') - ]; - - return $res->json([ - 'data' => $this->products[$id], - 'meta' => [ - 'updated_at' => $this->products[$id]['updated_at'], - 'changes' => 'full_update', - 'optimization' => 'JsonBufferPool v1.1.4+ active' - ] - ]); - } - - public function destroy($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $deletedProduct = $this->products[$id]; - unset($this->products[$id]); - - return $res->json([ - 'data' => $deletedProduct, - 'meta' => [ - 'deleted_at' => date('c'), - 'message' => 'Produto deletado com sucesso' - ] - ]); - } - - public function patch($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $body = $req->getBodyAsStdClass(); - $product = $this->products[$id]; - $changes = []; - - // Validate and update only provided fields - if (isset($body->name)) { - if (empty(trim($body->name))) { - return $res->status(422)->json([ - 'error' => ['name' => 'Nome não pode estar vazio'] - ]); - } - $product['name'] = trim($body->name); - $changes[] = 'name'; - } - - if (isset($body->price)) { - if (!is_numeric($body->price) || $body->price <= 0) { - return $res->status(422)->json([ - 'error' => ['price' => 'Preço deve ser um número positivo'] - ]); - } - $product['price'] = (float) $body->price; - $changes[] = 'price'; - } - - if (!empty($changes)) { - $product['updated_at'] = date('c'); - $this->products[$id] = $product; - } - - return $res->json([ - 'data' => $product, - 'meta' => [ - 'updated_at' => $product['updated_at'], - 'changes' => $changes, - 'change_count' => count($changes), - 'optimization' => 'JsonBufferPool v1.1.4+ partial update' - ] - ]); - } - - // Helper methods - private function validateProduct($data): array - { - $errors = []; - - if (empty($data->name)) { - $errors['name'] = 'Nome é obrigatório'; - } elseif (strlen($data->name) < 2) { - $errors['name'] = 'Nome deve ter pelo menos 2 caracteres'; - } - - if (!isset($data->price) || !is_numeric($data->price)) { - $errors['price'] = 'Preço deve ser um número'; - } elseif ($data->price <= 0) { - $errors['price'] = 'Preço deve ser maior que zero'; - } - - if (empty($data->category)) { - $errors['category'] = 'Categoria é obrigatória'; - } - - return $errors; - } - - private function filterProducts(array $filters): array - { - $filtered = $this->products; - - if (!empty($filters['category'])) { - $filtered = array_filter($filtered, function($product) use ($filters) { - return $product['category'] === $filters['category']; - }); - } - - if (!empty($filters['status'])) { - $filtered = array_filter($filtered, function($product) use ($filters) { - return $product['status'] === $filters['status']; - }); - } - - if (!empty($filters['search'])) { - $search = strtolower($filters['search']); - $filtered = array_filter($filtered, function($product) use ($search) { - return strpos(strtolower($product['name']), $search) !== false || - strpos(strtolower($product['description'] ?? ''), $search) !== false; - }); - } - - return $filtered; - } - - private function paginateResults(array $data, int $page, int $limit): array - { - $total = count($data); - $totalPages = ceil($total / $limit); - $offset = ($page - 1) * $limit; - - $paginatedData = array_slice($data, $offset, $limit, true); - - return [ - 'data' => array_values($paginatedData), - 'pagination' => [ - 'current_page' => $page, - 'per_page' => $limit, - 'total' => $total, - 'total_pages' => $totalPages, - 'from' => $total > 0 ? $offset + 1 : 0, - 'to' => min($offset + $limit, $total), - 'has_next' => $page < $totalPages, - 'has_prev' => $page > 1 - ] - ]; - } - - private function estimateDataSize(array $data): string - { - $size = strlen(json_encode($data)); - if ($size < 1024) return $size . ' bytes'; - if ($size < 1024 * 1024) return round($size / 1024, 1) . ' KB'; - return round($size / (1024 * 1024), 1) . ' MB'; - } -} - -class CategoryController -{ - private array $categories; - - public function __construct() - { - $this->categories = [ - 'electronics' => ['name' => 'Eletrônicos', 'description' => 'Dispositivos eletrônicos'], - 'books' => ['name' => 'Livros', 'description' => 'Livros e publicações'], - 'clothing' => ['name' => 'Roupas', 'description' => 'Vestuário e acessórios'], - 'home' => ['name' => 'Casa', 'description' => 'Itens para casa'] - ]; - } - - public function index($req, $res) - { - $categoriesWithCount = []; - - foreach ($this->categories as $slug => $category) { - $categoriesWithCount[] = [ - 'slug' => $slug, - 'name' => $category['name'], - 'description' => $category['description'], - 'links' => [ - 'self' => "/api/v1/categories/{$slug}", - 'products' => "/api/v1/categories/{$slug}/products" - ] - ]; - } - - return $res->json([ - 'data' => $categoriesWithCount, - 'meta' => [ - 'total_categories' => count($this->categories), - 'retrieved_at' => date('c'), - 'optimization_v114' => [ - 'json_strategy' => JsonBufferPool::shouldUsePooling($categoriesWithCount) - ? 'buffer_pool' : 'direct_encode', - 'performance_note' => 'Automatic optimization based on data size' - ] - ] - ]); - } - - public function show($req, $res) - { - $slug = $req->param('slug'); - - if (!isset($this->categories[$slug])) { - throw ContextualException::parameterError( - 'slug', - 'existing category slug', - $slug, - '/api/v1/categories/:slug' - ); - } - - $category = $this->categories[$slug]; - - return $res->json([ - 'data' => [ - 'slug' => $slug, - 'name' => $category['name'], - 'description' => $category['description'], - 'links' => [ - 'self' => "/api/v1/categories/{$slug}", - 'products' => "/api/v1/categories/{$slug}/products" - ] - ] - ]); - } -} - -class ApiController -{ - public function root($req, $res) - { - return $res->json([ - 'api' => 'PivotPHP RESTful API v1.1.4+', - 'version' => '1.0', - 'description' => 'API RESTful modernizada com novos recursos v1.1.4+', - 'base_url' => 'http://localhost:8000/api/v1', - 'features_v114' => [ - 'array_callables' => 'Native controller support ✅', - 'json_optimization' => 'Intelligent threshold pooling ✅', - 'error_diagnostics' => 'Enhanced contextual errors ✅', - 'performance_monitoring' => 'Real-time optimization stats ✅' - ], - 'documentation' => [ - 'Products Resource' => [ - 'GET /api/v1/products' => 'Listar produtos (com paginação e filtros)', - 'GET /api/v1/products/{id}' => 'Obter produto específico', - 'POST /api/v1/products' => 'Criar novo produto', - 'PUT /api/v1/products/{id}' => 'Atualizar produto completo', - 'PATCH /api/v1/products/{id}' => 'Atualizar produto parcial', - 'DELETE /api/v1/products/{id}' => 'Deletar produto' - ], - 'Categories Resource' => [ - 'GET /api/v1/categories' => 'Listar categorias', - 'GET /api/v1/categories/{slug}' => 'Obter categoria específica' - ] - ], - 'migration_from_old_version' => [ - 'before' => 'function($req, $res) { ... }', - 'after' => '[Controller::class, \'method\']', - 'benefits' => 'Better organization, IDE support, enhanced errors' - ] - ]); - } - - public function performance($req, $res) - { - $stats = JsonBufferPool::getStatistics(); - - return $res->json([ - 'framework' => 'PivotPHP Core v1.1.4+', - 'json_pool_stats' => $stats, - 'performance_metrics' => [ - 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), - 'optimization_active' => true, - 'threshold_bytes' => 256, - 'pool_efficiency' => $stats['efficiency'] ?? 'N/A' - ], - 'improvements_v114' => [ - 'automatic_threshold' => 'No configuration needed', - 'intelligent_optimization' => 'System decides when to use pooling', - 'zero_overhead' => 'Small responses use direct json_encode()', - 'performance_guarantee' => 'Never slower than standard encoding' - ] - ]); - } - - public function health($req, $res) - { - $health = [ - 'status' => 'healthy', - 'version' => Application::VERSION, - 'features' => [ - 'array_callables' => 'enabled', - 'json_optimization' => 'enabled', - 'contextual_errors' => 'enabled' - ], - 'checks' => [ - 'memory' => 'ok', - 'performance' => 'optimized', - 'errors' => 'enhanced' - ], - 'timestamp' => date('c') - ]; - - // Small response - should use direct json_encode() - $usePooling = JsonBufferPool::shouldUsePooling($health); - - $health['optimization'] = [ - 'uses_pooling' => $usePooling, - 'strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode', - 'note' => 'Health check optimized for minimal overhead' - ]; - - return $res->json($health); - } -} - -// =============================================== -// MIDDLEWARE v1.1.4+ (Array Callables) -// =============================================== - -class ApiMiddleware -{ - public static function headers($req, $res, $next) - { - $res->header('Content-Type', 'application/json; charset=utf-8'); - $res->header('X-API-Version', '1.0'); - $res->header('X-Powered-By', 'PivotPHP v1.1.4+'); - $res->header('X-Features', 'array-callables,json-optimization,enhanced-errors'); - $res->header('X-Request-ID', uniqid('req_', true)); - - return $next($req, $res); - } - - public static function cors($req, $res, $next) - { - $res->header('Access-Control-Allow-Origin', '*'); - $res->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS'); - $res->header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - if ($req->method() === 'OPTIONS') { - return $res->status(204)->send(''); - } - - return $next($req, $res); - } - - public static function performance($req, $res, $next) - { - $start = microtime(true); - $memoryBefore = memory_get_usage(true); - - $response = $next($req, $res); - - $duration = round((microtime(true) - $start) * 1000, 2); - $memoryUsed = memory_get_usage(true) - $memoryBefore; - - $res->header('X-Response-Time', $duration . 'ms'); - $res->header('X-Memory-Used', round($memoryUsed / 1024, 2) . 'KB'); - - return $response; - } - - public static function errorHandler($req, $res, $next) - { - try { - return $next($req, $res); - } catch (ContextualException $e) { - // ✅ Enhanced error handling v1.1.4+ - error_log("ContextualException: " . $e->getMessage()); - - return $res->status($e->getStatusCode())->json([ - 'error' => true, - 'message' => $e->getMessage(), - 'category' => $e->getCategory(), - 'context' => $e->getContext(), - 'suggestions' => $e->getSuggestions(), - 'debug' => $e->getDebugInfo(), - 'request_id' => $res->getHeader('X-Request-ID') - ]); - } catch (Exception $e) { - error_log("General Exception: " . $e->getMessage()); - - return $res->status(500)->json([ - 'error' => true, - 'message' => 'Internal Server Error', - 'request_id' => $res->getHeader('X-Request-ID') - ]); - } - } -} - -// =============================================== -// APPLICATION SETUP v1.1.4+ -// =============================================== - -$app = new Application(); - -// ✅ Apply middleware using array callables -$app->use([ApiMiddleware::class, 'cors']); -$app->use([ApiMiddleware::class, 'headers']); -$app->use([ApiMiddleware::class, 'performance']); -$app->use([ApiMiddleware::class, 'errorHandler']); - -// ✅ Initialize controllers -$productController = new ProductController(); -$categoryController = new CategoryController(); -$apiController = new ApiController(); - -// =============================================== -// ROUTES with Array Callables v1.1.4+ -// =============================================== - -// API Root & Documentation -$app->get('/api/v1/', [$apiController, 'root']); -$app->get('/api/v1/performance', [$apiController, 'performance']); -$app->get('/api/v1/health', [$apiController, 'health']); - -// ✅ Products Resource (Array Callables) -$app->get('/api/v1/products', [$productController, 'index']); -$app->get('/api/v1/products/:id<\\d+>', [$productController, 'show']); -$app->post('/api/v1/products', [$productController, 'store']); -$app->put('/api/v1/products/:id<\\d+>', [$productController, 'update']); -$app->patch('/api/v1/products/:id<\\d+>', [$productController, 'patch']); -$app->delete('/api/v1/products/:id<\\d+>', [$productController, 'destroy']); - -// ✅ Categories Resource (Array Callables) -$app->get('/api/v1/categories', [$categoryController, 'index']); -$app->get('/api/v1/categories/:slug<[a-z]+>', [$categoryController, 'show']); - -// Statistics endpoint -$app->get('/api/v1/stats', function($req, $res) use ($productController, $categoryController) { - // Aggregate stats from controllers - $stats = [ - 'api_version' => 'v1.1.4+', - 'total_products' => 3, // Simplified for demo - 'total_categories' => 4, - 'features' => [ - 'array_callables' => 'active', - 'json_optimization' => 'active', - 'enhanced_errors' => 'active' - ], - 'performance' => [ - 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'json_pool_stats' => JsonBufferPool::getStatistics() - ], - 'generated_at' => date('c') - ]; - - return $res->json($stats); -}); - -// Migration comparison endpoint -$app->get('/api/v1/migration-comparison', function($req, $res) { - return $res->json([ - 'title' => 'v1.1.3 → v1.1.4+ Migration Comparison', - 'old_approach' => [ - 'route_handlers' => 'function($req, $res) { ... }', - 'json_encoding' => 'json_encode($data)', - 'error_handling' => 'throw new Exception($message)', - 'organization' => 'Single file with closures' - ], - 'new_approach_v114' => [ - 'route_handlers' => '[Controller::class, \'method\']', - 'json_encoding' => 'JsonBufferPool::encodeWithPool($data) - automatic', - 'error_handling' => 'ContextualException::parameterError(...)', - 'organization' => 'Organized controllers with array callables' - ], - 'benefits' => [ - 'better_ide_support' => 'Full autocomplete and refactoring support', - 'automatic_optimization' => 'JsonBufferPool decides optimal strategy', - 'enhanced_debugging' => 'Contextual errors with suggestions', - 'cleaner_architecture' => 'Separated concerns and better organization' - ], - 'migration_effort' => 'Minimal - just replace closures with array callables' - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/04-api/rest-api-v114.php b/examples/04-api/rest-api-v114.php deleted file mode 100644 index 2943280..0000000 --- a/examples/04-api/rest-api-v114.php +++ /dev/null @@ -1,738 +0,0 @@ -products = [ - 1 => [ - 'id' => 1, - 'name' => 'iPhone 15 Pro', - 'description' => 'Smartphone Apple com chip A17 Pro', - 'price' => 8999.99, - 'category' => 'electronics', - 'stock' => 25, - 'sku' => 'IPHONE15PRO-256', - 'tags' => ['smartphone', 'apple', 'premium'], - 'status' => 'active', - 'created_at' => '2024-01-15T10:30:00Z', - 'updated_at' => '2024-01-15T10:30:00Z' - ], - 2 => [ - 'id' => 2, - 'name' => 'MacBook Pro M3', - 'description' => 'Laptop profissional com chip M3', - 'price' => 15999.99, - 'category' => 'electronics', - 'stock' => 12, - 'sku' => 'MACBOOK-M3-512', - 'tags' => ['laptop', 'apple', 'professional'], - 'status' => 'active', - 'created_at' => '2024-01-20T14:45:00Z', - 'updated_at' => '2024-01-20T14:45:00Z' - ], - 3 => [ - 'id' => 3, - 'name' => 'Clean Code', - 'description' => 'Livro sobre código limpo por Robert Martin', - 'price' => 89.90, - 'category' => 'books', - 'stock' => 50, - 'sku' => 'BOOK-CLEANCODE', - 'tags' => ['programming', 'development', 'bestseller'], - 'status' => 'active', - 'created_at' => '2024-01-10T09:15:00Z', - 'updated_at' => '2024-01-10T09:15:00Z' - ] - ]; - } - - public function index($req, $res) - { - // Query parameters - $page = max(1, (int) $req->get('page', 1)); - $limit = max(1, min(100, (int) $req->get('limit', 10))); - $sort = $req->get('sort', 'id'); - $order = strtolower($req->get('order', 'asc')) === 'desc' ? 'desc' : 'asc'; - - // Filters - $filters = [ - 'category' => $req->get('category'), - 'status' => $req->get('status', 'active'), - 'min_price' => $req->get('min_price'), - 'max_price' => $req->get('max_price'), - 'search' => $req->get('search') - ]; - - // Apply filters - $filteredProducts = $this->filterProducts($filters); - - // Sort products - $sortableFields = ['id', 'name', 'price', 'created_at', 'stock']; - if (in_array($sort, $sortableFields)) { - uasort($filteredProducts, function($a, $b) use ($sort, $order) { - $result = $a[$sort] <=> $b[$sort]; - return $order === 'desc' ? -$result : $result; - }); - } - - // Paginate results - $result = $this->paginateResults($filteredProducts, $page, $limit); - - // Add v1.1.4+ optimization info - $result['optimization_v114'] = [ - 'json_pooling' => JsonBufferPool::shouldUsePooling($result) ? 'active' : 'direct_encode', - 'data_size' => $this->estimateDataSize($result), - 'performance_note' => 'Automatic optimization based on data size' - ]; - - // Add filter info to response - $result['filters'] = array_filter($filters); - $result['sort'] = ['field' => $sort, 'order' => $order]; - - // Set pagination headers - $res->header('X-Total-Count', (string)$result['pagination']['total']); - $res->header('X-Page', (string)$page); - $res->header('X-Per-Page', (string)$limit); - - return $res->json($result); - } - - public function show($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - // ✅ NOVO v1.1.4+: Enhanced error diagnostics - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $product = $this->products[$id]; - - // Add related information - $response = [ - 'data' => $product, - 'meta' => [ - 'retrieved_at' => date('c'), - 'optimization' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($product), - 'strategy' => 'Single product - optimized for speed' - ], - 'links' => [ - 'self' => "/api/v1/products/{$id}", - 'update' => "/api/v1/products/{$id}", - 'delete' => "/api/v1/products/{$id}", - 'category' => "/api/v1/categories/{$product['category']}" - ] - ] - ]; - - return $res->json($response); - } - - public function store($req, $res) - { - $body = $req->getBodyAsStdClass(); - - // Validate input - $errors = $this->validateProduct($body); - - // Check for duplicate SKU - if (!empty($body->sku)) { - foreach ($this->products as $product) { - if ($product['sku'] === $body->sku) { - $errors['sku'] = 'SKU já existe'; - break; - } - } - } - - if (!empty($errors)) { - // ✅ NOVO v1.1.4+: Enhanced validation errors - return $res->status(422)->json([ - 'error' => [ - 'code' => 'VALIDATION_ERROR', - 'message' => 'Dados de entrada inválidos', - 'details' => $errors, - 'context' => [ - 'endpoint' => 'POST /api/v1/products', - 'received_fields' => array_keys((array)$body), - 'required_fields' => ['name', 'price', 'category'] - ], - 'suggestions' => [ - 'Verifique se todos os campos obrigatórios estão presentes', - 'Confirme que o preço é um número positivo', - 'Verifique se a categoria existe' - ] - ] - ]); - } - - // Create product - $product = [ - 'id' => $this->nextId++, - 'name' => trim($body->name), - 'description' => trim($body->description ?? ''), - 'price' => (float) $body->price, - 'category' => $body->category, - 'stock' => (int) ($body->stock ?? 0), - 'sku' => trim($body->sku ?? ''), - 'tags' => $body->tags ?? [], - 'status' => $body->status ?? 'active', - 'created_at' => date('c'), - 'updated_at' => date('c') - ]; - - $this->products[$product['id']] = $product; - - $response = [ - 'data' => $product, - 'meta' => [ - 'created_at' => $product['created_at'], - 'optimization' => [ - 'json_encoding' => 'Optimized with JsonBufferPool v1.1.4+', - 'performance_gain' => 'Automatic based on response size' - ], - 'links' => [ - 'self' => "/api/v1/products/{$product['id']}", - 'update' => "/api/v1/products/{$product['id']}", - 'delete' => "/api/v1/products/{$product['id']}" - ] - ] - ]; - - return $res->status(201)->json($response); - } - - public function update($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $body = $req->getBodyAsStdClass(); - $errors = $this->validateProduct($body); - - if (!empty($errors)) { - return $res->status(422)->json([ - 'error' => [ - 'code' => 'VALIDATION_ERROR', - 'message' => 'Dados de entrada inválidos', - 'details' => $errors - ] - ]); - } - - // Update product (full replacement) - $originalProduct = $this->products[$id]; - $this->products[$id] = [ - 'id' => $id, - 'name' => trim($body->name), - 'description' => trim($body->description ?? ''), - 'price' => (float) $body->price, - 'category' => $body->category, - 'stock' => (int) ($body->stock ?? 0), - 'sku' => trim($body->sku ?? ''), - 'tags' => $body->tags ?? [], - 'status' => $body->status ?? 'active', - 'created_at' => $originalProduct['created_at'], - 'updated_at' => date('c') - ]; - - return $res->json([ - 'data' => $this->products[$id], - 'meta' => [ - 'updated_at' => $this->products[$id]['updated_at'], - 'changes' => 'full_update', - 'optimization' => 'JsonBufferPool v1.1.4+ active' - ] - ]); - } - - public function destroy($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->products[$id])) { - throw ContextualException::parameterError( - 'id', - 'existing product ID', - $id, - '/api/v1/products/:id' - ); - } - - $deletedProduct = $this->products[$id]; - unset($this->products[$id]); - - return $res->json([ - 'data' => $deletedProduct, - 'meta' => [ - 'deleted_at' => date('c'), - 'message' => 'Produto deletado com sucesso' - ] - ]); - } - - // Helper methods - private function validateProduct($data): array - { - $errors = []; - - if (empty($data->name)) { - $errors['name'] = 'Nome é obrigatório'; - } elseif (strlen($data->name) < 2) { - $errors['name'] = 'Nome deve ter pelo menos 2 caracteres'; - } - - if (!isset($data->price) || !is_numeric($data->price)) { - $errors['price'] = 'Preço deve ser um número'; - } elseif ($data->price <= 0) { - $errors['price'] = 'Preço deve ser maior que zero'; - } - - if (empty($data->category)) { - $errors['category'] = 'Categoria é obrigatória'; - } - - return $errors; - } - - private function filterProducts(array $filters): array - { - $filtered = $this->products; - - if (!empty($filters['category'])) { - $filtered = array_filter($filtered, function($product) use ($filters) { - return $product['category'] === $filters['category']; - }); - } - - if (!empty($filters['status'])) { - $filtered = array_filter($filtered, function($product) use ($filters) { - return $product['status'] === $filters['status']; - }); - } - - if (!empty($filters['search'])) { - $search = strtolower($filters['search']); - $filtered = array_filter($filtered, function($product) use ($search) { - return strpos(strtolower($product['name']), $search) !== false || - strpos(strtolower($product['description'] ?? ''), $search) !== false; - }); - } - - return $filtered; - } - - private function paginateResults(array $data, int $page, int $limit): array - { - $total = count($data); - $totalPages = ceil($total / $limit); - $offset = ($page - 1) * $limit; - - $paginatedData = array_slice($data, $offset, $limit, true); - - return [ - 'data' => array_values($paginatedData), - 'pagination' => [ - 'current_page' => $page, - 'per_page' => $limit, - 'total' => $total, - 'total_pages' => $totalPages, - 'from' => $total > 0 ? $offset + 1 : 0, - 'to' => min($offset + $limit, $total), - 'has_next' => $page < $totalPages, - 'has_prev' => $page > 1 - ] - ]; - } - - private function estimateDataSize(array $data): string - { - $size = strlen(json_encode($data)); - if ($size < 1024) return $size . ' bytes'; - if ($size < 1024 * 1024) return round($size / 1024, 1) . ' KB'; - return round($size / (1024 * 1024), 1) . ' MB'; - } -} - -class CategoryController -{ - private array $categories; - - public function __construct() - { - $this->categories = [ - 'electronics' => ['name' => 'Eletrônicos', 'description' => 'Dispositivos eletrônicos'], - 'books' => ['name' => 'Livros', 'description' => 'Livros e publicações'], - 'clothing' => ['name' => 'Roupas', 'description' => 'Vestuário e acessórios'], - 'home' => ['name' => 'Casa', 'description' => 'Itens para casa'] - ]; - } - - public function index($req, $res) - { - $categoriesWithCount = []; - - foreach ($this->categories as $slug => $category) { - $categoriesWithCount[] = [ - 'slug' => $slug, - 'name' => $category['name'], - 'description' => $category['description'], - 'links' => [ - 'self' => "/api/v1/categories/{$slug}", - 'products' => "/api/v1/categories/{$slug}/products" - ] - ]; - } - - return $res->json([ - 'data' => $categoriesWithCount, - 'meta' => [ - 'total_categories' => count($this->categories), - 'retrieved_at' => date('c'), - 'optimization_v114' => [ - 'json_strategy' => JsonBufferPool::shouldUsePooling($categoriesWithCount) - ? 'buffer_pool' : 'direct_encode', - 'performance_note' => 'Automatic optimization based on data size' - ] - ] - ]); - } - - public function show($req, $res) - { - $slug = $req->param('slug'); - - if (!isset($this->categories[$slug])) { - throw ContextualException::parameterError( - 'slug', - 'existing category slug', - $slug, - '/api/v1/categories/:slug' - ); - } - - $category = $this->categories[$slug]; - - return $res->json([ - 'data' => [ - 'slug' => $slug, - 'name' => $category['name'], - 'description' => $category['description'], - 'links' => [ - 'self' => "/api/v1/categories/{$slug}", - 'products' => "/api/v1/categories/{$slug}/products" - ] - ] - ]); - } -} - -class ApiController -{ - public function root($req, $res) - { - // Large response - JsonBufferPool will automatically use pooling - $documentation = [ - 'api' => 'PivotPHP RESTful API v1.1.4+', - 'version' => '1.0', - 'description' => 'Demonstração completa de API RESTful com novos recursos v1.1.4+', - 'base_url' => 'http://localhost:8000/api/v1', - 'features_v114' => [ - 'array_callables' => 'Native controller support ✅', - 'json_optimization' => 'Intelligent threshold pooling ✅', - 'error_diagnostics' => 'Enhanced contextual errors ✅', - 'performance_monitoring' => 'Real-time optimization stats ✅' - ], - 'documentation' => [ - 'Products Resource' => [ - 'GET /api/v1/products' => 'Listar produtos (com paginação e filtros)', - 'GET /api/v1/products/{id}' => 'Obter produto específico', - 'POST /api/v1/products' => 'Criar novo produto', - 'PUT /api/v1/products/{id}' => 'Atualizar produto completo', - 'DELETE /api/v1/products/{id}' => 'Deletar produto' - ], - 'Categories Resource' => [ - 'GET /api/v1/categories' => 'Listar categorias', - 'GET /api/v1/categories/{slug}' => 'Obter categoria específica' - ] - ], - 'optimization_details' => [ - 'automatic_json_pooling' => 'JsonBufferPool decides based on response size', - 'threshold' => '256 bytes - smaller responses use direct json_encode()', - 'performance_gain' => 'Up to 98% faster for large responses', - 'memory_efficiency' => 'Automatic buffer reuse and optimization' - ], - 'examples' => array_fill(0, 15, [ - 'method' => 'GET', - 'endpoint' => '/api/v1/products', - 'description' => 'List products with advanced filtering', - 'parameters' => ['page', 'limit', 'category', 'search', 'min_price', 'max_price'] - ]) - ]; - - return $res->json($documentation); - } - - public function performance($req, $res) - { - $stats = JsonBufferPool::getStatistics(); - - return $res->json([ - 'framework' => 'PivotPHP Core v1.1.4+', - 'json_pool_stats' => $stats, - 'performance_metrics' => [ - 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), - 'optimization_active' => true, - 'threshold_bytes' => 256, - 'pool_efficiency' => $stats['efficiency'] ?? 'N/A' - ], - 'improvements_v114' => [ - 'automatic_threshold' => 'No configuration needed', - 'intelligent_optimization' => 'System decides when to use pooling', - 'zero_overhead' => 'Small responses use direct json_encode()', - 'performance_guarantee' => 'Never slower than standard encoding' - ] - ]); - } - - public function health($req, $res) - { - $health = [ - 'status' => 'healthy', - 'version' => Application::VERSION, - 'features' => [ - 'array_callables' => class_exists('PivotPHP\\Core\\Utils\\CallableResolver'), - 'json_optimization' => method_exists('PivotPHP\\Core\\Json\\Pool\\JsonBufferPool', 'shouldUsePooling'), - 'contextual_errors' => class_exists('PivotPHP\\Core\\Exceptions\\Enhanced\\ContextualException') - ], - 'checks' => [ - 'memory' => 'ok', - 'performance' => 'optimized', - 'errors' => 'enhanced' - ], - 'timestamp' => date('c') - ]; - - // Small response - should use direct json_encode() - $usePooling = JsonBufferPool::shouldUsePooling($health); - - $health['optimization'] = [ - 'uses_pooling' => $usePooling, - 'strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode', - 'note' => 'Health check optimized for minimal overhead' - ]; - - return $res->json($health); - } -} - -// =============================================== -// MIDDLEWARE v1.1.4+ -// =============================================== - -class ApiMiddleware -{ - public static function headers($req, $res, $next) - { - $res->header('Content-Type', 'application/json; charset=utf-8'); - $res->header('X-API-Version', '1.0'); - $res->header('X-Powered-By', 'PivotPHP v1.1.4+'); - $res->header('X-Features', 'array-callables,json-optimization,enhanced-errors'); - $res->header('X-Request-ID', uniqid('req_', true)); - - return $next($req, $res); - } - - public static function cors($req, $res, $next) - { - $res->header('Access-Control-Allow-Origin', '*'); - $res->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - $res->header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - if ($req->method() === 'OPTIONS') { - return $res->status(204)->send(''); - } - - return $next($req, $res); - } - - public static function performance($req, $res, $next) - { - $start = microtime(true); - $memoryBefore = memory_get_usage(true); - - $response = $next($req, $res); - - $duration = round((microtime(true) - $start) * 1000, 2); - $memoryUsed = memory_get_usage(true) - $memoryBefore; - - $res->header('X-Response-Time', $duration . 'ms'); - $res->header('X-Memory-Used', round($memoryUsed / 1024, 2) . 'KB'); - - return $response; - } - - public static function errorHandler($req, $res, $next) - { - try { - return $next($req, $res); - } catch (ContextualException $e) { - // ✅ Enhanced error handling v1.1.4+ - error_log("ContextualException: " . $e->getMessage()); - - return $res->status($e->getStatusCode())->json([ - 'error' => true, - 'message' => $e->getMessage(), - 'category' => $e->getCategory(), - 'context' => $e->getContext(), - 'suggestions' => $e->getSuggestions(), - 'debug' => $e->getDebugInfo(), - 'request_id' => $res->getHeader('X-Request-ID') - ]); - } catch (Exception $e) { - error_log("General Exception: " . $e->getMessage()); - - return $res->status(500)->json([ - 'error' => true, - 'message' => 'Internal Server Error', - 'request_id' => $res->getHeader('X-Request-ID') - ]); - } - } -} - -// =============================================== -// APPLICATION SETUP v1.1.4+ -// =============================================== - -$app = new Application(); - -// ✅ Apply middleware using array callables -$app->use([ApiMiddleware::class, 'cors']); -$app->use([ApiMiddleware::class, 'headers']); -$app->use([ApiMiddleware::class, 'performance']); -$app->use([ApiMiddleware::class, 'errorHandler']); - -// ✅ Initialize controllers (demonstrating dependency injection) -$productController = new ProductController(); -$categoryController = new CategoryController(); -$apiController = new ApiController(); - -// =============================================== -// ROUTES with Array Callables v1.1.4+ -// =============================================== - -// API Root & Documentation -$app->get('/api/v1/', [$apiController, 'root']); -$app->get('/api/v1/performance', [$apiController, 'performance']); -$app->get('/api/v1/health', [$apiController, 'health']); - -// Products Resource -$app->get('/api/v1/products', [$productController, 'index']); -$app->get('/api/v1/products/:id<\\d+>', [$productController, 'show']); -$app->post('/api/v1/products', [$productController, 'store']); -$app->put('/api/v1/products/:id<\\d+>', [$productController, 'update']); -$app->delete('/api/v1/products/:id<\\d+>', [$productController, 'destroy']); - -// Categories Resource -$app->get('/api/v1/categories', [$categoryController, 'index']); -$app->get('/api/v1/categories/:slug<[a-z]+>', [$categoryController, 'show']); - -// Demo endpoint to show JsonBufferPool threshold in action -$app->get('/api/v1/demo/json-optimization', function($req, $res) { - $size = $req->get('size', 'small'); - - switch($size) { - case 'small': - $data = ['message' => 'Small data', 'size' => 'small']; - break; - case 'medium': - $data = array_fill(0, 50, ['id' => rand(), 'data' => str_repeat('x', 50)]); - break; - case 'large': - $data = array_fill(0, 500, ['id' => rand(), 'data' => str_repeat('x', 100)]); - break; - default: - $data = ['error' => 'Invalid size parameter']; - } - - $usePooling = JsonBufferPool::shouldUsePooling($data); - $stats = JsonBufferPool::getStatistics(); - - return $res->json([ - 'demo' => 'JsonBufferPool Optimization v1.1.4+', - 'requested_size' => $size, - 'data' => $data, - 'optimization' => [ - 'uses_pooling' => $usePooling, - 'strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode', - 'threshold' => '256 bytes', - 'explanation' => $usePooling - ? 'Data size exceeds threshold - using buffer pool for optimization' - : 'Data size below threshold - using direct json_encode() for minimal overhead' - ], - 'pool_stats' => $stats, - 'test_urls' => [ - 'small' => '/api/v1/demo/json-optimization?size=small', - 'medium' => '/api/v1/demo/json-optimization?size=medium', - 'large' => '/api/v1/demo/json-optimization?size=large' - ] - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/05-performance/high-performance.php b/examples/05-performance/high-performance.php index 7c08793..13bc005 100644 --- a/examples/05-performance/high-performance.php +++ b/examples/05-performance/high-performance.php @@ -2,10 +2,10 @@ /** * ⚡ PivotPHP - Modo Performance Simplificado - * - * Demonstra recursos de performance do PivotPHP v1.2.0+ + * + * Demonstra recursos de performance do PivotPHP v2.0.0+ * Performance simplificada, JSON optimization, memory management e métricas - * + * * NOTA: Versão simplificada seguindo principio "Simplicidade sobre Otimização Prematura" * * 🚀 Como executar: @@ -32,7 +32,7 @@ $app->get('/', function ($req, $res) { return $res->json([ 'title' => 'PivotPHP - High Performance Examples', - 'description' => 'Demonstrações dos recursos de performance v1.2.0+', + 'description' => 'Demonstrações dos recursos de performance v2.0.0+', 'performance_features' => [ 'High Performance Mode' => [ 'description' => 'Modo otimizado para throughput máximo', @@ -40,7 +40,7 @@ 'benefits' => ['Object pooling', 'Memory optimization', 'Response caching'] ], 'JSON Buffer Pooling' => [ - 'description' => 'Pool de buffers para operações JSON v1.1.1', + 'description' => 'Pool de buffers para operações JSON v2.0.0', 'auto_optimization' => 'Detecta e otimiza datasets grandes automaticamente', 'performance_gain' => 'Até 300% de melhoria em JSON encoding/decoding' ], @@ -101,7 +101,7 @@ 'object_pooling' => $status['enabled'], 'response_caching' => $status['enabled'], 'memory_optimization' => $status['enabled'], - 'json_pooling' => true // Always available in v1.1.1 + 'json_pooling' => true // Always available in v2.0.0 ], 'performance_impact' => [ 'expected_throughput_gain' => $profile === 'PRODUCTION' ? '50-100%' : ($profile === 'DEVELOPMENT' ? '0-25%' : '0%'), @@ -118,7 +118,7 @@ return $res->json([ 'message' => 'Modo alta performance desativado', 'status' => ['enabled' => PerformanceMode::isEnabled()], - 'note' => 'JSON pooling permanece ativo (feature v1.1.1)' + 'note' => 'JSON pooling permanece ativo (feature v2.0.0)' ]); }); diff --git a/examples/07-advanced/array-callables-v114.php b/examples/07-advanced/array-callables-v114.php deleted file mode 100644 index 9362b71..0000000 --- a/examples/07-advanced/array-callables-v114.php +++ /dev/null @@ -1,513 +0,0 @@ -users = [ - 1 => ['id' => 1, 'name' => 'João Silva', 'email' => 'joao@example.com'], - 2 => ['id' => 2, 'name' => 'Maria Santos', 'email' => 'maria@example.com'], - 3 => ['id' => 3, 'name' => 'Pedro Oliveira', 'email' => 'pedro@example.com'] - ]; - } - - // ✅ Método público - Funciona com array callable - public function index($req, $res) - { - return $res->json([ - 'message' => 'Array callable funcionando! ✅', - 'method' => 'UserController::index', - 'type' => 'instance_method', - 'data' => array_values($this->users), - 'optimization' => [ - 'uses_pooling' => JsonBufferPool::shouldUsePooling($this->users), - 'array_callable_validation' => 'passed' - ] - ]); - } - - public function show($req, $res) - { - $id = (int) $req->param('id'); - - if (!isset($this->users[$id])) { - // Enhanced error with context - throw ContextualException::parameterError( - 'id', - 'existing user ID', - $id, - '/users/:id' - ); - } - - return $res->json([ - 'message' => 'User found with array callable! ✅', - 'method' => 'UserController::show', - 'user' => $this->users[$id] - ]); - } - - // ✅ Método estático - Funciona com array callable - public static function staticMethod($req, $res) - { - return $res->json([ - 'message' => 'Static method via array callable! ✅', - 'method' => 'UserController::staticMethod', - 'type' => 'static_method', - 'features_v114' => [ - 'array_callables' => 'Native support', - 'static_methods' => 'Fully supported', - 'validation' => 'Automatic' - ] - ]); - } - - // ❌ Método privado - NÃO funciona com array callable - private function privateMethod($req, $res) - { - return $res->json(['this' => 'should_not_work']); - } - - // ❌ Método protegido - NÃO funciona com array callable - protected function protectedMethod($req, $res) - { - return $res->json(['this' => 'should_not_work']); - } -} - -class ProductController -{ - // ✅ Dependency injection via constructor - private $logger; - - public function __construct($logger = null) - { - $this->logger = $logger ?: function($msg) { error_log($msg); }; - } - - public function list($req, $res) - { - ($this->logger)('ProductController::list called via array callable'); - - // Generate large dataset to demonstrate JsonBufferPool - $products = array_fill(0, 100, [ - 'id' => rand(1, 1000), - 'name' => 'Product ' . rand(1, 100), - 'description' => str_repeat('Lorem ipsum dolor sit amet. ', 10), - 'price' => rand(10, 1000) + rand(0, 99) / 100, - 'category' => ['electronics', 'books', 'clothing'][rand(0, 2)], - 'tags' => ['tag1', 'tag2', 'tag3'], - 'metadata' => [ - 'created_at' => date('c'), - 'updated_at' => date('c'), - 'status' => 'active' - ] - ]); - - return $res->json([ - 'message' => 'Large dataset via array callable! ✅', - 'method' => 'ProductController::list', - 'products' => $products, - 'optimization' => [ - 'data_size' => count($products) . ' products', - 'uses_pooling' => JsonBufferPool::shouldUsePooling($products), - 'performance_note' => 'JsonBufferPool automatically optimizes large responses', - 'v114_features' => 'Automatic threshold detection' - ], - 'pool_stats' => JsonBufferPool::getStatistics() - ]); - } -} - -class ValidationDemoController -{ - // ✅ Método público válido - public function validMethod($req, $res) - { - return $res->json([ - 'status' => 'success', - 'message' => 'This method is public and callable ✅', - 'validation' => 'passed' - ]); - } - - // ❌ Método privado inválido - private function invalidMethod($req, $res) - { - return $res->json([ - 'status' => 'error', - 'message' => 'This should never be reached' - ]); - } -} - -// Controller que não existe (para demonstrar erro) -class NonExistentController -{ - // Este controller será usado apenas para demonstrar erros -} - -// =============================================== -// UTILITY FUNCTIONS -// =============================================== - -function benchmarkCallableTypes($iterations = 1000) -{ - $results = []; - - // Test closure - $closure = function($req, $res) { return 'closure'; }; - $start = microtime(true); - for ($i = 0; $i < $iterations; $i++) { - is_callable($closure); - } - $results['closure'] = microtime(true) - $start; - - // Test array callable - $arrayCallable = [UserController::class, 'staticMethod']; - $start = microtime(true); - for ($i = 0; $i < $iterations; $i++) { - is_callable($arrayCallable); - } - $results['array_callable'] = microtime(true) - $start; - - // Test string function - $stringFunction = 'strlen'; - $start = microtime(true); - for ($i = 0; $i < $iterations; $i++) { - is_callable($stringFunction); - } - $results['string_function'] = microtime(true) - $start; - - return $results; -} - -function demonstrateCallableValidation() -{ - $tests = []; - - // ✅ Valid array callables - $validTests = [ - [UserController::class, 'index'], - [UserController::class, 'staticMethod'], - [new UserController(), 'index'] - ]; - - foreach ($validTests as $callable) { - try { - CallableResolver::resolve($callable); - $tests[] = [ - 'callable' => is_object($callable[0]) ? get_class($callable[0]) . '::' . $callable[1] : implode('::', $callable), - 'status' => 'valid', - 'message' => 'Array callable validation passed ✅' - ]; - } catch (Exception $e) { - $tests[] = [ - 'callable' => implode('::', $callable), - 'status' => 'invalid', - 'message' => $e->getMessage() - ]; - } - } - - // ❌ Invalid array callables - $invalidTests = [ - [ValidationDemoController::class, 'invalidMethod'], // private method - ['NonExistentClass', 'method'], // class doesn't exist - [ValidationDemoController::class, 'nonExistentMethod'] // method doesn't exist - ]; - - foreach ($invalidTests as $callable) { - try { - CallableResolver::resolve($callable); - $tests[] = [ - 'callable' => implode('::', $callable), - 'status' => 'unexpected_valid', - 'message' => 'This should have failed!' - ]; - } catch (Exception $e) { - $tests[] = [ - 'callable' => implode('::', $callable), - 'status' => 'correctly_invalid', - 'message' => 'Correctly rejected: ' . $e->getMessage() . ' ✅' - ]; - } - } - - return $tests; -} - -// =============================================== -// APPLICATION SETUP -// =============================================== - -$app = new Application(); - -// Initialize controllers with dependency injection -$userController = new UserController(); -$productController = new ProductController(function($msg) { - error_log("[ARRAY_CALLABLE_DEMO] $msg"); -}); -$validationController = new ValidationDemoController(); - -// =============================================== -// ROUTES - Array Callables v1.1.4+ -// =============================================== - -// Home page with documentation -$app->get('/', function($req, $res) { - return $res->json([ - 'title' => 'PivotPHP v1.1.4+ Array Callables Demo', - 'description' => 'Demonstração completa dos novos array callables nativos', - 'features' => [ - 'native_array_callables' => 'Support for [Controller::class, \'method\']', - 'automatic_validation' => 'CallableResolver validates public/private methods', - 'enhanced_errors' => 'ContextualException with detailed diagnostics', - 'performance_optimized' => 'Zero overhead validation' - ], - 'demo_endpoints' => [ - 'GET /' => 'This documentation', - 'GET /demo/static-method' => 'Static method via array callable', - 'GET /demo/instance-method' => 'Instance method via array callable', - 'GET /demo/large-response' => 'Large response with JsonBufferPool optimization', - 'GET /demo/validation' => 'Validation demonstration', - 'GET /demo/performance' => 'Performance benchmarks', - 'GET /users' => 'Users list via array callable', - 'GET /users/:id' => 'User detail with error handling', - 'GET /error-demo' => 'Error handling demonstration' - ], - 'syntax_examples' => [ - 'supported' => [ - 'instance_method' => '[$controller, \'method\']', - 'static_method' => '[Controller::class, \'method\']', - 'closure' => 'function($req, $res) { ... }', - 'named_function' => '\'functionName\'' - ], - 'not_supported' => [ - 'string_format' => '\'Controller@method\' - Use array callable instead!', - 'brace_params' => '"/route/{param}" - Use colon syntax ":param"' - ] - ] - ]); -}); - -// ✅ Array callables - Static method -$app->get('/demo/static-method', [UserController::class, 'staticMethod']); - -// ✅ Array callables - Instance method -$app->get('/demo/instance-method', [$userController, 'index']); - -// ✅ Array callables - Large response for JsonBufferPool demo -$app->get('/demo/large-response', [$productController, 'list']); - -// ✅ Array callables - Users resource -$app->get('/users', [$userController, 'index']); -$app->get('/users/:id<\\d+>', [$userController, 'show']); - -// Validation demonstration -$app->get('/demo/validation', function($req, $res) { - $validationResults = demonstrateCallableValidation(); - - return $res->json([ - 'title' => 'Array Callable Validation Demo v1.1.4+', - 'description' => 'Demonstra a validação automática de array callables', - 'validation_results' => $validationResults, - 'callableResolver_features' => [ - 'public_method_validation' => 'Only public methods are allowed', - 'class_existence_check' => 'Verifies class exists before validation', - 'method_existence_check' => 'Verifies method exists in class', - 'accessibility_validation' => 'Ensures method is publicly accessible', - 'enhanced_error_messages' => 'Detailed error messages with suggestions' - ], - 'summary' => [ - 'total_tests' => count($validationResults), - 'valid_callables' => count(array_filter($validationResults, fn($r) => $r['status'] === 'valid')), - 'correctly_rejected' => count(array_filter($validationResults, fn($r) => $r['status'] === 'correctly_invalid')) - ] - ]); -}); - -// Performance demonstration -$app->get('/demo/performance', function($req, $res) { - $iterations = (int) $req->get('iterations', 10000); - $benchmarkResults = benchmarkCallableTypes($iterations); - - return $res->json([ - 'title' => 'Array Callable Performance Demo v1.1.4+', - 'description' => 'Compara performance de diferentes tipos de callables', - 'iterations' => $iterations, - 'benchmark_results_seconds' => $benchmarkResults, - 'benchmark_results_milliseconds' => array_map(fn($time) => round($time * 1000, 3), $benchmarkResults), - 'performance_notes' => [ - 'array_callables' => 'Optimized validation with minimal overhead', - 'caching' => 'CallableResolver caches validation results internally', - 'zero_runtime_cost' => 'Validation happens only during route registration', - 'production_ready' => 'No performance impact on request handling' - ], - 'recommendations' => [ - 'use_array_callables' => 'For better code organization and IDE support', - 'prefer_static_methods' => 'For stateless operations', - 'use_instance_methods' => 'For stateful operations with dependency injection' - ] - ]); -}); - -// ✅ Valid array callable -$app->get('/demo/valid-callable', [$validationController, 'validMethod']); - -// ❌ Error demonstration - This will show enhanced error diagnostics -$app->get('/error-demo', function($req, $res) { - try { - // Try to register an invalid array callable - $invalidCallable = [ValidationDemoController::class, 'invalidMethod']; - CallableResolver::resolve($invalidCallable); - - return $res->json([ - 'error' => 'This should not happen - invalid callable was accepted!' - ]); - } catch (Exception $e) { - // This will demonstrate enhanced error diagnostics - if ($e instanceof ContextualException) { - return $res->status(400)->json([ - 'demonstration' => 'Enhanced Error Diagnostics v1.1.4+', - 'error_type' => 'ContextualException', - 'message' => $e->getMessage(), - 'category' => $e->getCategory(), - 'context' => $e->getContext(), - 'suggestions' => $e->getSuggestions(), - 'debug_info' => $e->getDebugInfo() - ]); - } else { - return $res->status(400)->json([ - 'demonstration' => 'Standard Exception', - 'error_type' => get_class($e), - 'message' => $e->getMessage(), - 'note' => 'This shows how enhanced errors provide more context than standard exceptions' - ]); - } - } -}); - -// Migration examples - showing old vs new syntax -$app->get('/demo/migration', function($req, $res) { - return $res->json([ - 'title' => 'Migration Guide - Array Callables v1.1.4+', - 'migration_examples' => [ - 'before_v114' => [ - 'description' => 'How routes were defined before v1.1.4', - 'code' => 'function($req, $res) { $controller = new UserController(); return $controller->index($req, $res); }', - 'issues' => [ - 'verbose_syntax' => 'Required closure wrapper for every route', - 'manual_instantiation' => 'Had to manually create controller instances', - 'no_validation' => 'No automatic validation of callable validity', - 'harder_testing' => 'More complex to test individual controller methods' - ] - ], - 'after_v114' => [ - 'description' => 'Clean array callable syntax in v1.1.4+', - 'code' => '[UserController::class, \'index\']', - 'benefits' => [ - 'clean_syntax' => 'Direct array callable support', - 'automatic_validation' => 'CallableResolver validates at registration time', - 'enhanced_errors' => 'Detailed error messages with context and suggestions', - 'performance_optimized' => 'Zero runtime overhead after validation', - 'ide_support' => 'Better IDE completion and refactoring support' - ] - ] - ], - 'migration_checklist' => [ - 'replace_closure_wrappers' => 'Replace closures with array callables where appropriate', - 'update_controller_methods' => 'Ensure all methods are public', - 'use_class_constants' => 'Use ControllerClass::class instead of string names', - 'test_thoroughly' => 'Verify all routes work with new syntax', - 'update_documentation' => 'Update route documentation to reflect new syntax' - ] - ]); -}); - -// Comparison endpoint - shows all supported syntaxes working together -$app->get('/demo/syntax-comparison', function($req, $res) { - return $res->json([ - 'title' => 'All Supported Route Handler Syntaxes v1.1.4+', - 'syntaxes' => [ - 'array_callable_static' => [ - 'example' => '[UserController::class, \'staticMethod\']', - 'endpoint' => '/demo/static-method', - 'description' => 'Static method via array callable', - 'benefits' => ['Clean syntax', 'IDE support', 'No instantiation needed'] - ], - 'array_callable_instance' => [ - 'example' => '[$controller, \'method\']', - 'endpoint' => '/demo/instance-method', - 'description' => 'Instance method via array callable', - 'benefits' => ['Dependency injection support', 'Stateful operations', 'Clean syntax'] - ], - 'closure' => [ - 'example' => 'function($req, $res) { return $res->json([...]); }', - 'endpoint' => '/', - 'description' => 'Anonymous function/closure', - 'benefits' => ['Inline logic', 'Quick prototyping', 'No separate class needed'] - ], - 'named_function' => [ - 'example' => '\'namedFunction\'', - 'endpoint' => 'N/A in this demo', - 'description' => 'Named function reference', - 'benefits' => ['Simple functions', 'Global utilities', 'Functional programming style'] - ] - ], - 'not_supported' => [ - 'string_controller_method' => [ - 'example' => '\'UserController@index\'', - 'reason' => 'Not considered callable by PHP', - 'migration' => 'Use [UserController::class, \'index\'] instead' - ], - 'brace_syntax' => [ - 'example' => '/route/{param}', - 'reason' => 'Reserved for regex definitions', - 'migration' => 'Use /route/:param instead' - ] - ] - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/07-advanced/performance-v1.1.3.php b/examples/07-advanced/performance-v1.1.3.php deleted file mode 100644 index 4bbb7bd..0000000 --- a/examples/07-advanced/performance-v1.1.3.php +++ /dev/null @@ -1,427 +0,0 @@ -get('/', function($req, $res) { - return $res->json([ - 'title' => 'PivotPHP v1.2.0 - Performance Simplificada Demo', - 'performance_improvements' => [ - 'framework_throughput' => '+116% improvement (20,400 → 44,092 ops/sec)', - 'object_pool_reuse' => [ - 'request_pool' => '100% reuse rate (was 0%)', - 'response_pool' => '99.9% reuse rate (was 0%)' - ], - 'json_optimization' => 'Automatic buffer pooling for large datasets', - 'memory_efficiency' => 'Smart garbage collection and pool warming' - ], - 'test_endpoints' => [ - 'GET /performance/metrics' => 'Real-time performance metrics', - 'GET /performance/json/{size}' => 'JSON optimization demo (small/medium/large)', - 'GET /performance/stress-test' => 'Framework stress test', - 'GET /performance/pool-stats' => 'Object pool statistics' - ], - 'version_info' => [ - 'framework' => 'PivotPHP Core v1.1.3', - 'php_version' => PHP_VERSION, - 'performance_mode' => PerformanceMode::isEnabled() ? 'ENABLED' : 'DISABLED' - ] - ]); -}); - -// 📊 Performance Metrics -$app->get('/performance/metrics', function($req, $res) { - // Performance monitor is always available in v1.2.0 - $monitor = new \PivotPHP\Core\Performance\PerformanceMonitor(); - - $metrics = $monitor->getPerformanceMetrics(); - $liveMetrics = $monitor->getLiveMetrics(); - - return $res->json([ - 'performance_metrics' => $metrics, - 'live_metrics' => $liveMetrics, - 'framework_improvements' => [ - 'baseline_v1_1_2' => '20,400 ops/sec', - 'current_v1_1_3' => '44,092 ops/sec', - 'improvement' => '+116%', - 'measured_on' => 'Docker environment' - ], - 'pool_efficiency' => [ - 'request_pool_reuse' => '100%', - 'response_pool_reuse' => '99.9%', - 'memory_pressure' => $liveMetrics['memory_pressure'] ?? 'unknown' - ], - 'timestamp' => date('c') - ]); -}); - -// 🎯 JSON Optimization Demo -$app->get('/performance/json/:size', function($req, $res) { - $size = $req->param('size'); - - // Generate different sized datasets - $data = match($size) { - 'small' => generateSmallDataset(), - 'medium' => generateMediumDataset(), - 'large' => generateLargeDataset(), - default => ['error' => 'Size must be small, medium, or large'] - }; - - if (isset($data['error'])) { - return $res->status(400)->json($data); - } - - // Measure JSON encoding performance - $startTime = microtime(true); - $startMemory = memory_get_usage(true); - - // This will automatically use JsonBufferPool for large datasets - $jsonString = json_encode($data); - - $endTime = microtime(true); - $endMemory = memory_get_usage(true); - - // Get JsonBufferPool statistics - $poolStats = JsonBufferPool::getStatistics(); - - return $res->json([ - 'dataset_info' => [ - 'size' => $size, - 'record_count' => count($data['records'] ?? []), - 'estimated_size' => strlen($jsonString) . ' bytes', - 'human_readable_size' => formatBytes(strlen($jsonString)) - ], - 'encoding_performance' => [ - 'encoding_time' => round(($endTime - $startTime) * 1000, 3) . ' ms', - 'memory_used' => formatBytes($endMemory - $startMemory), - 'ops_per_second' => round(1 / ($endTime - $startTime), 2) - ], - 'json_pool_stats' => $poolStats, - 'optimization_info' => [ - 'automatic_pooling' => $size === 'large' ? 'ACTIVE' : 'FALLBACK', - 'pool_benefits' => [ - 'reduced_gc_pressure' => true, - 'buffer_reuse' => $poolStats['reuse_rate'] ?? 0 . '%', - 'memory_efficiency' => true - ] - ], - 'v1_1_3_features' => [ - 'automatic_detection' => 'Arrays 10+ elements, objects 5+ properties', - 'transparent_fallback' => 'Small data uses traditional json_encode()', - 'zero_configuration' => 'Works out-of-the-box with existing code' - ] - ]); -}); - -// 🔥 Stress Test Endpoint -$app->get('/performance/stress-test', function($req, $res) { - $iterations = (int) $req->get('iterations', 100); - $maxIterations = 1000; // Safety limit - - if ($iterations > $maxIterations) { - return $res->status(400)->json([ - 'error' => "Maximum iterations is {$maxIterations}", - 'requested' => $iterations - ]); - } - - $startTime = microtime(true); - $startMemory = memory_get_usage(true); - - $results = []; - - for ($i = 0; $i < $iterations; $i++) { - // Simulate typical API operations - $iterationStart = microtime(true); - - // Create some data - $data = [ - 'id' => $i, - 'name' => "User {$i}", - 'email' => "user{$i}@example.com", - 'metadata' => [ - 'created_at' => date('c'), - 'iteration' => $i, - 'random_data' => str_repeat('x', rand(10, 100)) - ] - ]; - - // JSON encode (will use pooling if beneficial) - $json = json_encode($data); - - // Simulate some processing - $processed = json_decode($json, true); - $processed['processed'] = true; - - $iterationEnd = microtime(true); - - if ($i % 50 === 0 || $i < 10) { // Sample results - $results[] = [ - 'iteration' => $i, - 'time_ms' => round(($iterationEnd - $iterationStart) * 1000, 3), - 'data_size' => strlen($json) - ]; - } - } - - $endTime = microtime(true); - $endMemory = memory_get_usage(true); - - $totalTime = $endTime - $startTime; - $throughput = $iterations / $totalTime; - - return $res->json([ - 'stress_test_results' => [ - 'iterations' => $iterations, - 'total_time' => round($totalTime, 4) . ' seconds', - 'average_time_per_iteration' => round(($totalTime / $iterations) * 1000, 3) . ' ms', - 'throughput' => round($throughput, 2) . ' ops/sec', - 'memory_used' => formatBytes($endMemory - $startMemory), - 'peak_memory' => formatBytes(memory_get_peak_usage(true)) - ], - 'sample_iterations' => $results, - 'performance_comparison' => [ - 'v1_1_2_baseline' => '20,400 ops/sec', - 'current_result' => round($throughput, 2) . ' ops/sec', - 'framework_efficiency' => 'High (object pooling active)' - ], - 'system_info' => [ - 'php_version' => PHP_VERSION, - 'memory_limit' => ini_get('memory_limit'), - 'max_execution_time' => ini_get('max_execution_time') - ] - ]); -}); - -// 🏊 Object Pool Statistics -$app->get('/performance/pool-stats', function($req, $res) { - // Get HTTP factory stats - $factoryStats = OptimizedHttpFactory::getStatistics(); - - // Get JSON pool stats - $jsonStats = JsonBufferPool::getStatistics(); - - // Get performance monitor - $monitor = new \PivotPHP\Core\Performance\PerformanceMonitor(); - $monitorStats = $monitor ? $monitor->getLiveMetrics() : null; - - return $res->json([ - 'object_pool_statistics' => [ - 'http_factory' => $factoryStats, - 'json_buffer_pool' => $jsonStats, - 'performance_monitor' => $monitorStats - ], - 'v1_1_3_improvements' => [ - 'request_pool_reuse' => '100% (was 0% in v1.1.2)', - 'response_pool_reuse' => '99.9% (was 0% in v1.1.2)', - 'pool_warming' => 'Smart pre-allocation on startup', - 'garbage_collection' => 'Optimized object return-to-pool' - ], - 'pool_benefits' => [ - 'reduced_object_creation' => 'Massive reduction in new object instantiation', - 'memory_efficiency' => 'Reused objects reduce garbage collection pressure', - 'performance_boost' => '+116% framework throughput improvement', - 'sustained_performance' => 'Maintains high performance under load' - ], - 'monitoring_info' => [ - 'real_time_tracking' => 'Pool usage tracked in real-time', - 'adaptive_sizing' => 'Pools adjust size based on usage patterns', - 'production_ready' => 'Validated in Docker benchmarking environment' - ] - ]); -}); - -// 📈 Benchmark Comparison -$app->get('/performance/benchmark', function($req, $res) { - return $res->json([ - 'framework_benchmark_comparison' => [ - 'methodology' => 'Docker-based comparative testing (2025-07-11)', - 'environment' => 'Standardized containers for fair comparison', - 'results' => [ - [ - 'framework' => 'Slim 4', - 'performance' => '6,881 req/sec', - 'position' => '1st place' - ], - [ - 'framework' => 'Lumen', - 'performance' => '6,322 req/sec', - 'position' => '2nd place' - ], - [ - 'framework' => 'PivotPHP Core v1.1.3', - 'performance' => '6,227 req/sec', - 'position' => '3rd place', - 'note' => 'Excellent competitive performance' - ], - [ - 'framework' => 'Flight', - 'performance' => '3,179 req/sec', - 'position' => '4th place' - ] - ], - 'pivotphp_analysis' => [ - 'competitive_position' => '9.5% behind leader (excellent)', - 'vs_flight' => '96% faster than Flight', - 'latency' => '0.32ms average response time', - 'memory_footprint' => '1.61MB (ultra-efficient)', - 'docker_validated' => true - ] - ], - 'internal_performance_metrics' => [ - 'framework_throughput' => [ - 'v1_1_2' => '20,400 ops/sec', - 'v1_1_3' => '44,092 ops/sec', - 'improvement' => '+116%' - ], - 'json_operations' => [ - 'small_datasets' => '505K ops/sec (internal)', - 'medium_datasets' => '119K ops/sec (internal)', - 'large_datasets' => '214K ops/sec (internal)' - ], - 'object_pooling' => [ - 'request_reuse' => '0% → 100%', - 'response_reuse' => '0% → 99.9%', - 'pool_efficiency' => 'Revolutionary improvement' - ] - ], - 'performance_validation' => [ - 'docker_tested' => true, - 'multi_php_versions' => 'PHP 8.1-8.4 validated', - 'production_ready' => true, - 'sustained_performance' => 'Maintains performance under load' - ] - ]); -}); - -// 🔧 Helper Functions -function generateSmallDataset(): array -{ - return [ - 'records' => array_map(fn($i) => [ - 'id' => $i, - 'name' => "Item {$i}", - 'value' => rand(1, 100) - ], range(1, 5)), - 'metadata' => [ - 'size' => 'small', - 'count' => 5, - 'generated_at' => date('c') - ] - ]; -} - -function generateMediumDataset(): array -{ - return [ - 'records' => array_map(fn($i) => [ - 'id' => $i, - 'name' => "Record {$i}", - 'category' => ['tech', 'business', 'science', 'arts'][rand(0, 3)], - 'properties' => [ - 'created_at' => date('c'), - 'updated_at' => date('c'), - 'status' => 'active', - 'priority' => rand(1, 10), - 'tags' => explode(',', 'tag1,tag2,tag3,tag4') - ] - ], range(1, 50)), - 'metadata' => [ - 'size' => 'medium', - 'count' => 50, - 'generated_at' => date('c'), - 'estimated_json_size' => '~15KB' - ] - ]; -} - -function generateLargeDataset(): array -{ - return [ - 'records' => array_map(fn($i) => [ - 'id' => $i, - 'uuid' => sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - mt_rand(0, 0xffff), mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0x0fff) | 0x4000, - mt_rand(0, 0x3fff) | 0x8000, - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) - ), - 'name' => "Large Record {$i}", - 'description' => str_repeat("This is a detailed description for record {$i}. ", 10), - 'category' => ['electronics', 'clothing', 'books', 'sports', 'home'][rand(0, 4)], - 'properties' => [ - 'created_at' => date('c'), - 'updated_at' => date('c'), - 'status' => ['active', 'inactive', 'pending'][rand(0, 2)], - 'priority' => rand(1, 100), - 'score' => round(rand(0, 1000) / 10, 2), - 'metadata' => [ - 'source' => 'api', - 'version' => '1.0', - 'checksum' => md5("record-{$i}"), - 'flags' => array_map(fn($j) => "flag_{$j}", range(1, rand(3, 8))) - ] - ], - 'related_items' => array_map(fn($j) => [ - 'id' => $j, - 'type' => 'related', - 'weight' => rand(1, 10) - ], range(1, rand(5, 15))) - ], range(1, 500)), - 'metadata' => [ - 'size' => 'large', - 'count' => 500, - 'generated_at' => date('c'), - 'estimated_json_size' => '~500KB', - 'uses_json_pooling' => true - ] - ]; -} - -function formatBytes(int $bytes, int $precision = 2): string -{ - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - - for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { - $bytes /= 1024; - } - - return round($bytes, $precision) . ' ' . $units[$i]; -} - -// 🚀 Run the application -$app->run(); \ No newline at end of file diff --git a/examples/08-json-optimization/json-pool-demo-v114.php b/examples/08-json-optimization/json-pool-demo-v114.php deleted file mode 100644 index 8a202a5..0000000 --- a/examples/08-json-optimization/json-pool-demo-v114.php +++ /dev/null @@ -1,537 +0,0 @@ -json([ - 'title' => 'JsonBufferPool Optimization Demo v1.1.4+', - 'description' => 'Demonstra o sistema de otimização JSON com threshold inteligente', - 'features_v114' => [ - 'intelligent_threshold' => 'Automatic decision based on data size (256 bytes)', - 'zero_overhead' => 'Small data uses direct json_encode() for optimal performance', - 'automatic_pooling' => 'Large data automatically uses buffer pooling', - 'real_time_monitoring' => 'Live statistics and performance metrics', - 'production_ready' => 'Zero configuration needed for optimal performance' - ], - 'demo_endpoints' => [ - 'GET /demo/small' => 'Small data (uses direct json_encode)', - 'GET /demo/medium' => 'Medium data (may use pooling)', - 'GET /demo/large' => 'Large data (uses pooling optimization)', - 'GET /demo/benchmark' => 'Performance comparison', - 'GET /demo/threshold' => 'Threshold testing with different sizes', - 'GET /stats' => 'Real-time pool statistics', - 'GET /config' => 'Configuration options' - ], - 'optimization_logic' => [ - 'threshold' => '256 bytes by default', - 'small_data' => 'Below threshold → direct json_encode() (fastest)', - 'large_data' => 'Above threshold → buffer pooling (optimized)', - 'automatic' => 'No configuration needed - system decides optimally' - ], - 'pool_stats' => JsonBufferPool::getStatistics() - ]); - } - - public function smallData($req, $res) - { - // Small data - should use direct json_encode() - $data = [ - 'message' => 'This is small data', - 'type' => 'small', - 'timestamp' => time(), - 'optimization' => 'direct_json_encode' - ]; - - $usePooling = JsonBufferPool::shouldUsePooling($data); - $dataSize = strlen(json_encode($data)); - - $response = [ - 'demo' => 'Small Data Optimization', - 'data' => $data, - 'optimization_analysis' => [ - 'data_size_bytes' => $dataSize, - 'threshold_bytes' => 256, - 'uses_pooling' => $usePooling, - 'strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode', - 'explanation' => $usePooling - ? 'Data exceeds threshold - using buffer pool' - : 'Data below threshold - using direct json_encode() for minimal overhead', - 'performance_impact' => $usePooling ? 'Optimized' : 'Zero overhead' - ], - 'v114_benefits' => [ - 'automatic_decision' => 'System automatically chose optimal strategy', - 'no_configuration' => 'Zero setup required', - 'guaranteed_performance' => 'Never slower than standard json_encode()' - ] - ]; - - return $res->json($response); - } - - public function mediumData($req, $res) - { - // Medium data - may trigger pooling - $baseData = array_fill(0, 20, [ - 'id' => rand(1, 1000), - 'name' => 'Item ' . rand(1, 100), - 'description' => 'Medium size data item with some content to reach threshold', - 'metadata' => [ - 'created_at' => date('c'), - 'category' => 'demo', - 'tags' => ['json', 'optimization', 'demo'] - ] - ]); - - $usePooling = JsonBufferPool::shouldUsePooling($baseData); - $dataSize = strlen(json_encode($baseData)); - - $response = [ - 'demo' => 'Medium Data Optimization', - 'data' => $baseData, - 'optimization_analysis' => [ - 'data_size_bytes' => $dataSize, - 'data_size_kb' => round($dataSize / 1024, 2), - 'threshold_bytes' => 256, - 'uses_pooling' => $usePooling, - 'strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode', - 'explanation' => $usePooling - ? 'Data exceeds threshold - automatic buffer pooling activated for optimization' - : 'Data still below threshold - using direct json_encode()', - 'performance_benefit' => $usePooling ? '15-30% faster than standard encoding' : 'Minimal overhead' - ], - 'threshold_system' => [ - 'intelligent_detection' => 'System analyzes data size before encoding', - 'automatic_optimization' => 'No manual intervention required', - 'adaptive_strategy' => 'Chooses best approach for each response' - ] - ]; - - return $res->json($response); - } - - public function largeData($req, $res) - { - // Large data - should definitely use pooling - $count = (int) $req->get('count', 100); - $count = max(10, min(1000, $count)); // Limit for demo - - $largeData = array_fill(0, $count, [ - 'id' => rand(1, 10000), - 'title' => 'Large Dataset Item ' . rand(1, 1000), - 'description' => str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 5), - 'content' => str_repeat('This is substantial content to make the response large enough to trigger buffer pooling optimization. ', 3), - 'metadata' => [ - 'created_at' => date('c'), - 'updated_at' => date('c'), - 'version' => '1.0', - 'status' => 'active', - 'tags' => ['performance', 'optimization', 'json', 'pooling', 'demo'], - 'author' => [ - 'name' => 'Demo Author', - 'email' => 'demo@example.com', - 'profile' => [ - 'bio' => 'Demonstrating JsonBufferPool optimization capabilities', - 'location' => 'Brazil', - 'social' => [ - 'github' => 'https://github.com/example', - 'linkedin' => 'https://linkedin.com/in/example' - ] - ] - ] - ], - 'performance_data' => [ - 'complexity_score' => rand(1, 100), - 'processing_time_ms' => rand(10, 500), - 'memory_usage_kb' => rand(100, 1000), - 'optimization_level' => 'high' - ] - ]); - - $usePooling = JsonBufferPool::shouldUsePooling($largeData); - $dataSize = strlen(json_encode($largeData)); - $statsBefore = JsonBufferPool::getStatistics(); - - // Simulate processing time measurement - $start = microtime(true); - $json = JsonBufferPool::encodeWithPool($largeData); - $processingTime = microtime(true) - $start; - - $statsAfter = JsonBufferPool::getStatistics(); - - $response = [ - 'demo' => 'Large Data Optimization', - 'optimization_analysis' => [ - 'data_size_bytes' => $dataSize, - 'data_size_kb' => round($dataSize / 1024, 2), - 'data_size_mb' => round($dataSize / (1024 * 1024), 3), - 'item_count' => $count, - 'threshold_bytes' => 256, - 'uses_pooling' => $usePooling, - 'strategy' => $usePooling ? 'buffer_pool_optimization' : 'direct_json_encode', - 'processing_time_ms' => round($processingTime * 1000, 3), - 'expected_performance_gain' => $usePooling ? '98%+ faster than standard encoding' : 'Standard performance' - ], - 'pool_efficiency' => [ - 'buffer_reuse' => ($statsAfter['reuses'] ?? 0) > ($statsBefore['reuses'] ?? 0), - 'memory_efficiency' => 'Significant reduction in garbage collection pressure', - 'throughput_improvement' => $usePooling ? 'Up to 214K ops/sec for large datasets' : 'Standard throughput' - ], - 'v114_advantages' => [ - 'zero_configuration' => 'Automatic optimization without setup', - 'intelligent_threshold' => 'System chose buffer pooling for optimal performance', - 'production_ready' => 'Scales automatically with data size', - 'memory_efficient' => 'Buffer reuse reduces allocation overhead' - ], - 'data' => $largeData, - 'pool_stats_after' => $statsAfter - ]; - - return $res->json($response); - } - - public function benchmark($req, $res) - { - $iterations = min(1000, max(10, (int) $req->get('iterations', 100))); - - // Test data sets - $testSets = [ - 'small' => ['type' => 'small', 'data' => str_repeat('x', 50)], - 'medium' => array_fill(0, 10, ['id' => 1, 'data' => str_repeat('x', 50)]), - 'large' => array_fill(0, 100, ['id' => 1, 'data' => str_repeat('x', 100)]) - ]; - - $benchmarkResults = []; - - foreach ($testSets as $size => $data) { - $usePooling = JsonBufferPool::shouldUsePooling($data); - $dataSize = strlen(json_encode($data)); - - // Benchmark with JsonBufferPool - $start = microtime(true); - for ($i = 0; $i < $iterations; $i++) { - JsonBufferPool::encodeWithPool($data); - } - $poolTime = microtime(true) - $start; - - // Benchmark with standard json_encode - $start = microtime(true); - for ($i = 0; $i < $iterations; $i++) { - json_encode($data); - } - $standardTime = microtime(true) - $start; - - $poolOpsPerSec = $iterations / $poolTime; - $standardOpsPerSec = $iterations / $standardTime; - $improvementPercent = (($poolOpsPerSec - $standardOpsPerSec) / $standardOpsPerSec) * 100; - - $benchmarkResults[$size] = [ - 'data_size_bytes' => $dataSize, - 'uses_pooling' => $usePooling, - 'iterations' => $iterations, - 'pool_time_ms' => round($poolTime * 1000, 3), - 'standard_time_ms' => round($standardTime * 1000, 3), - 'pool_ops_per_sec' => round($poolOpsPerSec, 0), - 'standard_ops_per_sec' => round($standardOpsPerSec, 0), - 'improvement_percent' => round($improvementPercent, 1), - 'performance_note' => $usePooling - ? ($improvementPercent > 0 ? 'Pool optimization active' : 'Pool overhead minimal') - : 'Direct json_encode() used (optimal for small data)' - ]; - } - - return $res->json([ - 'demo' => 'JsonBufferPool Performance Benchmark v1.1.4+', - 'description' => 'Compara performance entre JsonBufferPool e json_encode() padrão', - 'test_parameters' => [ - 'iterations_per_test' => $iterations, - 'threshold_bytes' => 256, - 'optimization_strategy' => 'Automatic based on data size' - ], - 'benchmark_results' => $benchmarkResults, - 'interpretation' => [ - 'small_data' => 'Should show minimal difference (direct json_encode used)', - 'medium_data' => 'May show improvement if pooling is triggered', - 'large_data' => 'Should show significant improvement with pooling' - ], - 'v114_benefits' => [ - 'intelligent_optimization' => 'System automatically chooses best strategy', - 'no_performance_regression' => 'Small data never gets slower', - 'significant_gains' => 'Large data gets substantial performance boost', - 'zero_configuration' => 'Optimal performance out of the box' - ], - 'pool_stats' => JsonBufferPool::getStatistics() - ]); - } - - public function thresholdTesting($req, $res) - { - $sizes = [50, 100, 200, 256, 300, 500, 1000, 5000]; - $results = []; - - foreach ($sizes as $size) { - $data = array_fill(0, $size, 'x'); - $usePooling = JsonBufferPool::shouldUsePooling($data); - $actualSize = strlen(json_encode($data)); - - $results[] = [ - 'target_size' => $size, - 'actual_size_bytes' => $actualSize, - 'uses_pooling' => $usePooling, - 'threshold_crossed' => $actualSize >= 256, - 'optimization_strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode' - ]; - } - - return $res->json([ - 'demo' => 'Threshold Testing v1.1.4+', - 'description' => 'Demonstra quando o threshold de 256 bytes é atingido', - 'threshold_bytes' => 256, - 'test_results' => $results, - 'threshold_analysis' => [ - 'below_threshold' => count(array_filter($results, fn($r) => !$r['uses_pooling'])), - 'above_threshold' => count(array_filter($results, fn($r) => $r['uses_pooling'])), - 'threshold_accuracy' => 'System correctly identifies when to use pooling' - ], - 'optimization_benefits' => [ - 'no_overhead_small' => 'Data below 256 bytes uses fastest method', - 'automatic_optimization_large' => 'Data above 256 bytes gets pooling benefits', - 'intelligent_cutoff' => 'Threshold chosen for optimal overall performance' - ] - ]); - } - - public function stats($req, $res) - { - $stats = JsonBufferPool::getStatistics(); - $memoryUsage = [ - 'current_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2) - ]; - - return $res->json([ - 'title' => 'Real-time JsonBufferPool Statistics', - 'timestamp' => date('c'), - 'pool_statistics' => $stats, - 'memory_usage' => $memoryUsage, - 'performance_metrics' => [ - 'efficiency_percentage' => $stats['efficiency'] ?? 0, - 'total_operations' => $stats['total_operations'] ?? 0, - 'buffer_reuses' => $stats['reuses'] ?? 0, - 'new_allocations' => $stats['allocations'] ?? 0 - ], - 'optimization_status' => [ - 'threshold_bytes' => 256, - 'system_status' => 'Active and optimizing automatically', - 'performance_mode' => 'Intelligent threshold v1.1.4+', - 'configuration_required' => false - ], - 'interpretation' => [ - 'efficiency_good' => 'Above 80% indicates excellent buffer reuse', - 'efficiency_fair' => '50-80% indicates moderate optimization', - 'efficiency_low' => 'Below 50% may indicate mostly small data (which is optimal)' - ] - ]); - } - - public function config($req, $res) - { - return $res->json([ - 'title' => 'JsonBufferPool Configuration v1.1.4+', - 'description' => 'Opções de configuração para otimização avançada', - 'default_configuration' => [ - 'threshold_bytes' => 256, - 'max_pool_size' => 100, - 'enable_statistics' => true, - 'automatic_optimization' => true - ], - 'configuration_options' => [ - 'threshold_bytes' => [ - 'description' => 'Minimum data size to trigger pooling', - 'default' => 256, - 'recommended_range' => '128-512 bytes', - 'impact' => 'Lower = more pooling, Higher = less pooling' - ], - 'max_pool_size' => [ - 'description' => 'Maximum number of buffers in pool', - 'default' => 100, - 'recommended_range' => '50-500', - 'impact' => 'Higher = more memory, better reuse for high load' - ], - 'enable_statistics' => [ - 'description' => 'Enable real-time statistics collection', - 'default' => true, - 'production_note' => 'Disable in production for minimal overhead' - ] - ], - 'advanced_configuration_example' => [ - 'description' => 'Production-optimized configuration', - 'code' => 'JsonBufferPool::configure([ - "threshold_bytes" => 128, - "max_pool_size" => 500, - "enable_statistics" => false, - "warm_up_pool" => true -]);' - ], - 'tuning_guidelines' => [ - 'high_traffic' => 'Increase max_pool_size to 500+', - 'low_memory' => 'Decrease max_pool_size to 25-50', - 'small_responses' => 'Increase threshold_bytes to 512+', - 'large_responses' => 'Decrease threshold_bytes to 128', - 'production' => 'Disable statistics for minimal overhead' - ], - 'v114_advantages' => [ - 'zero_config_needed' => 'Works optimally without any configuration', - 'intelligent_defaults' => 'Default settings work well for most applications', - 'easy_tuning' => 'Simple configuration for specific use cases', - 'performance_monitoring' => 'Built-in statistics for optimization guidance' - ] - ]); - } -} - -// =============================================== -// APPLICATION SETUP -// =============================================== - -$app = new Application(); - -// Initialize controller -$jsonController = new JsonOptimizationController(); - -// =============================================== -// ROUTES - Array Callables with JSON Optimization -// =============================================== - -// Main demo routes -$app->get('/', [$jsonController, 'index']); -$app->get('/demo/small', [$jsonController, 'smallData']); -$app->get('/demo/medium', [$jsonController, 'mediumData']); -$app->get('/demo/large', [$jsonController, 'largeData']); -$app->get('/demo/benchmark', [$jsonController, 'benchmark']); -$app->get('/demo/threshold', [$jsonController, 'thresholdTesting']); - -// Monitoring and configuration -$app->get('/stats', [$jsonController, 'stats']); -$app->get('/config', [$jsonController, 'config']); - -// Interactive testing endpoint -$app->get('/test/:size', function($req, $res) { - $size = $req->param('size'); - - // Generate data based on size parameter - switch($size) { - case 'tiny': - $data = ['size' => 'tiny', 'bytes' => 'very small']; - break; - case 'small': - $data = array_fill(0, 5, ['item' => 'small data']); - break; - case 'medium': - $data = array_fill(0, 25, ['item' => str_repeat('medium data ', 5)]); - break; - case 'large': - $data = array_fill(0, 100, ['item' => str_repeat('large data content ', 10)]); - break; - case 'huge': - $data = array_fill(0, 500, ['item' => str_repeat('huge data content with lots of text ', 10)]); - break; - default: - return $res->status(400)->json([ - 'error' => 'Invalid size parameter', - 'valid_sizes' => ['tiny', 'small', 'medium', 'large', 'huge'], - 'example' => '/test/medium' - ]); - } - - $usePooling = JsonBufferPool::shouldUsePooling($data); - $dataSize = strlen(json_encode($data)); - - return $res->json([ - 'test' => "Interactive Size Test: {$size}", - 'data' => $data, - 'analysis' => [ - 'size_category' => $size, - 'data_size_bytes' => $dataSize, - 'data_size_kb' => round($dataSize / 1024, 2), - 'uses_pooling' => $usePooling, - 'optimization_strategy' => $usePooling ? 'buffer_pool' : 'direct_json_encode', - 'performance_expectation' => $usePooling - ? 'Optimized with buffer pooling' - : 'Fast direct encoding' - ], - 'try_other_sizes' => [ - '/test/tiny' => 'Very small data', - '/test/small' => 'Small data set', - '/test/medium' => 'Medium data set', - '/test/large' => 'Large data set', - '/test/huge' => 'Very large data set' - ] - ]); -}); - -// Real-time monitoring endpoint -$app->get('/monitor', function($req, $res) { - $stats = JsonBufferPool::getStatistics(); - - $res->header('Cache-Control', 'no-cache'); - $res->header('Content-Type', 'application/json'); - - return $res->json([ - 'timestamp' => microtime(true), - 'pool_stats' => $stats, - 'memory' => [ - 'usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2) - ], - 'system_info' => [ - 'php_version' => PHP_VERSION, - 'pivotphp_version' => Application::VERSION, - 'optimization_active' => true - ], - 'refresh_info' => [ - 'auto_refresh' => 'This endpoint provides real-time data', - 'suggested_interval' => '1-5 seconds for monitoring', - 'usage' => 'Use for performance monitoring dashboards' - ] - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/09-error-handling/enhanced-errors-v114.php b/examples/09-error-handling/enhanced-errors-v114.php deleted file mode 100644 index 698eebd..0000000 --- a/examples/09-error-handling/enhanced-errors-v114.php +++ /dev/null @@ -1,555 +0,0 @@ -json([ - 'title' => 'Enhanced Error Handling Demo v1.1.4+', - 'description' => 'Demonstra o sistema avançado de diagnóstico de erros', - 'features_v114' => [ - 'contextual_exceptions' => 'Detailed error context and suggestions', - 'error_categorization' => 'Automatic categorization of error types', - 'development_mode' => 'Rich debug information in development', - 'production_mode' => 'Clean error messages for production', - 'troubleshooting_suggestions' => 'Built-in suggestions for common issues' - ], - 'demo_endpoints' => [ - 'GET /error/route-not-found' => 'Route not found error with suggestions', - 'GET /error/invalid-parameter/abc' => 'Parameter validation error', - 'GET /error/handler-error' => 'Handler execution error', - 'GET /error/middleware-error' => 'Middleware error with context', - 'GET /error/custom-error' => 'Custom contextual error', - 'GET /error/validation' => 'Validation error with multiple fields', - 'GET /debug/stack-trace' => 'Error with full stack trace', - 'GET /production/error' => 'Production-mode error (clean)' - ], - 'error_categories' => [ - 'ROUTING' => 'Route-related errors (404, method not allowed)', - 'PARAMETER' => 'Parameter validation and type errors', - 'HANDLER' => 'Controller/handler execution errors', - 'MIDDLEWARE' => 'Middleware processing errors', - 'VALIDATION' => 'Data validation and business rule errors', - 'AUTHENTICATION' => 'Auth and permission errors', - 'SYSTEM' => 'System and infrastructure errors' - ], - 'error_information' => [ - 'context' => 'Detailed information about error circumstances', - 'suggestions' => 'Actionable suggestions for resolution', - 'debug_info' => 'Technical details for debugging (dev mode only)', - 'category' => 'Error classification for systematic handling', - 'request_id' => 'Unique identifier for error tracking' - ] - ]); - } - - public function routeNotFoundDemo($req, $res) - { - // Simulate available routes for better error context - $availableRoutes = [ - 'GET /', - 'GET /error/invalid-parameter/:id', - 'GET /error/handler-error', - 'GET /error/middleware-error', - 'GET /debug/stack-trace' - ]; - - throw ContextualException::routeNotFound( - 'GET', - '/non-existent-route', - $availableRoutes - ); - } - - public function invalidParameterDemo($req, $res) - { - $id = $req->param('id'); - - // Demonstrate parameter validation error - if (!is_numeric($id)) { - throw ContextualException::parameterError( - 'id', - 'integer', - $id, - '/error/invalid-parameter/:id' - ); - } - - return $res->json([ - 'message' => 'Parameter is valid', - 'id' => (int) $id - ]); - } - - public function handlerErrorDemo($req, $res) - { - try { - // Simulate trying to call a non-existent method - $invalidCallable = [NonExistentController::class, 'nonExistentMethod']; - CallableResolver::resolve($invalidCallable); - } catch (Exception $e) { - throw ContextualException::handlerError( - 'array_callable', - $e->getMessage(), - [ - 'class' => NonExistentController::class, - 'method' => 'nonExistentMethod', - 'callable_type' => 'array', - 'validation_failed' => true - ] - ); - } - } - - public function middlewareErrorDemo($req, $res) - { - $middlewareStack = [ - 'AuthMiddleware', - 'CorsMiddleware', - 'ErrorDemoMiddleware' - ]; - - throw ContextualException::middlewareError( - 'ErrorDemoMiddleware', - 'Simulated middleware processing error', - $middlewareStack - ); - } - - public function customErrorDemo($req, $res) - { - // Create custom contextual error - $context = [ - 'user_id' => 123, - 'action' => 'custom_operation', - 'resource' => 'demo_resource', - 'timestamp' => time(), - 'request_data' => $req->query() - ]; - - $suggestions = [ - 'Verify user has permission for this operation', - 'Check if the resource exists and is accessible', - 'Ensure all required parameters are provided', - 'Try again with valid authentication credentials' - ]; - - throw new ContextualException( - 403, - 'Custom operation failed due to insufficient permissions', - $context, - $suggestions, - 'CUSTOM_OPERATION' - ); - } - - public function validationErrorDemo($req, $res) - { - // Simulate complex validation error - $validationErrors = [ - 'name' => 'Name is required and must be at least 2 characters', - 'email' => 'Email format is invalid', - 'age' => 'Age must be between 18 and 120', - 'password' => 'Password must contain at least 8 characters with uppercase, lowercase and numbers' - ]; - - $context = [ - 'validation_rules' => [ - 'name' => 'required|min:2|max:100', - 'email' => 'required|email', - 'age' => 'required|integer|between:18,120', - 'password' => 'required|min:8|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/' - ], - 'received_data' => $req->body() ?: ['empty' => 'no data received'], - 'validation_engine' => 'PivotPHP Enhanced Validation v1.1.4+' - ]; - - $suggestions = [ - 'Provide all required fields: name, email, age, password', - 'Ensure email is in valid format (user@domain.com)', - 'Age must be a number between 18 and 120', - 'Password must be strong: 8+ chars, uppercase, lowercase, numbers', - 'Check API documentation for exact field requirements' - ]; - - throw new ContextualException( - 422, - 'Validation failed for multiple fields', - $context, - $suggestions, - 'VALIDATION' - ); - } - - public function stackTraceDemo($req, $res) - { - // Set development mode to show stack trace - putenv('APP_ENV=development'); - define('PIVOTPHP_DEBUG', true); - - // Create error with stack trace - throw new ContextualException( - 500, - 'Development error with full stack trace', - [ - 'debug_mode' => true, - 'development_environment' => true, - 'stack_trace_enabled' => true - ], - [ - 'This error includes full stack trace for debugging', - 'Stack trace is only shown in development mode', - 'In production, only clean error messages are displayed' - ], - 'DEBUG' - ); - } - - public function productionErrorDemo($req, $res) - { - // Set production mode to show clean errors - putenv('APP_ENV=production'); - - // Create error that would be cleaned in production - throw new ContextualException( - 500, - 'Production error with clean output', - [ - 'production_mode' => true, - 'sensitive_data' => 'This will be hidden in production', - 'internal_error_code' => 'ERR_PROD_001' - ], - [ - 'Contact support if the issue persists', - 'Check system status page for known issues', - 'Verify your request parameters and try again' - ], - 'PRODUCTION' - ); - } -} - -class DebugController -{ - public function errorAnalysis($req, $res) - { - return $res->json([ - 'title' => 'Error Analysis Tools v1.1.4+', - 'description' => 'Ferramentas para análise e debug de erros', - 'contextual_exception_benefits' => [ - 'detailed_context' => 'Rich contextual information about error circumstances', - 'actionable_suggestions' => 'Specific suggestions for resolving the issue', - 'error_categorization' => 'Systematic classification of error types', - 'environment_awareness' => 'Different output for development vs production', - 'debug_information' => 'Technical details for effective troubleshooting' - ], - 'error_handling_workflow' => [ - 'error_occurs' => 'Exception is thrown with context', - 'categorization' => 'Error is automatically categorized', - 'context_gathering' => 'Relevant context information is collected', - 'suggestion_generation' => 'Actionable suggestions are generated', - 'output_formatting' => 'Response is formatted based on environment', - 'logging' => 'Error details are logged for analysis' - ], - 'development_vs_production' => [ - 'development' => [ - 'full_context' => true, - 'suggestions' => true, - 'debug_info' => true, - 'stack_trace' => true, - 'technical_details' => true - ], - 'production' => [ - 'clean_messages' => true, - 'user_friendly' => true, - 'no_sensitive_data' => true, - 'minimal_technical_info' => true, - 'tracking_ids' => true - ] - ], - 'integration_examples' => [ - 'api_responses' => 'Consistent error format for API consumers', - 'logging_systems' => 'Rich context for log analysis', - 'monitoring_tools' => 'Categorized errors for better alerting', - 'user_interfaces' => 'User-friendly error messages with guidance' - ] - ]); - } -} - -// =============================================== -// ERROR HANDLING MIDDLEWARE -// =============================================== - -class ErrorHandlingMiddleware -{ - public static function globalErrorHandler($req, $res, $next) - { - try { - return $next($req, $res); - } catch (ContextualException $e) { - // Enhanced error handling for ContextualException - $isDevelopment = ($_ENV['APP_ENV'] ?? 'development') === 'development' || - defined('PIVOTPHP_DEBUG') && PIVOTPHP_DEBUG === true; - - $errorResponse = [ - 'error' => true, - 'status' => $e->getStatusCode(), - 'message' => $e->getMessage(), - 'category' => $e->getCategory(), - 'request_id' => uniqid('err_', true), - 'timestamp' => date('c') - ]; - - if ($isDevelopment) { - $errorResponse['context'] = $e->getContext(); - $errorResponse['suggestions'] = $e->getSuggestions(); - $errorResponse['debug'] = $e->getDebugInfo(); - $errorResponse['file'] = $e->getFile(); - $errorResponse['line'] = $e->getLine(); - } else { - // Production mode - clean, user-friendly errors - $errorResponse['help'] = [ - 'If this problem persists, please contact support', - 'Include the request_id when reporting this issue' - ]; - if (!empty($e->getSuggestions())) { - $errorResponse['suggestions'] = array_slice($e->getSuggestions(), 0, 2); // Only first 2 suggestions - } - } - - // Log the full error details - error_log("ContextualException [{$e->getCategory()}]: " . $e->getMessage()); - error_log("Context: " . json_encode($e->getContext())); - - return $res->status($e->getStatusCode())->json($errorResponse); - - } catch (Exception $e) { - // Standard exception handling - $errorResponse = [ - 'error' => true, - 'status' => 500, - 'message' => 'Internal Server Error', - 'request_id' => uniqid('err_', true), - 'timestamp' => date('c') - ]; - - $isDevelopment = ($_ENV['APP_ENV'] ?? 'development') === 'development'; - - if ($isDevelopment) { - $errorResponse['debug'] = [ - 'exception_class' => get_class($e), - 'original_message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => $e->getTraceAsString() - ]; - } - - error_log("Unhandled Exception: " . $e->getMessage()); - - return $res->status(500)->json($errorResponse); - } - } - - public static function requestLogger($req, $res, $next) - { - $requestId = uniqid('req_', true); - $res->header('X-Request-ID', $requestId); - - error_log("Request [{$requestId}]: {$req->method()} {$req->uri()}"); - - $start = microtime(true); - $response = $next($req, $res); - $duration = round((microtime(true) - $start) * 1000, 2); - - $res->header('X-Response-Time', $duration . 'ms'); - - return $response; - } -} - -// =============================================== -// APPLICATION SETUP -// =============================================== - -$app = new Application(); - -// Apply error handling middleware -$app->use([ErrorHandlingMiddleware::class, 'requestLogger']); -$app->use([ErrorHandlingMiddleware::class, 'globalErrorHandler']); - -// Initialize controllers -$errorController = new ErrorDemoController(); -$debugController = new DebugController(); - -// =============================================== -// ROUTES - Enhanced Error Handling Demo -// =============================================== - -// Main demo routes -$app->get('/', [$errorController, 'index']); - -// Error demonstration routes -$app->get('/error/route-not-found', [$errorController, 'routeNotFoundDemo']); -$app->get('/error/invalid-parameter/:id', [$errorController, 'invalidParameterDemo']); -$app->get('/error/handler-error', [$errorController, 'handlerErrorDemo']); -$app->get('/error/middleware-error', [$errorController, 'middlewareErrorDemo']); -$app->get('/error/custom-error', [$errorController, 'customErrorDemo']); -$app->get('/error/validation', [$errorController, 'validationErrorDemo']); - -// Debug and analysis routes -$app->get('/debug/stack-trace', [$errorController, 'stackTraceDemo']); -$app->get('/debug/analysis', [$debugController, 'errorAnalysis']); -$app->get('/production/error', [$errorController, 'productionErrorDemo']); - -// Interactive error testing -$app->get('/test/error/:type', function($req, $res) { - $type = $req->param('type'); - - switch($type) { - case 'simple': - throw new Exception('Simple exception for testing'); - - case 'contextual': - throw new ContextualException( - 400, - 'Test contextual error', - ['test_type' => 'interactive', 'user_choice' => $type], - ['This is a test error', 'Try different error types'], - 'TEST' - ); - - case 'validation': - throw ContextualException::parameterError( - 'type', - 'valid error type', - $type, - '/test/error/:type' - ); - - case 'auth': - throw new ContextualException( - 401, - 'Authentication required for this test', - ['endpoint' => '/test/error/auth', 'required_auth' => true], - ['Provide valid authentication', 'Check your API credentials'], - 'AUTHENTICATION' - ); - - case 'forbidden': - throw new ContextualException( - 403, - 'Access forbidden for this test resource', - ['user_role' => 'guest', 'required_role' => 'admin'], - ['Contact administrator for access', 'Verify your permissions'], - 'AUTHORIZATION' - ); - - default: - return $res->json([ - 'message' => 'Valid error type required', - 'available_types' => ['simple', 'contextual', 'validation', 'auth', 'forbidden'], - 'example' => '/test/error/contextual' - ]); - } -}); - -// Error statistics endpoint -$app->get('/stats/errors', function($req, $res) { - // This would typically come from a real error tracking system - $errorStats = [ - 'total_errors_today' => rand(10, 100), - 'error_rate_percent' => rand(1, 5), - 'most_common_categories' => [ - 'VALIDATION' => rand(30, 50) . '%', - 'ROUTING' => rand(20, 30) . '%', - 'PARAMETER' => rand(10, 20) . '%', - 'HANDLER' => rand(5, 15) . '%', - 'SYSTEM' => rand(1, 10) . '%' - ], - 'resolution_suggestions_effectiveness' => [ - 'users_who_retried_successfully' => rand(60, 80) . '%', - 'support_tickets_reduced' => rand(40, 60) . '%', - 'average_resolution_time' => rand(5, 15) . ' minutes' - ] - ]; - - return $res->json([ - 'title' => 'Error Handling Statistics', - 'description' => 'Impact of enhanced error diagnostics v1.1.4+', - 'statistics' => $errorStats, - 'contextual_exception_benefits' => [ - 'faster_debugging' => 'Detailed context reduces investigation time', - 'better_user_experience' => 'Clear suggestions help users resolve issues', - 'reduced_support_load' => 'Self-service resolution reduces support tickets', - 'improved_monitoring' => 'Categorized errors enable better alerting' - ], - 'timestamp' => date('c') - ]); -}); - -// Environment switcher for testing -$app->get('/env/:mode', function($req, $res) { - $mode = $req->param('mode'); - - if (!in_array($mode, ['development', 'production'])) { - return $res->status(400)->json([ - 'error' => 'Invalid environment mode', - 'valid_modes' => ['development', 'production'] - ]); - } - - putenv("APP_ENV={$mode}"); - - if ($mode === 'development') { - define('PIVOTPHP_DEBUG', true); - } - - return $res->json([ - 'message' => "Environment switched to {$mode} mode", - 'mode' => $mode, - 'debug_enabled' => $mode === 'development', - 'error_detail_level' => $mode === 'development' ? 'full' : 'minimal', - 'test_suggestion' => "Try /error/custom-error to see {$mode} error output" - ]); -}); - -$app->run(); \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index a8aa77c..33e78c4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ -# PivotPHP Core v1.2.0 - Complete Examples Collection 🚀 +# PivotPHP Core v2.0.0 - Complete Examples Collection 🚀 -This directory contains production-ready examples that demonstrate the full potential of PivotPHP Core v1.2.0, including simplified performance mode and clean architecture. +This directory contains production-ready examples that demonstrate the full potential of PivotPHP Core v2.0.0, including simplified performance mode and clean architecture. ## 🎯 What's New in v2.0.0 @@ -36,14 +36,13 @@ This directory contains production-ready examples that demonstrate the full pote - **rest-api.php** - Complete RESTful API with pagination, filters and validation ### 05-performance - Performance & Optimization -- **high-performance.php** - v1.2.0 simplified performance mode, JSON optimization, metrics +- **high-performance.php** - v2.0.0 simplified performance mode, JSON optimization, metrics ### 06-security - Security - **jwt-auth.php** - Complete JWT system with refresh tokens and authorization -### 07-advanced - v1.2.0 Advanced Features ✨ +### 07-advanced - v2.0.0 Advanced Features - **array-callables.php** - Array callable syntax demonstration -- **performance-v1.1.3.php** - Performance simplificada showcase (updated to v1.2.0) ## 🚀 Quick Start @@ -56,14 +55,14 @@ composer install ### Running Examples -#### 🆕 v1.2.0 Features +#### v2.0.0 Features ```bash -# Simplified performance mode (NEW!) +# Simplified performance mode php -S localhost:8000 examples/05-performance/high-performance.php curl http://localhost:8000/enable-high-performance # Enable simplified performance mode curl http://localhost:8000/metrics # Real-time performance data -# Array callable syntax (MAINTAINED!) +# Array callable syntax php -S localhost:8000 examples/07-advanced/array-callables.php curl http://localhost:8000/users # Instance method callable curl http://localhost:8000/admin/dashboard # Static method callable @@ -100,16 +99,16 @@ Each example file contains: - 📝 **Detailed explanatory comments** - Inline documentation - 🧪 **Test instructions** - Ready-to-use curl commands - 🎯 **Real-world use cases** - Practical implementation examples -- ⚡ **v1.2.0 features** - Latest framework capabilities +- ⚡ **v2.0.0 features** - Latest framework capabilities - 🔒 **Best practices** - Security and performance guidelines ## 🎯 Featured Examples -### 🆕 Simplified Performance Mode (v1.2.0) +### Simplified Performance Mode (v2.0.0) ```php use PivotPHP\Core\Performance\PerformanceMode; -// NEW: Simplified performance mode +// Simplified performance mode PerformanceMode::enable(PerformanceMode::PROFILE_PRODUCTION); $app->get('/api/data', function($req, $res) { @@ -118,7 +117,7 @@ $app->get('/api/data', function($req, $res) { }); ``` -### ✅ Array Callables (Maintained v1.2.0) +### ✅ Array Callables (v2.0.0) ```php class UserController { public function index($req, $res) { @@ -152,12 +151,12 @@ $app->get('/users/:id<\\d+>', function($req, $res) { ## 📊 Performance Showcase -### v1.2.0 Improvements -- **Framework Throughput**: 20,400 → 44,092 ops/sec (+116% maintained) -- **Object Pool Reuse**: 0% → 100% (Request), 0% → 99.9% (Response) - maintained -- **JSON Operations**: 505K ops/sec (small), 214K ops/sec (large) - Internal benchmarks +### v2.0.0 Performance +- **Framework Throughput**: 44,092 ops/sec (+116% vs previous architecture) +- **Object Pool Reuse**: 100% (Request), 99.9% (Response) +- **JSON Operations**: Automatic threshold at 256 bytes — small data via `json_encode()`, large data via pooling - **Docker Validated**: 6,227 req/sec in standardized containers (3rd place competitive) -- **Architecture**: Simplified following "Simplicidade sobre Otimização Prematura" +- **Architecture**: Simplified following "Simplicidade sobre Otimização Prematura", 18% code reduction ### Docker Framework Comparison | Framework | Performance | Position | @@ -176,12 +175,11 @@ $app->get('/users/:id<\\d+>', function($req, $res) { - Zero configuration to get started - PSR-7/PSR-15 compliance -### v1.2.0 Performance Features -- Simplified PerformanceMode (not HighPerformanceMode) -- Automatic JSON buffer pooling (maintained) -- Object pooling for Request/Response (maintained) -- Integrated memory optimizations (maintained) -- Smart garbage collection (maintained) +### v2.0.0 Performance Features +- Simplified `PerformanceMode` (replaces `HighPerformanceMode`) +- Automatic JSON buffer pooling with 256-byte threshold +- Object pooling for Request/Response (100%/99.9% reuse) +- Smart garbage collection ### Robust Security - JWT with refresh tokens @@ -215,45 +213,46 @@ $app->get('/users/:id<\\d+>', function($req, $res) { ## 📋 Examples Summary -| Category | Files | Key Features Demonstrated | -|----------|-------|---------------------------| -| **01-basics** | 4 files | Hello World, CRUD, Request/Response, JSON API | -| **02-routing** | 5 files | Regex, Parameters, Groups, Constraints, Static Files | -| **03-middleware** | 4 files | Custom, Stack, Complete Auth, CORS | -| **04-api** | 1 file | Complete REST with pagination and filters | -| **05-performance** | 1 file | Simplified Performance Mode v1.2.0 | -| **06-security** | 1 file | Complete JWT with refresh tokens | -| **07-advanced** | 2 files | Array callables, Performance v1.2.0 | -| **Total** | **17 examples** | **Complete framework coverage** | +| Category | Key Features Demonstrated | +|----------|---------------------------| +| **01-basics** | Hello World, CRUD, Request/Response, JSON API | +| **02-routing** | Regex, Parameters, Groups, Constraints, Static Files | +| **03-middleware** | Custom, Stack, Complete Auth, CORS | +| **04-api** | Complete REST with pagination and filters | +| **05-performance** | Simplified Performance Mode v2.0.0 | +| **06-security** | Complete JWT with refresh tokens | +| **07-advanced** | Array callables | +| **08-json-optimization** | JSON buffer pooling optimization | +| **09-error-handling** | Enhanced error diagnostics | ## 🔄 Migration from Previous Versions -### From v1.1.x to v1.2.0 +### From v1.x to v2.0.0 ```php -// OLD: HighPerformanceMode complex +// OLD: HighPerformanceMode (removed in v2.0.0) use PivotPHP\Core\Performance\HighPerformanceMode; -HighPerformanceMode::enable(HighPerformanceMode::PROFILE_EXTREME); +HighPerformanceMode::enable(HighPerformanceMode::PROFILE_EXTREME); // Class removed // NEW: Simplified PerformanceMode use PivotPHP\Core\Performance\PerformanceMode; PerformanceMode::enable(PerformanceMode::PROFILE_PRODUCTION); ``` -### Architectural Improvements (Automatic) -- Simplified architecture following "Simplicidade sobre Otimização Prematura" -- All performance optimizations maintained -- 100% backward compatibility via automatic aliases -- No code changes required for existing applications +### Breaking Changes (v2.0.0) +- Legacy namespace aliases removed — update `use` statements to current namespaces +- `HighPerformanceMode` replaced by `PerformanceMode` +- Deprecated and complex classes removed from the codebase (18% reduction) +- See [Migration Guide](../docs/MIGRATION_GUIDE.md) for complete list ## 🎯 Best Practices Demonstrated 1. **Security First**: All examples include proper input validation and security headers -2. **Performance Optimized**: Leverages v1.2.0 simplified optimizations +2. **Performance Optimized**: Leverages v2.0.0 simplified optimizations 3. **Type Safety**: PHP 8.1+ features with strict typing 4. **PSR Compliance**: PSR-7, PSR-15, PSR-12 standards followed 5. **Real-World Ready**: Production-grade error handling and logging --- -**PivotPHP Core v1.2.0** - Express.js for PHP with simplified architecture! 🐘⚡ -**Examples updated:** July 2025 +**PivotPHP Core v2.0.0** - Express.js for PHP with simplified architecture! 🐘⚡ +**Examples updated:** 2025 diff --git a/phpstan.neon b/phpstan.neon index 5628bc3..21c188d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,13 +9,11 @@ parameters: - examples reportUnmatchedIgnoredErrors: false ignoreErrors: - - '#Call to an undefined method [a-zA-Z0-9\\_]+::[a-zA-Z0-9_]+\(\)#' - - '#Access to an undefined property [a-zA-Z0-9\\_]+::\$[a-zA-Z0-9_]+#' - - '#Parameter \#[0-9]+ \$[a-zA-Z0-9_]+ of method [a-zA-Z0-9\\_]+::[a-zA-Z0-9_]+\(\) expects [^,]+, [^,]+ given#' - - '#Parameter \#[0-9]+ \$[a-zA-Z0-9_]+ of static method [a-zA-Z0-9\\_]+::[a-zA-Z0-9_]+\(\) expects [^,]+, [^,]+ given#' + # Array generic typing - low priority, tracked separately - '#Method [a-zA-Z0-9\\_]+::[a-zA-Z0-9_]+\(\) has parameter \$[a-zA-Z0-9_]+ with no value type specified in iterable type array#' - '#Method [a-zA-Z0-9\\_]+::[a-zA-Z0-9_]+\(\) return type has no value type specified in iterable type array#' - '#Property [a-zA-Z0-9\\_]+::\$[a-zA-Z0-9_]+ type has no value type specified in iterable type array#' + # Specific known issues - '#Call to an undefined static method Express\\Routing\\Router::(getGroupStats|warmupGroups|identifyByGroup|benchmarkGroupAccess|warmupCache)\(\)#' - '#Else branch is unreachable because ternary operator condition is always#' - '#Result of && is always#' diff --git a/src/Core/Application.php b/src/Core/Application.php index faadb7d..f47a7d6 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -1,10 +1,13 @@ */ protected array $middlewareAliases = [ + // @deprecated v2.1.0 — 'load-shedder' alias will be removed in v3.0.0. Use RateLimiter directly. 'load-shedder' => \PivotPHP\Core\Middleware\LoadShedder::class, 'rate-limiter' => \PivotPHP\Core\Middleware\RateLimiter::class, ]; @@ -312,14 +317,15 @@ protected function configureBasicErrorHandling(): void // Configuração básica de erro que funciona mesmo sem config carregado error_reporting(E_ALL); ini_set('log_errors', '1'); - - // Por enquanto, mostrar erros até config ser carregado - ini_set('display_errors', '1'); + ini_set('display_errors', '0'); set_error_handler([$this, 'handleError']); set_exception_handler( function (Throwable $e): void { - $this->handleException($e); + $response = $this->handleException($e); + if (!$response->isSent()) { + $response->emit(); + } } ); } @@ -346,7 +352,10 @@ protected function configureErrorHandling(): void set_error_handler([$this, 'handleError']); set_exception_handler( function (Throwable $e): void { - $this->handleException($e); + $response = $this->handleException($e); + if (!$response->isSent()) { + $response->emit(); + } } ); } @@ -362,7 +371,7 @@ protected function loadDefaultMiddlewares(): void if (is_array($middlewares)) { foreach ($middlewares as $middleware) { - if (is_string($middleware) || is_callable($middleware)) { + if (is_callable($middleware)) { $this->middlewares->add($middleware); } } @@ -407,46 +416,68 @@ public function register(string|ServiceProvider $provider): self * @param mixed $middleware Middleware a ser adicionado * @return $this */ - public function use($middleware): self + public function use(mixed $middleware): self { - // Check if it's a middleware alias + // Resolve alias string ('load-shedder' → class name) if (is_string($middleware) && isset($this->middlewareAliases[$middleware])) { $middleware = $this->middlewareAliases[$middleware]; } - // If middleware is a string class name, resolve it if (is_string($middleware) && class_exists($middleware)) { - $middlewareInstance = $this->container->has($middleware) - ? $this->container->get($middleware) - : new $middleware(); - - // Convert to callable format expected by MiddlewareStack - if (is_object($middlewareInstance) && method_exists($middlewareInstance, 'handle')) { - $callable = function ($request, $response, $next) use ($middlewareInstance) { - return $middlewareInstance->handle($request, $response, $next); - }; - } else { - throw new \InvalidArgumentException('Middleware must have a handle method'); - } - - $this->middlewares->add($callable); + $this->middlewares->add($this->resolveClassMiddleware($middleware)); } elseif (is_callable($middleware)) { $this->middlewares->add($middleware); } else { - // Try to make it callable - if (is_object($middleware) && method_exists($middleware, 'handle')) { - $callable = function ($request, $response, $next) use ($middleware) { - return $middleware->handle($request, $response, $next); - }; - $this->middlewares->add($callable); - } else { - throw new \InvalidArgumentException('Middleware must be callable or have a handle method'); - } + $this->middlewares->add($this->wrapObjectMiddleware($middleware)); } return $this; } + /** + * Resolve a middleware class name into a callable, using the container when available. + * + * @param string $class Fully-qualified class name of the middleware + * @return callable + * @throws \InvalidArgumentException When the resolved instance has no handle() method + */ + private function resolveClassMiddleware(string $class): callable + { + $instance = $this->container->has($class) + ? $this->container->get($class) + : new $class(); + + if (!is_object($instance) || !method_exists($instance, 'handle')) { + throw new \InvalidArgumentException( + "Middleware class '{$class}' must have a handle() method" + ); + } + + return function ($request, $response, $next) use ($instance) { + return $instance->handle($request, $response, $next); + }; + } + + /** + * Wraps a middleware object's handle() method into a callable. + * + * @param mixed $middleware Object expected to expose a handle() method + * @return callable + * @throws \InvalidArgumentException When the value is not an object or lacks a handle() method + */ + private function wrapObjectMiddleware(mixed $middleware): callable + { + if (!is_object($middleware) || !method_exists($middleware, 'handle')) { + throw new \InvalidArgumentException( + 'Middleware must be callable or an object with a handle() method' + ); + } + + return function ($request, $response, $next) use ($middleware) { + return $middleware->handle($request, $response, $next); + }; + } + /** * Alias for the use method for middleware registration * @@ -466,26 +497,30 @@ public function middleware($middleware, array $options = []): self } /** - * Get middleware by name + * Retorna as opções de um middleware registrado por nome. * - * @param string $name - * @return mixed + * As opções são armazenadas via middleware() quando chamado com o segundo argumento. + * Retorna null se o middleware não foi registrado ou não possui opções. + * + * @param string $name Nome do middleware + * @return mixed Opções do middleware ou null se não encontrado */ - public function getMiddleware(string $name) + public function getMiddleware(string $name): mixed { - // This would need to be implemented based on how middlewares are stored - // For now, return null + if ($this->container->has("middleware.{$name}.options")) { + return $this->container->get("middleware.{$name}.options"); + } return null; } /** * Registra uma rota GET. * - * @param string $path Caminho da rota - * @param mixed $handler Handler da rota + * @param string $path Caminho da rota + * @param callable|array $handler Handler da rota * @return $this */ - public function get(string $path, $handler): self + public function get(string $path, callable|array $handler): self { $this->router->get($path, $handler); return $this; @@ -494,11 +529,11 @@ public function get(string $path, $handler): self /** * Registra uma rota POST. * - * @param string $path Caminho da rota - * @param mixed $handler Handler da rota + * @param string $path Caminho da rota + * @param callable|array $handler Handler da rota * @return $this */ - public function post(string $path, $handler): self + public function post(string $path, callable|array $handler): self { $this->router->post($path, $handler); return $this; @@ -507,11 +542,11 @@ public function post(string $path, $handler): self /** * Registra uma rota PUT. * - * @param string $path Caminho da rota - * @param mixed $handler Handler da rota + * @param string $path Caminho da rota + * @param callable|array $handler Handler da rota * @return $this */ - public function put(string $path, $handler): self + public function put(string $path, callable|array $handler): self { $this->router->put($path, $handler); return $this; @@ -520,11 +555,11 @@ public function put(string $path, $handler): self /** * Registra uma rota DELETE. * - * @param string $path Caminho da rota - * @param mixed $handler Handler da rota + * @param string $path Caminho da rota + * @param callable|array $handler Handler da rota * @return $this */ - public function delete(string $path, $handler): self + public function delete(string $path, callable|array $handler): self { $this->router->delete($path, $handler); return $this; @@ -533,11 +568,11 @@ public function delete(string $path, $handler): self /** * Registra uma rota PATCH. * - * @param string $path Caminho da rota - * @param mixed $handler Handler da rota + * @param string $path Caminho da rota + * @param callable|array $handler Handler da rota * @return $this */ - public function patch(string $path, $handler): self + public function patch(string $path, callable|array $handler): self { $this->router->patch($path, $handler); return $this; @@ -561,7 +596,7 @@ public function staticFiles( array $options = [] ): self { // Registra cada arquivo encontrado como uma rota individual - \PivotPHP\Core\Routing\StaticFileManager::registerDirectory($routePrefix, $physicalPath, $this, $options); + StaticFileManager::registerDirectory($routePrefix, $physicalPath, $this, $options); return $this; } @@ -772,13 +807,11 @@ public function handleException( $response = $response ?: new Response(); $debug = $this->config->get('app.debug', false); - // Log do erro usando PSR-3 logger - $this->logException($e); - // Determinar status code $statusCode = $e instanceof HttpException ? $e->getStatusCode() : 500; if ($debug) { + $this->logException($e); return $response ->status($statusCode) ->json( @@ -793,8 +826,6 @@ public function handleException( } else { // Em produção, gerar ID único para o erro e logar detalhes $errorId = uniqid('err_', true); - - // Log detalhado para análise posterior $this->logException($e, $errorId); return $response @@ -873,7 +904,7 @@ public function addEventListener(string $eventType, callable $listener): self { if ($this->container->has('listeners')) { $listenerProvider = $this->container->get('listeners'); - if ($listenerProvider instanceof \PivotPHP\Core\Providers\ListenerProvider) { + if ($listenerProvider instanceof EventsListenerProvider) { $listenerProvider->addListener($eventType, $listener); // Rastrear listener $this->registeredListeners[$eventType][] = $listener; @@ -891,7 +922,7 @@ public function clearEventListeners(): void { if ($this->container->has('listeners')) { $listenerProvider = $this->container->get('listeners'); - if ($listenerProvider instanceof \PivotPHP\Core\Providers\ListenerProvider) { + if ($listenerProvider instanceof EventsListenerProvider) { foreach ($this->registeredListeners as $eventType => $listeners) { foreach ($listeners as $listener) { $listenerProvider->removeListener($eventType, $listener); @@ -982,8 +1013,11 @@ public function run(): void { $response = $this->handle(); - // Delegar toda a lógica de emissão para o Response - $response->emit(); + // Application é o único ponto de emissão do ciclo de vida da requisição. + // A checagem evita reemitir caso o handler já tenha chamado emit() manualmente. + if (!$response->isSent()) { + $response->emit(); + } } /** diff --git a/src/Core/Container.php b/src/Core/Container.php index 9c235a8..6653430 100644 --- a/src/Core/Container.php +++ b/src/Core/Container.php @@ -1,5 +1,7 @@ getType(); - if ($type && !$type->isBuiltin()) { + if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { $className = $type->getName(); $dependencies[] = $this->make($className); continue; diff --git a/src/Events/EventDispatcher.php b/src/Events/EventDispatcher.php index 2e7b60d..33c2d26 100644 --- a/src/Events/EventDispatcher.php +++ b/src/Events/EventDispatcher.php @@ -1,22 +1,39 @@ listenerProvider = $listenerProvider; + } + /** * Register a listener for an event */ @@ -30,9 +47,42 @@ public function listen(string $event, callable $listener): void } /** - * Dispatch an event + * Dispatch a PSR-14 event object. + * + * {@inheritdoc} + */ + public function dispatch(object $event): object + { + if ($this->listenerProvider !== null) { + $listeners = $this->listenerProvider->getListenersForEvent($event); + + foreach ($listeners as $listener) { + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + break; + } + + $listener($event); + } + } + + return $event; + } + + /** + * Fire a simple string-named event with optional data array. + * + * This method implements the lightweight string-based event system built into + * this dispatcher (via `listen()`). It is intentionally separate from the + * PSR-14 `dispatch(object): object` method, which operates on typed event objects + * and requires a `ListenerProviderInterface`. + * + * Use `fire()` for simple internal hooks. Use `dispatch()` for PSR-14 interoperable + * events that can be shared across packages. + * + * @param array $data + * @since 2.0.0 */ - public function dispatch(string $event, array $data = []): bool + public function fire(string $event, array $data = []): bool { if (!isset($this->listeners[$event])) { return false; @@ -60,6 +110,8 @@ public function removeListeners(string $event): void /** * Get all registered events + * + * @return array */ public function getEvents(): array { diff --git a/src/Events/ListenerProvider.php b/src/Events/ListenerProvider.php new file mode 100644 index 0000000..2c0c981 --- /dev/null +++ b/src/Events/ListenerProvider.php @@ -0,0 +1,112 @@ +> + */ + private array $listeners = []; + + /** + * {@inheritdoc} + * + * @return iterable + */ + public function getListenersForEvent(object $event): iterable + { + $eventClass = get_class($event); + + // Return listeners for exact class match + if (isset($this->listeners[$eventClass])) { + yield from $this->listeners[$eventClass]; + } + + // Return listeners for parent classes and interfaces + foreach (class_parents($event) as $parentClass) { + if (isset($this->listeners[$parentClass])) { + yield from $this->listeners[$parentClass]; + } + } + + foreach (class_implements($event) as $interface) { + if (isset($this->listeners[$interface])) { + yield from $this->listeners[$interface]; + } + } + } + + /** + * Add a listener for an event + */ + public function addListener(string $eventType, callable $listener): void + { + if (!isset($this->listeners[$eventType])) { + $this->listeners[$eventType] = []; + } + + $this->listeners[$eventType][] = $listener; + } + + /** + * Remove all listeners for an event type + */ + public function removeListeners(string $eventType): void + { + unset($this->listeners[$eventType]); + } + + /** + * Remove um listener específico de um tipo de evento + */ + public function removeListener(string $eventType, callable $listener): void + { + if (!isset($this->listeners[$eventType])) { + return; + } + foreach ($this->listeners[$eventType] as $i => $registered) { + // Comparação de closures/callables + if ( + $registered === $listener || ( + is_object($registered) + && is_object($listener) + && $registered == $listener + ) + ) { + unset($this->listeners[$eventType][$i]); + } + } + // Reindexa o array para evitar buracos + $this->listeners[$eventType] = array_values($this->listeners[$eventType]); + // Remove o tipo se não houver mais listeners + if (empty($this->listeners[$eventType])) { + unset($this->listeners[$eventType]); + } + } + + /** + * Check if there are listeners for an event type + */ + public function hasListeners(string $eventType): bool + { + return isset($this->listeners[$eventType]) && !empty($this->listeners[$eventType]); + } + + /** + * Get all registered event types + * + * @return array + */ + public function getEventTypes(): array + { + return array_keys($this->listeners); + } +} diff --git a/src/Http/Adapters/GlobalsToServerRequestAdapter.php b/src/Http/Adapters/GlobalsToServerRequestAdapter.php index 6c67b2a..06fe196 100644 --- a/src/Http/Adapters/GlobalsToServerRequestAdapter.php +++ b/src/Http/Adapters/GlobalsToServerRequestAdapter.php @@ -63,7 +63,7 @@ private static function createUriFromServer(array $server): Uri $scheme = (!empty($server['HTTPS']) && $server['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $server['HTTP_HOST'] ?? $server['SERVER_NAME'] ?? 'localhost'; $port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null; - $path = parse_url($server['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'; + $path = parse_url($server['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $query = $server['QUERY_STRING'] ?? ''; $uri = new Uri(); @@ -153,7 +153,13 @@ private static function normalizeNestedFiles(array $file): array */ private static function createUploadedFile(array $file): UploadedFile { - $stream = Stream::createFromFile($file['tmp_name']); + $tmpName = $file['tmp_name'] ?? ''; + + if (!file_exists($tmpName) || !is_readable($tmpName)) { + $stream = Stream::createFromString(''); + } else { + $stream = Stream::createFromFile($tmpName); + } return new UploadedFile( $stream, diff --git a/src/Http/Adapters/Psr7PoolAdapter.php b/src/Http/Adapters/Psr7PoolAdapter.php index 6e187fb..a52d5a9 100644 --- a/src/Http/Adapters/Psr7PoolAdapter.php +++ b/src/Http/Adapters/Psr7PoolAdapter.php @@ -30,7 +30,7 @@ public function getServerRequest( string $protocol = '1.1', ?array $cookies = null ): ServerRequestInterface { - return Psr7Pool::getServerRequest($method, $uri, $body, $headers, $protocol, $cookies); + return Psr7Pool::getServerRequest($method, $uri, $body, $headers, $protocol, $cookies ?? []); } /** diff --git a/src/Http/CustomHeaderCollection.php b/src/Http/CustomHeaderCollection.php new file mode 100644 index 0000000..868b416 --- /dev/null +++ b/src/Http/CustomHeaderCollection.php @@ -0,0 +1,83 @@ + */ + private array $customHeaders; + + /** @var array */ + protected array $headers; + + /** + * @param array $customHeaders + */ + public function __construct(array $customHeaders = []) + { + $this->customHeaders = $customHeaders; + $this->headers = []; + + // Process custom headers first + foreach ($customHeaders as $key => $value) { + $key = trim($key, ':'); + $key = self::headerToCamel($key); + $this->headers[$key] = $value; + } + + // Fall back to environment headers not already overridden + $existingHeaders = function_exists('getallheaders') ? getallheaders() : []; + if (empty($existingHeaders)) { + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headerName = str_replace( + ' ', + '-', + ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))) + ); + $key = self::headerToCamel($headerName); + + if (!isset($this->headers[$key])) { + $this->headers[$key] = $value; + } + } + } + } + } + + /** + * Override getHeader to handle custom headers properly. + */ + public function getHeader($name): ?string + { + if (isset($this->customHeaders[$name])) { + return (string) $this->customHeaders[$name]; + } + + $key = self::headerToCamel(trim($name, ':')); + $value = $this->headers[$key] ?? null; + return $value !== null && (is_string($value) || is_numeric($value)) ? (string) $value : null; + } + + /** + * Override hasHeader to check both formats. + */ + public function hasHeader($name): bool + { + if (isset($this->customHeaders[$name])) { + return true; + } + + $key = self::headerToCamel(trim($name, ':')); + return isset($this->headers[$key]); + } + +} diff --git a/src/Http/HeaderRequest.php b/src/Http/HeaderRequest.php index f940824..15d2827 100644 --- a/src/Http/HeaderRequest.php +++ b/src/Http/HeaderRequest.php @@ -1,5 +1,7 @@ headers = []; foreach ($headers as $key => $value) { - $key = trim($key, ':'); // Remove leading colon - $camelCaseKey = explode('-', $key); // Remove any suffix after a hyphen - $camelCaseKey = array_map('ucfirst', $camelCaseKey); - $camelCaseKey = implode('', $camelCaseKey); - $key = lcfirst($camelCaseKey); // Convert to camelCase + $key = self::headerToCamel($key); $this->headers[$key] = $value; } } @@ -158,4 +156,16 @@ public function acceptsHtml(): bool $accept = $this->accept(); return $accept && (strpos($accept, 'text/html') !== false || strpos($accept, '*/*') !== false); } + + /** + * Convert a hyphenated header name to camelCase. + * Example: "Content-Type" → "contentType" + */ + public static function headerToCamel(string $header): string + { + $header = trim($header, ':'); + $parts = explode('-', $header); + $parts = array_map('ucfirst', $parts); + return lcfirst(implode('', $parts)); + } } diff --git a/src/Http/Pool/Psr7Pool.php b/src/Http/Pool/Psr7Pool.php index 03eae47..2944e15 100644 --- a/src/Http/Pool/Psr7Pool.php +++ b/src/Http/Pool/Psr7Pool.php @@ -242,11 +242,25 @@ private static function resetServerRequest( string $version, array $serverParams ): ServerRequestInterface { - return $request + $request = $request ->withMethod($method) ->withUri($uri) ->withBody($body) ->withProtocolVersion($version); + + // Remover headers existentes antes de aplicar os novos + // Captura os nomes em array separado para evitar mutação durante iteração + $existingHeaders = array_keys($request->getHeaders()); + foreach ($existingHeaders as $name) { + $request = $request->withoutHeader($name); + } + + // Aplicar headers do novo request + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + + return $request; } /** @@ -315,7 +329,9 @@ private static function resetStream(StreamInterface $stream, string $content): S } if ($stream->isWritable()) { - $stream->truncate(0); + if (method_exists($stream, 'truncate')) { + $stream->truncate(0); + } $stream->write($content); $stream->rewind(); return $stream; diff --git a/src/Http/Psr7/Adapters/HeaderPoolAdapter.php b/src/Http/Psr7/Adapters/HeaderPoolAdapter.php index 9a9f914..a2ce1bc 100644 --- a/src/Http/Psr7/Adapters/HeaderPoolAdapter.php +++ b/src/Http/Psr7/Adapters/HeaderPoolAdapter.php @@ -28,9 +28,10 @@ public function getNormalizedName(string $name): string public function getHeaderValues(string $name, mixed $value): array { if (is_array($value)) { + /** @var array $value */ return HeaderPool::getHeaderValues($name, $value); } - return HeaderPool::getHeaderValues($name, (array)$value); + return HeaderPool::getHeaderValues($name, (string)$value); } /** @@ -39,9 +40,10 @@ public function getHeaderValues(string $name, mixed $value): array public function getValidatedHeaderValues(string $name, mixed $value): array { if (is_array($value)) { + /** @var array $value */ return HeaderPool::getValidatedHeaderValues($name, $value); } - return HeaderPool::getValidatedHeaderValues($name, (array)$value); + return HeaderPool::getValidatedHeaderValues($name, (string)$value); } /** diff --git a/src/Http/Psr7/ServerRequest.php b/src/Http/Psr7/ServerRequest.php index 89cb934..bcb392f 100644 --- a/src/Http/Psr7/ServerRequest.php +++ b/src/Http/Psr7/ServerRequest.php @@ -218,7 +218,7 @@ private static function createUriFromGlobals(): Uri $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost'; $port = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null; - $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'; + $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $query = $_SERVER['QUERY_STRING'] ?? ''; $uri = new Uri(); diff --git a/src/Http/Request.php b/src/Http/Request.php index 7bf80cc..1a21f1a 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -1,8 +1,11 @@ cachedInput === null) { $input = @file_get_contents('php://input'); if ($input === false) { error_log('Failed to read from php://input stream'); - self::$cachedInput = ''; + $this->cachedInput = ''; } else { - self::$cachedInput = $input; + $this->cachedInput = $input; } } - return self::$cachedInput; + return $this->cachedInput; } /** @@ -202,7 +209,10 @@ private function initializePsr7Request(): void error_log('JSON decode error in request body: ' . json_last_error_msg()); $this->psr7Request = $this->psr7Request->withParsedBody($_POST); } else { - $this->psr7Request = $this->psr7Request->withParsedBody($decoded ?: $_POST); + $parsed = $decoded ?: $_POST; + $this->psr7Request = $this->psr7Request->withParsedBody( + is_array($parsed) || is_object($parsed) ? $parsed : null + ); } } } @@ -503,97 +513,7 @@ public function header(string $name): ?string */ public function setHeaders(array $headers): self { - // Criar um novo HeaderRequest com headers customizados - $this->headers = new class ($headers) extends HeaderRequest { - /** @var array */ - private array $customHeaders; - - /** @var array */ - protected array $headers; - - /** - * @param array $customHeaders - */ - public function __construct(array $customHeaders = []) - { - $this->customHeaders = $customHeaders; - $this->headers = []; - - // Primeiro processar headers customizados - foreach ($customHeaders as $key => $value) { - $key = trim($key, ':'); // Remove leading colon - $camelCaseKey = explode('-', $key); - $camelCaseKey = array_map('ucfirst', $camelCaseKey); - $camelCaseKey = implode('', $camelCaseKey); - $key = lcfirst($camelCaseKey); // Convert to camelCase - $this->headers[$key] = $value; - } - - // Depois processar headers padrão se não foram sobrescritos - $existingHeaders = function_exists('getallheaders') ? getallheaders() : []; - if (empty($existingHeaders)) { - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headerName = str_replace( - ' ', - '-', - ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))) - ); - $camelCaseKey = explode('-', $headerName); - $camelCaseKey = array_map('ucfirst', $camelCaseKey); - $camelCaseKey = implode('', $camelCaseKey); - $key = lcfirst($camelCaseKey); - - // Only add if not already set by custom headers - if (!isset($this->headers[$key])) { - $this->headers[$key] = $value; - } - } - } - } - } - - /** - * Override getHeader to handle test headers properly - */ - public function getHeader($name): ?string - { - // First check if it's in our custom headers (exact match) - if (isset($this->customHeaders[$name])) { - return (string) $this->customHeaders[$name]; - } - - // Then check camelCase version - $key = trim($name, ':'); - $camelCaseKey = explode('-', $key); - $camelCaseKey = array_map('ucfirst', $camelCaseKey); - $camelCaseKey = implode('', $camelCaseKey); - $key = lcfirst($camelCaseKey); - - $value = $this->headers[$key] ?? null; - return $value !== null && (is_string($value) || is_numeric($value)) ? (string) $value : null; - } - - /** - * Override hasHeader to check both formats - */ - public function hasHeader($name): bool - { - // Check exact match first - if (isset($this->customHeaders[$name])) { - return true; - } - - // Check camelCase version - $key = trim($name, ':'); - $camelCaseKey = explode('-', $key); - $camelCaseKey = array_map('ucfirst', $camelCaseKey); - $camelCaseKey = implode('', $camelCaseKey); - $key = lcfirst($camelCaseKey); - - return isset($this->headers[$key]); - } - }; + $this->headers = new CustomHeaderCollection($headers); return $this; } @@ -980,20 +900,14 @@ private function parseBody(): void return; } - $input = file_get_contents('php://input'); - if ($input !== false) { + $input = $this->getCachedInput(); + if ($input !== '') { $decoded = json_decode($input); - if ($decoded instanceof stdClass) { - $this->body = $decoded; - } else { - $this->body = new stdClass(); + if (json_last_error() === JSON_ERROR_NONE) { + // Valid JSON — use decoded value; cast arrays/scalars to object for type safety + $this->body = $decoded instanceof stdClass ? $decoded : (object)($decoded ?? []); + return; } - } else { - $this->body = new stdClass(); - } - - if (json_last_error() == JSON_ERROR_NONE) { - return; } if (!empty($_POST)) { @@ -1076,24 +990,12 @@ public function getParam(string $key, mixed $default = null): mixed * Get the client IP address * * @return string + * @deprecated Use ip() instead. getIp() does not validate against private/reserved ranges. */ public function getIp(): string { - // Check for IP behind proxy - if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); - return trim($ips[0]); - } - - if (!empty($_SERVER['HTTP_X_REAL_IP'])) { - return $_SERVER['HTTP_X_REAL_IP']; - } - - if (!empty($_SERVER['HTTP_CLIENT_IP'])) { - return $_SERVER['HTTP_CLIENT_IP']; - } - - return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; + trigger_error('Request::getIp() is deprecated. Use Request::ip() instead.', E_USER_DEPRECATED); + return $this->ip(); } /** @@ -1123,7 +1025,12 @@ public function getQuery(string $key, mixed $default = null): mixed } /** - * Get bodyAsStdClass + * Retorna o corpo da requisição como stdClass (API Express.js). + * + * Método preferido para acesso ao body em handlers de rota. + * Retorna um stdClass vazio para métodos sem body (GET, HEAD, OPTIONS, DELETE). + * + * @return stdClass */ public function getBodyAsStdClass(): stdClass { diff --git a/src/Http/Response.php b/src/Http/Response.php index c2135f3..5065f75 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -1,5 +1,7 @@ testMode = true; - } - // PSR-7 response será inicializado apenas quando necessário (lazy loading) + // Use setTestMode(true) in tests that need to suppress output } /** @@ -214,6 +202,12 @@ public function getStatusCode(): int /** * Define o modo teste (não faz echo direto). + * + * Em modo teste, os métodos json(), text(), html() e emit() não emitem saída. + * Deve ser ativado explicitamente em testes unitários: $response->setTestMode(true). + * A auto-detecção de ambiente de teste foi removida na v2.0.0. + * + * @param bool $testMode true para ativar modo teste, false para desativar */ public function setTestMode(bool $testMode): self { @@ -287,11 +281,6 @@ public function json(mixed $data): self $this->psr7Response = $this->psr7Response->withBody($pool->getStream($encoded)); } - // Só faz echo se não estiver em modo teste e emissão automática estiver habilitada - if (!$this->testMode && !$this->disableAutoEmit) { - $this->emit(); - } - return $this; } @@ -312,11 +301,6 @@ public function text(mixed $text): self $this->psr7Response = $this->psr7Response->withBody($pool->getStream($textString)); } - // Só faz echo se não estiver em modo teste e emissão automática estiver habilitada - if (!$this->testMode && !$this->disableAutoEmit) { - $this->emit(); - } - return $this; } @@ -337,11 +321,6 @@ public function html(mixed $html): self $this->psr7Response = $this->psr7Response->withBody($pool->getStream($htmlString)); } - // Só faz echo se não estiver em modo teste e emissão automática estiver habilitada - if (!$this->testMode && !$this->disableAutoEmit) { - $this->emit(); - } - return $this; } @@ -833,11 +812,13 @@ private function sanitizeForJson(mixed $data): mixed } /** - * Define se a emissão automática está desabilitada. + * @deprecated Desde a remoção do auto-emit, json()/text()/html() nunca emitem + * sozinhos — a emissão é sempre responsabilidade de Application::run() (ou de + * uma chamada explícita a emit()). Método mantido como no-op para compatibilidade + * com código que ainda o chama. */ public function disableAutoEmit(bool $disable = true): self { - $this->disableAutoEmit = $disable; return $this; } diff --git a/src/Logging/FileHandler.php b/src/Logging/FileHandler.php deleted file mode 100644 index a543ab2..0000000 --- a/src/Logging/FileHandler.php +++ /dev/null @@ -1,57 +0,0 @@ -filePath = $filePath; - $this->dateFormat = $dateFormat; - - // Cria o diretório se não existir - $dir = dirname($filePath); - if (!is_dir($dir)) { - if (!@mkdir($dir, 0755, true) && !is_dir($dir)) { - throw new RuntimeException("Cannot create log directory: {$dir}"); - } - } - } - - /** - * Handle the request - */ - public function handle(array $record): void - { - $message = $this->format($record); - $written = @file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND | LOCK_EX); - if ($written === false) { - error_log('Failed to write to log file: ' . $this->filePath); - } - } - - private function format(array $record): string - { - $datetime = $record['datetime']->format($this->dateFormat); - $level = $record['level_name']; - $message = $record['message']; - - $context = ''; - if (!empty($record['context'])) { - $context = ' ' . json_encode($record['context']); - } - - return "[{$datetime}][{$level}] {$message}{$context}"; - } -} diff --git a/src/Logging/LogHandlerInterface.php b/src/Logging/LogHandlerInterface.php deleted file mode 100644 index 143cb91..0000000 --- a/src/Logging/LogHandlerInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - $record The log record to handle - */ - public function handle(array $record): void; -} diff --git a/src/Logging/Logger.php b/src/Logging/Logger.php deleted file mode 100644 index 9d88771..0000000 --- a/src/Logging/Logger.php +++ /dev/null @@ -1,171 +0,0 @@ - - */ - private static array $levels = [ - self::EMERGENCY => 'EMERGENCY', - self::ALERT => 'ALERT', - self::CRITICAL => 'CRITICAL', - self::ERROR => 'ERROR', - self::WARNING => 'WARNING', - self::NOTICE => 'NOTICE', - self::INFO => 'INFO', - self::DEBUG => 'DEBUG' - ]; - - /** - * @var array - */ - private array $handlers = []; - private int $level = self::DEBUG; - - public function __construct(int $level = self::DEBUG) - { - $this->level = $level; - } - - /** - * Adiciona um handler - */ - public function addHandler(LogHandlerInterface $handler): void - { - $this->handlers[] = $handler; - } - - /** - * Define o nível mínimo de log - */ - public function setLevel(int $level): void - { - $this->level = $level; - } - - /** - * Registra uma mensagem de log - * @param array $context - */ - public function log( - int $level, - string $message, - array $context = [] - ): void { - if ($level > $this->level) { - return; - } - - $record = [ - 'level' => $level, - 'level_name' => self::$levels[$level], - 'message' => $message, - 'context' => $context, - 'datetime' => new \DateTime(), - 'extra' => [] - ]; - - foreach ($this->handlers as $handler) { - $handler->handle($record); - } - } - - /** - * Métodos de conveniência - * @param array $context - */ - public function emergency(string $message, array $context = []): void - { - $this->log(self::EMERGENCY, $message, $context); - } - - /** - * Log an alert message - * - * @param array $context - */ - public function alert(string $message, array $context = []): void - { - $this->log(self::ALERT, $message, $context); - } - - /** - * Log a critical error message - * - * @param string $message The log message - * @param array $context Additional context data - */ - public function critical(string $message, array $context = []): void - { - $this->log(self::CRITICAL, $message, $context); - } - - /** - * Log an error message - * - * @param string $message The log message - * @param array $context Additional context data - */ - public function error(string $message, array $context = []): void - { - $this->log(self::ERROR, $message, $context); - } - - /** - * Log a warning message - * - * @param string $message The log message - * @param array $context Additional context data - */ - public function warning(string $message, array $context = []): void - { - $this->log(self::WARNING, $message, $context); - } - - /** - * Log a notice message - * - * @param string $message The log message - * @param array $context Additional context data - */ - public function notice(string $message, array $context = []): void - { - $this->log(self::NOTICE, $message, $context); - } - - /** - * Log an informational message - * - * @param string $message The log message - * @param array $context Additional context data - */ - public function info(string $message, array $context = []): void - { - $this->log(self::INFO, $message, $context); - } - - /** - * Log a debug message - * - * @param string $message The log message - * @param array $context Additional context data - */ - public function debug(string $message, array $context = []): void - { - $this->log(self::DEBUG, $message, $context); - } -} diff --git a/src/Logging/PsrLogger.php b/src/Logging/PsrLogger.php new file mode 100644 index 0000000..b61ef1c --- /dev/null +++ b/src/Logging/PsrLogger.php @@ -0,0 +1,148 @@ +logPath = $logPath ?: ($_ENV['LOG_PATH'] ?? sys_get_temp_dir() . '/pivotphp.log'); + $this->dateFormat = $dateFormat; + $this->logLevels = [ + LogLevel::EMERGENCY => 0, + LogLevel::ALERT => 1, + LogLevel::CRITICAL => 2, + LogLevel::ERROR => 3, + LogLevel::WARNING => 4, + LogLevel::NOTICE => 5, + LogLevel::INFO => 6, + LogLevel::DEBUG => 7, + ]; + } + + /** + * {@inheritdoc} + * + * @param string|\Stringable $level + */ + public function log( + $level, + string|\Stringable $message, + array $context = [] + ): void { + $levelString = (string)$level; + + if (!isset($this->logLevels[$levelString])) { + throw new \InvalidArgumentException("Unknown log level: {$levelString}"); + } + + $logEntry = $this->formatMessage($levelString, $message, $context); + + $this->writeToFile($logEntry); + } + + /** + * Format the log message + */ + private function formatMessage( + string $level, + string|\Stringable $message, + array $context + ): string { + $timestamp = date($this->dateFormat); + $levelUpper = strtoupper($level); + + // Interpolate context values into message placeholders + $message = $this->interpolate((string)$message, $context); + + // Format: [2023-12-07 10:30:45] INFO: Message content + $logEntry = "[{$timestamp}] {$levelUpper}: {$message}"; + + // Add context if provided + if (!empty($context)) { + $logEntry .= ' ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + return $logEntry . PHP_EOL; + } + + /** + * Interpolate context values into the message placeholders + */ + private function interpolate(string $message, array $context): string + { + // Build a replacement array with braces around the context keys + $replace = []; + foreach ($context as $key => $val) { + // Check that the value can be cast to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + // Interpolate replacement values into the message and return + return strtr($message, $replace); + } + + /** + * Write log entry to file + */ + private function writeToFile(string $logEntry): void + { + try { + // Create directory if it doesn't exist + $dir = dirname($this->logPath); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + // Append to log file + file_put_contents($this->logPath, $logEntry, FILE_APPEND | LOCK_EX); + } catch (\Throwable $e) { + // Fallback to error_log if file writing fails + error_log("Express Logger Error: " . $e->getMessage()); + error_log($logEntry); + } + } + + /** + * Set the log file path + */ + public function setLogPath(string $path): void + { + $this->logPath = $path; + } + + /** + * Get the current log file path + */ + public function getLogPath(): string + { + return $this->logPath; + } + + /** + * Clear the log file + */ + public function clear(): bool + { + return file_put_contents($this->logPath, '') !== false; + } +} diff --git a/src/Middleware/Http/ApiDocumentationMiddleware.php b/src/Middleware/Http/ApiDocumentationMiddleware.php index 30521a3..48345e8 100644 --- a/src/Middleware/Http/ApiDocumentationMiddleware.php +++ b/src/Middleware/Http/ApiDocumentationMiddleware.php @@ -4,9 +4,9 @@ namespace PivotPHP\Core\Middleware\Http; -use PivotPHP\Core\Core\Application; +use PivotPHP\Core\Http\Psr7\Response as Psr7Response; +use PivotPHP\Core\Http\Psr7\Stream; use PivotPHP\Core\Http\Request; -use PivotPHP\Core\Http\Response; use PivotPHP\Core\Routing\Router; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -17,7 +17,13 @@ * API Documentation Middleware * * Simple and effective automatic API documentation generation for the microframework. - * Provides automatic OpenAPI/Swagger documentation at /docs endpoint. + * Generates an OpenAPI 3.0.0 specification from all routes registered in the Router. + * Each route produces a basic path entry with its HTTP method and path. + * No PHPDoc comment parsing is performed — metadata is derived from registered routes only. + * + * Provides two endpoints: + * - /docs JSON OpenAPI 3.0.0 specification + * - /swagger Swagger UI interface (loads Swagger UI from unpkg CDN) * * Following 'Simplicidade sobre Otimização Prematura' principle. */ @@ -68,26 +74,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface private function handleApiDocs(ServerRequestInterface $request): ResponseInterface { try { - // Get the application instance from the request - $app = $request->getAttribute('app'); - - if (!$app instanceof Application) { - return $this->createErrorResponse('Application not found in request', 500); - } - - // Generate OpenAPI documentation from routes - $docs = $this->generateOpenApiDocs($app); - - // Create response - $response = new Response(); - $body = $response->getBody(); - if (is_object($body)) { - $body->write(json_encode($docs, JSON_PRETTY_PRINT)); - } + $docs = $this->generateOpenApiDocs(); - return $response + $json = json_encode($docs, JSON_THROW_ON_ERROR); + return (new Psr7Response(200)) ->withHeader('Content-Type', 'application/json') - ->withHeader('Access-Control-Allow-Origin', '*'); + ->withHeader('Access-Control-Allow-Origin', '*') + ->withBody(Stream::createFromString($json)); } catch (\Exception $e) { return $this->createErrorResponse('Error generating documentation: ' . $e->getMessage(), 500); } @@ -96,10 +89,9 @@ private function handleApiDocs(ServerRequestInterface $request): ResponseInterfa /** * Generate OpenAPI documentation from application routes * - * @param Application $app * @return array */ - private function generateOpenApiDocs(Application $app): array + private function generateOpenApiDocs(): array { $baseUrl = $this->baseUrl ?? 'http://localhost:8080'; @@ -144,15 +136,9 @@ private function generateOpenApiDocs(Application $app): array */ private function handleSwaggerUi(ServerRequestInterface $request): ResponseInterface { - $swaggerHtml = $this->getSwaggerUiHtml(); - - $response = new Response(); - $body = $response->getBody(); - if (is_object($body)) { - $body->write($swaggerHtml); - } - - return $response->withHeader('Content-Type', 'text/html'); + return (new Psr7Response(200)) + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody(Stream::createFromString($this->getSwaggerUiHtml())); } /** @@ -205,21 +191,16 @@ private function getSwaggerUiHtml(): string */ private function createErrorResponse(string $message, int $statusCode = 500): ResponseInterface { - $response = new Response(); - $body = $response->getBody(); - if (is_object($body)) { - $body->write(json_encode(['error' => $message])); - } - - return $response - ->withStatus($statusCode) - ->withHeader('Content-Type', 'application/json'); + $json = json_encode(['error' => $message], JSON_THROW_ON_ERROR); + return (new Psr7Response($statusCode)) + ->withHeader('Content-Type', 'application/json') + ->withBody(Stream::createFromString($json)); } /** * Magic method for direct invocation */ - public function __invoke(Request $request, Response $response, callable $next): ResponseInterface + public function __invoke(Request $request, ResponseInterface $response, callable $next): ResponseInterface { return $this->process($request, $this->createHandler($next)); } diff --git a/src/Middleware/LoadShedder.php b/src/Middleware/LoadShedder.php index 84f41e8..0c95efd 100644 --- a/src/Middleware/LoadShedder.php +++ b/src/Middleware/LoadShedder.php @@ -4,6 +4,7 @@ namespace PivotPHP\Core\Middleware; +use PivotPHP\Core\Http\Psr7\Factory\StreamFactory; use PivotPHP\Core\Http\Request; use PivotPHP\Core\Http\Response; @@ -14,6 +15,8 @@ * Simple and effective protection against overload. * * Following 'Simplicidade sobre Otimização Prematura' principle. + * + * @deprecated v2.1.0 Use \PivotPHP\Core\Middleware\RateLimiter instead. */ class LoadShedder { @@ -38,6 +41,7 @@ class LoadShedder */ public function __construct(int $maxRequests = 100, int $windowSeconds = 60) { + trigger_error('LoadShedder is deprecated. Use RateLimiter instead.', E_USER_DEPRECATED); $this->maxRequests = $maxRequests; $this->windowSeconds = $windowSeconds; } @@ -109,19 +113,19 @@ public function __invoke(Request $request, Response $response, callable $next): */ public function handle(Request $request, Response $response, callable $next): Response { + trigger_error('LoadShedder is deprecated. Use RateLimiter instead.', E_USER_DEPRECATED); if ($this->shouldShed($request)) { + $json = json_encode( + [ + 'error' => 'Too Many Requests', + 'message' => 'Rate limit exceeded. Please try again later.', + 'retry_after' => $this->windowSeconds, + ] + ) ?: '{"error":"Too Many Requests"}'; return $response ->withStatus(429, 'Too Many Requests') ->withHeader('Content-Type', 'application/json') - ->withBody( - json_encode( - [ - 'error' => 'Too Many Requests', - 'message' => 'Rate limit exceeded. Please try again later.', - 'retry_after' => $this->windowSeconds, - ] - ) - ); + ->withBody((new StreamFactory())->createStream($json)); } return $next($request, $response); diff --git a/src/Middleware/MiddlewareStack.php b/src/Middleware/MiddlewareStack.php index cc729a2..2b40fb2 100644 --- a/src/Middleware/MiddlewareStack.php +++ b/src/Middleware/MiddlewareStack.php @@ -1,5 +1,7 @@ [ - function ($req, $resp, $next) { - $resp->setHeader('Access-Control-Allow-Origin', '*'); - return $next($req, $resp); - } - ], - 'json' => [ - function ($req, $resp, $next) { - $resp->setHeader('Content-Type', 'application/json'); - return $next($req, $resp); - } - ], - 'security' => [ - function ($req, $resp, $next) { - $resp->setHeader('X-Frame-Options', 'DENY'); - $resp->setHeader('X-Content-Type-Options', 'nosniff'); - return $next($req, $resp); - } - ] - ]; - - foreach ($commonMiddlewarePatterns as $name => $middlewares) { - self::compileGroupMiddlewares('warmup:' . $name, $middlewares); - } - } - /** * Benchmark de pipeline específico */ diff --git a/src/Middleware/Performance/RateLimitMiddleware.php b/src/Middleware/Performance/RateLimitMiddleware.php index 7f665f8..a2c1dd4 100644 --- a/src/Middleware/Performance/RateLimitMiddleware.php +++ b/src/Middleware/Performance/RateLimitMiddleware.php @@ -11,6 +11,8 @@ /** * PSR-15 Rate Limiting Middleware + * + * @deprecated v2.1.0 Use \PivotPHP\Core\Middleware\RateLimiter instead. This class uses $_SESSION which violates HTTP statelessness. */ class RateLimitMiddleware implements MiddlewareInterface { @@ -18,6 +20,7 @@ class RateLimitMiddleware implements MiddlewareInterface public function __construct(array $options = []) { + trigger_error('RateLimitMiddleware is deprecated and uses $_SESSION. Use RateLimiter instead.', E_USER_DEPRECATED); $this->options = array_merge( [ 'windowMs' => 900000, // 15 minutos @@ -68,14 +71,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($currentCount >= $this->options['max']) { $factory = new \PivotPHP\Core\Http\Psr7\Factory\ResponseFactory(); $response = $factory->createResponse($this->options['statusCode']); - $response->getBody()->write( - json_encode( - [ - 'error' => true, - 'message' => $this->options['message'] - ] - ) - ); + $body = json_encode( + [ + 'error' => true, + 'message' => $this->options['message'] + ] + ) ?: '{"error":true}'; + $response->getBody()->write($body); return $response->withHeader('Content-Type', 'application/json'); } // Registra esta requisição diff --git a/src/Middleware/RateLimiter.php b/src/Middleware/RateLimiter.php index e15a0a8..fd67d02 100644 --- a/src/Middleware/RateLimiter.php +++ b/src/Middleware/RateLimiter.php @@ -65,7 +65,7 @@ public function __construct(array $config = []) // Set default key generator if not provided if (!$this->config['key_generator']) { $this->config['key_generator'] = function (Request $request) { - return $request->getIp(); + return $request->ip(); }; } } diff --git a/src/Providers/EventDispatcher.php b/src/Providers/EventDispatcher.php index cb4b6ff..d4ea7f5 100644 --- a/src/Providers/EventDispatcher.php +++ b/src/Providers/EventDispatcher.php @@ -10,6 +10,8 @@ /** * Simple PSR-14 compliant event dispatcher implementation + * + * @deprecated v2.1.0 Use \PivotPHP\Core\Events\EventDispatcher instead. */ class EventDispatcher implements EventDispatcherInterface { @@ -17,6 +19,10 @@ class EventDispatcher implements EventDispatcherInterface public function __construct(ListenerProviderInterface $listenerProvider) { + trigger_error( + 'PivotPHP\\Core\\Providers\\EventDispatcher is deprecated. Use PivotPHP\\Core\\Events\\EventDispatcher instead.', + E_USER_DEPRECATED + ); $this->listenerProvider = $listenerProvider; } diff --git a/src/Providers/EventServiceProvider.php b/src/Providers/EventServiceProvider.php index 9803305..6394a4b 100644 --- a/src/Providers/EventServiceProvider.php +++ b/src/Providers/EventServiceProvider.php @@ -5,6 +5,8 @@ namespace PivotPHP\Core\Providers; use PivotPHP\Core\Core\Application; +use PivotPHP\Core\Events\EventDispatcher as EventsEventDispatcher; +use PivotPHP\Core\Events\ListenerProvider as EventsListenerProvider; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; @@ -22,7 +24,7 @@ public function register(): void $this->app->singleton( ListenerProviderInterface::class, function () { - return new ListenerProvider(); + return new EventsListenerProvider(); } ); @@ -30,9 +32,9 @@ function () { $this->app->singleton( EventDispatcherInterface::class, function () { - /** @var ListenerProviderInterface $listenerProvider */ + /** @var ListenerProviderInterface $listenerProvider */ $listenerProvider = $this->app->resolve(ListenerProviderInterface::class); - return new EventDispatcher($listenerProvider); + return new EventsEventDispatcher($listenerProvider); } ); diff --git a/src/Providers/ExtensionManager.php b/src/Providers/ExtensionManager.php index 51e1128..89ebd03 100644 --- a/src/Providers/ExtensionManager.php +++ b/src/Providers/ExtensionManager.php @@ -101,7 +101,9 @@ public function registerExtension(string $name, mixed $extension): void } } - $this->register($name, $extension); + if (is_callable($extension)) { + $this->register($name, $extension); + } } /** diff --git a/src/Providers/ListenerProvider.php b/src/Providers/ListenerProvider.php index a244913..349c0e8 100644 --- a/src/Providers/ListenerProvider.php +++ b/src/Providers/ListenerProvider.php @@ -8,6 +8,8 @@ /** * Simple listener provider implementation + * + * @deprecated v2.1.0 Use \PivotPHP\Core\Events\ListenerProvider instead. */ class ListenerProvider implements ListenerProviderInterface { @@ -16,6 +18,14 @@ class ListenerProvider implements ListenerProviderInterface */ private array $listeners = []; + public function __construct() + { + trigger_error( + 'PivotPHP\\Core\\Providers\\ListenerProvider is deprecated. Use PivotPHP\\Core\\Events\\ListenerProvider instead.', + E_USER_DEPRECATED + ); + } + /** * {@inheritdoc} * diff --git a/src/Providers/Logger.php b/src/Providers/Logger.php index 3d7595b..d7846fd 100644 --- a/src/Providers/Logger.php +++ b/src/Providers/Logger.php @@ -9,6 +9,8 @@ /** * Simple PSR-3 compliant logger implementation + * + * @deprecated v2.1.0 Use \PivotPHP\Core\Logging\PsrLogger instead. */ class Logger extends AbstractLogger { @@ -23,6 +25,7 @@ public function __construct( string $logPath = '', string $dateFormat = 'Y-m-d H:i:s' ) { + trigger_error('PivotPHP\\Core\\Providers\\Logger is deprecated. Use PivotPHP\\Core\\Logging\\PsrLogger instead.', E_USER_DEPRECATED); $this->logPath = $logPath ?: ($_ENV['LOG_PATH'] ?? sys_get_temp_dir() . '/express-php.log'); $this->dateFormat = $dateFormat; $this->logLevels = [ diff --git a/src/Providers/LoggingServiceProvider.php b/src/Providers/LoggingServiceProvider.php index cbda8b5..e687973 100644 --- a/src/Providers/LoggingServiceProvider.php +++ b/src/Providers/LoggingServiceProvider.php @@ -5,6 +5,7 @@ namespace PivotPHP\Core\Providers; use PivotPHP\Core\Core\Application; +use PivotPHP\Core\Logging\PsrLogger; use Psr\Log\LoggerInterface; /** @@ -21,7 +22,7 @@ public function register(): void LoggerInterface::class, function () { $logPath = $this->getLogPath(); - return new \PivotPHP\Core\Providers\Logger($logPath); + return new PsrLogger($logPath); } ); diff --git a/src/Support/HookManager.php b/src/Support/HookManager.php index 3db8565..bc29f29 100644 --- a/src/Support/HookManager.php +++ b/src/Support/HookManager.php @@ -188,7 +188,7 @@ function ($a, $b) { */ protected function registerWithEventSystem(string $hook): void { - /** @var \PivotPHP\Core\Providers\ListenerProvider $listenerProvider */ + /** @var \PivotPHP\Core\Events\ListenerProvider $listenerProvider */ $listenerProvider = $this->app->make('listeners'); // Remove listener antigo, se existir diff --git a/src/Support/Str.php b/src/Support/Str.php index 3a0573e..94acb47 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -1,5 +1,7 @@ |string $keys Chaves a serem mantidas * @return array */ - public static function only(array $array, $keys): array + public static function only(array $array, array|string $keys): array { $keys = is_array($keys) ? $keys : func_get_args()[1]; @@ -183,7 +185,7 @@ public static function only(array $array, $keys): array * @param array|string $keys Chaves a serem removidas * @return array */ - public static function except(array $array, $keys): array + public static function except(array $array, array|string $keys): array { $keys = is_array($keys) ? $keys : func_get_args()[1]; diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index 45dfe3c..b088399 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -1,7 +1,11 @@ addError($field, 'required'); return false; } @@ -122,7 +124,7 @@ private function validateRule(string $field, $value, string $rule): bool break; case 'integer': - if (!filter_var($value, FILTER_VALIDATE_INT)) { + if (filter_var($value, FILTER_VALIDATE_INT) === false) { $this->addError($field, 'integer'); return false; } @@ -202,7 +204,7 @@ private function getMessage( $messageKey = "{$field}.{$rule}"; if (isset($this->messages[$messageKey])) { - return $this->replaceParams($this->messages[$messageKey], $params); + return $this->replaceParams((string)$this->messages[$messageKey], $params); } // Mensagens padrão diff --git a/src/aliases-performance-tools.php b/src/aliases-performance-tools.php index a250ab1..c4f8420 100644 --- a/src/aliases-performance-tools.php +++ b/src/aliases-performance-tools.php @@ -1,7 +1,5 @@ dispatch('user.created', ['id' => 1]); +``` + +Agora recebe um `TypeError` porque `dispatch()` espera `object`, nao `string`. + +### 2. `Providers\EventDispatcher` (deprecated) e `Events\EventDispatcher` sao inconsistentes em `fire()` + +`Providers\EventDispatcher` (deprecated) implementa apenas `dispatch(object): object` (PSR-14). +`Events\EventDispatcher` (novo) implementa `dispatch()` PSR-14 **e** `fire()` string-based. +Codigo que migrate de `Providers\EventDispatcher` para `Events\EventDispatcher` descobrira +`fire()` como API nova sem documentacao de migracao clara. + +### 3. `HookManager` usa `dispatch()` do EventDispatcher + +```php +// src/Support/HookManager.php, linhas 87 e 103 +$this->dispatcher->dispatch($event); +``` + +Isso funciona porque `HookManager` injeta um `EventDispatcherInterface` (PSR-14), e o +`dispatch(object)` e o correto aqui. Porem o `HookManager` nao usa `fire()` — entao +os listeners registrados via `listen(string, callable)` no `Events\EventDispatcher` +nunca sao disparados pelos hooks. Essa e uma inconsistencia de design: dois mecanismos +de events paralelos sem conexao. + +## Impacto Tecnico +- `TypeError` para codigo existente que chamava dispatch com string +- Dois sistemas de event paralelos sem interoperabilidade (listen/fire vs PSR-14) +- Migracao implicita sem documentacao + +## Risco +Alto para codigo externo que usava EventDispatcher diretamente + +## Solucao Recomendada + +### Opcao A (retrocompatibilidade maxima) +Adicionar `fire()` como alias de `dispatch()` com verificacao de tipo: +```php +// Nao e possivel sem quebrar a assinatura PSR-14 +``` + +### Opcao B (documentacao clara — recomendada) +1. Adicionar `@deprecated` no metodo `fire()` de `Events\EventDispatcher` com instrucao + de uso de listeners PSR-14 para o futuro +2. Adicionar CHANGELOG com nota explicita de breaking change +3. Documentar que `listen()`/`fire()` e para eventos simples internos e + `addEventListener()`/`dispatchEvent()` e para eventos PSR-14 com objetos + +### Opcao C (unificar) +Remover `listen()`/`fire()` do `Events\EventDispatcher` e usar apenas PSR-14. +Isso e mais limpo mas requer migracao de todos os usos internos de `fire()`. + +## Prioridade +Alta (se ha codigo externo usando EventDispatcher diretamente) +Media (se apenas uso interno) + +## Esforco Estimado +1-2 horas (analise de impacto + documentacao + possivelmente deprecation notices) + +## Arquivos Afetados +- `src/Events/EventDispatcher.php` +- `src/Support/HookManager.php` (verificar integracao) +- `CHANGELOG.md` ou `UPGRADE.md` (documentar breaking change) + +## Criterios de Aceite +- [ ] Breaking change documentado explicitamente +- [ ] Codigo que usava dispatch() string-based recebe mensagem de erro clara (nao TypeError anonimo) +- [ ] HookManager e Events\EventDispatcher tem integracao clara e testada +- [ ] PHPStan Level 9 sem erros diff --git a/tasks/2026-05-29-headerrequest-falta-strict-types.md b/tasks/2026-05-29-headerrequest-falta-strict-types.md new file mode 100644 index 0000000..f16a17a --- /dev/null +++ b/tasks/2026-05-29-headerrequest-falta-strict-types.md @@ -0,0 +1,65 @@ +# HeaderRequest.php: Ausencia de declare(strict_types=1) + +## Titulo +`HeaderRequest` nao possui `declare(strict_types=1)` enquanto `CustomHeaderCollection` possui + +## Contexto +`src/Http/CustomHeaderCollection.php` (novo arquivo) possui `declare(strict_types=1)` na +primeira linha. A classe pai `src/Http/HeaderRequest.php` nao possui essa declaracao. +O projeto exige PHPStan Level 9 e PSR-12, e o padrao do projeto e usar strict_types. + +## Problema Identificado + +```php +// src/Http/HeaderRequest.php - linha 1 +body` apenas quando o resultado +e `instanceof stdClass`. + +## Problema Identificado + +```php +// src/Http/Request.php, linhas 900-905 +$decoded = json_decode($input); +if ($decoded instanceof stdClass) { + $this->body = $decoded; +} else { + $this->body = new stdClass(); // BUG: JSON array tambem cai aqui +} + +if (json_last_error() == JSON_ERROR_NONE) { + return; // early return mesmo com body descartado +} +``` + +Cenario problemático: um cliente envia `Content-Type: application/json` com body `[1,2,3]` +(JSON array valido). `json_decode('[1,2,3]')` retorna um `array`, nao um `stdClass`. +A condicao `instanceof stdClass` e falsa, entao `$this->body` e setado para `new stdClass()` +(vazio). Em seguida, `json_last_error() == JSON_ERROR_NONE` e `true`, dispara o `return` +antecipado — e o `$_POST` nunca e consultado como fallback. + +Resultado: body completamente perdido para JSON arrays validos. + +O metodo `initializePsr7Request()` (linha 207) trata esse cenario corretamente com +`$decoded ?: $_POST`, criando inconsistencia entre a representacao Express.js (`$this->body`) +e a representacao PSR-7 (`parsedBody`). + +## Impacto Tecnico +- `$req->body` ou `$req->input('key')` retornam objeto vazio para payloads JSON array +- Inconsistencia entre `getBodyAsStdClass()` e `getParsedBody()` para o mesmo request +- Handlers de rota que recebem listas como `POST /batch` com `[{...},{...}]` falham silenciosamente + +## Risco +Alto — bug de corretude silencioso em producao + +## Solucao Recomendada +Tratar o resultado de `json_decode` de forma mais abrangente: + +```php +private function parseBody(): void +{ + if ($this->method === 'GET') { + $this->body = new stdClass(); + return; + } + + $input = $this->getCachedInput(); + if ($input !== '') { + $decoded = json_decode($input); + + if (json_last_error() === JSON_ERROR_NONE) { + if ($decoded instanceof stdClass) { + $this->body = $decoded; + } elseif (is_array($decoded)) { + // JSON array: converter para stdClass preservando indices + $this->body = (object) $decoded; + } else { + $this->body = new stdClass(); + } + return; + } + } + + // Fallback para form POST + if (!empty($_POST)) { + $this->body = new stdClass(); + foreach ($_POST as $key => $value) { + $this->body->{$key} = $value; + } + return; + } + + $this->body = new stdClass(); +} +``` + +## Prioridade +Alta + +## Esforco Estimado +30 minutos (implementacao + testes unitarios) + +## Arquivos Afetados +- `src/Http/Request.php` (metodo `parseBody`, linhas 891-920) +- `tests/Http/RequestTest.php` (adicionar casos de teste para JSON array) + +## Criterios de Aceite +- [ ] `$req->input()` funciona para payload JSON array +- [ ] `$req->body` nao e objeto vazio quando o payload e um JSON array valido +- [ ] Comportamento de fallback para `$_POST` nao e afetado +- [ ] Teste cobrindo `POST /endpoint` com `body = [1,2,3]` +- [ ] Teste cobrindo `POST /endpoint` com `body = {"key":"value"}` +- [ ] Teste cobrindo `POST /endpoint` com form urlencoded +- [ ] Teste cobrindo body vazio diff --git a/tasks/2026-05-29-psr7pool-reset-header-iteration-mutacao.md b/tasks/2026-05-29-psr7pool-reset-header-iteration-mutacao.md new file mode 100644 index 0000000..98a85a6 --- /dev/null +++ b/tasks/2026-05-29-psr7pool-reset-header-iteration-mutacao.md @@ -0,0 +1,76 @@ +# Psr7Pool::resetServerRequest Itera e Muta Colecao Simultaneamente + +## Titulo +`resetServerRequest()` itera sobre `getHeaders()` enquanto remove headers via `withoutHeader()` + +## Contexto +`src/Http/Pool/Psr7Pool.php` implementa reuso de objetos PSR-7 via pooling. O metodo +`resetServerRequest()` foi modificado para limpar os headers existentes antes de aplicar +os novos, evitando contaminacao entre requests. + +## Problema Identificado + +```php +// src/Http/Pool/Psr7Pool.php, linhas 252-259 +foreach ($request->getHeaders() as $name => $values) { + $request = $request->withoutHeader($name); +} +``` + +Embora PSR-7 seja imutavel (cada `withoutHeader()` retorna nova instancia), o `foreach` +itera sobre os headers da instancia **original** (`$request` antes da primeira substituicao). +Porem `$request` e reatribuido dentro do loop — entao na segunda iteracao, `$request` aponta +para a nova instancia (sem o primeiro header), mas o `foreach` ainda percorre os headers +da copia original. + +Isso e tecnicamente seguro porque PHP captura o array de `getHeaders()` na primeira chamada +do `foreach`, mas cria uma **armadilha cognitiva grave**: + +1. Parece que `$request->getHeaders()` seria recalculado a cada iteracao (nao e) +2. A variavel `$name` do `foreach` e `$values` podem ser confundidas como atualizadas +3. Se a implementacao de `ServerRequestInterface` for trocada por uma que retorne + um `Generator` ou `Iterator` lazy, o comportamento mudaria + +Alem disso, o mesmo padrao ocorre em `resetResponse()` (linhas 284-288). + +## Impacto Tecnico +- Codigo correto atualmente, mas fragil e enganoso +- Pode quebrar silenciosamente com implementacoes alternativas de PSR-7 +- Dificulta code review (revisor precisa raciocinar sobre avaliacao antecipada do foreach) + +## Risco +Medio (correto hoje, arriscado com mudancas futuras) + +## Solucao Recomendada + +Capturar a lista de headers antes do loop e iterar sobre ela: + +```php +// Antes +foreach ($request->getHeaders() as $name => $values) { + $request = $request->withoutHeader($name); +} + +// Depois +$existingHeaders = array_keys($request->getHeaders()); +foreach ($existingHeaders as $name) { + $request = $request->withoutHeader($name); +} +``` + +O mesmo padrao deve ser aplicado em `resetResponse()`. + +## Prioridade +Media + +## Esforco Estimado +10 minutos + +## Arquivos Afetados +- `src/Http/Pool/Psr7Pool.php` (metodos `resetServerRequest` linhas 252-259 e `resetResponse` linhas 284-288) + +## Criterios de Aceite +- [ ] `resetServerRequest()` captura keys antes do foreach +- [ ] `resetResponse()` captura keys antes do foreach +- [ ] Testes de pool passam sem regressao +- [ ] PHPStan Level 9 sem erros diff --git a/tasks/2026-05-29-psrlogger-nome-hardcoded-express-php-log.md b/tasks/2026-05-29-psrlogger-nome-hardcoded-express-php-log.md new file mode 100644 index 0000000..dae70bb --- /dev/null +++ b/tasks/2026-05-29-psrlogger-nome-hardcoded-express-php-log.md @@ -0,0 +1,61 @@ +# PsrLogger: Nome de Log Hardcoded com Referencia ao Nome Antigo do Projeto + +## Titulo +`PsrLogger` usa nome de arquivo `express-php.log` hardcoded, referenciando nome depreciado + +## Contexto +`src/Logging/PsrLogger.php` e a nova implementacao PSR-3 do framework (v2.0.0+). +No construtor, o path padrao para o log e definido como `sys_get_temp_dir() . '/express-php.log'`. + +## Problema Identificado + +```php +// src/Logging/PsrLogger.php, linha 26 +$this->logPath = $logPath ?: ($_ENV['LOG_PATH'] ?? sys_get_temp_dir() . '/express-php.log'); +``` + +1. **Nome historico errado**: O framework se chama `PivotPHP`, nao `express-php`. O nome + `express-php` provavelmente e um residuo do nome original do projeto. Isso confunde + operadores que procuram logs em producao. + +2. **Sem separador de diretorio**: `sys_get_temp_dir() . '/express-php.log'` concatena + manualmente uma `/`. Em Windows, `sys_get_temp_dir()` retorna caminho com `\` e a + concatenacao com `/` pode gerar path invalido. Usar `DIRECTORY_SEPARATOR` ou + `rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'pivotphp.log'` seria correto. + +3. **Leitura de `$_ENV['LOG_PATH']`**: A variavel de ambiente e lida diretamente de `$_ENV` + sem passar pelo sistema de configuracao do framework (`Config`). Isso cria dois caminhos + paralelos de configuracao. + +## Impacto Tecnico +- Logs do framework aparecem como `express-php.log` — confuso em producao +- Potencial path invalido em ambientes Windows +- Inconsistencia de configuracao: ignora `Config` e le `$_ENV` diretamente + +## Risco +Baixo (funcional, mas confuso) + +## Solucao Recomendada + +```php +// Antes +$this->logPath = $logPath ?: ($_ENV['LOG_PATH'] ?? sys_get_temp_dir() . '/express-php.log'); + +// Depois +$defaultLog = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'pivotphp.log'; +$this->logPath = $logPath ?: ($_ENV['LOG_PATH'] ?? $defaultLog); +``` + +## Prioridade +Baixa + +## Esforco Estimado +5 minutos + +## Arquivos Afetados +- `src/Logging/PsrLogger.php` (linha 26) + +## Criterios de Aceite +- [ ] Nome do arquivo de log padrao e `pivotphp.log` (ou configuravel sem valor hardcoded errado) +- [ ] Separador de diretorio usa `DIRECTORY_SEPARATOR` +- [ ] PHPStan Level 9 passa diff --git a/tasks/2026-05-29-request-missing-use-import-custom-header-collection.md b/tasks/2026-05-29-request-missing-use-import-custom-header-collection.md new file mode 100644 index 0000000..51ae4d8 --- /dev/null +++ b/tasks/2026-05-29-request-missing-use-import-custom-header-collection.md @@ -0,0 +1,66 @@ +# Missing use Import: CustomHeaderCollection in Request.php + +## Titulo +Falta `use` para `CustomHeaderCollection` em `Request.php` + +## Contexto +Em `Request::setHeaders()` (linha 511), a classe `CustomHeaderCollection` é instanciada +diretamente sem o `use` correspondente no bloco de imports do arquivo. + +## Problema Identificado + +`src/Http/Request.php` utiliza `new CustomHeaderCollection($headers)` na linha 511, mas a +declaração `use PivotPHP\Core\Http\CustomHeaderCollection;` está ausente no topo do arquivo. + +Como ambas as classes estão no mesmo namespace `PivotPHP\Core\Http`, o código funciona em +runtime (PHP resolve pelo namespace corrente), mas: + +1. PHPStan Level 9 pode não detectar o símbolo como resolvido dependendo da configuração +2. IDEs não conseguem oferecer autocompletar nem análise estática sem o `use` +3. Viola o principio de legibilidade: qualquer leitor assume que a classe vem de fora se o + namespace for diferente, ou que é implícita se for o mesmo — inconsistente com o padrão + do projeto onde os demais imports do mesmo namespace são declarados explicitamente + (ex: `use PivotPHP\Core\Http\HeaderRequest;` já existe) + +## Impacto Técnico +- Inconsistência de estilo com o restante do arquivo +- Possível falso-negativo em ferramentas de análise estática +- Dificulta rastreabilidade em refatorações futuras (grep por `use` não encontra o uso) + +## Risco +Baixo em runtime, Médio para manutenção + +## Solução Recomendada +Adicionar no bloco `use` de `src/Http/Request.php`: + +```php +use PivotPHP\Core\Http\CustomHeaderCollection; +``` + +## Prioridade +Baixa + +## Esforço Estimado +5 minutos + +## Arquivos Afetados +- `src/Http/Request.php` (linha 511, bloco use) + +## Exemplo de Melhoria +```php +// Antes (imports existentes — incompleto) +use PivotPHP\Core\Http\HeaderRequest; +use PivotPHP\Core\Http\Contracts\AttributeInterface; +// ... outros imports + +// Depois +use PivotPHP\Core\Http\CustomHeaderCollection; +use PivotPHP\Core\Http\HeaderRequest; +use PivotPHP\Core\Http\Contracts\AttributeInterface; +// ... outros imports +``` + +## Criterios de Aceite +- [ ] `use PivotPHP\Core\Http\CustomHeaderCollection;` presente no bloco de imports +- [ ] PHPStan Level 9 passa sem erros +- [ ] PSR-12 check passa sem violacoes diff --git a/tasks/2026-05-29-static-cached-input-isolacao-entre-requests.md b/tasks/2026-05-29-static-cached-input-isolacao-entre-requests.md new file mode 100644 index 0000000..546ef5d --- /dev/null +++ b/tasks/2026-05-29-static-cached-input-isolacao-entre-requests.md @@ -0,0 +1,98 @@ +# Propriedade Estatica cachedInput Quebra Isolamento Entre Requests + +## Titulo +`Request::$cachedInput` estatico impede isolamento em ambientes de multiplas requisicoes + +## Contexto +`getCachedInput()` armazena o resultado de `php://input` em `private static ?string $cachedInput`. +Em um processo PHP tradicional (FPM, Apache mod_php), cada request e um processo separado, +entao a propriedade estatica e segura. Porem o projeto se posiciona como microframework de +alta performance e menciona pooling de objetos PSR-7. + +## Problema Identificado + +```php +// src/Http/Request.php, linhas 92-109 +private static ?string $cachedInput = null; + +private function getCachedInput(): string +{ + if (self::$cachedInput === null) { + $input = @file_get_contents('php://input'); + // ... + self::$cachedInput = $input; + } + return self::$cachedInput; +} +``` + +Problemas identificados: + +1. **Ambiente de testes**: Como os testes rodam no mesmo processo PHP, uma vez que + `$cachedInput` e populado por um teste, ele persiste para todos os testes subsequentes + que criam instancias de `Request`. Isso causa falsos positivos/negativos em testes que + tentam simular bodies diferentes (ex: testes de `parseBody`). + +2. **Runtime ReactPHP/Swoole**: Se o framework for usado em contexto async (Swoole, + RoadRunner, ReactPHP), onde multiplas requisicoes compartilham o mesmo processo, o + cache estatico servira o body da primeira requisicao para todas as subsequentes. + +3. **`Psr7Pool::warmUp()`** cria `ServerRequest` com `getStream('')`, que internamente + chamara `getCachedInput()`, populando o cache com string vazia e potencialmente + descartando o body real da proxima requisicao real. + +## Impacto Tecnico +- Testes que dependem de `parseBody()` podem ser flaky +- Incompatibilidade com servidores async (Swoole, RoadRunner) +- Bug de corretude em qualquer ambiente onde o processo e reutilizado + +## Risco +Medio-Alto (critico para ambientes async, medio para testes) + +## Solucao Recomendada + +Trocar a propriedade estatica por propriedade de instancia: + +```php +// Antes +private static ?string $cachedInput = null; + +private function getCachedInput(): string +{ + if (self::$cachedInput === null) { ... } + return self::$cachedInput; +} + +// Depois +private ?string $cachedInput = null; + +private function getCachedInput(): string +{ + if ($this->cachedInput === null) { ... } + return $this->cachedInput; +} +``` + +Adicionar metodo de reset para testes, se necessario: +```php +public static function resetInputCache(): void +{ + // Nao precisa mais — cada instancia tem seu proprio cache +} +``` + +## Prioridade +Media (Alta se Swoole/RoadRunner for alvo) + +## Esforco Estimado +15 minutos (mudanca + verificacao de testes) + +## Arquivos Afetados +- `src/Http/Request.php` (linhas 92, 99, 103, 105, 108) +- `tests/Http/RequestTest.php` (verificar se testes passam sem interferencia entre si) + +## Criterios de Aceite +- [ ] Propriedade `$cachedInput` e de instancia, nao estatica +- [ ] Cada instancia de `Request` le `php://input` de forma independente +- [ ] Testes de `parseBody` com bodies diferentes nao interferem entre si +- [ ] PHPStan Level 9 continua passando diff --git a/tasks/2026-05-29-validator-required-false-negative-integer-zero.md b/tasks/2026-05-29-validator-required-false-negative-integer-zero.md new file mode 100644 index 0000000..8e70a37 --- /dev/null +++ b/tasks/2026-05-29-validator-required-false-negative-integer-zero.md @@ -0,0 +1,81 @@ +# Validator: Regra required Gera Falso Negativo para Integer Zero + +## Titulo +`Validator::validateRule()` com regra `required` trata `0` (inteiro) como valor ausente + +## Contexto +`src/Validation/Validator.php` foi atualizado com `declare(strict_types=1)` na v2.0.0. +A logica de validacao da regra `required` usa `empty()` com excecao explicitamente para +strings `'0'` e inteiro `0`. + +## Problema Identificado + +```php +// src/Validation/Validator.php, linhas 105-109 +case 'required': + if (empty($value) && $value !== '0' && $value !== 0) { + $this->addError($field, 'required'); + return false; + } + break; +``` + +A correcao para `$value !== 0` aborda o caso basico, mas existem outros valores falsy +que representam dados validos em APIs: + +1. **`0.0` (float zero)**: `empty(0.0)` retorna `true` e `0.0 !== '0'` e `0.0 !== 0` + sao ambos `true`. Entao um campo de preco `price: 0.0` seria rejeitado como ausente. + +2. **`false` (boolean false)**: `empty(false)` retorna `true`. Um campo boolean `active: false` + e valido e presente, mas seria rejeitado. + +3. **`[]` (array vazio)**: Dependendo do contexto, um array vazio pode ser um valor + valido intencional (ex: lista de permissoes vazia). + +A abordagem com `empty()` e excecoes pontuais e fragil — a cada tipo descoberto, uma +nova excecao e necessaria. + +## Impacto Tecnico +- `price: 0.0` em um formulario de produto falha validacao `required` incorretamente +- `active: false` em um payload de usuario falha validacao `required` incorretamente +- Comportamento surpreendente para consumidores da API + +## Risco +Medio (depende dos tipos de dados usados pela aplicacao) + +## Solucao Recomendada + +Substituir a abordagem por verificacao explicita de `null` e string vazia: + +```php +// Antes +if (empty($value) && $value !== '0' && $value !== 0) { + $this->addError($field, 'required'); + return false; +} + +// Depois — verifica apenas ausencia real de valor +if ($value === null || $value === '') { + $this->addError($field, 'required'); + return false; +} +``` + +Essa abordagem e semanticamente mais precisa: `required` significa "o campo nao pode +ser null ou string vazia", nao "o campo nao pode ser falsy". + +## Prioridade +Media + +## Esforco Estimado +20 minutos (implementacao + testes para todos os tipos falsy) + +## Arquivos Afetados +- `src/Validation/Validator.php` (linhas 105-110, case 'required') +- Adicionar testes em `tests/` para `Validator` cobrindo: `0`, `0.0`, `false`, `[]`, `''`, `null` + +## Criterios de Aceite +- [ ] `required` aceita `0` (int), `0.0` (float), `false` (bool) como valores validos +- [ ] `required` rejeita `null` e `''` (string vazia) +- [ ] Testes unitarios para todos os tipos falsy +- [ ] Sem regressao nos testes existentes de validacao diff --git a/tests/Core/ContainerTest.php b/tests/Core/ContainerTest.php index b5d64cf..2d69b6a 100644 --- a/tests/Core/ContainerTest.php +++ b/tests/Core/ContainerTest.php @@ -35,6 +35,11 @@ class ContainerTest extends TestCase protected function setUp(): void { + // Suppress E_USER_DEPRECATED from the deprecated Core\Container class under test + set_error_handler(static function (int $errno): bool { + return $errno === E_USER_DEPRECATED; + }, E_ALL); + // Reset singleton instance for isolated testing $this->resetContainerSingleton(); $this->container = Container::getInstance(); @@ -42,6 +47,7 @@ protected function setUp(): void protected function tearDown(): void { + restore_error_handler(); // Clean up singleton after each test $this->resetContainerSingleton(); } diff --git a/tests/Core/ContainerTestSimple.php b/tests/Core/ContainerTestSimple.php deleted file mode 100644 index bd29bcf..0000000 --- a/tests/Core/ContainerTestSimple.php +++ /dev/null @@ -1,70 +0,0 @@ -assertInstanceOf(Container::class, $container); - - // Test container registers itself - $resolved = $container->make(Container::class); - $this->assertSame($container, $resolved); - } - - public function testBasicBinding(): void - { - $container = Container::getInstance(); - - // Reset the container state by flushing it - $container->flush(); - - // Test basic class binding - $container->bind('test', stdClass::class); - - // Check if it's bound - $this->assertTrue($container->bound('test')); - - // Basic binding test without debug output - - // Try to resolve it - $instance = $container->make('test'); - $this->assertInstanceOf(stdClass::class, $instance); - } - - public function testDebugContainerState(): void - { - $container = Container::getInstance(); - $container->flush(); - - $container->bind('debug_test', stdClass::class); - - // Access the private bindings property to see the actual structure - $reflection = new \ReflectionClass($container); - $bindingsProperty = $reflection->getProperty('bindings'); - $bindingsProperty->setAccessible(true); - $bindings = $bindingsProperty->getValue($container); - - // Verify binding structure is correct - $this->assertArrayHasKey('debug_test', $bindings); - $this->assertArrayHasKey('concrete', $bindings['debug_test']); - $this->assertArrayHasKey('singleton', $bindings['debug_test']); - $this->assertArrayHasKey('instance', $bindings['debug_test']); - - $this->assertTrue(true); // Just complete the test - } -} diff --git a/tests/Core/RateLimitMiddlewareTestPsr15.php b/tests/Core/RateLimitMiddlewareTestPsr15.php index c116e29..6bbcf7a 100644 --- a/tests/Core/RateLimitMiddlewareTestPsr15.php +++ b/tests/Core/RateLimitMiddlewareTestPsr15.php @@ -3,7 +3,7 @@ namespace PivotPHP\Core\Tests\Core; use PHPUnit\Framework\TestCase; -use PivotPHP\Core\Http\Psr15\Middleware\RateLimitMiddleware; +use PivotPHP\Core\Middleware\Performance\RateLimitMiddleware; use PivotPHP\Core\Http\Psr7\ServerRequest; use PivotPHP\Core\Http\Psr7\Response; use Psr\Http\Server\RequestHandlerInterface; diff --git a/tests/Http/HeaderRequestTest.php b/tests/Http/HeaderRequestTest.php index dc1fd49..99197be 100644 --- a/tests/Http/HeaderRequestTest.php +++ b/tests/Http/HeaderRequestTest.php @@ -7,6 +7,24 @@ class HeaderRequestTest extends TestCase { + protected function setUp(): void + { + $_SERVER['HTTP_CONTENT_TYPE'] = 'application/json'; + $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer token123'; + $_SERVER['HTTP_X_API_KEY'] = 'api-key-value'; + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0'; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9'; + } + + protected function tearDown(): void + { + unset($_SERVER['HTTP_CONTENT_TYPE']); + unset($_SERVER['HTTP_AUTHORIZATION']); + unset($_SERVER['HTTP_X_API_KEY']); + unset($_SERVER['HTTP_USER_AGENT']); + unset($_SERVER['HTTP_ACCEPT_LANGUAGE']); + } + public function testBasicHeaderRequest(): void { $headerRequest = new HeaderRequest(); @@ -51,4 +69,223 @@ public function testHeaderRequestCaseInsensitive(): void // Test basic functionality without expecting specific headers $this->assertIsArray($headerRequest->getAllHeaders()); } + + public function testHeaderInitialization(): void + { + $headerRequest = new HeaderRequest(); + $this->assertInstanceOf(HeaderRequest::class, $headerRequest); + } + + public function testHeaderConversionToCamelCase(): void + { + $this->setMockHeaders( + [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer token123', + 'X-API-Key' => 'api-key-value', + 'User-Agent' => 'Mozilla/5.0' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('application/json', $headerRequest->contentType); + $this->assertEquals('Bearer token123', $headerRequest->authorization); + $this->assertEquals('api-key-value', $headerRequest->xApiKey); + $this->assertEquals('Mozilla/5.0', $headerRequest->userAgent); + } + + public function testGetHeaderMethod(): void + { + $this->setMockHeaders( + [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer token123' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('application/json', $headerRequest->getHeader('contentType')); + $this->assertEquals('Bearer token123', $headerRequest->getHeader('authorization')); + $this->assertNull($headerRequest->getHeader('nonExistent')); + } + + public function testGetAllHeaders(): void + { + $mockHeaders = [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer token123' + ]; + + $this->setMockHeaders($mockHeaders); + $headerRequest = new HeaderRequest(); + + $allHeaders = $headerRequest->getAllHeaders(); + $this->assertIsArray($allHeaders); + $this->assertArrayHasKey('contentType', $allHeaders); + $this->assertArrayHasKey('authorization', $allHeaders); + } + + public function testHasHeaderMethod(): void + { + $this->setMockHeaders( + [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer token123' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertTrue($headerRequest->hasHeader('contentType')); + $this->assertTrue($headerRequest->hasHeader('authorization')); + $this->assertFalse($headerRequest->hasHeader('nonExistent')); + $this->assertFalse($headerRequest->hasHeader('')); + } + + public function testMagicGetWithNonExistentHeader(): void + { + $this->setMockHeaders( + [ + 'Content-Type' => 'application/json' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertNull($headerRequest->nonExistent); + $this->assertNull($headerRequest->someRandomHeader); + } + + public function testEmptyHeaders(): void + { + $this->setMockHeaders([]); + + $headerRequest = new HeaderRequest(); + + $allHeaders = $headerRequest->getAllHeaders(); + $this->assertTrue(is_array($allHeaders) || is_null($allHeaders)); + if (is_array($allHeaders)) { + $this->assertEmpty($allHeaders); + } + $this->assertFalse($headerRequest->hasHeader('anything')); + $this->assertNull($headerRequest->getHeader('anything')); + } + + public function testHeadersWithColonPrefix(): void + { + $this->setMockHeaders( + [ + ':Content-Type' => 'application/json', + ':Authorization' => 'Bearer token123' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('application/json', $headerRequest->contentType); + $this->assertEquals('Bearer token123', $headerRequest->authorization); + } + + public function testComplexHeaderNames(): void + { + $this->setMockHeaders( + [ + 'X-Forwarded-For' => '192.168.1.1', + 'X-Real-IP' => '10.0.0.1', + 'Accept-Encoding' => 'gzip, deflate', + 'Cache-Control' => 'no-cache' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('192.168.1.1', $headerRequest->xForwardedFor); + $this->assertEquals('10.0.0.1', $headerRequest->xRealIp); + $this->assertEquals('gzip, deflate', $headerRequest->acceptEncoding); + $this->assertEquals('no-cache', $headerRequest->cacheControl); + } + + public function testHeadersWithSpecialCharacters(): void + { + $this->setMockHeaders( + [ + 'Custom-Header' => 'value with spaces and symbols !@#$%', + 'X-Test' => 'áéíóú çñü' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('value with spaces and symbols !@#$%', $headerRequest->customHeader); + $this->assertEquals('áéíóú çñü', $headerRequest->xTest); + } + + public function testCaseInsensitiveAccess(): void + { + $this->setMockHeaders( + [ + 'Content-Type' => 'application/json' + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('application/json', $headerRequest->contentType); + $this->assertEquals('application/json', $headerRequest->getHeader('contentType')); + $this->assertTrue($headerRequest->hasHeader('contentType')); + } + + public function testMultipleHeaderInstances(): void + { + $this->setMockHeaders( + [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer token123' + ] + ); + + $headerRequest1 = new HeaderRequest(); + $headerRequest2 = new HeaderRequest(); + + $this->assertEquals($headerRequest1->getAllHeaders(), $headerRequest2->getAllHeaders()); + $this->assertEquals($headerRequest1->contentType, $headerRequest2->contentType); + } + + public function testHeaderValueTypes(): void + { + $this->setMockHeaders( + [ + 'X-Numeric' => '123', + 'X-Boolean' => 'true', + 'X-Empty' => '', + 'X-Null' => null + ] + ); + + $headerRequest = new HeaderRequest(); + + $this->assertEquals('123', $headerRequest->xNumeric); + $this->assertEquals('true', $headerRequest->xBoolean); + $this->assertEquals('', $headerRequest->xEmpty); + $this->assertNull($headerRequest->xNull); + } + + /** + * Helper method to set headers via $_SERVER + */ + private function setMockHeaders(array $headers): void + { + foreach ($_SERVER as $key => $value) { + if (strpos($key, 'HTTP_') === 0) { + unset($_SERVER[$key]); + } + } + + foreach ($headers as $name => $value) { + $serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); + $_SERVER[$serverKey] = $value; + } + } } diff --git a/tests/Http/RequestTest.php b/tests/Http/RequestTest.php index 582c55c..2e40a1f 100644 --- a/tests/Http/RequestTest.php +++ b/tests/Http/RequestTest.php @@ -4,10 +4,19 @@ use PHPUnit\Framework\TestCase; use PivotPHP\Core\Http\Request; -use PivotPHP\Core\Http\Response; +use PivotPHP\Core\Http\HeaderRequest; +use InvalidArgumentException; class RequestTest extends TestCase { + protected function setUp(): void + { + $_GET = []; + $_POST = []; + $_FILES = []; + $_SERVER = []; + } + public function testBasicRequestCreation(): void { $request = new Request('GET', '/test', '/test'); @@ -71,4 +80,149 @@ public function testRequestHasBody(): void $this->assertIsObject($request->body); } + + public function testRequestInitialization(): void + { + $request = new Request('GET', '/users/:id', '/users/123'); + + $this->assertEquals('GET', $request->method); + $this->assertEquals('/users/:id', $request->path); + $this->assertEquals('/users/123', $request->pathCallable); + $this->assertIsObject($request->params); + $this->assertIsObject($request->query); + $this->assertTrue(is_array($request->body) || is_object($request->body)); + $this->assertInstanceOf(HeaderRequest::class, $request->headers); + } + + public function testMethodNormalization(): void + { + $request = new Request('post', '/users', '/users'); + $this->assertEquals('POST', $request->method); + + $request = new Request('PUT', '/users/:id', '/users/123'); + $this->assertEquals('PUT', $request->method); + } + + public function testPathCallableSlashNormalization(): void + { + $request = new Request('GET', '/users', '/users'); + $this->assertEquals('/users', $request->pathCallable); + + $request = new Request('GET', '/users/', '/users/'); + $this->assertEquals('/users/', $request->pathCallable); + } + + public function testParameterExtraction(): void + { + $_SERVER['QUERY_STRING'] = 'page=1&limit=10'; + + $request = new Request('GET', '/users/:id', '/users/123'); + + $this->assertEquals('1', $request->query->page ?? null); + $this->assertEquals('10', $request->query->limit ?? null); + + unset($_SERVER['QUERY_STRING']); + } + + public function testBodyParsing(): void + { + $_POST = ['name' => 'John', 'email' => 'john@example.com']; + + $request = new Request('POST', '/users', '/users'); + + $this->assertEquals('John', $request->body->name ?? null); + $this->assertEquals('john@example.com', $request->body->email ?? null); + + $_POST = []; + } + + public function testFilesHandling(): void + { + $_FILES = [ + 'avatar' => [ + 'name' => 'avatar.jpg', + 'type' => 'image/jpeg', + 'tmp_name' => '/tmp/php123', + 'error' => 0, + 'size' => 12345 + ] + ]; + + $request = new Request('POST', '/upload', '/upload'); + + $this->assertArrayHasKey('avatar', $request->files); + $this->assertEquals('avatar.jpg', $request->files['avatar']['name']); + } + + public function testInvalidPropertyAccess(): void + { + $request = new Request('GET', '/test', '/test'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Property invalid does not exist in Request class'); + + $request->invalid; + } + + public function testRouteParameterParsing(): void + { + $request = new Request('GET', '/users/:id/posts/:postId', '/users/123/posts/456'); + + $this->assertInstanceOf('stdClass', $request->params); + } + + public function testEmptyQueryParameters(): void + { + $_GET = []; + + $request = new Request('GET', '/users', '/users'); + + $this->assertInstanceOf('stdClass', $request->query); + $this->assertEmpty((array)$request->query); + } + + public function testEmptyBodyParameters(): void + { + $_POST = []; + + $request = new Request('POST', '/users', '/users'); + + $this->assertTrue( + is_null($request->body) || + (is_object($request->body) && empty((array)$request->body)) || + (is_array($request->body) && empty($request->body)) + ); + } + + public function testComplexRoutePattern(): void + { + $request = new Request( + 'GET', + '/api/v1/users/:userId/posts/:postId/comments', + '/api/v1/users/123/posts/456/comments' + ); + + $this->assertEquals('GET', $request->method); + $this->assertEquals('/api/v1/users/:userId/posts/:postId/comments', $request->path); + $this->assertEquals('/api/v1/users/123/posts/456/comments', $request->pathCallable); + } + + public function testSpecialCharactersInPath(): void + { + $request = new Request('GET', '/search', '/search'); + + $this->assertEquals('/search', $request->pathCallable); + } + + public function testRequestWithArrayParameters(): void + { + $_SERVER['QUERY_STRING'] = 'tags[]=php&tags[]=javascript&filters[active]=true&filters[category]=tech'; + + $request = new Request('GET', '/posts', '/posts'); + + $this->assertIsArray($request->query->tags ?? []); + $this->assertEquals(['php', 'javascript'], $request->query->tags ?? []); + + unset($_SERVER['QUERY_STRING']); + } } diff --git a/tests/Http/ResponseTest.php b/tests/Http/ResponseTest.php index cb5be1e..7b36a01 100644 --- a/tests/Http/ResponseTest.php +++ b/tests/Http/ResponseTest.php @@ -7,6 +7,14 @@ class ResponseTest extends TestCase { + private Response $response; + + protected function setUp(): void + { + $this->response = new Response(); + $this->response->setTestMode(true); + } + public function testBasicResponseCreation(): void { $response = new Response(); @@ -84,4 +92,228 @@ public function testResponseRedirectWithCustomStatus(): void $this->assertEquals(301, $response->getStatusCode()); $this->assertEquals('/new-path', $response->getHeaderLine('Location')); } + + public function testResponseInitialization(): void + { + $this->assertInstanceOf(Response::class, $this->response); + } + + public function testStatusMethod(): void + { + $result = $this->response->status(404); + $this->assertInstanceOf(Response::class, $result); + $this->assertSame($this->response, $result); + $this->assertEquals(404, $this->response->getStatusCode()); + } + + public function testHeaderMethod(): void + { + $result = $this->response->header('Content-Type', 'application/json'); + $this->assertInstanceOf(Response::class, $result); + $this->assertSame($this->response, $result); + + $headers = $this->response->getHeaders(); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertEquals('application/json', $headers['Content-Type']); + } + + public function testJsonResponse(): void + { + $data = ['message' => 'Hello World', 'status' => 'success']; + + $result = $this->response->json($data); + + $this->assertInstanceOf(Response::class, $result); + $this->assertEquals(json_encode($data), $this->response->getBody()); + + $headers = $this->response->getHeaders(); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertStringContainsString('application/json', $headers['Content-Type']); + } + + public function testTextResponse(): void + { + $text = 'Hello World'; + + $result = $this->response->text($text); + + $this->assertInstanceOf(Response::class, $result); + $this->assertEquals($text, $this->response->getBody()); + + $headers = $this->response->getHeaders(); + $this->assertEquals('text/plain; charset=utf-8', $headers['Content-Type']); + } + + public function testHtmlResponse(): void + { + $html = '

Hello World

This is HTML content

'; + + $result = $this->response->html($html); + + $this->assertInstanceOf(Response::class, $result); + $this->assertEquals($html, $this->response->getBody()); + + $headers = $this->response->getHeaders(); + $this->assertEquals('text/html; charset=utf-8', $headers['Content-Type']); + } + + public function testMethodChaining(): void + { + $data = ['message' => 'Created successfully']; + + $result = $this->response + ->status(201) + ->header('X-Custom-Header', 'custom-value') + ->json($data); + + $this->assertInstanceOf(Response::class, $result); + $this->assertEquals(json_encode($data), $this->response->getBody()); + $this->assertEquals(201, $this->response->getStatusCode()); + + $headers = $this->response->getHeaders(); + $this->assertEquals('custom-value', $headers['X-Custom-Header']); + } + + public function testComplexJsonResponse(): void + { + $complexData = [ + 'users' => [ + ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'], + ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'] + ], + 'pagination' => [ + 'page' => 1, + 'limit' => 10, + 'total' => 2 + ], + 'meta' => [ + 'timestamp' => '2023-01-01T00:00:00Z', + 'version' => '1.0.0' + ] + ]; + + $this->response->json($complexData); + + $this->assertEquals(json_encode($complexData), $this->response->getBody()); + } + + public function testEmptyJsonResponse(): void + { + $this->response->json([]); + + $this->assertEquals('[]', $this->response->getBody()); + } + + public function testNullJsonResponse(): void + { + $this->response->json(null); + + $this->assertEquals('null', $this->response->getBody()); + } + + public function testBooleanJsonResponse(): void + { + $response1 = new Response(); + $response1->setTestMode(true); + $response1->json(true); + $this->assertEquals('true', $response1->getBody()); + + $response2 = new Response(); + $response2->setTestMode(true); + $response2->json(false); + $this->assertEquals('false', $response2->getBody()); + } + + public function testNumericJsonResponse(): void + { + $response1 = new Response(); + $response1->setTestMode(true); + $response1->json(42); + $this->assertEquals('42', $response1->getBody()); + + $response2 = new Response(); + $response2->setTestMode(true); + $response2->json(3.14); + $this->assertEquals('3.14', $response2->getBody()); + } + + public function testStringJsonResponse(): void + { + $this->response->json('Hello World'); + + $this->assertEquals('"Hello World"', $this->response->getBody()); + } + + public function testEmptyTextResponse(): void + { + $this->response->text(''); + + $this->assertEquals('', $this->response->getBody()); + } + + public function testMultilineTextResponse(): void + { + $text = "Line 1\nLine 2\nLine 3"; + + $this->response->text($text); + + $this->assertEquals($text, $this->response->getBody()); + } + + public function testHtmlWithSpecialCharacters(): void + { + $html = '
Special chars: & < > " '
'; + + $this->response->html($html); + + $this->assertEquals($html, $this->response->getBody()); + } + + public function testMultipleHeaders(): void + { + $result = $this->response + ->header('Content-Type', 'application/json') + ->header('X-Custom-Header', 'custom-value') + ->header('Cache-Control', 'no-cache'); + + $this->assertInstanceOf(Response::class, $result); + + $headers = $this->response->getHeaders(); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('X-Custom-Header', $headers); + $this->assertArrayHasKey('Cache-Control', $headers); + $this->assertEquals('application/json', $headers['Content-Type']); + $this->assertEquals('custom-value', $headers['X-Custom-Header']); + $this->assertEquals('no-cache', $headers['Cache-Control']); + } + + public function testStatusCodes(): void + { + $response1 = new Response(); + $response1->status(200); + $this->assertEquals(200, $response1->getStatusCode()); + + $response2 = new Response(); + $response2->status(404); + $this->assertEquals(404, $response2->getStatusCode()); + + $response3 = new Response(); + $response3->status(500); + $this->assertEquals(500, $response3->getStatusCode()); + + $response4 = new Response(); + $response4->status(201); + $this->assertEquals(201, $response4->getStatusCode()); + } + + public function testTestModeToggle(): void + { + $this->assertTrue($this->response->isTestMode()); + + $this->response->setTestMode(false); + $this->assertFalse($this->response->isTestMode()); + + $this->response->setTestMode(true); + $this->assertTrue($this->response->isTestMode()); + } } diff --git a/tests/Middleware/Core/BaseMiddlewareTest.php b/tests/Middleware/Core/BaseMiddlewareTest.php index 15bc9c4..bb69fe9 100644 --- a/tests/Middleware/Core/BaseMiddlewareTest.php +++ b/tests/Middleware/Core/BaseMiddlewareTest.php @@ -27,7 +27,7 @@ protected function setUp(): void parent::setUp(); $this->middleware = new TestableBaseMiddleware(); $this->request = new Request('GET', '/test', '/test'); - $this->response = new Response(); + $this->response = (new Response())->setTestMode(true); } /** diff --git a/tests/Middleware/Performance/RateLimitMiddlewareTest.php b/tests/Middleware/Performance/RateLimitMiddlewareTest.php index 6192796..df6aa6d 100644 --- a/tests/Middleware/Performance/RateLimitMiddlewareTest.php +++ b/tests/Middleware/Performance/RateLimitMiddlewareTest.php @@ -12,6 +12,19 @@ class RateLimitMiddlewareTest extends TestCase { + protected function setUp(): void + { + // Suppress E_USER_DEPRECATED from the deprecated RateLimitMiddleware class under test + set_error_handler(static function (int $errno): bool { + return $errno === E_USER_DEPRECATED; + }, E_ALL); + } + + protected function tearDown(): void + { + restore_error_handler(); + } + public function testRateLimitMiddlewareBasicFunctionality(): void { $middleware = new RateLimitMiddleware(); diff --git a/tests/Middleware/RateLimiterTest.php b/tests/Middleware/RateLimiterTest.php index 717c0dd..067f252 100644 --- a/tests/Middleware/RateLimiterTest.php +++ b/tests/Middleware/RateLimiterTest.php @@ -25,7 +25,7 @@ protected function setUp(): void { parent::setUp(); $this->request = new Request('GET', '/test', '/test'); - $this->response = new Response(); + $this->response = (new Response())->setTestMode(true); } /** diff --git a/tests/Middleware/SimpleLoadShedderTest.php b/tests/Middleware/SimpleLoadShedderTest.php index 97c625e..310788f 100644 --- a/tests/Middleware/SimpleLoadShedderTest.php +++ b/tests/Middleware/SimpleLoadShedderTest.php @@ -16,9 +16,19 @@ class SimpleLoadShedderTest extends TestCase protected function setUp(): void { + // Suppress E_USER_DEPRECATED from the deprecated LoadShedder class under test + set_error_handler(static function (int $errno): bool { + return $errno === E_USER_DEPRECATED; + }, E_ALL); + $this->loadShedder = new LoadShedder(5, 60); // 5 requests per 60 seconds } + protected function tearDown(): void + { + restore_error_handler(); + } + public function testAllowsRequestsUnderLimit(): void { $request = new Request('GET', '/test', '/test'); diff --git a/tests/Services/HeaderRequestTest.php b/tests/Services/HeaderRequestTest.php deleted file mode 100644 index 73d0027..0000000 --- a/tests/Services/HeaderRequestTest.php +++ /dev/null @@ -1,255 +0,0 @@ -assertInstanceOf(HeaderRequest::class, $headerRequest); - } - - public function testHeaderConversionToCamelCase(): void - { - // Mock headers for testing - $this->setMockHeaders( - [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer token123', - 'X-API-Key' => 'api-key-value', - 'User-Agent' => 'Mozilla/5.0' - ] - ); - - $headerRequest = new HeaderRequest(); - - // Test access via magic method - $this->assertEquals('application/json', $headerRequest->contentType); - $this->assertEquals('Bearer token123', $headerRequest->authorization); - $this->assertEquals('api-key-value', $headerRequest->xApiKey); - $this->assertEquals('Mozilla/5.0', $headerRequest->userAgent); - } - - public function testGetHeaderMethod(): void - { - $this->setMockHeaders( - [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer token123' - ] - ); - - $headerRequest = new HeaderRequest(); - - $this->assertEquals('application/json', $headerRequest->getHeader('contentType')); - $this->assertEquals('Bearer token123', $headerRequest->getHeader('authorization')); - $this->assertNull($headerRequest->getHeader('nonExistent')); - } - - public function testGetAllHeaders(): void - { - $mockHeaders = [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer token123' - ]; - - $this->setMockHeaders($mockHeaders); - $headerRequest = new HeaderRequest(); - - $allHeaders = $headerRequest->getAllHeaders(); - $this->assertIsArray($allHeaders); - $this->assertArrayHasKey('contentType', $allHeaders); - $this->assertArrayHasKey('authorization', $allHeaders); - } - - public function testHasHeaderMethod(): void - { - $this->setMockHeaders( - [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer token123' - ] - ); - - $headerRequest = new HeaderRequest(); - - $this->assertTrue($headerRequest->hasHeader('contentType')); - $this->assertTrue($headerRequest->hasHeader('authorization')); - $this->assertFalse($headerRequest->hasHeader('nonExistent')); - $this->assertFalse($headerRequest->hasHeader('')); - } - - public function testMagicGetWithNonExistentHeader(): void - { - $this->setMockHeaders( - [ - 'Content-Type' => 'application/json' - ] - ); - - $headerRequest = new HeaderRequest(); - - $this->assertNull($headerRequest->nonExistent); - $this->assertNull($headerRequest->someRandomHeader); - } - - public function testEmptyHeaders(): void - { - $this->setMockHeaders([]); - - $headerRequest = new HeaderRequest(); - - $allHeaders = $headerRequest->getAllHeaders(); - $this->assertTrue(is_array($allHeaders) || is_null($allHeaders)); - if (is_array($allHeaders)) { - $this->assertEmpty($allHeaders); - } - $this->assertFalse($headerRequest->hasHeader('anything')); - $this->assertNull($headerRequest->getHeader('anything')); - } - - public function testHeadersWithColonPrefix(): void - { - $this->setMockHeaders( - [ - ':Content-Type' => 'application/json', - ':Authorization' => 'Bearer token123' - ] - ); - - $headerRequest = new HeaderRequest(); - - // The constructor should trim the leading colon - $this->assertEquals('application/json', $headerRequest->contentType); - $this->assertEquals('Bearer token123', $headerRequest->authorization); - } - - public function testComplexHeaderNames(): void - { - $this->setMockHeaders( - [ - 'X-Forwarded-For' => '192.168.1.1', - 'X-Real-IP' => '10.0.0.1', - 'Accept-Encoding' => 'gzip, deflate', - 'Cache-Control' => 'no-cache' - ] - ); - - $headerRequest = new HeaderRequest(); - - $this->assertEquals('192.168.1.1', $headerRequest->xForwardedFor); - $this->assertEquals('10.0.0.1', $headerRequest->xRealIp); - $this->assertEquals('gzip, deflate', $headerRequest->acceptEncoding); - $this->assertEquals('no-cache', $headerRequest->cacheControl); - } - - public function testHeadersWithSpecialCharacters(): void - { - $this->setMockHeaders( - [ - 'Custom-Header' => 'value with spaces and symbols !@#$%', - 'X-Test' => 'áéíóú çñü' - ] - ); - - $headerRequest = new HeaderRequest(); - - $this->assertEquals('value with spaces and symbols !@#$%', $headerRequest->customHeader); - $this->assertEquals('áéíóú çñü', $headerRequest->xTest); - } - - public function testCaseInsensitiveAccess(): void - { - $this->setMockHeaders( - [ - 'Content-Type' => 'application/json' - ] - ); - - $headerRequest = new HeaderRequest(); - - // Should work with exact camelCase - $this->assertEquals('application/json', $headerRequest->contentType); - $this->assertEquals('application/json', $headerRequest->getHeader('contentType')); - $this->assertTrue($headerRequest->hasHeader('contentType')); - } - - /** - * Helper method to set headers via $_SERVER - */ - private function setMockHeaders(array $headers): void - { - // Limpar headers existentes - foreach ($_SERVER as $key => $value) { - if (strpos($key, 'HTTP_') === 0) { - unset($_SERVER[$key]); - } - } - - // Converter headers para formato $_SERVER - foreach ($headers as $name => $value) { - $serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); - $_SERVER[$serverKey] = $value; - } - } - - public function testMultipleHeaderInstances(): void - { - $this->setMockHeaders( - [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer token123' - ] - ); - - $headerRequest1 = new HeaderRequest(); - $headerRequest2 = new HeaderRequest(); - - // Both instances should have the same headers - $this->assertEquals($headerRequest1->getAllHeaders(), $headerRequest2->getAllHeaders()); - $this->assertEquals($headerRequest1->contentType, $headerRequest2->contentType); - } - - public function testHeaderValueTypes(): void - { - $this->setMockHeaders( - [ - 'X-Numeric' => '123', - 'X-Boolean' => 'true', - 'X-Empty' => '', - 'X-Null' => null - ] - ); - - $headerRequest = new HeaderRequest(); - - $this->assertEquals('123', $headerRequest->xNumeric); - $this->assertEquals('true', $headerRequest->xBoolean); - $this->assertEquals('', $headerRequest->xEmpty); - $this->assertNull($headerRequest->xNull); - } -} diff --git a/tests/Services/RequestTest.php b/tests/Services/RequestTest.php deleted file mode 100644 index 4f9e205..0000000 --- a/tests/Services/RequestTest.php +++ /dev/null @@ -1,173 +0,0 @@ -assertEquals('GET', $request->method); - $this->assertEquals('/users/:id', $request->path); - $this->assertEquals('/users/123', $request->pathCallable); - $this->assertIsObject($request->params); - $this->assertIsObject($request->query); - // Para GET, o body é um array vazio conforme o código - $this->assertTrue(is_array($request->body) || is_object($request->body)); - $this->assertInstanceOf(HeaderRequest::class, $request->headers); - } - - public function testMethodNormalization(): void - { - $request = new Request('post', '/users', '/users'); - $this->assertEquals('POST', $request->method); - - $request = new Request('PUT', '/users/:id', '/users/123'); - $this->assertEquals('PUT', $request->method); - } - - public function testPathCallableSlashNormalization(): void - { - // pathCallable should be preserved as-is for proper route matching - $request = new Request('GET', '/users', '/users'); - $this->assertEquals('/users', $request->pathCallable); - - $request = new Request('GET', '/users/', '/users/'); - $this->assertEquals('/users/', $request->pathCallable); - } - - public function testParameterExtraction(): void - { - $_SERVER['QUERY_STRING'] = 'page=1&limit=10'; - - $request = new Request('GET', '/users/:id', '/users/123'); - - $this->assertEquals('1', $request->query->page ?? null); - $this->assertEquals('10', $request->query->limit ?? null); - - // Limpar $_SERVER após o teste - unset($_SERVER['QUERY_STRING']); - } - - public function testBodyParsing(): void - { - $_POST = ['name' => 'John', 'email' => 'john@example.com']; - - $request = new Request('POST', '/users', '/users'); - - $this->assertEquals('John', $request->body->name ?? null); - $this->assertEquals('john@example.com', $request->body->email ?? null); - - // Limpar $_POST após o teste - $_POST = []; - } - - public function testFilesHandling(): void - { - $_FILES = [ - 'avatar' => [ - 'name' => 'avatar.jpg', - 'type' => 'image/jpeg', - 'tmp_name' => '/tmp/php123', - 'error' => 0, - 'size' => 12345 - ] - ]; - - $request = new Request('POST', '/upload', '/upload'); - - $this->assertArrayHasKey('avatar', $request->files); - $this->assertEquals('avatar.jpg', $request->files['avatar']['name']); - } - - public function testInvalidPropertyAccess(): void - { - $request = new Request('GET', '/test', '/test'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Property invalid does not exist in Request class'); - - $request->invalid; - } - - public function testRouteParameterParsing(): void - { - $request = new Request('GET', '/users/:id/posts/:postId', '/users/123/posts/456'); - - // O método parseRoute deve extrair os parâmetros - $this->assertInstanceOf('stdClass', $request->params); - } - - public function testEmptyQueryParameters(): void - { - $_GET = []; - - $request = new Request('GET', '/users', '/users'); - - $this->assertInstanceOf('stdClass', $request->query); - $this->assertEmpty((array)$request->query); - } - - public function testEmptyBodyParameters(): void - { - $_POST = []; - - $request = new Request('POST', '/users', '/users'); - - // Para POST sem dados, o corpo fica como null (json_decode de string vazia) - // ou pode ser um objeto vazio se $_POST foi processado - $this->assertTrue( - is_null($request->body) || - (is_object($request->body) && empty((array)$request->body)) || - (is_array($request->body) && empty($request->body)) - ); - } - - public function testComplexRoutePattern(): void - { - $request = new Request( - 'GET', - '/api/v1/users/:userId/posts/:postId/comments', - '/api/v1/users/123/posts/456/comments' - ); - - $this->assertEquals('GET', $request->method); - $this->assertEquals('/api/v1/users/:userId/posts/:postId/comments', $request->path); - $this->assertEquals('/api/v1/users/123/posts/456/comments', $request->pathCallable); - } - - public function testSpecialCharactersInPath(): void - { - $request = new Request('GET', '/search', '/search'); - - $this->assertEquals('/search', $request->pathCallable); - } - - public function testRequestWithArrayParameters(): void - { - $_SERVER['QUERY_STRING'] = 'tags[]=php&tags[]=javascript&filters[active]=true&filters[category]=tech'; - - $request = new Request('GET', '/posts', '/posts'); - - $this->assertIsArray($request->query->tags ?? []); - $this->assertEquals(['php', 'javascript'], $request->query->tags ?? []); - - // Limpar $_SERVER após o teste - unset($_SERVER['QUERY_STRING']); - } -} diff --git a/tests/Services/ResponseTest.php b/tests/Services/ResponseTest.php deleted file mode 100644 index 67388a2..0000000 --- a/tests/Services/ResponseTest.php +++ /dev/null @@ -1,248 +0,0 @@ -response = new Response(); - // Ativar modo teste para não fazer echo direto - $this->response->setTestMode(true); - } - - protected function tearDown(): void - { - // Cleanup não é mais necessário com modo teste - } - - public function testResponseInitialization(): void - { - $this->assertInstanceOf(Response::class, $this->response); - } - - public function testStatusMethod(): void - { - $result = $this->response->status(404); - $this->assertInstanceOf(Response::class, $result); - $this->assertSame($this->response, $result); // Should return same instance for chaining - $this->assertEquals(404, $this->response->getStatusCode()); - } - - public function testHeaderMethod(): void - { - $result = $this->response->header('Content-Type', 'application/json'); - $this->assertInstanceOf(Response::class, $result); - $this->assertSame($this->response, $result); // Should return same instance for chaining - - $headers = $this->response->getHeaders(); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertEquals('application/json', $headers['Content-Type']); - } - - public function testJsonResponse(): void - { - $data = ['message' => 'Hello World', 'status' => 'success']; - - $result = $this->response->json($data); - - $this->assertInstanceOf(Response::class, $result); - $this->assertEquals(json_encode($data), $this->response->getBody()); - - // Verificar se os headers foram definidos corretamente - $headers = $this->response->getHeaders(); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertStringContainsString('application/json', $headers['Content-Type']); - } - - public function testTextResponse(): void - { - $text = 'Hello World'; - - $result = $this->response->text($text); - - $this->assertInstanceOf(Response::class, $result); - $this->assertEquals($text, $this->response->getBody()); - - $headers = $this->response->getHeaders(); - $this->assertEquals('text/plain; charset=utf-8', $headers['Content-Type']); - } - - public function testHtmlResponse(): void - { - $html = '

Hello World

This is HTML content

'; - - $result = $this->response->html($html); - - $this->assertInstanceOf(Response::class, $result); - $this->assertEquals($html, $this->response->getBody()); - - $headers = $this->response->getHeaders(); - $this->assertEquals('text/html; charset=utf-8', $headers['Content-Type']); - } - - public function testMethodChaining(): void - { - $data = ['message' => 'Created successfully']; - - $result = $this->response - ->status(201) - ->header('X-Custom-Header', 'custom-value') - ->json($data); - - $this->assertInstanceOf(Response::class, $result); - $this->assertEquals(json_encode($data), $this->response->getBody()); - $this->assertEquals(201, $this->response->getStatusCode()); - - $headers = $this->response->getHeaders(); - $this->assertEquals('custom-value', $headers['X-Custom-Header']); - } - - public function testComplexJsonResponse(): void - { - $complexData = [ - 'users' => [ - ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'], - ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'] - ], - 'pagination' => [ - 'page' => 1, - 'limit' => 10, - 'total' => 2 - ], - 'meta' => [ - 'timestamp' => '2023-01-01T00:00:00Z', - 'version' => '1.0.0' - ] - ]; - - $this->response->json($complexData); - - $this->assertEquals(json_encode($complexData), $this->response->getBody()); - } - - public function testEmptyJsonResponse(): void - { - $this->response->json([]); - - $this->assertEquals('[]', $this->response->getBody()); - } - - public function testNullJsonResponse(): void - { - $this->response->json(null); - - $this->assertEquals('null', $this->response->getBody()); - } - - public function testBooleanJsonResponse(): void - { - $response1 = new Response(); - $response1->setTestMode(true); - $response1->json(true); - $this->assertEquals('true', $response1->getBody()); - - $response2 = new Response(); - $response2->setTestMode(true); - $response2->json(false); - $this->assertEquals('false', $response2->getBody()); - } - - public function testNumericJsonResponse(): void - { - $response1 = new Response(); - $response1->setTestMode(true); - $response1->json(42); - $this->assertEquals('42', $response1->getBody()); - - $response2 = new Response(); - $response2->setTestMode(true); - $response2->json(3.14); - $this->assertEquals('3.14', $response2->getBody()); - } - - public function testStringJsonResponse(): void - { - $this->response->json('Hello World'); - - $this->assertEquals('"Hello World"', $this->response->getBody()); - } - - public function testEmptyTextResponse(): void - { - $this->response->text(''); - - $this->assertEquals('', $this->response->getBody()); - } - - public function testMultilineTextResponse(): void - { - $text = "Line 1\nLine 2\nLine 3"; - - $this->response->text($text); - - $this->assertEquals($text, $this->response->getBody()); - } - - public function testHtmlWithSpecialCharacters(): void - { - $html = '
Special chars: & < > " '
'; - - $this->response->html($html); - - $this->assertEquals($html, $this->response->getBody()); - } - - public function testMultipleHeaders(): void - { - $result = $this->response - ->header('Content-Type', 'application/json') - ->header('X-Custom-Header', 'custom-value') - ->header('Cache-Control', 'no-cache'); - - $this->assertInstanceOf(Response::class, $result); - - $headers = $this->response->getHeaders(); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertArrayHasKey('X-Custom-Header', $headers); - $this->assertArrayHasKey('Cache-Control', $headers); - $this->assertEquals('application/json', $headers['Content-Type']); - $this->assertEquals('custom-value', $headers['X-Custom-Header']); - $this->assertEquals('no-cache', $headers['Cache-Control']); - } - - public function testStatusCodes(): void - { - $response1 = new Response(); - $response1->status(200); - $this->assertEquals(200, $response1->getStatusCode()); - - $response2 = new Response(); - $response2->status(404); - $this->assertEquals(404, $response2->getStatusCode()); - - $response3 = new Response(); - $response3->status(500); - $this->assertEquals(500, $response3->getStatusCode()); - - $response4 = new Response(); - $response4->status(201); - $this->assertEquals(201, $response4->getStatusCode()); - } - - public function testTestModeToggle(): void - { - $this->assertTrue($this->response->isTestMode()); - - $this->response->setTestMode(false); - $this->assertFalse($this->response->isTestMode()); - - $this->response->setTestMode(true); - $this->assertTrue($this->response->isTestMode()); - } -} diff --git a/tests/Support/HttpTestCase.php b/tests/Support/HttpTestCase.php new file mode 100644 index 0000000..85f3e2e --- /dev/null +++ b/tests/Support/HttpTestCase.php @@ -0,0 +1,36 @@ +originalServer = $_SERVER; + $_GET = []; + $_POST = []; + $_FILES = []; + $_COOKIE = []; + } + + protected function tearDown(): void + { + parent::tearDown(); + $_SERVER = $this->originalServer; + $_GET = []; + $_POST = []; + $_FILES = []; + $_COOKIE = []; + } +} diff --git a/tests/Support/StrTest.php b/tests/Support/StrTest.php index da47477..de4d545 100644 --- a/tests/Support/StrTest.php +++ b/tests/Support/StrTest.php @@ -7,6 +7,19 @@ class StrTest extends TestCase { + protected function setUp(): void + { + // Suppress E_USER_DEPRECATED from deprecated Str methods under test + set_error_handler(static function (int $errno): bool { + return $errno === E_USER_DEPRECATED; + }, E_ALL); + } + + protected function tearDown(): void + { + restore_error_handler(); + } + public function testCamel(): void { $this->assertEquals('expressPhp', Str::camel('express_php')); From 9b65f01825a624323f95bf2a49ad8a3aa5df9ab3 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:22:51 -0300 Subject: [PATCH 02/14] docs+test: close out EventDispatcher fire()/listen() vs PSR-14 dispatch() task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed HookManager doesn't touch fire()/listen() at all — it manages its own prioritized listeners and wires them into ListenerProvider directly via PSR-14, dispatching through the Hook event class. fire()/listen() remain a deliberately separate, lightweight string-based mechanism, but had zero test coverage until now (grep confirmed no production caller either). - Added tests/Events/EventDispatcherTest.php: covers dispatch() via ListenerProviderInterface, fire()/listen() independently, propagation stop on false return, and confirms the two listener sets don't leak into each other. - Documented the dispatch(string,array) -> fire(string,array) rename (breaking since 2.0.0, no BC alias exists — dispatch() must stay PSR-14-only per EventDispatcherInterface) in CHANGELOG.md. - Marked tasks/2026-05-29-event-dispatcher-*.md acceptance criteria resolved/explained. --- CHANGELOG.md | 14 ++ ...ire-renomeacao-sem-retrocompatibilidade.md | 19 ++- tests/Events/EventDispatcherTest.php | 138 ++++++++++++++++++ 3 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 tests/Events/EventDispatcherTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index db2de76..8bf0a2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to the PivotPHP Framework will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### ⚠️ Clarified Breaking Change (introduced in 2.0.0, documented here) + +- `Events\EventDispatcher::dispatch()` is PSR-14 only (`dispatch(object $event): object`). + The pre-2.0 string-based dispatch (`dispatch(string $event, array $data)`) was renamed to + `fire(string $event, array $data): bool` and was never given a backward-compatible alias — + code still calling `dispatch()` with a string now gets a `TypeError`, not a deprecation + notice. `fire()`/`listen()` are a separate, lightweight event mechanism, unconnected to the + PSR-14 `dispatch()`/`ListenerProviderInterface` path and to `HookManager` (which manages its + own listeners against a `ListenerProvider` directly). If you need PSR-14 interoperable + events, use `dispatch()`/`addEventListener()`; for simple internal string-named hooks with + no cross-package interop, use `fire()`/`listen()`. + ## [2.0.0] - 2025-11-15 - Modular Routing & Legacy Cleanup Edition ### 🎯 **Major Breaking Changes - Architectural Modernization** diff --git a/tasks/2026-05-29-event-dispatcher-fire-renomeacao-sem-retrocompatibilidade.md b/tasks/2026-05-29-event-dispatcher-fire-renomeacao-sem-retrocompatibilidade.md index b2a956e..0be87aa 100644 --- a/tasks/2026-05-29-event-dispatcher-fire-renomeacao-sem-retrocompatibilidade.md +++ b/tasks/2026-05-29-event-dispatcher-fire-renomeacao-sem-retrocompatibilidade.md @@ -82,7 +82,18 @@ Media (se apenas uso interno) - `CHANGELOG.md` ou `UPGRADE.md` (documentar breaking change) ## Criterios de Aceite -- [ ] Breaking change documentado explicitamente -- [ ] Codigo que usava dispatch() string-based recebe mensagem de erro clara (nao TypeError anonimo) -- [ ] HookManager e Events\EventDispatcher tem integracao clara e testada -- [ ] PHPStan Level 9 sem erros +- [x] Breaking change documentado explicitamente — nota adicionada em `CHANGELOG.md` (`[Unreleased]`) +- [ ] Codigo que usava dispatch() string-based recebe mensagem de erro clara (nao TypeError anonimo) — + nao resolvido: `dispatch(object): object` e a assinatura PSR-14 da interface implementada, + nao ha como interceptar um `string` no mesmo metodo sem violar o contrato da interface + (ver "Opcao A" acima, ja descartada). Mitigado via documentacao (CHANGELOG + docblock de `fire()`). +- [x] HookManager e Events\EventDispatcher tem integracao clara e testada — confirmado que + `HookManager` nao usa `fire()`/`listen()`: ele gerencia seus proprios listeners + (`addAction`/`addFilter`) e os registra diretamente em `ListenerProvider` via PSR-14 + (`registerWithEventSystem()`), disparando atraves do evento `Hook` + `dispatch()`. + `fire()`/`listen()` seguem sendo um mecanismo separado, deliberadamente desconectado, + agora coberto por `tests/Events/EventDispatcherTest.php` (antes sem nenhum teste). +- [x] PHPStan Level 9 sem erros — confirmado (`composer phpstan`) + +**Status: Resolvido (2026-07-15).** Opcao B (documentacao clara) foi a adotada — nao ha +alias retrocompativel possivel para `dispatch()` sem violar `EventDispatcherInterface` (PSR-14). diff --git a/tests/Events/EventDispatcherTest.php b/tests/Events/EventDispatcherTest.php new file mode 100644 index 0000000..8d6b7b1 --- /dev/null +++ b/tests/Events/EventDispatcherTest.php @@ -0,0 +1,138 @@ +addListener(\stdClass::class, function ($event) use (&$received) { + $received = $event; + }); + + $event = new \stdClass(); + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $received); + $this->assertSame($event, $result); + } + + public function testDispatchWithoutListenerProviderIsNoop(): void + { + $dispatcher = new EventDispatcher(); + $event = new \stdClass(); + + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $result); + } + + public function testFireInvokesStringBasedListeners(): void + { + $dispatcher = new EventDispatcher(); + + $received = null; + $dispatcher->listen('user.created', function ($data) use (&$received) { + $received = $data; + }); + + $result = $dispatcher->fire('user.created', ['id' => 1]); + + $this->assertTrue($result); + $this->assertSame(['id' => 1], $received); + } + + public function testFireReturnsFalseWhenNoListenersRegistered(): void + { + $dispatcher = new EventDispatcher(); + + $this->assertFalse($dispatcher->fire('nothing.registered')); + } + + public function testFireStopsPropagationWhenListenerReturnsFalse(): void + { + $dispatcher = new EventDispatcher(); + + $calls = []; + $dispatcher->listen('pipeline', function () use (&$calls) { + $calls[] = 'first'; + return false; + }); + $dispatcher->listen('pipeline', function () use (&$calls) { + $calls[] = 'second'; + }); + + $result = $dispatcher->fire('pipeline'); + + $this->assertFalse($result); + $this->assertSame(['first'], $calls); + } + + public function testFireAndDispatchAreIndependentListenerSets(): void + { + // Registering a PSR-14 listener must not make fire() see it, and + // vice versa — they are deliberately separate systems. + $provider = new ListenerProvider(); + $dispatcher = new EventDispatcher($provider); + + $psr14Called = false; + $provider->addListener(\stdClass::class, function () use (&$psr14Called) { + $psr14Called = true; + }); + + $dispatcher->fire('stdClass'); + + $this->assertFalse($psr14Called); + $this->assertSame(0, $dispatcher->getListenerCount('stdClass')); + } + + public function testRemoveListenersClearsOnlyGivenEvent(): void + { + $dispatcher = new EventDispatcher(); + $dispatcher->listen('a', fn() => true); + $dispatcher->listen('b', fn() => true); + + $dispatcher->removeListeners('a'); + + $this->assertSame(0, $dispatcher->getListenerCount('a')); + $this->assertSame(1, $dispatcher->getListenerCount('b')); + } + + public function testGetEventsListsRegisteredEventNames(): void + { + $dispatcher = new EventDispatcher(); + $dispatcher->listen('a', fn() => true); + $dispatcher->listen('b', fn() => true); + + $this->assertSame(['a', 'b'], $dispatcher->getEvents()); + } + + public function testClearAllRemovesEveryListener(): void + { + $dispatcher = new EventDispatcher(); + $dispatcher->listen('a', fn() => true); + $dispatcher->listen('b', fn() => true); + + $dispatcher->clearAll(); + + $this->assertSame([], $dispatcher->getEvents()); + } +} From 8f39e769cd3fc62cd4f653c2a79d46903ca26fc0 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:26:44 -0300 Subject: [PATCH 03/14] fix(pool): stop leaking serverParams between pooled ServerRequest reuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Psr7Pool::resetServerRequest() received $serverParams but never applied it — ServerRequestInterface (PSR-7) doesn't define a with*() method for server params since they're meant to be read-only, so there was no way to reset them on a reused instance. Headers were already being cleared/reapplied correctly (previous fix), but server params (REMOTE_ADDR, HTTPS, auth data injected by the SAPI, etc.) from the previous request could leak into the next one served by the same pooled object — a real data-leak risk in concurrent/pooled environments (Swoole, ReactPHP, FrankenPHP). Added ServerRequest::withServerParams() (practical extension beyond the formal PSR-7 interface, same pattern as the existing withCookieParams()) and wired it into resetServerRequest(). Covered by Psr7PoolTest::testResetServerRequestDoesNotLeakHeadersOrServerParamsBetweenReuses(). Also updated docs/technical/INCONSISTENCIES_REPORT.md: marked C-01 (dual IoC containers) as resolved via the v2.1.0 deprecation cycle, C-02 (getCachedInput bypass in parseBody) as already fixed, and C-03 (this fix) as resolved. --- docs/technical/INCONSISTENCIES_REPORT.md | 17 ++++++++++ src/Http/Pool/Psr7Pool.php | 7 +++++ src/Http/Psr7/ServerRequest.php | 16 ++++++++++ tests/Http/Pool/Psr7PoolTest.php | 40 ++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md index 4705b6d..9156106 100644 --- a/docs/technical/INCONSISTENCIES_REPORT.md +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -60,6 +60,12 @@ Os testes em `tests/Core/ContainerTest.php` cobrem exclusivamente `Core\Containe 2. Mover os testes para cobrir `Providers\Container`. 3. Definir um unico container como padrao em toda a documentacao. +**Status: Parcialmente resolvido (2026-07-15).** `Core\Container` foi marcado `@deprecated v2.1.0` +(ver `docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md` ITEM-001), com remocao planejada para v3.0.0. +`Application` ja usa exclusivamente `Providers\Container`. `tests/Core/ContainerTest.php` continua +cobrindo o container deprecated (intencional durante o ciclo de deprecation) — nao foi movido para +cobrir `Providers\Container` porque ja existe cobertura propria deste ultimo em outros testes. + --- ### C-02 — Leitura dupla de `php://input` ignora cache e pode esvaziar o body PSR-7 @@ -99,6 +105,10 @@ O body PSR-7 e populado via `getCachedInput()` (linha 181), enquanto o body Expr **Recomendacao:** Substituir `file_get_contents('php://input')` na linha 988 por `$this->getCachedInput()`. +**Status: Resolvido.** `parseBody()` ja usa `$this->getCachedInput()` (nao ha mais leitura +direta de `php://input` nesse metodo). Ver tambem `tasks/2026-05-29-parsebody-logic-bug-json-array-fallback.md`, +que documenta uma correcao relacionada (fallback de array/escalar JSON) no mesmo metodo. + --- ### C-03 — `Psr7Pool::resetServerRequest()` descarta `$headers` e `$serverParams` @@ -140,6 +150,13 @@ private static function resetServerRequest( **Recomendacao:** Aplicar `withoutHeader()` para limpar todos os headers existentes antes de aplicar os novos, e aplicar `$serverParams` via metodo `withServerParams()`. +**Status: Resolvido (2026-07-15).** Headers ja eram limpos via `withoutHeader()` antes desta +correcao. `$serverParams` continuava sendo recebido e completamente ignorado — `ServerRequestInterface` +(PSR-7) nao define um metodo `with*` para isso, entao `ServerRequest::withServerParams()` foi +adicionado (extensao pratica, fora da interface formal, no mesmo padrao de `withCookieParams()`) +e passou a ser usado em `resetServerRequest()`. Coberto por +`Psr7PoolTest::testResetServerRequestDoesNotLeakHeadersOrServerParamsBetweenReuses()`. + --- ## Secao 2 — Inconsistencias de Impacto Alto diff --git a/src/Http/Pool/Psr7Pool.php b/src/Http/Pool/Psr7Pool.php index 2944e15..49a015d 100644 --- a/src/Http/Pool/Psr7Pool.php +++ b/src/Http/Pool/Psr7Pool.php @@ -260,6 +260,13 @@ private static function resetServerRequest( $request = $request->withHeader($name, $value); } + // Aplicar serverParams do novo request — sem isso, serverParams do request + // anterior (ex.: REMOTE_ADDR, HTTPS, dados de auth via SAPI) permaneceriam + // no objeto reaproveitado do pool + if ($request instanceof ServerRequest) { + $request = $request->withServerParams($serverParams); + } + return $request; } diff --git a/src/Http/Psr7/ServerRequest.php b/src/Http/Psr7/ServerRequest.php index bcb392f..b54ee42 100644 --- a/src/Http/Psr7/ServerRequest.php +++ b/src/Http/Psr7/ServerRequest.php @@ -78,6 +78,22 @@ public function getServerParams() return $this->serverParams; } + /** + * Return an instance with the specified server parameters. + * + * Not part of the PSR-7 ServerRequestInterface (server params are read-only there), + * but required to safely reset a pooled ServerRequest between requests — without it, + * server params from a previous request would leak into the reused instance. + * + * @param array $serverParams + */ + public function withServerParams(array $serverParams): static + { + $clone = clone $this; + $clone->serverParams = $serverParams; + return $clone; + } + /** * Retrieve cookies. */ diff --git a/tests/Http/Pool/Psr7PoolTest.php b/tests/Http/Pool/Psr7PoolTest.php index 89e080d..017562c 100644 --- a/tests/Http/Pool/Psr7PoolTest.php +++ b/tests/Http/Pool/Psr7PoolTest.php @@ -508,6 +508,46 @@ public function testObjectImmutability(): void $this->assertEquals(404, $modifiedResponse->getStatusCode()); $this->assertNotSame($response, $modifiedResponse); } + + /** + * A reused pooled ServerRequest must not leak headers or server params from + * the previous request it served — sensitive data (Authorization, Cookie, + * REMOTE_ADDR, etc.) from request N must never surface in request N+1. + */ + public function testResetServerRequestDoesNotLeakHeadersOrServerParamsBetweenReuses(): void + { + $uri = new Uri('/first'); + $firstHeaders = ['Authorization' => 'Bearer secret-token', 'X-User-Id' => '42']; + $firstServerParams = ['REMOTE_ADDR' => '10.0.0.1', 'HTTPS' => 'on']; + + $first = Psr7Pool::getServerRequest( + 'GET', + $uri, + Stream::createFromString(''), + $firstHeaders, + '1.1', + $firstServerParams + ); + $this->assertEquals('Bearer secret-token', $first->getHeaderLine('Authorization')); + $this->assertEquals('10.0.0.1', $first->getServerParams()['REMOTE_ADDR']); + + Psr7Pool::returnServerRequest($first); + + // Reused instance, different request, no Authorization/X-User-Id this time + $second = Psr7Pool::getServerRequest( + 'GET', + new Uri('/second'), + Stream::createFromString(''), + ['Content-Type' => 'application/json'], + '1.1', + ['REMOTE_ADDR' => '10.0.0.2'] + ); + + $this->assertFalse($second->hasHeader('Authorization')); + $this->assertFalse($second->hasHeader('X-User-Id')); + $this->assertEquals('application/json', $second->getHeaderLine('Content-Type')); + $this->assertEquals(['REMOTE_ADDR' => '10.0.0.2'], $second->getServerParams()); + } } /** From 6bc6a13aafeaf6c0262789ed44caa9857005ca8f Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:36:05 -0300 Subject: [PATCH 04/14] docs: verify and close out A-01 through A-06 (INCONSISTENCIES_REPORT.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited all 6 high-impact items against current code: - A-01 (three incompatible rate limiters): resolved — RateLimitMiddleware and LoadShedder already @deprecated v2.1.0 in favor of RateLimiter. - A-02 (MiddlewareStack::warmupCommonPipelines() calling undefined Response::setHeader()): resolved — the method no longer exists. - A-03 (Request::ip() vs getIp(), the latter unvalidated and spoofable via X-Forwarded-For): resolved — getIp() is deprecated and now delegates to ip(), inheriting its validation. - A-04 (ApiDocumentationMiddleware instantiating the Express.js Response instead of a PSR-7 type): resolved — it uses Psr7Response directly now. - A-05 (Validator::FILTER_VALIDATE_INT truthiness bug rejecting 0): resolved — already uses strict `=== false` comparison. - A-06 (MiddlewareStack's 5 static properties unsafe under async servers): was undocumented — added an explicit warning to the class docblock covering which properties are affected, the practical impact under Swoole/ReactPHP/FrankenPHP, and that clearCache() must be called explicitly between requests in those environments. The recommendation only asked for documentation, not a refactor to instance state — that's a larger architectural change out of scope here. Every item's report entry now has a Status note recording what was verified and why. No behavior changes except A-06's docblock addition. --- docs/technical/INCONSISTENCIES_REPORT.md | 31 ++++++++++++++++++++++++ src/Middleware/MiddlewareStack.php | 14 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md index 9156106..2fff51a 100644 --- a/docs/technical/INCONSISTENCIES_REPORT.md +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -190,6 +190,11 @@ O projeto contem tres mecanismos de controle de taxa de requisicoes com designs **Recomendacao:** Consolidar em uma unica interface com implementacoes intercambiaveis (storage em memoria, Redis, sessao). Extrair a logica de estado para um `RateLimitStorage` injetavel. +**Status: Resolvido.** `RateLimitMiddleware` e `LoadShedder` marcados `@deprecated v2.1.0` +(`trigger_error(E_USER_DEPRECATED)` em ambos), apontando para `RateLimiter` como implementacao +canonica. Nao ha mais ambiguidade sobre qual usar; as duas deprecated serao removidas em v3.0.0 +(ver `DEPRECATION_AND_REMOVAL_PLAN.md` ITEM-003/ITEM-004). + --- ### A-02 — `MiddlewareStack::warmupCommonPipelines()` chama metodo inexistente @@ -221,6 +226,9 @@ O metodo correto em `src/Http/Response.php` e `header(string $name, string $valu **Recomendacao:** Substituir todas as ocorrencias de `setHeader(` por `header(` no metodo `warmupCommonPipelines()` e adicionar testes cobrindo o warmup. +**Status: Resolvido.** `warmupCommonPipelines()` nao existe mais em `MiddlewareStack.php` (metodo +removido, nao apenas corrigido) — confirmado via busca no projeto inteiro. + --- ### A-03 — Dois metodos de obtencao de IP com logicas incompativeis @@ -262,6 +270,11 @@ public function getIp(): string **Recomendacao:** Deprecar `getIp()` e unificar no metodo `ip()`. Documentar explicitamente o comportamento de validacao. +**Status: Resolvido.** `getIp()` marcado `@deprecated`, emite `trigger_error(E_USER_DEPRECATED)` e +delega para `ip()` internamente — o problema de seguranca (spoofing via `X-Forwarded-For` sem +validacao) desaparece porque `getIp()` agora executa a mesma validacao de `ip()` +(`DEPRECATION_AND_REMOVAL_PLAN.md` ITEM-002). + --- ### A-04 — `ApiDocumentationMiddleware` viola PSR-15 instanciando `Response` Express.js @@ -289,6 +302,12 @@ A assinatura do metodo `process()` retorna `ResponseInterface`, mas o objeto cri **Recomendacao:** Injetar um `ResponseFactoryInterface` via construtor e substituir `new Response()` por `$this->responseFactory->createResponse()`. +**Status: Resolvido.** `ApiDocumentationMiddleware` nao instancia mais `Response` (Express.js) — +usa `Psr7Response` (implementacao PSR-7 propria do framework, em `src/Http/Psr7/`) diretamente +via `withHeader()`/`withBody()` imutaveis. Nao e exatamente a factory injetada sugerida na +recomendacao, mas resolve o problema real: o middleware nao depende mais da classe Express.js +especifica do framework, apenas de tipos PSR-7. + --- ### A-05 — Bug no `Validator`: inteiro zero falha validacao incorretamente @@ -320,6 +339,11 @@ Substituir por comparacao estrita: if (filter_var($value, FILTER_VALIDATE_INT) === false) { ``` +**Status: Resolvido.** `Validator.php` ja usa `filter_var($value, FILTER_VALIDATE_INT) === false` +(comparacao estrita) exatamente como recomendado. Ver tambem a correcao relacionada da regra +`required` (`0`/`'0'`/`0.0`/`false` tratados como valores presentes) em +`tasks/2026-05-29-validator-required-false-negative-integer-zero.md`. + --- ### A-06 — Estado estatico em `MiddlewareStack` incompativel com servidores asincronos @@ -350,6 +374,13 @@ private static ?SerializationCacheInterface $serializationCache = null; **Recomendacao:** Documentar explicitamente a incompatibilidade com servidores asincronos. Para suporte asincrono, converter propriedades estaticas para instancia ou usar contexto por corrotina. +**Status: Resolvido (2026-07-15).** Documentado explicitamente no docblock da classe +`MiddlewareStack` (a recomendacao pedia documentacao, nao refatoracao — converter as 5 +propriedades estaticas para instancia/corrotina e uma mudanca arquitetural maior, fora de +escopo desta rodada). O docblock explica quais propriedades sao afetadas, o impacto pratico +em Swoole/ReactPHP/FrankenPHP, e que `clearCache()` deve ser chamado explicitamente entre +requisicoes nesses ambientes. + --- ### A-07 — `Response.json()` com auto-emit oculto exige dois flags para desativar diff --git a/src/Middleware/MiddlewareStack.php b/src/Middleware/MiddlewareStack.php index 2b40fb2..3f5c364 100644 --- a/src/Middleware/MiddlewareStack.php +++ b/src/Middleware/MiddlewareStack.php @@ -15,6 +15,20 @@ /** * Classe para gerenciar e executar uma stack de middlewares com otimizações. * Incorpora cache de pipelines, otimizações de execução e estatísticas. + * + * ATENÇÃO — Incompatível com servidores assíncronos de longa duração (Swoole, + * ReactPHP, FrankenPHP em modo worker): $compiledPipelines, $stats, + * $groupMiddlewares, $compiler e $serializationCache são propriedades + * estáticas e persistem entre requisições dentro do mesmo worker/processo. + * Um pipeline compilado para uma rota pode ser reaproveitado incorretamente + * por outra, e não há isolamento de contexto entre requisições concorrentes + * sem reset manual — clearCache() limpa $compiledPipelines, $stats, + * $groupMiddlewares e o cache de serialização, mas precisa ser chamado + * explicitamente entre requisições. Em ambientes PHP tradicionais (PHP-FPM, + * Apache mod_php, um processo por requisição) isso é seguro por padrão. + * Para rodar sob um servidor assíncrono, chame clearCache() no fim de cada + * requisição ou migre para propriedades de instância por contexto de + * corrotina/worker. */ class MiddlewareStack { From f67dbaf13c28cdb5ccebc1f07d6b88d1f82c2d65 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:43:28 -0300 Subject: [PATCH 05/14] fix: extract Response::defaultErrorMessage(), dedupe exception handler closure (B-04, B-05) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B-05: handleException() hardcoded `$statusCode === 404 ? 'Not Found' : 'Internal Server Error'` for the production (non-debug) error message — any status other than 404 (403, 401, 400, 405, 503...) always showed "Internal Server Error", discarding the exception's real HTTP semantics. Response::error() already had a proper status->message map, just not reusable. Extracted it into Response::defaultErrorMessage(), used by both error() and Application::handleException(). Covered by testExceptionHandlingProductionModeUsesStatusSpecificMessage() — it would have failed against the old hardcoded ternary. B-04: configureBasicErrorHandling() and configureErrorHandling() each registered set_exception_handler() with an identical anonymous closure (handleException() + isSent()-guarded emit()). Extracted into a public handleUncaughtException() method, now unit-testable in isolation (testHandleUncaughtExceptionEmitsErrorResponse()) instead of being an anonymous closure nobody could exercise directly. Also updated docs/technical/INCONSISTENCIES_REPORT.md: A-06 and A-07 were already fully resolved in earlier commits on this branch but the status table still said "Pendente Sprint 4" / "Parcialmente Corrigido" — corrected both entries, plus B-04 and B-05. --- docs/technical/INCONSISTENCIES_REPORT.md | 8 ++--- src/Core/Application.php | 36 ++++++++++--------- src/Http/Response.php | 32 +++++++++++------ tests/Core/ApplicationTest.php | 46 ++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 31 deletions(-) diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md index 2fff51a..23577d9 100644 --- a/docs/technical/INCONSISTENCIES_REPORT.md +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -682,8 +682,8 @@ Ultima atualizacao: 2026-05-29 | A-03 | Dois metodos de IP com logicas distintas | Alto | Depreciado (`getIp()` delega para `ip()`; `@deprecated` + `trigger_error`; remocao v3.0.0) | | A-04 | `ApiDocumentationMiddleware` viola PSR-15 | Alto | Corrigido (usa `Psr7Response` + `Stream` puros; sem instanciar `Response` Express.js) | | A-05 | Bug Validator: inteiro zero falha validacao | Alto | Corrigido (comparacao estrita `=== false`) | -| A-06 | Estado estatico em `MiddlewareStack` | Alto | Pendente Sprint 4 | -| A-07 | `Response.json()` com auto-emit oculto | Alto | Parcialmente Corrigido (`setTestMode()` sincroniza `$disableAutoEmit`; condicoes simplificadas; extracao de `ResponseEmitter` pendente Sprint 4) | +| A-06 | Estado estatico em `MiddlewareStack` | Alto | Corrigido (2026-07-15: incompatibilidade documentada explicitamente no docblock da classe) | +| A-07 | `Response.json()` com auto-emit oculto | Alto | Corrigido (2026-07-15: auto-emit removido de `json()`/`text()`/`html()`; `Application::run()` e o unico ponto de emissao, guardado por `isSent()`; `disableAutoEmit()` mantido como no-op para BC) | | M-01 | Dois `PoolManager` com designs opostos | Medio | Pendente Sprint 4 | | M-02 | Conversao camelCase duplicada em 6 locais | Medio | Corrigido (`HeaderRequest::headerToCamel()` centralizado; `CustomHeaderCollection` herda) | | M-03 | `Utils` e `Str` duplicam case conversion | Medio | Corrigido (`Utils::camelCase/snake/kebab` delegam para `Str`) | @@ -698,8 +698,8 @@ Ultima atualizacao: 2026-05-29 | B-01 | `Application` God Class (1229 linhas) | Baixo | Pendente Sprint 4 | | B-02 | `Str` reimplementa funcoes nativas PHP 8.0+ | Baixo | Depreciado (`@deprecated` + `trigger_error` nos 3 metodos; remocao v3.0.0) | | B-03 | `Arr::only/except` sem type hint em `$keys` | Baixo | Corrigido (type hint `array|string` adicionado) | -| B-04 | Error handling duplicado em `Application` | Baixo | Pendente Sprint 4 | -| B-05 | Magic strings HTTP em `handleException()` | Baixo | Pendente Sprint 4 | +| B-04 | Error handling duplicado em `Application` | Baixo | Corrigido (2026-07-15: a logica de status via `instanceof HttpException` ja era unica, dentro de `handleException()`; a duplicacao real era a closure identica de `set_exception_handler()` em `configureBasicErrorHandling()`/`configureErrorHandling()`, extraida para o metodo publico `handleUncaughtException()`, agora testavel isoladamente) | +| B-05 | Magic strings HTTP em `handleException()` | Baixo | Corrigido (2026-07-15: `Response::defaultErrorMessage()` extraido e reutilizado por `error()` e `handleException()`; cobre todos os status do mapa, nao so 404) | --- diff --git a/src/Core/Application.php b/src/Core/Application.php index f47a7d6..6c84128 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -320,14 +320,7 @@ protected function configureBasicErrorHandling(): void ini_set('display_errors', '0'); set_error_handler([$this, 'handleError']); - set_exception_handler( - function (Throwable $e): void { - $response = $this->handleException($e); - if (!$response->isSent()) { - $response->emit(); - } - } - ); + set_exception_handler([$this, 'handleUncaughtException']); } /** @@ -350,14 +343,23 @@ protected function configureErrorHandling(): void } set_error_handler([$this, 'handleError']); - set_exception_handler( - function (Throwable $e): void { - $response = $this->handleException($e); - if (!$response->isSent()) { - $response->emit(); - } - } - ); + set_exception_handler([$this, 'handleUncaughtException']); + } + + /** + * Handler registrado via set_exception_handler() para exceções que escapam + * completamente do fluxo handle()/run() (ex.: erros durante o bootstrap). + * Único ponto responsável por converter e emitir a resposta de erro nesse + * cenário — handleException() apenas monta a Response, sem emiti-la. + * + * @return void + */ + public function handleUncaughtException(Throwable $e): void + { + $response = $this->handleException($e); + if (!$response->isSent()) { + $response->emit(); + } } /** @@ -833,7 +835,7 @@ public function handleException( ->json( [ 'error' => true, - 'message' => $statusCode === 404 ? 'Not Found' : 'Internal Server Error', + 'message' => Response::defaultErrorMessage($statusCode), 'error_id' => $errorId ] ); diff --git a/src/Http/Response.php b/src/Http/Response.php index 5065f75..2c4cd85 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -370,21 +370,33 @@ public function error(int $code, string $message = ''): self $this->status($code); if (empty($message)) { - $messages = [ - 400 => 'Bad Request', - 401 => 'Unauthorized', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 500 => 'Internal Server Error', - 503 => 'Service Unavailable' - ]; - $message = $messages[$code] ?? 'Error'; + $message = self::defaultErrorMessage($code); } return $this->json(['error' => $message, 'code' => $code]); } + /** + * Mensagem padrão para um status HTTP de erro. + * + * Fonte única de verdade reutilizada por error() e por + * Application::handleException(), evitando magic strings duplicadas. + */ + public static function defaultErrorMessage(int $code): string + { + $messages = [ + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 500 => 'Internal Server Error', + 503 => 'Service Unavailable' + ]; + + return $messages[$code] ?? 'Error'; + } + /** * Envia uma resposta de sucesso padronizada. */ diff --git a/tests/Core/ApplicationTest.php b/tests/Core/ApplicationTest.php index 76c175f..20c2a8e 100644 --- a/tests/Core/ApplicationTest.php +++ b/tests/Core/ApplicationTest.php @@ -389,6 +389,34 @@ function ($_, $res) { $this->assertEquals(403, $response->getStatusCode()); } + /** + * Production mode must use the status-appropriate default message for any + * HTTP status, not just 404 — handleException() previously hardcoded + * 'Not Found' vs 'Internal Server Error' as the only two options. + */ + public function testExceptionHandlingProductionModeUsesStatusSpecificMessage(): void + { + $this->app->configure(['app.debug' => false]); + + $this->app->get( + '/forbidden', + function ($_, $res) { + throw new HttpException(403, 'Access denied'); + } + ); + + $this->app->boot(); + + $request = new Request('GET', '/forbidden', '/forbidden'); + $response = $this->app->handle($request); + + $this->assertEquals(403, $response->getStatusCode()); + + $responseBody = $response->getBody(); + $body = json_decode(is_string($responseBody) ? $responseBody : (string) $responseBody, true); + $this->assertEquals('Forbidden', $body['message']); + } + /** * Test configuration loading */ @@ -617,4 +645,22 @@ public function testConfigureMethod(): void $this->assertEquals('1.0.0', $appConfig->get('app.version')); $this->assertEquals('localhost', $appConfig->get('database.host')); } + + /** + * handleUncaughtException() is the set_exception_handler() callback for + * exceptions that escape the handle()/run() flow entirely (e.g. bootstrap + * errors). It must emit the error response itself — nothing else will. + */ + public function testHandleUncaughtExceptionEmitsErrorResponse(): void + { + $this->app->boot(); + + ob_start(); + $this->app->handleUncaughtException(new \RuntimeException('boom')); + $output = ob_get_clean(); + + $body = json_decode((string) $output, true); + $this->assertTrue($body['error']); + $this->assertArrayHasKey('error_id', $body); + } } From c884da4fc99c106bdd9c6ec4e39429e9d57f398b Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:46:42 -0300 Subject: [PATCH 06/14] fix: decouple ExtensionManager from the concrete Application class (M-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExtensionManager held a hard dependency on Application, but never actually calls any Application-specific method — it only stores the reference and passes it along to registered extension callbacks/classes, which are the ones that use the real API surface. Added Core\ApplicationInterface as a marker interface (intentionally empty: there's no concrete method ExtensionManager itself needs) and made Application implement it. ExtensionManager now type-hints against the interface instead of the 1000+ line concrete class. Runtime behavior is unchanged — the object passed to extensions is still the real Application instance with its full API, only the declared type inside ExtensionManager changed. --- docs/technical/INCONSISTENCIES_REPORT.md | 2 +- src/Core/Application.php | 2 +- src/Core/ApplicationInterface.php | 18 ++++++++++++++++++ src/Providers/ExtensionManager.php | 8 ++++---- 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 src/Core/ApplicationInterface.php diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md index 23577d9..6b94065 100644 --- a/docs/technical/INCONSISTENCIES_REPORT.md +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -690,7 +690,7 @@ Ultima atualizacao: 2026-05-29 | M-04 | `Application::use()` sem type hint | Medio | Corrigido (decomposto em `resolveClassMiddleware()` e `wrapObjectMiddleware()`) | | M-05 | `LoadShedder::$requestCounts` cresce sem limite | Medio | Depreciado (classe depreciada; remocao v3.0.0) | | M-06 | Namespace `Providers/` contem implementacoes | Medio | Parcialmente Corrigido (`EventDispatcher` e `ListenerProvider` movidos para `Events/`; `Providers/` depreciados; `Container` pendente Sprint 4) | -| M-07 | `ExtensionManager` acoplado a `Application` | Medio | Pendente Sprint 4 | +| M-07 | `ExtensionManager` acoplado a `Application` | Medio | Corrigido (2026-07-15: `Core\ApplicationInterface` criada — marker interface, ja que `ExtensionManager` nunca chama metodos especificos de `Application`, so repassa a referencia adiante; `Application implements ApplicationInterface`; `ExtensionManager` tipado contra a interface) | | M-08 | `createUploadedFile()` sem verificacao de erro | Medio | Corrigido (verificacao de `file_exists` adicionada) | | M-09 | Dois `Logger.php` em namespaces distintos | Medio | Corrigido (`Logging/PsrLogger.php` criado; `Providers/Logger.php` depreciado; `Logging/Logger.php` morto removido) | | M-10 | Classe anonima de 90 linhas sem testabilidade | Medio | Corrigido (extraida para `CustomHeaderCollection` em `src/Http/`) | diff --git a/src/Core/Application.php b/src/Core/Application.php index 6c84128..3b3d806 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -39,7 +39,7 @@ * - Tratamento de erros * - Resposta HTTP */ -class Application +class Application implements ApplicationInterface { /** * Versão do framework. diff --git a/src/Core/ApplicationInterface.php b/src/Core/ApplicationInterface.php new file mode 100644 index 0000000..3c05500 --- /dev/null +++ b/src/Core/ApplicationInterface.php @@ -0,0 +1,18 @@ +app = $app; } @@ -276,7 +276,7 @@ private function updateStats(): void /** * Get application instance */ - public function getApp(): Application + public function getApp(): ApplicationInterface { return $this->app; } From 024b8dbec6d7a57a85228f6ea572af971ff55c7a Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:51:27 -0300 Subject: [PATCH 07/14] docs: clarify the two PoolManager classes are unrelated by design (M-01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Http\Pool\PoolManager (instance-based, generic rent()/return()/borrow() on arbitrary named pools) and Http\Psr7\Pool\PoolManager (100% static, coordinates the specific PSR-7 pools: ResponsePool/HeaderPool/ OperationsCache) share a class name but not a purpose or a shape. Full unification behind a single PoolManagerInterface — the report's original recommendation — turns out to not be viable without rewriting one of the two from scratch: PHP interfaces only cover instance methods, and Http\Psr7\Pool\PoolManager has none (everything is static). Confirmed neither class is actually used on the framework's hot path today (real pooling goes through HttpPoolFacade/Psr7Pool), so a disproportionate rewrite isn't justified just to satisfy an interface. Documented the distinction explicitly in both classes' docblocks instead — that removes the actual reported impact ("ambiguity in which pool to use"). Full architectural unification is out of scope. --- docs/technical/INCONSISTENCIES_REPORT.md | 2 +- src/Http/Pool/PoolManager.php | 15 ++++++++++++++- src/Http/Psr7/Pool/PoolManager.php | 9 ++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md index 6b94065..63859ec 100644 --- a/docs/technical/INCONSISTENCIES_REPORT.md +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -684,7 +684,7 @@ Ultima atualizacao: 2026-05-29 | A-05 | Bug Validator: inteiro zero falha validacao | Alto | Corrigido (comparacao estrita `=== false`) | | A-06 | Estado estatico em `MiddlewareStack` | Alto | Corrigido (2026-07-15: incompatibilidade documentada explicitamente no docblock da classe) | | A-07 | `Response.json()` com auto-emit oculto | Alto | Corrigido (2026-07-15: auto-emit removido de `json()`/`text()`/`html()`; `Application::run()` e o unico ponto de emissao, guardado por `isSent()`; `disableAutoEmit()` mantido como no-op para BC) | -| M-01 | Dois `PoolManager` com designs opostos | Medio | Pendente Sprint 4 | +| M-01 | Dois `PoolManager` com designs opostos | Medio | Parcialmente resolvido (2026-07-15: unificacao completa via `PoolManagerInterface` comum e tecnicamente inviavel sem reescrever um dos dois do zero — `Http\Psr7\Pool\PoolManager` e 100% estatico, e interfaces PHP nao cobrem metodos estaticos; `Http\Pool\PoolManager` e por instancia. Confirmado que nenhuma das duas classes e usada no caminho de producao hoje (pooling real usa `HttpPoolFacade`/`Psr7Pool`). Documentada a distincao explicitamente no docblock de ambas para eliminar a ambiguidade — o impacto tecnico original ("ambiguidade na escolha do pool") fica resolvido; a unificacao arquitetural full fica fora de escopo) | | M-02 | Conversao camelCase duplicada em 6 locais | Medio | Corrigido (`HeaderRequest::headerToCamel()` centralizado; `CustomHeaderCollection` herda) | | M-03 | `Utils` e `Str` duplicam case conversion | Medio | Corrigido (`Utils::camelCase/snake/kebab` delegam para `Str`) | | M-04 | `Application::use()` sem type hint | Medio | Corrigido (decomposto em `resolveClassMiddleware()` e `wrapObjectMiddleware()`) | diff --git a/src/Http/Pool/PoolManager.php b/src/Http/Pool/PoolManager.php index 1681510..cdcce9c 100644 --- a/src/Http/Pool/PoolManager.php +++ b/src/Http/Pool/PoolManager.php @@ -5,12 +5,25 @@ namespace PivotPHP\Core\Http\Pool; /** - * Pool Manager + * Pool Manager (generic, instance-based) * * Simple and effective object pooling for the microframework. * Provides basic pooling functionality without unnecessary complexity. * * Following 'Simplicidade sobre Otimização Prematura' principle. + * + * NÃO CONFUNDIR com \PivotPHP\Core\Http\Psr7\Pool\PoolManager — são classes + * diferentes, com propósitos diferentes, apesar do nome igual: + * - Esta classe (Http\Pool\PoolManager): pool genérico por instância, + * rent()/return()/borrow() em pools nomeados arbitrariamente por string. + * Não tem conhecimento de HTTP/PSR-7. + * - Http\Psr7\Pool\PoolManager: coordenador 100% estático dos pools PSR-7 + * específicos (ResponsePool, HeaderPool, OperationsCache). Sendo estático, + * não pode implementar a mesma interface de instância que esta classe — + * unificação real exigiria reescrever um dos dois de raiz, o que não se + * justifica hoje: nenhuma das duas classes é usada no caminho de produção + * do framework (o pooling real de request/response é feito via + * HttpPoolFacade/Psr7Pool, não por nenhum destes dois PoolManager). */ class PoolManager { diff --git a/src/Http/Psr7/Pool/PoolManager.php b/src/Http/Psr7/Pool/PoolManager.php index aca647e..9d30ee5 100644 --- a/src/Http/Psr7/Pool/PoolManager.php +++ b/src/Http/Psr7/Pool/PoolManager.php @@ -13,7 +13,14 @@ use PivotPHP\Core\Http\Psr7\Adapters\HeaderPoolAdapter; /** - * Pool Manager for coordinating all object pools and caches + * Pool Manager for coordinating all object pools and caches (static, PSR-7 specific) + * + * NÃO CONFUNDIR com \PivotPHP\Core\Http\Pool\PoolManager — são classes + * diferentes, com propósitos diferentes, apesar do nome igual. Ver o + * docblock daquela classe para a distinção completa. Esta classe é 100% + * estática por design (coordena os pools PSR-7 globais do processo) e + * não é usada no caminho quente do framework hoje — o pooling real de + * request/response é feito via HttpPoolFacade/Psr7Pool. * * @package PivotPHP\Core\Http\Psr7\Pool * @since 2.1.1 From ce340a87fe1a6939fca0b37476d98bfa79facb52 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 15:52:49 -0300 Subject: [PATCH 08/14] docs: document why Providers\Container stays put (M-06) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventDispatcher and Logger were already moved out of Providers/ to Events/ and Logging/ respectively, with deprecated shims left behind (prior commits on this branch). Container is the one item from M-06 still in Providers/ — but it's the active, canonical container (Application uses it exclusively; Core\Container is the deprecated one, per C-01). Moving it now would be a third identity change for the same class in a short span (Core\Container -> Providers\Container -> yet another namespace) with no real benefit to consumers. Documented that reasoning directly in the class docblock instead of moving it. --- docs/technical/INCONSISTENCIES_REPORT.md | 2 +- src/Providers/Container.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/technical/INCONSISTENCIES_REPORT.md b/docs/technical/INCONSISTENCIES_REPORT.md index 63859ec..fbc322f 100644 --- a/docs/technical/INCONSISTENCIES_REPORT.md +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -689,7 +689,7 @@ Ultima atualizacao: 2026-05-29 | M-03 | `Utils` e `Str` duplicam case conversion | Medio | Corrigido (`Utils::camelCase/snake/kebab` delegam para `Str`) | | M-04 | `Application::use()` sem type hint | Medio | Corrigido (decomposto em `resolveClassMiddleware()` e `wrapObjectMiddleware()`) | | M-05 | `LoadShedder::$requestCounts` cresce sem limite | Medio | Depreciado (classe depreciada; remocao v3.0.0) | -| M-06 | Namespace `Providers/` contem implementacoes | Medio | Parcialmente Corrigido (`EventDispatcher` e `ListenerProvider` movidos para `Events/`; `Providers/` depreciados; `Container` pendente Sprint 4) | +| M-06 | Namespace `Providers/` contem implementacoes | Medio | Parcialmente resolvido, decisao final (2026-07-15): `EventDispatcher`/`ListenerProvider`/`Logger` movidos para `Events/`/`Logging/`, versoes em `Providers/` deprecated. `Container` fica deliberadamente em `Providers/` — e a implementacao canonica ativa (ver C-01); move-la agora seria uma terceira mudanca de identidade (`Core\Container` -> `Providers\Container` -> outro namespace) sem beneficio real. Decisao documentada no docblock da classe. | | M-07 | `ExtensionManager` acoplado a `Application` | Medio | Corrigido (2026-07-15: `Core\ApplicationInterface` criada — marker interface, ja que `ExtensionManager` nunca chama metodos especificos de `Application`, so repassa a referencia adiante; `Application implements ApplicationInterface`; `ExtensionManager` tipado contra a interface) | | M-08 | `createUploadedFile()` sem verificacao de erro | Medio | Corrigido (verificacao de `file_exists` adicionada) | | M-09 | Dois `Logger.php` em namespaces distintos | Medio | Corrigido (`Logging/PsrLogger.php` criado; `Providers/Logger.php` depreciado; `Logging/Logger.php` morto removido) | diff --git a/src/Providers/Container.php b/src/Providers/Container.php index fed10f2..2614d7c 100644 --- a/src/Providers/Container.php +++ b/src/Providers/Container.php @@ -12,6 +12,18 @@ /** * Simple PSR-11 compliant container implementation + * + * Nota arquitetural: esta classe é a implementação canônica ativa do + * container (Application usa exclusivamente esta, não Core\Container — + * ver docs/technical/DEPRECATION_AND_REMOVAL_PLAN.md ITEM-001). O + * namespace Providers/ historicamente deveria conter apenas providers que + * *registram* serviços, não os serviços em si (ver + * docs/technical/INCONSISTENCIES_REPORT.md M-06) — Logger e + * EventDispatcher já foram movidos para namespaces mais corretos + * (Logging\PsrLogger, Events\EventDispatcher). Container permanece aqui + * deliberadamente: movê-lo agora seria uma terceira mudança de identidade + * em pouco tempo (Core\Container → Providers\Container → outro + * namespace), sem benefício real para quem consome a classe. */ class Container implements ContainerInterface { From ef59abf6483f63d1702063e45a9508c60cf66800 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 16:00:54 -0300 Subject: [PATCH 09/14] style: fix PSR-12 violations in EventDispatcherTest.php (phpcbf) Multi-line addListener()/listen() calls didn't follow the project's one-argument-per-line convention for multi-line calls. Caught by composer cs:check while doing a final quality pass on this branch. --- tests/Events/EventDispatcherTest.php | 47 ++++++++++++++++++---------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/Events/EventDispatcherTest.php b/tests/Events/EventDispatcherTest.php index 8d6b7b1..0faf54b 100644 --- a/tests/Events/EventDispatcherTest.php +++ b/tests/Events/EventDispatcherTest.php @@ -24,9 +24,12 @@ public function testDispatchInvokesListenerProviderListeners(): void $dispatcher = new EventDispatcher($provider); $received = null; - $provider->addListener(\stdClass::class, function ($event) use (&$received) { - $received = $event; - }); + $provider->addListener( + \stdClass::class, + function ($event) use (&$received) { + $received = $event; + } + ); $event = new \stdClass(); $result = $dispatcher->dispatch($event); @@ -50,9 +53,12 @@ public function testFireInvokesStringBasedListeners(): void $dispatcher = new EventDispatcher(); $received = null; - $dispatcher->listen('user.created', function ($data) use (&$received) { - $received = $data; - }); + $dispatcher->listen( + 'user.created', + function ($data) use (&$received) { + $received = $data; + } + ); $result = $dispatcher->fire('user.created', ['id' => 1]); @@ -72,13 +78,19 @@ public function testFireStopsPropagationWhenListenerReturnsFalse(): void $dispatcher = new EventDispatcher(); $calls = []; - $dispatcher->listen('pipeline', function () use (&$calls) { - $calls[] = 'first'; - return false; - }); - $dispatcher->listen('pipeline', function () use (&$calls) { - $calls[] = 'second'; - }); + $dispatcher->listen( + 'pipeline', + function () use (&$calls) { + $calls[] = 'first'; + return false; + } + ); + $dispatcher->listen( + 'pipeline', + function () use (&$calls) { + $calls[] = 'second'; + } + ); $result = $dispatcher->fire('pipeline'); @@ -94,9 +106,12 @@ public function testFireAndDispatchAreIndependentListenerSets(): void $dispatcher = new EventDispatcher($provider); $psr14Called = false; - $provider->addListener(\stdClass::class, function () use (&$psr14Called) { - $psr14Called = true; - }); + $provider->addListener( + \stdClass::class, + function () use (&$psr14Called) { + $psr14Called = true; + } + ); $dispatcher->fire('stdClass'); From 6e340f15be8c8c5159693a9240a255ac6528c98f Mon Sep 17 00:00:00 2001 From: Caio Fernandes <34290014+code-cfernandes@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:14:49 -0300 Subject: [PATCH 10/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Logging/PsrLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging/PsrLogger.php b/src/Logging/PsrLogger.php index b61ef1c..4c6c0e6 100644 --- a/src/Logging/PsrLogger.php +++ b/src/Logging/PsrLogger.php @@ -117,7 +117,7 @@ private function writeToFile(string $logEntry): void file_put_contents($this->logPath, $logEntry, FILE_APPEND | LOCK_EX); } catch (\Throwable $e) { // Fallback to error_log if file writing fails - error_log("Express Logger Error: " . $e->getMessage()); + error_log("PivotPHP Logger Error: " . $e->getMessage()); error_log($logEntry); } } From af6d6d76721773eb7ef156d539b06e1316016f00 Mon Sep 17 00:00:00 2001 From: Caio Fernandes <34290014+code-cfernandes@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:15:17 -0300 Subject: [PATCH 11/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Middleware/LoadShedder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Middleware/LoadShedder.php b/src/Middleware/LoadShedder.php index 0c95efd..d0c88cf 100644 --- a/src/Middleware/LoadShedder.php +++ b/src/Middleware/LoadShedder.php @@ -113,7 +113,6 @@ public function __invoke(Request $request, Response $response, callable $next): */ public function handle(Request $request, Response $response, callable $next): Response { - trigger_error('LoadShedder is deprecated. Use RateLimiter instead.', E_USER_DEPRECATED); if ($this->shouldShed($request)) { $json = json_encode( [ From 41fdd7bb863a2c7105f1c079abf4c14c1446cbb6 Mon Sep 17 00:00:00 2001 From: Caio Fernandes <34290014+code-cfernandes@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:15:46 -0300 Subject: [PATCH 12/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Support/HttpTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Support/HttpTestCase.php b/tests/Support/HttpTestCase.php index 85f3e2e..5b05ee7 100644 --- a/tests/Support/HttpTestCase.php +++ b/tests/Support/HttpTestCase.php @@ -8,7 +8,7 @@ /** * Base test case for HTTP tests. - * Safely saves and restores superglobals between tests. + * Saves and restores $_SERVER, and resets other superglobals between tests. */ abstract class HttpTestCase extends TestCase { From 0efd27b52544d7e3db2b02817b924c39916c598c Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 16:21:55 -0300 Subject: [PATCH 13/14] fix: CustomHeaderCollection dropped all headers when getallheaders() succeeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flagged by Copilot code review. The environment-header fallback checked getallheaders() only to decide whether to *skip* merging — the merge loop lived inside `if (empty($existingHeaders))`, so it only ever ran when getallheaders() returned nothing. Whenever it returned real headers (the normal case under Apache/PHP-FPM), they were fetched and then silently discarded: Request::setHeaders() would end up with only the explicit custom overrides, dropping Authorization, Cookie, and everything else from the actual request. Extracted the merge logic (skip keys already set by an explicit override) into mergeMissingHeaders(), now called unconditionally with whichever source has data — getallheaders() when non-empty, otherwise the parsed $_SERVER['HTTP_*'] fallback (extracted into parseServerHeaders()). There's now a single merge code path instead of two copies where only one worked, so the bug can't be silently reintroduced by editing just one branch. getallheaders() can't be meaningfully mocked in tests: CustomHeaderCollection guards the call with function_exists('getallheaders'), which always resolves against the global function regardless of any same-namespace override, and the CLI SAPI PHPUnit runs under doesn't define it at all. Added tests/Http/CustomHeaderCollectionTest.php covering mergeMissingHeaders() directly via reflection (the exact logic that was broken) plus the real, exercisable $_SERVER fallback path and override precedence. --- src/Http/CustomHeaderCollection.php | 58 ++++++++++++---- tests/Http/CustomHeaderCollectionTest.php | 84 +++++++++++++++++++++++ 2 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 tests/Http/CustomHeaderCollectionTest.php diff --git a/src/Http/CustomHeaderCollection.php b/src/Http/CustomHeaderCollection.php index 868b416..a64b3e9 100644 --- a/src/Http/CustomHeaderCollection.php +++ b/src/Http/CustomHeaderCollection.php @@ -35,22 +35,51 @@ public function __construct(array $customHeaders = []) // Fall back to environment headers not already overridden $existingHeaders = function_exists('getallheaders') ? getallheaders() : []; - if (empty($existingHeaders)) { - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headerName = str_replace( - ' ', - '-', - ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))) - ); - $key = self::headerToCamel($headerName); - - if (!isset($this->headers[$key])) { - $this->headers[$key] = $value; - } - } + $this->mergeMissingHeaders( + !empty($existingHeaders) ? $existingHeaders : self::parseServerHeaders() + ); + } + + /** + * Merges header names/values into $this->headers, skipping any key + * already set (custom headers passed to the constructor always win). + * + * @param array $source + */ + private function mergeMissingHeaders(array $source): void + { + foreach ($source as $headerName => $value) { + $key = self::headerToCamel($headerName); + + if (!isset($this->headers[$key])) { + $this->headers[$key] = $value; + } + } + } + + /** + * Parses HTTP_* entries out of $_SERVER into a plain header-name => value + * array. Used as the fallback when getallheaders() isn't available + * (e.g. CLI SAPI). + * + * @return array + */ + private static function parseServerHeaders(): array + { + $headers = []; + + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headerName = str_replace( + ' ', + '-', + ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))) + ); + $headers[$headerName] = $value; } } + + return $headers; } /** @@ -79,5 +108,4 @@ public function hasHeader($name): bool $key = self::headerToCamel(trim($name, ':')); return isset($this->headers[$key]); } - } diff --git a/tests/Http/CustomHeaderCollectionTest.php b/tests/Http/CustomHeaderCollectionTest.php new file mode 100644 index 0000000..d261be2 --- /dev/null +++ b/tests/Http/CustomHeaderCollectionTest.php @@ -0,0 +1,84 @@ + 'Bearer explicit-override']); + + $this->invokeMergeMissingHeaders( + $collection, + [ + 'Authorization' => 'Bearer from-environment', + 'Cookie' => 'session=abc123', + ] + ); + + // Explicit constructor override must win over the merged source + $this->assertSame('Bearer explicit-override', $collection->getHeader('Authorization')); + // Header only present in the merged source must now be available + $this->assertTrue($collection->hasHeader('Cookie')); + $this->assertSame('session=abc123', $collection->getHeader('Cookie')); + } + + public function testFallsBackToServerHeadersWhenGetallheadersUnavailable(): void + { + // Under the CLI SAPI, function_exists('getallheaders') is false, + // so the constructor always takes the $_SERVER fallback path — + // this exercises that path end-to-end, for real. + $_SERVER['HTTP_X_FALLBACK_TEST'] = 'fallback-value'; + + try { + $collection = new CustomHeaderCollection(); + $this->assertSame('fallback-value', $collection->getHeader('X-Fallback-Test')); + } finally { + unset($_SERVER['HTTP_X_FALLBACK_TEST']); + } + } + + public function testConstructorCustomHeadersTakePriorityOverServerFallback(): void + { + $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer from-server'; + + try { + $collection = new CustomHeaderCollection(['Authorization' => 'Bearer explicit']); + $this->assertSame('Bearer explicit', $collection->getHeader('Authorization')); + } finally { + unset($_SERVER['HTTP_AUTHORIZATION']); + } + } + + /** + * @param array $source + */ + private function invokeMergeMissingHeaders(CustomHeaderCollection $collection, array $source): void + { + $reflection = new ReflectionClass($collection); + $method = $reflection->getMethod('mergeMissingHeaders'); + $method->setAccessible(true); + $method->invoke($collection, $source); + } +} From 7db9ac762947e3c2ca37e505a29f537f2119f1e7 Mon Sep 17 00:00:00 2001 From: Caio Fernandes Date: Wed, 15 Jul 2026 16:26:03 -0300 Subject: [PATCH 14/14] fix: don't reuse pooled streams that lack truncate() (Psr7Pool::resetStream) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flagged by Copilot code review. truncate() isn't part of StreamInterface (PSR-7) — resetStream() guarded the call with method_exists() and, when absent, fell through to write() anyway. write() only overwrites the bytes matching the new content's length; if the new content is shorter than whatever the reused stream held from its previous request/response, the leftover trailing bytes stay in the stream. Same class of bug as the serverParams pool leak (C-03) — data from one request leaking into the next via a pooled object, just at the stream/body level this time. The framework's own Stream implementation always has truncate(), so this was latent (the pool never actually saw a stream without it) but real for any other StreamInterface implementation the pool might be handed. Now resetStream() only reuses the stream when both isWritable() and method_exists(..., 'truncate') hold; otherwise it falls back to a fresh Stream::createFromString($content), same as the non-writable case already did. Added WritableSeekableStreamWithoutTruncate to Psr7PoolTest — a minimal StreamInterface implementation that's writable/seekable but deliberately has no truncate(). Verified the new test fails without the fix (residual bytes leak: 'Hi' + pool return('Original long content here') produced 'Hiiginal long content here') and passes with it. --- src/Http/Pool/Psr7Pool.php | 18 ++--- tests/Http/Pool/Psr7PoolTest.php | 117 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 8 deletions(-) diff --git a/src/Http/Pool/Psr7Pool.php b/src/Http/Pool/Psr7Pool.php index 49a015d..a5af6ec 100644 --- a/src/Http/Pool/Psr7Pool.php +++ b/src/Http/Pool/Psr7Pool.php @@ -331,20 +331,22 @@ private static function resetUri(UriInterface $uri, string $uriString): UriInter */ private static function resetStream(StreamInterface $stream, string $content): StreamInterface { - if ($stream->isSeekable()) { - $stream->rewind(); - } - - if ($stream->isWritable()) { - if (method_exists($stream, 'truncate')) { - $stream->truncate(0); + // truncate() não faz parte de StreamInterface (PSR-7) — sem ele, write() + // após rewind() só sobrescreve os bytes correspondentes ao novo conteúdo; + // se o conteúdo novo for menor que o residual do uso anterior no pool, + // os bytes finais antigos permaneceriam no stream (vazamento de dados + // entre requisições). Sem truncate() disponível, não reaproveitar. + if ($stream->isWritable() && method_exists($stream, 'truncate')) { + if ($stream->isSeekable()) { + $stream->rewind(); } + $stream->truncate(0); $stream->write($content); $stream->rewind(); return $stream; } - // Se não conseguir resetar, criar novo + // Se não conseguir resetar com segurança, criar novo return Stream::createFromString($content); } diff --git a/tests/Http/Pool/Psr7PoolTest.php b/tests/Http/Pool/Psr7PoolTest.php index 017562c..e5f07f0 100644 --- a/tests/Http/Pool/Psr7PoolTest.php +++ b/tests/Http/Pool/Psr7PoolTest.php @@ -293,6 +293,23 @@ public function testStreamReset(): void $this->assertEquals(0, $stats['pool_sizes']['streams']); } + /** + * Regression test: a pooled stream without truncate() must never be + * reused with leftover bytes from its previous content. If the new + * content is shorter than what the stream held before, and truncate() + * isn't available, resetStream() must fall back to a brand new stream + * instead of writing over a subset of the old bytes. + */ + public function testStreamWithoutTruncateIsNotReusedWithResidualBytes(): void + { + $stream = new WritableSeekableStreamWithoutTruncate('Original long content here'); + Psr7Pool::returnStream($stream); + + $reused = Psr7Pool::getStream('Hi'); + + $this->assertEquals('Hi', (string) $reused); + } + /** * Test response header reset */ @@ -641,3 +658,103 @@ public function truncate(int $size): void throw new \RuntimeException('Stream is not writable'); } } + +/** + * A writable, seekable StreamInterface implementation that does NOT expose + * truncate() — simulates a third-party PSR-7 stream implementation the pool + * might have to reuse, where StreamInterface (PSR-7) doesn't guarantee + * truncate() exists at all. + */ +class WritableSeekableStreamWithoutTruncate implements StreamInterface +{ + private string $content; + private int $position = 0; + + public function __construct(string $content) + { + $this->content = $content; + } + + public function __toString(): string + { + return $this->content; + } + + public function close(): void + { + } + + public function detach() + { + return null; + } + + public function getSize(): ?int + { + return strlen($this->content); + } + + public function tell(): int + { + return $this->position; + } + + public function eof(): bool + { + return $this->position >= strlen($this->content); + } + + public function isSeekable(): bool + { + return true; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + $this->position = $offset; + } + + public function rewind(): void + { + $this->position = 0; + } + + public function isWritable(): bool + { + return true; + } + + public function write(string $string): int + { + // Mimics a real stream write: overwrites from the current position + // without clearing whatever came after it — exactly why skipping + // truncate() is unsafe. + $this->content = substr_replace($this->content, $string, $this->position, strlen($string)); + $this->position += strlen($string); + return strlen($string); + } + + public function isReadable(): bool + { + return true; + } + + public function read(int $length): string + { + $chunk = substr($this->content, $this->position, $length); + $this->position += strlen($chunk); + return $chunk; + } + + public function getContents(): string + { + return substr($this->content, $this->position); + } + + public function getMetadata(?string $key = null) + { + return null; + } + + // Intentionally no truncate() method — that's the point of this class. +}