Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use default php binary instead of respecting PHP_BINARY when automatic binary detection fails for non-CLI SAPIs #50

Merged
merged 5 commits into from
Nov 12, 2021
Merged
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/composer.lock
/examples/users.db
/vendor/
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,33 @@ Let's take these projects to the next level together! 🚀
## Quickstart example

The following example code demonstrates how this library can be used to open an
existing SQLite database file (or automatically create it on first run) and then
`INSERT` a new record to the database:
existing SQLite database file (or automatically create it on the first run) and
then `INSERT` a new record to the database:

```php
<?php

require __DIR__ . '/vendor/autoload.php';

$factory = new Clue\React\SQLite\Factory();
$db = $factory->openLazy(__DIR__ . '/users.db');

$db = $factory->openLazy('users.db');
$db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
$db->exec('CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING)');

$name = 'Alice';
$db->query('INSERT INTO foo (bar) VALUES (?)', [$name])->then(
$db->query('INSERT INTO user (name) VALUES (?)', [$name])->then(
function (Clue\React\SQLite\Result $result) use ($name) {
echo 'New ID for ' . $name . ': ' . $result->insertId . PHP_EOL;
},
function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
);

$db->quit();
```

See also the [examples](examples).
See also the [examples](examples/).

## Usage

Expand Down
14 changes: 8 additions & 6 deletions examples/insert.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
require __DIR__ . '/../vendor/autoload.php';

$factory = new Clue\React\SQLite\Factory();
$db = $factory->openLazy(__DIR__ . '/users.db');

$n = isset($argv[1]) ? $argv[1] : 1;
$db = $factory->openLazy('test.db');

