Skip to content

Use promises for query() method #61

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

Merged
merged 1 commit into from
Jun 25, 2018
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
43 changes: 26 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ $connection = new React\MySQL\Connection($loop, array(

$connection->connect(function () {});

$connection->query('SELECT * FROM book', function (QueryCommand $command) {
if ($command->hasError()) {
$error = $command->getError();
echo 'Error: ' . $error->getMessage() . PHP_EOL;
} else {
$connection->query('SELECT * FROM book')->then(
function (QueryCommand $command) {
print_r($command->resultFields);
print_r($command->resultRows);
echo count($command->resultRows) . ' row(s) in set' . PHP_EOL;
},
function (Exception $error) {
echo 'Error: ' . $error->getMessage() . PHP_EOL;
}
});
);

$connection->close();

Expand Down Expand Up @@ -122,25 +122,31 @@ i.e. it MUST NOT be called more than once.

#### query()

The `query(string $query, callable|null $callback, mixed ...$params): QueryCommand|null` method can be used to
The `query(string $query, array $params = array()): PromiseInterface` method can be used to
perform an async query.

This method returns a promise that will resolve with a `QueryCommand` on
success or will reject with an `Exception` on error. The MySQL protocol
is inherently sequential, so that all queries will be performed in order
and outstanding queries will be put into a queue to be executed once the
previous queries are completed.

```php
$connection->query('CREATE TABLE test ...');
$connection->query('INSERT INTO test (id) VALUES (1)');
```

If this SQL statement returns a result set (such as from a `SELECT`
statement), this method will buffer everything in memory until the result
set is completed and will then invoke the `$callback` function. This is
set is completed and will then resolve the resulting promise. This is
the preferred method if you know your result set to not exceed a few
dozens or hundreds of rows. If the size of your result set is either
unknown or known to be too large to fit into memory, you should use the
[`queryStream()`](#querystream) method instead.

```php
$connection->query($query, function (QueryCommand $command) {
if ($command->hasError()) {
// test whether the query was executed successfully
// get the error object, instance of Exception.
$error = $command->getError();
echo 'Error: ' . $error->getMessage() . PHP_EOL;
} elseif (isset($command->resultRows)) {
$connection->query($query)->then(function (QueryCommand $command) {
if (isset($command->resultRows)) {
// this is a response to a SELECT etc. with some rows (0+)
print_r($command->resultFields);
print_r($command->resultRows);
Expand All @@ -152,14 +158,17 @@ $connection->query($query, function (QueryCommand $command) {
}
echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL;
}
}, function (Exception $error) {
// the query was not executed successfully
echo 'Error: ' . $error->getMessage() . PHP_EOL;
});
```

You can optionally pass any number of `$params` that will be bound to the
You can optionally pass an array of `$params` that will be bound to the
query like this:

```php
$connection->query('SELECT * FROM user WHERE id > ?', $fn, $id);
$connection->query('SELECT * FROM user WHERE id > ?', [$id]);
```

The given `$sql` parameter MUST contain a single statement. Support
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"php": ">=5.4.0",
"evenement/evenement": "^3.0 || ^2.1 || ^1.1",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"react/promise": "^2.7",
"react/socket": "^1.0 || ^0.8"
},
"require-dev": {
Expand Down
12 changes: 5 additions & 7 deletions examples/01-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@
$connection->connect(function () {});

$query = isset($argv[1]) ? $argv[1] : 'select * from book';
$connection->query($query, function (QueryCommand $command) {
if ($command->hasError()) {
// test whether the query was executed successfully
// get the error object, instance of Exception.
$error = $command->getError();
echo 'Error: ' . $error->getMessage() . PHP_EOL;
} elseif (isset($command->resultRows)) {
$connection->query($query)->then(function (QueryCommand $command) {
if (isset($command->resultRows)) {
// this is a response to a SELECT etc. with some rows (0+)
print_r($command->resultFields);
print_r($command->resultRows);
Expand All @@ -35,6 +30,9 @@
}
echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL;
}
}, function (Exception $error) {
// the query was not executed successfully
echo 'Error: ' . $error->getMessage() . PHP_EOL;
});

$connection->close();
Expand Down
14 changes: 6 additions & 8 deletions examples/11-interactive.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

use React\MySQL\Commands\QueryCommand;
use React\Stream\ReadableResourceStream;
use React\MySQL\ConnectionInterface;
use React\Stream\ReadableResourceStream;

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

Expand Down Expand Up @@ -43,13 +43,8 @@
}

$time = microtime(true);
$connection->query($query, function (QueryCommand $command) use ($time) {
if ($command->hasError()) {
// test whether the query was executed successfully
// get the error object, instance of Exception.
$error = $command->getError();
echo 'Error: ' . $error->getMessage() . PHP_EOL;
} elseif (isset($command->resultRows)) {
$connection->query($query)->then(function (QueryCommand $command) use ($time) {
if (isset($command->resultRows)) {
// this is a response to a SELECT etc. with some rows (0+)
echo implode("\t", array_column($command->resultFields, 'name')) . PHP_EOL;

Expand Down Expand Up @@ -79,6 +74,9 @@
PHP_EOL
);
}
}, function (Exception $error) {
// the query was not executed successfully
echo 'Error: ' . $error->getMessage() . PHP_EOL;
});
});

Expand Down
43 changes: 19 additions & 24 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use React\MySQL\Io\Executor;
use React\MySQL\Io\Parser;
use React\MySQL\Io\Query;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface as SocketConnectionInterface;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
Expand Down Expand Up @@ -91,55 +92,49 @@ public function __construct(LoopInterface $loop, array $connectOptions = array()
/**
* {@inheritdoc}
*/
public function query($sql, $callback = null, $params = null)
public function query($sql, array $params = array())
{
$query = new Query($sql);
if ($params) {
$query->bindParamsFromArray($params);
}

$command = new QueryCommand($this);
$command->setQuery($query);

$args = func_get_args();
array_shift($args); // Remove $sql parameter.

if (!is_callable($callback)) {
if ($args) {
$query->bindParamsFromArray($args);
}

return $this->_doCommand($command);
try {
$this->_doCommand($command);
} catch (\Exception $e) {
return \React\Promise\reject($e);
}

array_shift($args); // Remove $callback

if ($args) {
$query->bindParamsFromArray($args);
}
$this->_doCommand($command);
$deferred = new Deferred();

// store all result set rows until result set end
$rows = array();
$command->on('result', function ($row) use (&$rows) {
$rows[] = $row;
});
$command->on('end', function ($command) use ($callback, &$rows) {
$command->on('end', function ($command) use ($deferred, &$rows) {
$command->resultRows = $rows;
$rows = array();
$callback($command, $this);

$deferred->resolve($command);
});

// resolve / reject status reply (response without result set)
$command->on('error', function ($err, $command) use ($callback) {
$callback($command, $this);
$command->on('error', function ($error) use ($deferred) {
$deferred->reject($error);
});
$command->on('success', function ($command) use ($callback) {
$callback($command, $this);
$command->on('success', function (QueryCommand $command) use ($deferred) {
$deferred->resolve($command);
});

return $deferred->promise();
}

public function queryStream($sql, $params = array())
{
$query = new Query($sql);

if ($params) {
$query->bindParamsFromArray($params);
}
Expand Down
41 changes: 24 additions & 17 deletions src/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace React\MySQL;

use React\MySQL\Commands\QueryCommand;
use React\Stream\ReadableStreamInterface;
use React\Promise\PromiseInterface;

/**
* Interface ConnectionInterface
Expand All @@ -24,22 +24,28 @@ interface ConnectionInterface
/**
* Performs an async query.
*
* This method returns a promise that will resolve with a `QueryCommand` on
* success or will reject with an `Exception` on error. The MySQL protocol
* is inherently sequential, so that all queries will be performed in order
* and outstanding queries will be put into a queue to be executed once the
* previous queries are completed.
*
* ```php
* $connection->query('CREATE TABLE test ...');
* $connection->query('INSERT INTO test (id) VALUES (1)');
* ```
*
* If this SQL statement returns a result set (such as from a `SELECT`
* statement), this method will buffer everything in memory until the result
* set is completed and will then invoke the `$callback` function. This is
* set is completed and will then resolve the resulting promise. This is
* the preferred method if you know your result set to not exceed a few
* dozens or hundreds of rows. If the size of your result set is either
* unknown or known to be too large to fit into memory, you should use the
* [`queryStream()`](#querystream) method instead.
*
* ```php
* $connection->query($query, function (QueryCommand $command) {
* if ($command->hasError()) {
* // test whether the query was executed successfully
* // get the error object, instance of Exception.
* $error = $command->getError();
* echo 'Error: ' . $error->getMessage() . PHP_EOL;
* } elseif (isset($command->resultRows)) {
* $connection->query($query)->then(function (QueryCommand $command) {
* if (isset($command->resultRows)) {
* // this is a response to a SELECT etc. with some rows (0+)
* print_r($command->resultFields);
* print_r($command->resultRows);
Expand All @@ -51,28 +57,29 @@ interface ConnectionInterface
* }
* echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL;
* }
* }, function (Exception $error) {
* // the query was not executed successfully
* echo 'Error: ' . $error->getMessage() . PHP_EOL;
* });
* ```
*
* You can optionally pass any number of `$params` that will be bound to the
* You can optionally pass an array of `$params` that will be bound to the
* query like this:
*
* ```php
* $connection->query('SELECT * FROM user WHERE id > ?', $fn, $id);
* $connection->query('SELECT * FROM user WHERE id > ?', [$id]);
* ```
*
* The given `$sql` parameter MUST contain a single statement. Support
* for multiple statements is disabled for security reasons because it
* could allow for possible SQL injection attacks and this API is not
* suited for exposing multiple possible results.
*
* @param string $sql MySQL sql statement.
* @param callable|null $callback Query result handler callback.
* @param mixed $params,... Parameters which should bind to query.
* @return QueryCommand|null Return QueryCommand if $callback not specified.
* @throws Exception if the connection is not initialized or already closed/closing
* @param string $sql SQL statement
* @param array $params Parameters which should be bound to query
* @return PromiseInterface Returns a Promise<QueryCommand,Exception>
*/
public function query($sql, $callback = null, $params = null);
public function query($sql, array $params = array());

/**
* Performs an async query and streams the rows of the result set.
Expand Down
14 changes: 8 additions & 6 deletions tests/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,19 @@ public function testCloseWithoutConnectThrows()
$conn->close(function () { });
}

/**
* @expectedException React\MySQL\Exception
* @expectedExceptionMessage Can't send command
*/
public function testQueryWithoutConnectThrows()
public function testQueryWithoutConnectRejects()
{
$options = $this->getConnectionOptions();
$loop = \React\EventLoop\Factory::create();
$conn = new Connection($loop, $options);

$conn->query('SELECT 1', function () { });
$conn->query('SELECT 1')->then(
$this->expectCallableNever(),
function (\Exception $error) {
$this->assertInstanceOf('React\MySQL\Exception', $error);
$this->assertSame('Can\'t send command', $error->getMessage());
}
);
}

/**
Expand Down
11 changes: 5 additions & 6 deletions tests/NoResultQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace React\Tests\MySQL;

use React\MySQL\Commands\QueryCommand;

class NoResultQueryTest extends BaseTestCase
{
public function setUp()
Expand All @@ -26,8 +28,7 @@ public function testUpdateSimpleNonExistentReportsNoAffectedRows()
$connection = new \React\MySQL\Connection($loop, $this->getConnectionOptions());
$connection->connect(function () {});

$connection->query('update book set created=999 where id=999', function ($command, $conn) {
$this->assertEquals(false, $command->hasError());
$connection->query('update book set created=999 where id=999')->then(function (QueryCommand $command) {
$this->assertEquals(0, $command->affectedRows);
});

Expand All @@ -42,8 +43,7 @@ public function testInsertSimpleReportsFirstInsertId()
$connection = new \React\MySQL\Connection($loop, $this->getConnectionOptions());
$connection->connect(function () {});

$connection->query("insert into book (`name`) values ('foo')", function ($command, $conn) {
$this->assertEquals(false, $command->hasError());
$connection->query("insert into book (`name`) values ('foo')")->then(function (QueryCommand $command) {
$this->assertEquals(1, $command->affectedRows);
$this->assertEquals(1, $command->insertId);
});
Expand All @@ -60,8 +60,7 @@ public function testUpdateSimpleReportsAffectedRow()
$connection->connect(function () {});

$connection->query("insert into book (`name`) values ('foo')");
$connection->query('update book set created=999 where id=1', function ($command, $conn) {
$this->assertEquals(false, $command->hasError());
$connection->query('update book set created=999 where id=1')->then(function (QueryCommand $command) {
$this->assertEquals(1, $command->affectedRows);
});

Expand Down
Loading