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/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..fbc322f --- /dev/null +++ b/docs/technical/INCONSISTENCIES_REPORT.md @@ -0,0 +1,757 @@ +# 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. + +**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 + +**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()`. + +**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` + +**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()`. + +**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 + +### 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. + +**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 + +**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. + +**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 + +**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. + +**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 + +**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()`. + +**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 + +**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) { +``` + +**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 + +**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. + +**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 + +**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 | 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 | 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()`) | +| M-05 | `LoadShedder::$requestCounts` cresce sem limite | Medio | Depreciado (classe depreciada; remocao v3.0.0) | +| 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) | +| 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 | 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) | + +--- + +## 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..3b3d806 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,16 +317,10 @@ 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); - } - ); + set_exception_handler([$this, 'handleUncaughtException']); } /** @@ -344,11 +343,23 @@ protected function configureErrorHandling(): void } set_error_handler([$this, 'handleError']); - set_exception_handler( - function (Throwable $e): void { - $this->handleException($e); - } - ); + 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(); + } } /** @@ -362,7 +373,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 +418,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 +499,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 +531,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 +544,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 +557,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 +570,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 +598,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 +809,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 +828,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 @@ -802,7 +835,7 @@ public function handleException( ->json( [ 'error' => true, - 'message' => $statusCode === 404 ? 'Not Found' : 'Internal Server Error', + 'message' => Response::defaultErrorMessage($statusCode), 'error_id' => $errorId ] ); @@ -873,7 +906,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 +924,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 +1015,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/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 @@ +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..a64b3e9 --- /dev/null +++ b/src/Http/CustomHeaderCollection.php @@ -0,0 +1,111 @@ + */ + 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() : []; + $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; + } + + /** + * 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/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/Pool/Psr7Pool.php b/src/Http/Pool/Psr7Pool.php index 03eae47..a5af6ec 100644 --- a/src/Http/Pool/Psr7Pool.php +++ b/src/Http/Pool/Psr7Pool.php @@ -242,11 +242,32 @@ 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); + } + + // 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; } /** @@ -310,18 +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()) { + // 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/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/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 diff --git a/src/Http/Psr7/ServerRequest.php b/src/Http/Psr7/ServerRequest.php index 89cb934..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. */ @@ -218,7 +234,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..2c4cd85 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; } @@ -391,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. */ @@ -833,11 +824,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..4c6c0e6 --- /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("PivotPHP 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..d0c88cf 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; } @@ -110,18 +114,17 @@ public function __invoke(Request $request, Response $response, callable $next): public function handle(Request $request, Response $response, callable $next): Response { 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..3f5c364 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/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 { 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..1e1175e 100644 --- a/src/Providers/ExtensionManager.php +++ b/src/Providers/ExtensionManager.php @@ -4,7 +4,7 @@ namespace PivotPHP\Core\Providers; -use PivotPHP\Core\Core\Application; +use PivotPHP\Core\Core\ApplicationInterface; /** * Extension Manager @@ -43,12 +43,12 @@ class ExtensionManager /** * Application instance */ - private Application $app; + private ApplicationInterface $app; /** * Constructor */ - public function __construct(Application $app) + public function __construct(ApplicationInterface $app) { $this->app = $app; } @@ -101,7 +101,9 @@ public function registerExtension(string $name, mixed $extension): void } } - $this->register($name, $extension); + if (is_callable($extension)) { + $this->register($name, $extension); + } } /** @@ -274,7 +276,7 @@ private function updateStats(): void /** * Get application instance */ - public function getApp(): Application + public function getApp(): ApplicationInterface { return $this->app; } 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 +- [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/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/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); + } } 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/Events/EventDispatcherTest.php b/tests/Events/EventDispatcherTest.php new file mode 100644 index 0000000..0faf54b --- /dev/null +++ b/tests/Events/EventDispatcherTest.php @@ -0,0 +1,153 @@ +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()); + } +} 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); + } +} 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/Pool/Psr7PoolTest.php b/tests/Http/Pool/Psr7PoolTest.php index 89e080d..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 */ @@ -508,6 +525,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()); + } } /** @@ -601,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. +} 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..5b05ee7 --- /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'));