diff --git a/.gitignore b/.gitignore index c8153b5..0dcaa7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /composer.lock +/examples/users.db /vendor/ diff --git a/README.md b/README.md index bc9441a..4b93a83 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ 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 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 diff --git a/examples/insert.php b/examples/insert.php index 7c2b262..94e6b9f 100644 --- a/examples/insert.php +++ b/examples/insert.php @@ -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; diff --git a/examples/query.php b/examples/query.php new file mode 100644 index 0000000..ed649f2 --- /dev/null +++ b/examples/query.php @@ -0,0 +1,28 @@ +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(); diff --git a/examples/search.php b/examples/search.php index f03f547..21f5986 100644 --- a/examples/search.php +++ b/examples/search.php @@ -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) { diff --git a/src/Factory.php b/src/Factory.php index 245121e..7236bfa 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -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 . '>&-'; } } @@ -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; } } @@ -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; diff --git a/tests/FunctionalExampleTest.php b/tests/FunctionalExampleTest.php new file mode 100644 index 0000000..3036713 --- /dev/null +++ b/tests/FunctionalExampleTest.php @@ -0,0 +1,78 @@ +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 3assertEquals('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)); + } +}