Skip to content

Commit

Permalink
Create core classes for the package.
Browse files Browse the repository at this point in the history
  • Loading branch information
dustingraham committed Apr 24, 2016
1 parent de7f69d commit 3a6e2ec
Show file tree
Hide file tree
Showing 13 changed files with 824 additions and 12 deletions.
129 changes: 118 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,124 @@
# react-mysql
Nuclear MySQL Reactor
# ReactMysql

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

# Examples

Connection::init($loop);
## Working

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

Connection::query returns a promise. This has all of the normal promise interface options.
$ ./run
Starting loop...
DB Created.
Run Query: 0
Found rows: 0
Run Query: 1
Found rows: 1
Current memory usage: 735.117K
Run Query: 2
Found rows: 0
Run Query: 3
Found rows: 1
Run Query: 4
Found rows: 1
Current memory usage: 735.117K
Run Query: 5
Found rows: 0
Current memory usage: 733.602K
Current memory usage: 733.602K
Current memory usage: 733.602K
Loop finished, all timers halted.

# Credits
Inspiration from:
- https://github.com/kaja47/async-mysql
- https://github.com/bixuehujin/reactphp-mysql
This won't work out of the box without the database configured.
As of this point, database configuration is hard coded.
Still need to pull out the configs. You will also need to
set up a database with some data to query. Check back later
for more!

## TODO

A lot.

This is not production ready. Still tons to do on the query builder.
While I hate to reinvent the wheel, I have not found a lightweight
injectable query builder that is not tied to a massive framework.

## Plans (Future Examples)

These are just plans for now. It may change wildly as we develop.

### Current Development Example

Here is an example of what is currently working for the most part.

$loop = React\EventLoop\Factory::create();

ConnectionFactory::init($loop);

$db = new \DustinGraham\ReactMysql\Database();

$db->createCommand("SELECT * FROM `table` WHERE id = :id;", [':id' => $id])
->execute()->then(
function($result)
{
$rows = $result->fetch_all(MYSQLI_ASSOC);
$result->close();
// Do something with $rows.
}
);


### Original Big Picture Plans

Here are some examples of how it may be, eventually.
It would be nice to hide away some of the current boilerplate.

Connection::init($loop);

Connection::query(
'SELECT * FROM `table` WHERE `column` = ? AND `column2` = ?;',
['red', 'white']
)->then(function($result) { ... });

Connection::query(...) returns a promise.

$db = new Database();
$db->createCommand('SELECT * FROM table WHERE id = :id', [':id' => 1])
->execute()
->then(function($results) {
echo $results[0]->name;
});


And another idea...

DB::loadModel('id', ' =', '3')->then(function($model) use ($socket) {
$socket->send('Your name is '.$model->name);
});

## Difficulties

There were many difficulties.

At this point, I can not find any libraries that handle parameterized queries
without using PDO or prepared statements.

MYSQLI_ASYNC does not support prepared statements and parameter binding. So we had to write it ourselves.

The mysqli::real_escape_string requires a link. But, the link is one of many.
Last minute escaping once the command and connection were married from the pool.
Could potentially have one dedicated link for escaping.

## Credits