$promise = $db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
$promise->then(null, 'printf');
$db->exec(
'CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING)'
)->then(null, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

$n = isset($argv[1]) ? $argv[1] : 1;
for ($i = 0; $i < $n; ++$i) {
$db->exec("INSERT INTO foo (bar) VALUES ('This is a test')")->then(function (Clue\React\SQLite\Result $result) {
$db->exec("INSERT INTO user (name) VALUES ('Alice')")->then(function (Clue\React\SQLite\Result $result) {
echo 'New row ' . $result->insertId . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
Expand Down
28 changes: 28 additions & 0 deletions examples/query.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

// $ php examples/query.php "INSERT INTO user (name) VALUES ('Bob'),('Carol')"
// $ php examples/query.php "DELETE FROM user WHERE name = ?" "Carol"

require __DIR__ . '/../vendor/autoload.php';

$factory = new Clue\React\SQLite\Factory();
$db = $factory->openLazy(__DIR__ . '/users.db');

$query = isset($argv[1]) ? $argv[1] : 'SELECT 42 AS value';
$args = array_slice(isset($argv) ? $argv : [], 2);

$db->query($query, $args)->then(function (Clue\React\SQLite\Result $result) {
if ($result->columns !== null) {
echo implode("\t", $result->columns) . PHP_EOL;
foreach ($result->rows as $row) {
echo implode("\t", $row) . PHP_EOL;
}
} else {
echo "changed\tid". PHP_EOL;
echo $result->changed . "\t" . $result->insertId . PHP_EOL;
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

$db->quit();
6 changes: 3 additions & 3 deletions examples/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
require __DIR__ . '/../vendor/autoload.php';

$factory = new Clue\React\SQLite\Factory();
$db = $factory->openLazy(__DIR__ . '/users.db');

$search = isset($argv[1]) ? $argv[1] : 'foo';
$db = $factory->openLazy('test.db');
$search = isset($argv[1]) ? $argv[1] : '';

$db->query('SELECT * FROM foo WHERE bar LIKE ?', ['%' . $search . '%'])->then(function (Clue\React\SQLite\Result $result) {
$db->query('SELECT * FROM user WHERE name LIKE ?', ['%' . $search . '%'])->then(function (Clue\React\SQLite\Result $result) {
echo 'Found ' . count($result->rows) . ' rows: ' . PHP_EOL;
echo implode("\t", $result->columns) . PHP_EOL;
foreach ($result->rows as $row) {
Expand Down
27 changes: 17 additions & 10 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,12 @@ private function openProcessIo($filename, $flags = null)
\defined('STDERR') ? \STDERR : \fopen('php://stderr', 'w')
);

// do not inherit open FDs by explicitly overwriting existing FDs with dummy files
// do not inherit open FDs by explicitly overwriting existing FDs with dummy files.
// Accessing /dev/null with null spec requires PHP 7.4+, older PHP versions may be restricted due to open_basedir, so let's reuse STDERR here.
// additionally, close all dummy files in the child process again
foreach ($fds as $fd) {
if ($fd > 2) {
$pipes[$fd] = array('file', '/dev/null', 'r');
$pipes[$fd] = \PHP_VERSION_ID >= 70400 ? ['null'] : $pipes[2];
$command .= ' ' . $fd . '>&-';
}
}
Expand Down Expand Up @@ -375,7 +376,7 @@ private function openSocketIo($filename, $flags = null)
private function which($bin)
{
foreach (\explode(\PATH_SEPARATOR, \getenv('PATH')) as $path) {
if (\is_executable($path . \DIRECTORY_SEPARATOR . $bin)) {
if (@\is_executable($path . \DIRECTORY_SEPARATOR . $bin)) {
return $path . \DIRECTORY_SEPARATOR . $bin;
}
}
Expand All @@ -396,20 +397,26 @@ private function resolve($filename)

/**
* @return string
* @codeCoverageIgnore Covered by `/tests/FunctionalExampleTest.php` instead.
*/
private function php()
{
// if this is the php-cgi binary, check if we can execute the php binary instead
$binary = \PHP_BINARY;
$candidate = \str_replace('-cgi', '', $binary);
if ($candidate !== $binary && \is_executable($candidate)) {
$binary = $candidate; // @codeCoverageIgnore
$binary = 'php';
if (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') {
// use same PHP_BINARY in CLI mode, but do not use same binary for CGI/FPM
$binary = \PHP_BINARY;
} else {
// if this is the php-cgi binary, check if we can execute the php binary instead
$candidate = \str_replace('-cgi', '', \PHP_BINARY);
if ($candidate !== \PHP_BINARY && @\is_executable($candidate)) {
$binary = $candidate;
}
}

// if `php` is a symlink to the php binary, use the shorter `php` name
// this is purely cosmetic feature for the process list
if (\realpath($this->which('php')) === $binary) {
$binary = 'php'; // @codeCoverageIgnore
if ($binary !== 'php' && \realpath($this->which('php')) === $binary) {
$binary = 'php';
}

return $binary;
Expand Down
78 changes: 78 additions & 0 deletions tests/FunctionalExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Clue\Tests\React\SQLite;

use PHPUnit\Framework\TestCase;

class FunctionalExampleTest extends TestCase
{
public function testQueryExampleReturnsDefaultValue()
{
$output = $this->execExample(escapeshellarg(PHP_BINARY) . ' query.php');

$this->assertEquals('value' . "\n" . '42' . "\n", $output);
}

public function testQueryExampleReturnsCalculatedValueFromPlaceholderVariables()
{
$output = $this->execExample(escapeshellarg(PHP_BINARY) . ' query.php "SELECT ?+? AS result" 1 2');

$this->assertEquals('result' . "\n" . '3' . "\n", $output);
}

public function testQueryExampleExecutedWithCgiReturnsDefaultValueAfterContentTypeHeader()
{
if (!$this->canExecute('php-cgi --version')) {
$this->markTestSkipped('Unable to execute "php-cgi"');
}

$output = $this->execExample('php-cgi query.php');

$this->assertStringEndsWith("\n\n" . 'value' . "\n" . '42' . "\n", $output);
}

public function testQueryExampleWithOpenBasedirRestrictedReturnsDefaultValue()
{
$output = $this->execExample(escapeshellarg(PHP_BINARY) . ' -dopen_basedir=' . escapeshellarg(dirname(__DIR__)) . ' query.php');

$this->assertEquals('value' . "\n" . '42' . "\n", $output);
}

public function testQueryExampleWithOpenBasedirRestrictedAndAdditionalFileDescriptorReturnsDefaultValue()
{
if (DIRECTORY_SEPARATOR === '\\') {
$this->markTestSkipped('Not supported on Windows');
}

$output = $this->execExample(escapeshellarg(PHP_BINARY) . ' -dopen_basedir=' . escapeshellarg(dirname(__DIR__)) . ' query.php 3</dev/null');

$this->assertEquals('value' . "\n" . '42' . "\n", $output);
}

public function testQueryExampleExecutedWithCgiAndOpenBasedirRestrictedRunsDefaultPhpAndReturnsDefaultValueAfterContentTypeHeader()
{
if (!$this->canExecute('php-cgi --version') || !$this->canExecute('php --version')) {
$this->markTestSkipped('Unable to execute "php-cgi" or "php"');
}

$output = $this->execExample('php-cgi -dopen_basedir=' . escapeshellarg(dirname(__DIR__)) . ' query.php');

$this->assertStringEndsWith("\n\n" . 'value' . "\n" . '42' . "\n", $output);
}

private function canExecute($command)
{
$code = 1;
$null = DIRECTORY_SEPARATOR === '\\' ? 'NUL' : '/dev/null';
system("$command >$null 2>$null", $code);

return $code === 0;
}

private function execExample($command)
{
chdir(__DIR__ . '/../examples/');

return str_replace("\r\n", "\n", shell_exec($command));
}
}