Skip to content

Latest commit

 

History

History
531 lines (395 loc) · 14.3 KB

File metadata and controls

531 lines (395 loc) · 14.3 KB

PivotPHP ReactPHP Implementation Guide

Based on Real Development Experience

This guide documents the exact steps, challenges, and solutions discovered during the actual development and integration of ReactPHP with PivotPHP Core.

Table of Contents

  1. Prerequisites & Setup
  2. PSR-7 Compatibility Challenge
  3. Request/Response Type Conversion
  4. Testing Implementation
  5. Common Pitfalls & Solutions
  6. Performance Validation
  7. Production Deployment

Prerequisites & Setup

Initial Environment Setup

# Start with a clean environment
mkdir my-reactphp-project
cd my-reactphp-project

# Install PivotPHP Core first
composer require pivotphp/core

# Check current PSR-7 version
php vendor/pivotphp/core/scripts/switch-psr7-version.php --check

The Critical PSR-7 Discovery

Key Learning: PivotPHP Core v1.0.1+ includes built-in PSR-7 dual support that we initially missed!

# The magic command that solved our compatibility issues
php vendor/pivotphp/core/scripts/switch-psr7-version.php 1
composer update psr/http-message

# Install ReactPHP (now it works!)
composer require react/http react/socket

What this script does:

  • Removes return type declarations from PSR-7 classes
  • Updates composer.json to require PSR-7 ^1.1
  • Adds PHPDoc annotations for IDE support
  • Makes ReactPHP installation seamless

PSR-7 Compatibility Challenge

The Problem We Faced

Initially, we encountered this error:

Fatal error: Declaration of React\Http\Io\AbstractMessage::getProtocolVersion() 
must be compatible with Psr\Http\Message\MessageInterface::getProtocolVersion(): string

Failed Approaches

  1. Custom PSR-7 Adapter: Created complex wrapper classes
  2. Version Forcing: Tried to force ReactPHP to use PSR-7 v2.x
  3. Manual Interface Implementation: Attempted to bridge manually

The Winning Solution

# PivotPHP Core already had the solution built-in!
php vendor/pivotphp/core/scripts/switch-psr7-version.php 1
composer update

Lesson Learned: Always check framework documentation and built-in tools before implementing custom solutions.

Request/Response Type Conversion

The Type Mismatch Challenge

Problem: PivotPHP's Application::handle() expects PivotPHP\Core\Http\Request, but ReactPHP provides Psr\Http\Message\ServerRequestInterface.

Understanding PivotPHP Request Architecture

Key discovery: PivotPHP Request class is immutable by design:

// ❌ This doesn't work - no setter methods
$pivotRequest->headers->set('content-type', 'application/json');
$pivotRequest->query->page = 2;
$pivotRequest->body->username = 'newuser';

// ✅ This works - data is set during construction from globals
$_SERVER['HTTP_CONTENT_TYPE'] = 'application/json';
$_GET['page'] = '2';
$_POST['username'] = 'newuser';
$pivotRequest = new Request('POST', '/users', '/users');

RequestBridge Implementation Strategy

The solution was to manipulate global state temporarily:

public function convertFromReact(ServerRequestInterface $reactRequest): \PivotPHP\Core\Http\Request
{
    // Save current global state
    $originalServer = $_SERVER ?? [];
    $originalGet = $_GET ?? [];
    $originalPost = $_POST ?? [];
    
    try {
        // Set up globals for PivotPHP Request
        $_SERVER = $this->prepareServerVariables($reactRequest);
        $_GET = $reactRequest->getQueryParams();
        $_POST = $this->preparePostData($reactRequest);
        
        // Create PivotPHP Request (reads from globals)
        $pivotRequest = new \PivotPHP\Core\Http\Request(
            $reactRequest->getMethod(),
            $uri->getPath(),
            $uri->getPath()
        );
        
        return $pivotRequest;
        
    } finally {
        // Always restore original state
        $_SERVER = $originalServer;
        $_GET = $originalGet;
        $_POST = $originalPost;
    }
}

Header Name Conversion Discovery

Critical Finding: PivotPHP converts header names to camelCase:

// Headers are converted during Request construction:
// 'Content-Type' → 'contentType'
// 'Authorization' → 'authorization'  
// 'X-API-Key' → 'xApiKey'
// 'Accept-Language' → 'acceptLanguage'

// ❌ Wrong way to access headers
$contentType = $request->header('Content-Type'); // Returns null

// ✅ Correct way to access headers
$contentType = $request->header('contentType');
$auth = $request->header('authorization');
$apiKey = $request->header('xApiKey');

// ✅ Alternative access methods
$contentType = $request->headers->contentType;
$contentType = $request->headers->contentType();

Testing Implementation

