This guide documents the exact steps, challenges, and solutions discovered during the actual development and integration of ReactPHP with PivotPHP Core.
- Prerequisites & Setup
- PSR-7 Compatibility Challenge
- Request/Response Type Conversion
- Testing Implementation
- Common Pitfalls & Solutions
- Performance Validation
- Production Deployment
# 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 --checkKey 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/socketWhat 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
Initially, we encountered this error:
Fatal error: Declaration of React\Http\Io\AbstractMessage::getProtocolVersion()
must be compatible with Psr\Http\Message\MessageInterface::getProtocolVersion(): string
- Custom PSR-7 Adapter: Created complex wrapper classes
- Version Forcing: Tried to force ReactPHP to use PSR-7 v2.x
- Manual Interface Implementation: Attempted to bridge manually
# PivotPHP Core already had the solution built-in!
php vendor/pivotphp/core/scripts/switch-psr7-version.php 1
composer updateLesson Learned: Always check framework documentation and built-in tools before implementing custom solutions.
Problem: PivotPHP's Application::handle() expects PivotPHP\Core\Http\Request, but ReactPHP provides Psr\Http\Message\ServerRequestInterface.
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');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;
}
}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();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;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();
}
}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);
}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');
}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);
}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');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();
}Recent Enhancements (v0.0.2+):
- Output Buffer Isolation: TestCase now properly manages output buffers to prevent test interference
- Callback Verification: AssertionHelper provides reliable callback testing utilities
- Specific Assertions: Tests use exact status codes instead of ranges for clear expectations
- 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');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 loadKey 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']);
}
}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-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();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.logProduction .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=trueHealth 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);
});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
- ✅ PSR-7 Dual Support: Using built-in version switching
- ✅ Request Bridge: Proper global state management
- ✅ Express.js Pattern: Response as parameter, not return value
- ✅ Header Handling: Understanding camelCase conversion
- ✅ Testing Strategy: Comprehensive test coverage
- ✅ Performance Focus: Event-driven, non-blocking execution
- Start with PSR-7 v1.x from the beginning
- Design with continuous runtime in mind
- Use dependency injection for all services
- Plan for connection persistence
- Switch PSR-7 version first:
php scripts/switch-psr7-version.php 1 - Update controllers to Express.js style
- Test thoroughly with validation project
- Deploy gradually with load balancing
- Use the provided validation project as template
- Run quality checks:
composer quality:check - Test with real load scenarios
- 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.