Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"php-mock/php-mock-phpunit": "^2.10",
"phpstan/phpstan": "^1.12",
"nyholm/psr7": "^1.8",
"guzzlehttp/guzzle": "^7.8",
"ext-json": "*",
"ext-xml": "*",
"ext-curl": "*",
Expand Down
8 changes: 8 additions & 0 deletions test/known_failures_Linux.json
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@
"nodejs-nodejs_compact_websocket-domain",
"nodejs-nodejs_header_websocket-domain",
"nodejs-nodejs_json_websocket-domain",
"nodejs-php_binary-accel_http-ip",
"nodejs-php_binary_http-ip",
"nodejs-php_compact_http-ip",
"nodejs-php_json_http-ip",
"nodejs-py_binary-accel_http-domain",
"nodejs-py_binary-accel_http-ip",
"nodejs-py_binary-accel_http-ip-ssl",
Expand Down Expand Up @@ -644,6 +648,10 @@
"perl-netstd_multi-binary_buffered-ip-ssl",
"perl-netstd_multi-binary_framed-ip",
"perl-netstd_multi-binary_framed-ip-ssl",
"php-cpp_accel-binary_http-ip",
"php-cpp_binary_http-ip",
"php-cpp_compact_http-ip",
"php-cpp_json_http-ip",
"py-cpp_accel-binary_http-domain",
"py-cpp_accel-binary_http-ip",
"py-cpp_accel-binary_http-ip-ssl",
Expand Down
46 changes: 46 additions & 0 deletions test/php/HttpRouter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Router script for PHP's built-in web server (`php -S`).
*
* Invoked once per HTTP request by the cli-server SAPI. The protocol is
* selected via the THRIFT_TEST_PROTOCOL environment variable, set by the
* cross-test launcher in TestServer.php before pcntl_exec'ing into php -S.
*/

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/protocols.php';
require_once __DIR__ . '/HttpServer.php';

// ThriftTest generated classes are not in Composer's PSR-4 map; register the
// classmap loader before requiring Handler.php so its `implements ThriftTestIf`
// resolves correctly.
$loader = new \Thrift\ClassLoader\ThriftClassLoader();
$loader->registerDefinition('ThriftTest', __DIR__ . '/gen-php-classmap');
$loader->register();

require_once __DIR__ . '/Handler.php';

$protocolFactory = thrift_test_protocol_factory(getenv('THRIFT_TEST_PROTOCOL') ?: 'binary');
$processor = new ThriftTest\ThriftTestProcessor(new Handler());

(new HttpServer($processor, $protocolFactory))->serve();
117 changes: 117 additions & 0 deletions test/php/HttpServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

declare(strict_types=1);

use Thrift\Factory\TProtocolFactory;
use Thrift\Transport\TMemoryBuffer;
use Thrift\Type\TMessageType;

/**
* Cross-test HTTP request handler for PHP's built-in web server (`php -S`).
*
* Reads php://input, peeks the Thrift message type, and for ONEWAY sends an
* empty HTTP 200 immediately while running the handler in a forked child so
* the client's one-way call returns without waiting for handler execution.
*
* Cli-server specific: closing STDIN/STDOUT/STDERR in the child relies on
* php -S worker semantics and does not translate to php-fpm or mod_php.
*/
class HttpServer
{
public function __construct(
private object $processor,
private TProtocolFactory $protocolFactory,
) {
}

public function serve(): void
{
$requestBody = (string) file_get_contents('php://input');

header('Content-Type: application/x-thrift');

if ($this->peekMessageType($requestBody) === TMessageType::ONEWAY
&& function_exists('pcntl_fork')
&& $this->dispatchOnewayAsync($requestBody)
) {
return;
}

$this->dispatchSync($requestBody);
}

private function peekMessageType(string $body): ?int
{
if ($body === '') {
return null;
}
try {
$type = null;
$name = null;
$seqid = null;
$this->protocolFactory
->getProtocol(new TMemoryBuffer($body))
->readMessageBegin($name, $type, $seqid);

return $type;
} catch (\Throwable) {
return null;
}
}

private function dispatchOnewayAsync(string $body): bool
{
header('Content-Length: 0');
$pid = pcntl_fork();
if ($pid > 0) {
return true;
}
if ($pid === 0) {
if (defined('STDIN')) {
fclose(STDIN);
}
if (defined('STDOUT')) {
fclose(STDOUT);
}
if (defined('STDERR')) {
fclose(STDERR);
}
$this->processor->process(
$this->protocolFactory->getProtocol(new TMemoryBuffer($body)),
$this->protocolFactory->getProtocol(new TMemoryBuffer()),
);
exit(0);
}

return false;
}

private function dispatchSync(string $body): void
{
$output = new TMemoryBuffer();
$this->processor->process(
$this->protocolFactory->getProtocol(new TMemoryBuffer($body)),
$this->protocolFactory->getProtocol($output),
);
echo $output->getBuffer();
}
}
45 changes: 19 additions & 26 deletions test/php/TestClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,10 @@
use Thrift\Protocol\TCompactProtocol;
use Thrift\Protocol\TJSONProtocol;