Test Environment Setup

Key Learning: Testing required understanding PivotPHP's factory classes:

// ❌ This class doesn't exist
use PivotPHP\Core\Http\Factory\Psr17Factory;

// ✅ Correct factory classes
use PivotPHP\Core\Http\Psr7\Factory\RequestFactory;
use PivotPHP\Core\Http\Psr7\Factory\ResponseFactory;
use PivotPHP\Core\Http\Psr7\Factory\ServerRequestFactory;
use PivotPHP\Core\Http\Psr7\Factory\StreamFactory;
use PivotPHP\Core\Http\Psr7\Factory\UriFactory;

Corrected Test Structure

abstract class TestCase extends BaseTestCase
{
    protected Application $app;
    protected LoopInterface $loop;
    protected RequestFactory $requestFactory;
    protected ResponseFactory $responseFactory;
    protected ServerRequestFactory $serverRequestFactory;
    protected StreamFactory $streamFactory;
    protected UriFactory $uriFactory;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->loop = Loop::get();
        $this->requestFactory = new RequestFactory();
        $this->responseFactory = new ResponseFactory();
        $this->serverRequestFactory = new ServerRequestFactory();
        $this->streamFactory = new StreamFactory();
        $this->uriFactory = new UriFactory();
        $this->app = $this->createApplication();
    }
}

Express.js Style Route Testing

Discovery: PivotPHP uses Express.js pattern where response is passed as parameter:

// ❌ Wrong pattern (tried to return response)
$router->get('/users', function ($request) {
    return Response::json(['users' => []]);
});

// ✅ Correct Express.js pattern (response as parameter)
$router->get('/users', function ($request, $response) {
    $response->json(['users' => []]);
});

// ✅ Controller pattern
public function index(Request $request, Response $response): void
{
    $response->json($data);
}

Common Pitfalls & Solutions

1. Container Method Confusion

Problem: Assumed Container didn't have has() method

Reality: PivotPHP Container correctly implements PSR-11:

// ✅ This works perfectly
if ($app->has('router')) {
    $router = $app->make('router');
}

2. Response Pattern Misunderstanding

Problem: Tried to return Response objects from controllers

Solution: Use Express.js style with response parameter:

// ❌ Wrong
public function index(Request $request): Response
{
    return Response::json($data);
}

// ✅ Correct
public function index(Request $request, Response $response): void
{
    $response->json($data);
}

3. Service Provider Boot Order

Problem: Accessing services before application boot

Solution: Always boot before accessing services:

// ✅ Correct order
$app->register(new AppServiceProvider());
$app->boot(); // Boot first!

// Now services are available
$config = $app->make('config');

4. Event Loop Management in Tests

Problem: Loop::create() method doesn't exist in ReactPHP

Solution: Use existing loop instance:

protected function tearDown(): void
{
    $this->loop->stop(); // Just stop, don't recreate
    parent::tearDown();
}

5. Test Quality Improvements

Recent Enhancements (v0.0.2+):

  1. Output Buffer Isolation: TestCase now properly manages output buffers to prevent test interference
  2. Callback Verification: AssertionHelper provides reliable callback testing utilities
  3. Specific Assertions: Tests use exact status codes instead of ranges for clear expectations
  4. PHPUnit Best Practices: Proper instance method usage for expectNotToPerformAssertions()
// ✅ Improved callback testing
[$wrapper, $verifier] = AssertionHelper::createCallbackVerifier($this, $callback, $expectedArgs);
$result = $wrapper('arg1', 'arg2');
$verifier(); // Verify callback was called with correct arguments

// ✅ Specific status code assertions
$this->assertEquals(400, $response->getStatusCode()); // Not 400 || 500

// ✅ Proper header testing without automatic headers
$request = (new ServerRequest('GET', new Uri('http://example.com')))->withoutHeader('Host');

Performance Validation

Benchmarking Results

From our validation project testing:

# Traditional PHP-FPM baseline
wrk -t12 -c400 -d30s http://localhost/traditional

# ReactPHP implementation  
wrk -t12 -c400 -d30s http://localhost:8080/

# Results showed significant improvements:
# - 2-3x higher throughput
# - 50% lower memory per request
# - Faster response times under load

Memory Management

Key Insight: Continuous runtime means shared state:

// Application and services persist across requests
class MyController 
{
    private static $cache = []; // Shared across all requests
    
    public function index($request, $response)
    {
        // Cache persists for lifetime of server
        if (!isset(self::$cache['expensive_data'])) {
            self::$cache['expensive_data'] = $this->fetchExpensiveData();
        }
        
        $response->json(self::$cache['expensive_data']);
    }
}

Connection Persistence

Benefit: Database connections stay alive:

