Skip to content

Commit 3a6e2ec

Browse files
committed
Create core classes for the package.
1 parent de7f69d commit 3a6e2ec

File tree

13 files changed

+824
-12
lines changed

13 files changed

+824
-12
lines changed

README.md

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,124 @@
1-
# react-mysql
2-
Nuclear MySQL Reactor
1+
# ReactMysql
32

3+
Non-blocking MySQLi database access with PHP.
4+
Designed to work with [reactphp/react](https://github.com/reactphp/react).
45

5-
# Examples
66

7-
Connection::init($loop);
7+
## Working
88

9-
Connection::query('SELECT * FROM `table` WHERE `column` = ? AND `column2` = ?;', ['red', 'white'])
10-
->then(function($result) { ... });
9+
This __is__ working. But it is nowhere near complete.
1110

12-
Connection::query returns a promise. This has all of the normal promise interface options.
11+
$ ./run
12+
Starting loop...
13+
DB Created.
14+
Run Query: 0
15+
Found rows: 0
16+
Run Query: 1
17+
Found rows: 1
18+
Current memory usage: 735.117K
19+
Run Query: 2
20+
Found rows: 0
21+
Run Query: 3
22+
Found rows: 1
23+
Run Query: 4
24+
Found rows: 1
25+
Current memory usage: 735.117K
26+
Run Query: 5
27+
Found rows: 0
28+
Current memory usage: 733.602K
29+
Current memory usage: 733.602K
30+
Current memory usage: 733.602K
31+
Loop finished, all timers halted.
1332

14-
# Credits
15-
Inspiration from:
16-
- https://github.com/kaja47/async-mysql
17-
- https://github.com/bixuehujin/reactphp-mysql
33+
This won't work out of the box without the database configured.
34+
As of this point, database configuration is hard coded.
35+
Still need to pull out the configs. You will also need to
36+
set up a database with some data to query. Check back later
37+
for more!
38+
39+
## TODO
40+
41+
A lot.
42+
43+
This is not production ready. Still tons to do on the query builder.
44+
While I hate to reinvent the wheel, I have not found a lightweight
45+
injectable query builder that is not tied to a massive framework.
46+
47+
## Plans (Future Examples)
48+
49+
These are just plans for now. It may change wildly as we develop.
50+
51+
### Current Development Example
52+
53+
Here is an example of what is currently working for the most part.
54+
55+
$loop = React\EventLoop\Factory::create();
56+
57+
ConnectionFactory::init($loop);
58+
59+
$db = new \DustinGraham\ReactMysql\Database();
60+
61+
$db->createCommand("SELECT * FROM `table` WHERE id = :id;", [':id' => $id])
62+
->execute()->then(
63+
function($result)
64+
{
65+
$rows = $result->fetch_all(MYSQLI_ASSOC);
66+
$result->close();
67+
68+
// Do something with $rows.
69+
}
70+
);
71+
72+
73+
### Original Big Picture Plans
74+
75+
Here are some examples of how it may be, eventually.
76+
It would be nice to hide away some of the current boilerplate.
77+
78+
Connection::init($loop);
79+
80+
Connection::query(
81+
'SELECT * FROM `table` WHERE `column` = ? AND `column2` = ?;',
82+
['red', 'white']
83+
)->then(function($result) { ... });
84+
85+
Connection::query(...) returns a promise.
86+
87+
$db = new Database();
88+
$db->createCommand('SELECT * FROM table WHERE id = :id', [':id' => 1])
89+
->execute()
90+
->then(function($results) {
91+
echo $results[0]->name;
92+
});
93+
94+
95+
And another idea...
96+
97+
DB::loadModel('id', ' =', '3')->then(function($model) use ($socket) {
98+
$socket->send('Your name is '.$model->name);
99+
});
100+
101+
## Difficulties
102+
103+
There were many difficulties.
104+
105+
At this point, I can not find any libraries that handle parameterized queries
106+
without using PDO or prepared statements.
107+
108+
MYSQLI_ASYNC does not support prepared statements and parameter binding. So we had to write it ourselves.
109+
110+
The mysqli::real_escape_string requires a link. But, the link is one of many.
111+
Last minute escaping once the command and connection were married from the pool.
112+
Could potentially have one dedicated link for escaping.
113+
114+
## Credits
115+
116+
Much appreciation to the hard work over at [reactphp/react](https://github.com/reactphp/react).
117+
118+
Inspired by similar projects:
119+
- [kaja47/async-mysql](https://github.com/kaja47/async-mysql)
120+
- [bixuehujin/reactphp-mysql](https://github.com/bixuehujin/reactphp-mysql)
121+
122+
## License
123+
124+
DustinGraham/ReactMysql is released under the [MIT](https://github.com/dustingraham/react-mysql/blob/master/LICENSE) license.

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@
1818
"psr-4": {
1919
"DustinGraham\\ReactMysql\\": "src/"
2020
}
21+
},
22+
"autoload-dev": {
23+
"psr-4": {
24+
"DustinGraham\\ReactMysql\\Tests\\": "tests/"
25+
}
2126
}
2227
}

run

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env php
2+
<?php
3+
error_reporting(E_ALL);
4+
ini_set("display_errors", 1);
5+
6+
require __DIR__.'/vendor/autoload.php';
7+
8+
echo 'Starting loop...'.PHP_EOL;
9+
10+
$loop = React\EventLoop\Factory::create();
11+
12+
\DustinGraham\ReactMysql\ConnectionFactory::init($loop);
13+
14+
$db = new \DustinGraham\ReactMysql\Database();
15+
echo 'DB Created.'.PHP_EOL;
16+
17+
$j = 0;
18+
$loop->addPeriodicTimer(0.3, function ($timer) use (&$j)
19+
{
20+
$memory = memory_get_usage() / 1024;
21+
$formatted = number_format($memory, 3).'K';
22+
echo "Current memory usage: {$formatted}\n";
23+
24+
if ($j++ > 3) $timer->cancel();
25+
});
26+
27+
$i = 0;
28+
$loop->addPeriodicTimer(0.1, function ($timer) use (&$i, $db)
29+
{
30+
echo "Run Query: $i\n";
31+
32+
$db->createCommand(
33+
'SELECT * FROM `react`.`react` WHERE id = :test',
34+
[':test' => $i]
35+
)->execute()->then(
36+
function($result)
37+
{
38+
if (is_null($result))
39+
{
40+
echo 'Null result...'.PHP_EOL.PHP_EOL;
41+
exit;
42+
}
43+
44+
$rows = $result->fetch_all(MYSQLI_ASSOC);
45+
$result->close();
46+
47+
echo 'Found rows: '.count($rows).PHP_EOL;
48+
}
49+
);
50+
51+
if ($i++ >= 5) $timer->cancel();
52+
});
53+
54+
$loop->run();
55+
56+
echo 'Loop finished, all timers halted.'.PHP_EOL;

src/Command.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php namespace DustinGraham\ReactMysql;
2+
3+
class Command
4+
{
5+
/**
6+
* @var Database the command is associated with.
7+
*/
8+
public $db;
9+
10+
/**
11+
* @var string
12+
*/
13+
public $sql;
14+
15+
/**
16+
* @var array
17+
*/
18+
protected $params = [];
19+
20+
public function __construct(Database $database, $sql = null)
21+
{
22+
$this->db = $database;
23+
$this->sql = $sql;
24+
}
25+
26+
/**
27+
* @param string|array $key
28+
* @param string|null $value
29+
* @return $this
30+
*/
31+
public function bind($key, $value = null)
32+
{
33+
if (is_array($key))
34+
{
35+
// TODO: Is this cludgy?
36+
$this->bindValues($key);
37+
}
38+
else
39+
{
40+
$this->params[$key] = $value;
41+
}
42+
43+
return $this;
44+
}
45+
46+
/**
47+
* @param $params
48+
* @return $this
49+
*/
50+
public function bindValues($params)
51+
{
52+
foreach ($params as $k => $v)
53+
{
54+
$this->params[$k] = $v;
55+
}
56+
57+
return $this;
58+
}
59+
60+
/**
61+
* @param Connection $connection
62+
* @return string
63+
*/
64+
public function getPreparedQuery(Connection $connection)
65+
{
66+
$this->params = $connection->escape($this->params);
67+
68+
return strtr($this->sql, $this->params);
69+
}
70+
71+
/**
72+
* @return \React\Promise\PromiseInterface
73+
*/
74+
public function execute()
75+
{
76+
return $this->db->executeCommand($this);
77+
}
78+
}

src/Connection.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php namespace DustinGraham\ReactMysql;
2+
3+
use React\EventLoop\LoopInterface;
4+
use React\EventLoop\Timer\TimerInterface;
5+
use React\Promise\Deferred;
6+
7+
class Connection
8+
{
9+
/**
10+
* @var \mysqli
11+
*/
12+
protected $mysqli;
13+
14+
/**
15+
* @var LoopInterface
16+
*/
17+
protected $loop;
18+
19+
/**
20+
* @var float
21+
*/
22+
protected $pollInterval = 0.01;
23+
24+
public function __construct(\mysqli $mysqli, LoopInterface $loop)
25+
{
26+
$this->mysqli = $mysqli;
27+
$this->loop = $loop;
28+
}
29+
30+
public function escape($data)
31+
{
32+
if (is_array($data))
33+
{
34+
$data = array_map([$this, 'escape'], $data);
35+
}
36+
else
37+
{
38+
$data = $this->mysqli->real_escape_string($data);
39+
}
40+
41+
return $data;
42+
}
43+
44+
public function execute(Command $command)
45+
{
46+
$query = $command->getPreparedQuery($this);
47+
48+
$status = $this->mysqli->query($query, MYSQLI_ASYNC);
49+
if ($status === false)
50+
{
51+
throw new \Exception($this->mysqli->error);
52+
}
53+
54+
$deferred = new Deferred();
55+
56+
$this->loop->addPeriodicTimer(
57+
$this->pollInterval,
58+
function (TimerInterface $timer) use ($deferred)
59+
{
60+
$reads = $errors = $rejects = [$this->mysqli];
61+
62+
// Non-blocking requires a zero wait time.
63+
$this->mysqli->poll($reads, $errors, $rejects, 0);
64+
65+
$read = in_array($this->mysqli, $reads, true);
66+
$error = in_array($this->mysqli, $errors, true);
67+
$reject = in_array($this->mysqli, $rejects, true);
68+
69+
if ($read)
70+
{
71+
$result = $this->mysqli->reap_async_query();
72+
if ($result === false)
73+
{
74+
$deferred->reject(new \Exception($this->mysqli->error));
75+
}
76+
else
77+
{
78+
// Success!!
79+
$deferred->resolve($result);
80+
}
81+
}
82+
else if ($error)
83+
{
84+
$deferred->reject(new \Exception($this->mysqli->error));
85+
}
86+
else if ($reject)
87+
{
88+
$deferred->reject(new \Exception($this->mysqli->error));
89+
}
90+
91+
// If poll yielded something for this connection, we're done!
92+
if ($read || $error || $reject)
93+
{
94+
$timer->cancel();
95+
}
96+
}
97+
);
98+
99+
return $deferred->promise();
100+
}
101+
}

0 commit comments

Comments
 (0)