/** Include the socket layer */
use Thrift\Transport\TSocket;
use Thrift\Transport\TSocketPool;

/** Include the socket layer */
use Thrift\Transport\TFramedTransport;
use Thrift\Transport\TBufferedTransport;
use Thrift\Transport\TFramedTransport;
use Thrift\Transport\TPsrHttpClient;
use Thrift\Transport\TSocketPool;

/**
* Minimal PSR-3 logger that forwards every message to PHP's error_log
Expand Down Expand Up @@ -119,26 +116,22 @@ function makeProtocol($transport, $PROTO)
}
}

$hosts = array('localhost');

$logger = new StderrLogger();
$socket = new TSocket($host, $port, false, $logger);
$socket = new TSocketPool($hosts, $port, false, $logger);

if ($MODE == 'inline') {
$transport = $socket;
$testClient = new \ThriftTest\ThriftTestClient($transport);
} else if ($MODE == 'framed') {
$framedSocket = new TFramedTransport($socket);
$transport = $framedSocket;
$protocol = makeProtocol($transport, $PROTO);
$testClient = new \ThriftTest\ThriftTestClient($protocol);
} else {
$bufferedSocket = new TBufferedTransport($socket, 1024, 1024);
$transport = $bufferedSocket;
$protocol = makeProtocol($transport, $PROTO);
$testClient = new \ThriftTest\ThriftTestClient($protocol);
}
// TPsrHttpClient buffers internally, so no framed/buffered wrapper is needed.
// Inline mode passes the raw transport to the generated client without a
// protocol wrapper, matching the legacy code path.
$transport = match ($MODE) {
'http' => new TPsrHttpClient(sprintf('http://127.0.0.1:%d/', $port)),
default => new TSocketPool(['localhost'], $port, false, new StderrLogger()),
};
$transport = match ($MODE) {
'framed' => new TFramedTransport($transport),
'http', 'inline' => $transport,
default => new TBufferedTransport($transport, 1024, 1024),
};

$testClient = new \ThriftTest\ThriftTestClient(
$MODE === 'inline' ? $transport : makeProtocol($transport, $PROTO)
);

$transport->open();

Expand Down
68 changes: 47 additions & 21 deletions test/php/TestServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
error_reporting(E_ALL);

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/protocols.php';

$opts = getopt(
'h::',
Expand Down Expand Up @@ -47,6 +48,51 @@
$transport = $opts['transport'] ?? 'buffered';
$protocol = $opts['protocol'] ?? 'binary';

// HTTP transport: re-exec under PHP's built-in web server with HttpRouter.php
// as the per-request handler. php -S TCP-binds the port so the cross-runner's
// readiness probe (socket.connect_ex) succeeds; the router script runs the
// Thrift HTTP server (see Thrift\Server\THttpServer) for each request.
if ($transport === 'http') {
if (!function_exists('pcntl_exec')) {
fwrite(STDERR, "PHP HTTP cross-test requires ext-pcntl.\n");
exit(1);
}
$cmdline = @file_get_contents('/proc/self/cmdline');
if ($cmdline === false) {
fwrite(STDERR, "PHP HTTP cross-test requires /proc/self/cmdline (Linux).\n");
exit(1);
}
$parts = explode("\0", rtrim($cmdline, "\0"));
// parts[0] = php binary, then -d/-z flags, then this script, then script args.
$scriptIdx = null;
foreach ($parts as $idx => $part) {
if ($idx > 0 && basename($part) === basename(__FILE__)) {
$scriptIdx = $idx;
break;
}
}
if ($scriptIdx === null) {
fwrite(STDERR, "Cannot locate script name in /proc/self/cmdline.\n");
exit(1);
}
// pcntl_exec does NOT do PATH resolution — use the absolute path from
// PHP_BINARY rather than argv[0], which may be a bare "php".
$phpBinary = PHP_BINARY;
$phpFlags = array_slice($parts, 1, $scriptIdx - 1);

// $_ENV is empty under the default variables_order=GPCS, so build the env
// for pcntl_exec from getenv() and inject our protocol selector.
$env = getenv();
$env['THRIFT_TEST_PROTOCOL'] = $protocol;

$newArgs = array_merge($phpFlags, ['-S', '127.0.0.1:' . $port, __DIR__ . '/HttpRouter.php']);

echo "Starting the Test server (HTTP via php -S)...\n";

pcntl_exec($phpBinary, $newArgs, $env);
fwrite(STDERR, "pcntl_exec failed\n");
exit(1);
}

$loader = new Thrift\ClassLoader\ThriftClassLoader();
$loader->registerDefinition('ThriftTest', __DIR__ . '/gen-php-classmap');
Expand All @@ -71,27 +117,7 @@
$serverTransportFactory = new \Thrift\Factory\TTransportFactory();
}

switch ($protocol) {
case 'binary':
$protocolFactory = new \Thrift\Factory\TBinaryProtocolFactory(false, true);
break;
case 'accel':
if (!function_exists('thrift_protocol_write_binary')) {
fwrite(STDERR, "Acceleration extension is not loaded\n");
exit(1);
}
$protocolFactory = new \Thrift\Factory\TBinaryProtocolAcceleratedFactory();
break;
case 'compact':
$protocolFactory = new \Thrift\Factory\TCompactProtocolFactory();
break;
case 'json':
$protocolFactory = new \Thrift\Factory\TJSONProtocolFactory();
break;
default:
fwrite(STDERR, "--protocol must be one of {binary|compact|json|accel}\n");
exit(1);
}
$protocolFactory = thrift_test_protocol_factory($protocol);

// `localhost` may resolve to an IPv6-only listener in newer PHP/runtime combinations,
// while some cross-test clients still connect via 127.0.0.1. Bind explicitly to IPv4.
Expand Down
49 changes: 49 additions & 0 deletions test/php/protocols.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Build a protocol factory by cross-test protocol name. Shared between the
* socket-server CLI entry point (TestServer.php) and the cli-server HTTP
* router (HttpRouter.php).
*
* Uses error_log() rather than STDERR because the constant is undefined in
* the cli-server SAPI.
*/
function thrift_test_protocol_factory(string $protocol): \Thrift\Factory\TProtocolFactory
{
switch ($protocol) {
case 'binary':
return new \Thrift\Factory\TBinaryProtocolFactory(false, true);
case 'accel':
if (!function_exists('thrift_protocol_write_binary')) {
error_log('Acceleration extension is not loaded');
exit(1);
}
return new \Thrift\Factory\TBinaryProtocolAcceleratedFactory();
case 'compact':
return new \Thrift\Factory\TCompactProtocolFactory();
case 'json':
return new \Thrift\Factory\TJSONProtocolFactory();
default:
error_log('--protocol must be one of {binary|compact|json|accel}');
exit(1);
}
}
Loading
Loading