// Connection established once, reused for all requests
$app->singleton('database', function() {
    return new PDO($dsn, $user, $pass, [
        PDO::ATTR_PERSISTENT => true
    ]);
});

Production Deployment

Server Configuration

Production-ready server script (server.php):

<?php

declare(strict_types=1);

use PivotPHP\Core\Core\Application;
use PivotPHP\ReactPHP\Server\ReactServer;
use React\EventLoop\Loop;

// Bootstrap application
$app = require __DIR__ . '/bootstrap/app.php';

// Configuration
$host = $_ENV['REACTPHP_HOST'] ?? '0.0.0.0';
$port = $_ENV['REACTPHP_PORT'] ?? 8080;
$workers = $_ENV['REACTPHP_WORKERS'] ?? 1;

// Server setup
$loop = Loop::get();
$server = new ReactServer($app, $loop);

// Graceful shutdown
pcntl_signal(SIGTERM, function() use ($server) {
    echo "Received SIGTERM, shutting down gracefully...\n";
    $server->stop();
});

pcntl_signal(SIGINT, function() use ($server) {
    echo "Received SIGINT, shutting down gracefully...\n";
    $server->stop();
});

// Start server
echo "🚀 Production ReactPHP Server\n";
echo "📍 Host: {$host}:{$port}\n";
echo "⚡ Workers: {$workers}\n";
echo "🔧 PHP: " . PHP_VERSION . "\n";
echo "💾 Memory: " . ini_get('memory_limit') . "\n";
echo "🛑 Press Ctrl+C to stop\n\n";

$server->listen("{$host}:{$port}");
$loop->run();

Process Management

Supervisor configuration (/etc/supervisor/conf.d/reactphp.conf):

[program:reactphp]
command=php /var/www/server.php
directory=/var/www
user=www-data
autostart=true
autorestart=true
stderr_logfile=/var/log/reactphp.err.log
stdout_logfile=/var/log/reactphp.out.log

Environment Variables

Production .env example:

APP_ENV=production
APP_DEBUG=false

REACTPHP_HOST=0.0.0.0
REACTPHP_PORT=8080
REACTPHP_WORKERS=4
REACTPHP_MEMORY_LIMIT=512M

# Database settings (persistent connections recommended)
DB_CONNECTION=mysql
DB_PERSISTENT=true
DB_POOL_SIZE=10

# Cache settings
CACHE_DRIVER=redis
REDIS_PERSISTENT=true

Monitoring & Logging

Health check endpoint:

$app->get('/health', function ($request, $response) {
    $health = [
        'status' => 'healthy',
        'timestamp' => time(),
        'uptime' => time() - $_SERVER['REQUEST_TIME'],
        'memory_usage' => memory_get_usage(true),
        'memory_peak' => memory_get_peak_usage(true),
        'memory_limit' => ini_get('memory_limit')
    ];
    
    $response->json($health);
});

Validation Project Structure

The complete validation project we built includes:

pivotphp-validation/
├── src/
│   ├── Controllers/           # Express.js style controllers
│   ├── Providers/            # Service providers  
│   └── Models/               # Data models
├── scripts/
│   ├── server-reactphp.php   # Production server script
│   └── debug-reactphp.php    # Debug mode server
├── docs/
│   ├── ISSUES_AND_FIXES.md   # Problems and solutions
│   ├── RESOLVED.md           # Resolved implementation issues
│   └── FINAL_TEST_RESULT.md  # Success validation
└── CLAUDE.md                 # Project context and commands

Key Success Factors

  1. ✅ PSR-7 Dual Support: Using built-in version switching
  2. ✅ Request Bridge: Proper global state management
  3. ✅ Express.js Pattern: Response as parameter, not return value
  4. ✅ Header Handling: Understanding camelCase conversion
  5. ✅ Testing Strategy: Comprehensive test coverage
  6. ✅ Performance Focus: Event-driven, non-blocking execution

Final Recommendations

For New Projects

  1. Start with PSR-7 v1.x from the beginning
  2. Design with continuous runtime in mind
  3. Use dependency injection for all services
  4. Plan for connection persistence

For Existing Projects

  1. Switch PSR-7 version first: php scripts/switch-psr7-version.php 1
  2. Update controllers to Express.js style
  3. Test thoroughly with validation project
  4. Deploy gradually with load balancing

Development Workflow

  1. Use the provided validation project as template
  2. Run quality checks: composer quality:check
  3. Test with real load scenarios
  4. Monitor memory usage in production

This implementation guide reflects the real challenges, discoveries, and solutions from actually building the ReactPHP integration with PivotPHP Core. The key insight is that PivotPHP Core v1.0.1+ already includes most of the necessary compatibility features - you just need to know how to activate and use them properly.