Skip to content

Commit 52273c7

Browse files
committed
Refactor to use dedicated Factory to open Database instance
This is done to achieve better separation of concerns in preparation for creating multiple database launchers for Windows support in the future.
1 parent dc98482 commit 52273c7

File tree

6 files changed

+216
-152
lines changed

6 files changed

+216
-152
lines changed

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ built on top of [ReactPHP](https://reactphp.org/).
77

88
* [Quickstart example](#quickstart-example)
99
* [Usage](#usage)
10+
* [Factory](#factory)
11+
* [open()](#open)
1012
* [Database](#database)
1113
* [exec()](#exec)
1214
* [query()](#query)
@@ -27,9 +29,10 @@ existing SQLite database file (or automatically create it on first run) and then
2729

2830
```php
2931
$loop = React\EventLoop\Factory::create();
32+
$factory = new Clue\React\SQLite\Factory($loop);
3033

3134
$name = 'Alice';
32-
Clue\React\SQLite\Database::open($loop, 'users.db')->then(
35+
$factory->open('users.db')->then(
3336
function (Clue\React\SQLite\Database $db) use ($name) {
3437
$db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
3538

@@ -53,15 +56,19 @@ See also the [examples](examples).
5356

5457
## Usage
5558

56-
### Database
59+
### Factory
5760

58-
The `Database` class represents a connection that is responsible for
59-
comunicating with your SQLite database wrapper, managing the connection state
60-
and sending your database queries.
61+
The `Factory` is responsible for opening your [`Database`](#database) instance.
62+
It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage).
63+
64+
```php
65+
$loop = React\EventLoop\Factory::create();
66+
$factory = new Clue\React\SQLite\Factory($loop);
67+
```
6168

6269
#### open()
6370

64-
The static `open(LoopInterface $loop, string $filename, int $flags = null): PromiseInterface<Database>` method can be used to
71+
The `open(string $filename, int $flags = null): PromiseInterface<Database>` method can be used to
6572
open a new database connection for the given SQLite database file.
6673

6774
This method returns a promise that will resolve with a `Database` on
@@ -71,7 +78,7 @@ to run all SQLite commands and queries in a separate process without
7178
blocking the main process.
7279

7380
```php
74-
Database::open($loop, 'users.db')->then(function (Database $db) {
81+
$factory->open('users.db')->then(function (Database $db) {
7582
// database ready
7683
// $db->query('INSERT INTO users (name) VALUES ("test")');
7784
// $db->quit();
@@ -84,14 +91,20 @@ The optional `$flags` parameter is used to determine how to open the
8491
SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
8592

8693
```php
87-
Database::open($loop, 'users.db', SQLITE3_OPEN_READONLY)->then(function (Database $db) {
94+
$factory->open('users.db', SQLITE3_OPEN_READONLY)->then(function (Database $db) {
8895
// database ready (read-only)
8996
// $db->quit();
9097
}, function (Exception $e) {
9198
echo 'Error: ' . $e->getMessage() . PHP_EOL;
9299
});
93100
```
94101

102+
### Database
103+
104+
The `Database` class represents a connection that is responsible for
105+
comunicating with your SQLite database wrapper, managing the connection state
106+
and sending your database queries.
107+
95108
#### exec()
96109

97110
The `exec(string $query): PromiseInterface<Result>` method can be used to

examples/insert.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?php
22

3-
use React\EventLoop\Factory;
43
use Clue\React\SQLite\Database;
4+
use Clue\React\SQLite\Factory;
55
use Clue\React\SQLite\Result;
66

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

9-
$loop = Factory::create();
9+
$loop = React\EventLoop\Factory::create();
10+
$factory = new Factory($loop);
1011

1112
$n = isset($argv[1]) ? $argv[1] : 1;
12-
Database::open($loop, 'test.db')->then(function (Database $db) use ($n) {
13+
$factory->open('test.db')->then(function (Database $db) use ($n) {
1314
$db->exec('CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY AUTOINCREMENT, bar STRING)');
1415

1516
for ($i = 0; $i < $n; ++$i) {

examples/search.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?php
22

3-
use React\EventLoop\Factory;
43
use Clue\React\SQLite\Database;
4+
use Clue\React\SQLite\Factory;
55
use Clue\React\SQLite\Result;
66

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

9-
$loop = Factory::create();
9+
$loop = React\EventLoop\Factory::create();
10+
$factory = new Factory($loop);
1011

1112
$search = isset($argv[1]) ? $argv[1] : 'foo';
12-
Database::open($loop, 'test.db')->then(function (Database $db) use ($search){
13+
$factory->open('test.db')->then(function (Database $db) use ($search){
1314
$db->query('SELECT * FROM foo WHERE bar LIKE ?', ['%' . $search . '%'])->then(function (Result $result) {
1415
echo 'Found ' . count($result->rows) . ' rows: ' . PHP_EOL;
1516
echo implode("\t", $result->columns) . PHP_EOL;

src/Database.php

Lines changed: 8 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Clue\React\NDJson\Decoder;
66
use Evenement\EventEmitter;
77
use React\ChildProcess\Process;
8-
use React\EventLoop\LoopInterface;
98
use React\Promise\Deferred;
109
use React\Promise\PromiseInterface;
1110

@@ -45,109 +44,17 @@
4544
*/
4645
class Database extends EventEmitter
4746
{
48-
/**
49-
* Opens a new database connection for the given SQLite database file.
50-
*
51-
* This method returns a promise that will resolve with a `Database` on
52-
* success or will reject with an `Exception` on error. The SQLite extension
53-
* is inherently blocking, so this method will spawn an SQLite worker process
54-
* to run all SQLite commands and queries in a separate process without
55-
* blocking the main process.
56-
*
57-
* ```php
58-
* Database::open($loop, 'users.db')->then(function (Database $db) {
59-
* // database ready
60-
* // $db->query('INSERT INTO users (name) VALUES ("test")');
61-
* // $db->quit();
62-
* }, function (Exception $e) {
63-
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
64-
* });
65-
* ```
66-
*
67-
* The optional `$flags` parameter is used to determine how to open the
68-
* SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
69-
*
70-
* ```php
71-
* Database::open($loop, 'users.db', SQLITE3_OPEN_READONLY)->then(function (Database $db) {
72-
* // database ready (read-only)
73-
* // $db->quit();
74-
* }, function (Exception $e) {
75-
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
76-
* });
77-
* ```
78-
*
79-
* @param LoopInterface $loop
80-
* @param string $filename
81-
* @param ?int $flags
82-
* @return PromiseInterface<Database> Resolves with Database instance or rejects with Exception
83-
*/
84-
public static function open(LoopInterface $loop, $filename, $flags = null)
85-
{
86-
$command = 'exec ' . \escapeshellarg(\PHP_BINARY) . ' ' . \escapeshellarg(__DIR__ . '/../res/sqlite-worker.php');
87-
88-
// Try to get list of all open FDs (Linux/Mac and others)
89-
$fds = @\scandir('/dev/fd');
90-
91-
// Otherwise try temporarily duplicating file descriptors in the range 0-1024 (FD_SETSIZE).
92-
// This is known to work on more exotic platforms and also inside chroot
93-
// environments without /dev/fd. Causes many syscalls, but still rather fast.
94-
// @codeCoverageIgnoreStart
95-
if ($fds === false) {
96-
$fds = array();
97-
for ($i = 0; $i <= 1024; ++$i) {
98-
$copy = @\fopen('php://fd/' . $i, 'r');
99-
if ($copy !== false) {
100-
$fds[] = $i;
101-
\fclose($copy);
102-
}
103-
}
104-
}
105-
// @codeCoverageIgnoreEnd
106-
107-
// launch process with default STDIO pipes
108-
$pipes = array(
109-
array('pipe', 'r'),
110-
array('pipe', 'w'),
111-
array('pipe', 'w')
112-
);
113-
114-
// do not inherit open FDs by explicitly overwriting existing FDs with dummy files
115-
// additionally, close all dummy files in the child process again
116-
foreach ($fds as $fd) {
117-
if ($fd > 2) {
118-
$pipes[$fd] = array('file', '/dev/null', 'r');
119-
$command .= ' ' . $fd . '>&-';
120-
}
121-
}
122-
123-
// default `sh` only accepts single-digit FDs, so run in bash if needed
124-
if ($fds && \max($fds) > 9) {
125-
$command = 'exec bash -c ' . \escapeshellarg($command);
126-
}
127-
128-
$process = new Process($command, null, null, $pipes);
129-
$process->start($loop);
130-
131-
$db = new Database($process);
132-
$args = array($filename);
133-
if ($flags !== null) {
134-
$args[] = $flags;
135-
}
136-
137-
return $db->send('open', $args)->then(function () use ($db) {
138-
return $db;
139-
}, function ($e) use ($db) {
140-
$db->close();
141-
throw $e;
142-
});
143-
}
144-
14547
private $process;
14648
private $pending = array();
14749
private $id = 0;
14850
private $closed = false;
14951

150-
private function __construct(Process $process)
52+
/**
53+
* @internal see Factory instead
54+
* @see Factory
55+
* @param Process $process
56+
*/
57+
public function __construct(Process $process)
15158
{
15259
$this->process = $process;
15360

@@ -363,7 +270,8 @@ public function close()
363270
$this->removeAllListeners();
364271
}
365272

366-
private function send($method, array $params)
273+
/** @internal */
274+
public function send($method, array $params)
367275
{
368276
if (!$this->process->stdin->isWritable()) {
369277
return \React\Promise\reject(new \RuntimeException('Database closed'));

src/Factory.php

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
namespace Clue\React\SQLite;
4+
5+
use React\ChildProcess\Process;
6+
use React\EventLoop\LoopInterface;
7+
8+
class Factory
9+
{
10+
private $loop;
11+
12+
/**
13+
* The `Factory` is responsible for opening your [`Database`](#database) instance.
14+
* It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage).
15+
*
16+
* ```php
17+
* $loop = \React\EventLoop\Factory::create();
18+
* $factory = new Factory($loop);
19+
* ```
20+
*
21+
* @param LoopInterface $loop
22+
*/
23+
public function __construct(LoopInterface $loop)
24+
{
25+
$this->loop = $loop;
26+
}
27+
28+
/**
29+
* Opens a new database connection for the given SQLite database file.
30+
*
31+
* This method returns a promise that will resolve with a `Database` on
32+
* success or will reject with an `Exception` on error. The SQLite extension
33+
* is inherently blocking, so this method will spawn an SQLite worker process
34+
* to run all SQLite commands and queries in a separate process without
35+
* blocking the main process.
36+
*
37+
* ```php
38+
* $factory->open('users.db')->then(function (Database $db) {
39+
* // database ready
40+
* // $db->query('INSERT INTO users (name) VALUES ("test")');
41+
* // $db->quit();
42+
* }, function (Exception $e) {
43+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
44+
* });
45+
* ```
46+
*
47+
* The optional `$flags` parameter is used to determine how to open the
48+
* SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
49+
*
50+
* ```php
51+
* $factory->open('users.db', SQLITE3_OPEN_READONLY)->then(function (Database $db) {
52+
* // database ready (read-only)
53+
* // $db->quit();
54+
* }, function (Exception $e) {
55+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
56+
* });
57+
* ```
58+
*
59+
* @param string $filename
60+
* @param ?int $flags
61+
* @return PromiseInterface<Database> Resolves with Database instance or rejects with Exception
62+
*/
63+
public function open($filename, $flags = null)
64+
{
65+
$command = 'exec ' . \escapeshellarg(\PHP_BINARY) . ' ' . \escapeshellarg(__DIR__ . '/../res/sqlite-worker.php');
66+
67+
// Try to get list of all open FDs (Linux/Mac and others)
68+
$fds = @\scandir('/dev/fd');
69+
70+
// Otherwise try temporarily duplicating file descriptors in the range 0-1024 (FD_SETSIZE).
71+
// This is known to work on more exotic platforms and also inside chroot
72+
// environments without /dev/fd. Causes many syscalls, but still rather fast.
73+
// @codeCoverageIgnoreStart
74+
if ($fds === false) {
75+
$fds = array();
76+
for ($i = 0; $i <= 1024; ++$i) {
77+
$copy = @\fopen('php://fd/' . $i, 'r');
78+
if ($copy !== false) {
79+
$fds[] = $i;
80+
\fclose($copy);
81+
}
82+
}
83+
}
84+
// @codeCoverageIgnoreEnd
85+
86+
// launch process with default STDIO pipes
87+
$pipes = array(
88+
array('pipe', 'r'),
89+
array('pipe', 'w'),
90+
array('pipe', 'w')
91+
);
92+
93+
// do not inherit open FDs by explicitly overwriting existing FDs with dummy files
94+
// additionally, close all dummy files in the child process again
95+
foreach ($fds as $fd) {
96+
if ($fd > 2) {
97+
$pipes[$fd] = array('file', '/dev/null', 'r');
98+
$command .= ' ' . $fd . '>&-';
99+
}
100+
}
101+
102+
// default `sh` only accepts single-digit FDs, so run in bash if needed
103+
if ($fds && \max($fds) > 9) {
104+
$command = 'exec bash -c ' . \escapeshellarg($command);
105+
}
106+
107+
$process = new Process($command, null, null, $pipes);
108+
$process->start($this->loop);
109+
110+
$db = new Database($process);
111+
$args = array($filename);
112+
if ($flags !== null) {
113+
$args[] = $flags;
114+
}
115+
116+
return $db->send('open', $args)->then(function () use ($db) {
117+
return $db;
118+
}, function ($e) use ($db) {
119+
$db->close();
120+
throw $e;
121+
});
122+
}
123+
}

0 commit comments

Comments
 (0)