Much appreciation to the hard work over at [reactphp/react](https://github.com/reactphp/react).

Inspired by similar projects:
- [kaja47/async-mysql](https://github.com/kaja47/async-mysql)
- [bixuehujin/reactphp-mysql](https://github.com/bixuehujin/reactphp-mysql)

## License

DustinGraham/ReactMysql is released under the [MIT](https://github.com/dustingraham/react-mysql/blob/master/LICENSE) license.
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@
"psr-4": {
"DustinGraham\\ReactMysql\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"DustinGraham\\ReactMysql\\Tests\\": "tests/"
}
}
}
56 changes: 56 additions & 0 deletions run
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env php
<?php
error_reporting(E_ALL);
ini_set("display_errors", 1);

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

echo 'Starting loop...'.PHP_EOL;

$loop = React\EventLoop\Factory::create();

\DustinGraham\ReactMysql\ConnectionFactory::init($loop);

$db = new \DustinGraham\ReactMysql\Database();
echo 'DB Created.'.PHP_EOL;

$j = 0;
$loop->addPeriodicTimer(0.3, function ($timer) use (&$j)
{
$memory = memory_get_usage() / 1024;
$formatted = number_format($memory, 3).'K';
echo "Current memory usage: {$formatted}\n";

if ($j++ > 3) $timer->cancel();
});

$i = 0;
$loop->addPeriodicTimer(0.1, function ($timer) use (&$i, $db)
{
echo "Run Query: $i\n";

$db->createCommand(
'SELECT * FROM `react`.`react` WHERE id = :test',
[':test' => $i]
)->execute()->then(
function($result)
{
if (is_null($result))
{
echo 'Null result...'.PHP_EOL.PHP_EOL;
exit;
}

$rows = $result->fetch_all(MYSQLI_ASSOC);
$result->close();

echo 'Found rows: '.count($rows).PHP_EOL;
}
);

if ($i++ >= 5) $timer->cancel();
});

$loop->run();

echo 'Loop finished, all timers halted.'.PHP_EOL;
78 changes: 78 additions & 0 deletions src/Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php namespace DustinGraham\ReactMysql;

class Command
{
/**
* @var Database the command is associated with.
*/
public $db;

/**
* @var string
*/
public $sql;

/**
* @var array
*/
protected $params = [];

public function __construct(Database $database, $sql = null)
{
$this->db = $database;
$this->sql = $sql;
}

/**
* @param string|array $key
* @param string|null $value
* @return $this
*/
public function bind($key, $value = null)
{
if (is_array($key))
{
// TODO: Is this cludgy?
$this->bindValues($key);
}
else
{
$this->params[$key] = $value;
}

return $this;
}

/**
* @param $params
* @return $this
*/
public function bindValues($params)
{
foreach ($params as $k => $v)
{
$this->params[$k] = $v;
}

return $this;
}

/**
* @param Connection $connection
* @return string
*/
public function getPreparedQuery(Connection $connection)
{
$this->params = $connection->escape($this->params);

return strtr($this->sql, $this->params);
}

/**
* @return \React\Promise\PromiseInterface
*/
public function execute()
{
return $this->db->executeCommand($this);
}
}
101 changes: 101 additions & 0 deletions src/Connection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php namespace DustinGraham\ReactMysql;

use React\EventLoop\LoopInterface;
use React\EventLoop\Timer\TimerInterface;
use React\Promise\Deferred;

class Connection
{
/**
* @var \mysqli
*/
protected $mysqli;

/**
* @var LoopInterface
*/
protected $loop;

/**
* @var float
*/
protected $pollInterval = 0.01;

public function __construct(\mysqli $mysqli, LoopInterface $loop)
{
$this->mysqli = $mysqli;
$this->loop = $loop;
}

public function escape($data)
{
if (is_array($data))
{
$data = array_map([$this, 'escape'], $data);
}
else
{
$data = $this->mysqli->real_escape_string($data);
}

return $data;
}

public function execute(Command $command)
{
$query = $command->getPreparedQuery($this);

$status = $this->mysqli->query($query, MYSQLI_ASYNC);
if ($status === false)
{
throw new \Exception($this->mysqli->error);
}

$deferred = new Deferred();

$this->loop->addPeriodicTimer(
$this->pollInterval,
function (TimerInterface $timer) use ($deferred)
{
$reads = $errors = $rejects = [$this->mysqli];

// Non-blocking requires a zero wait time.
$this->mysqli->poll($reads, $errors, $rejects, 0);

$read = in_array($this->mysqli, $reads, true);
$error = in_array($this->mysqli, $errors, true);
$reject = in_array($this->mysqli, $rejects, true);

if ($read)
{
$result = $this->mysqli->reap_async_query();
if ($result === false)
{
$deferred->reject(new \Exception($this->mysqli->error));
}
else
{
// Success!!
$deferred->resolve($result);
}
}
else if ($error)
{
$deferred->reject(new \Exception($this->mysqli->error));
}
else if ($reject)
{
$deferred->reject(new \Exception($this->mysqli->error));
}

// If poll yielded something for this connection, we're done!
if ($read || $error || $reject)
{
$timer->cancel();
}
}
);

return $deferred->promise();
}
}
Loading

0 comments on commit 3a6e2ec

Please sign in to comment.