Skip to content

Commit

Permalink
introduce affordances to restrict queries during response preparation
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Jan 12, 2023
1 parent 3dff584 commit d733414
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 1 deletion.
71 changes: 71 additions & 0 deletions src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ class Connection implements ConnectionInterface
*/
protected static $resolvers = [];

/**
* Indicate that queries should be prevented.
*
* @var bool
*/
protected $preventQueries = false;

/**
* Create a new database connection instance.
*
Expand Down Expand Up @@ -705,6 +712,10 @@ public function prepareBindings(array $bindings)
*/
protected function run($query, $bindings, Closure $callback)
{
if ($this->preventQueries) {
throw new RuntimeException("Queries are being prevented. Attempting to run query [{$query}].");
}

foreach ($this->beforeExecutingCallbacks as $beforeExecutingCallback) {
$beforeExecutingCallback($query, $bindings, $this);
}
Expand Down Expand Up @@ -793,6 +804,56 @@ protected function getElapsedTime($start)
return round((microtime(true) - $start) * 1000, 2);
}

/**
* Indicate that an exception should be thrown when a query is to be run.
*
* @param \Closure|null $callback
* @return mixed
*/
public function preventQueries($callback = null)
{
if ($callback === null) {
$this->preventQueries = true;

return;
}

$preventQueriesCache = $this->preventQueries;

$this->preventQueries = true;

try {
return $callback();
} finally {
$this->preventQueries = $preventQueriesCache;
}
}

/**
* Restore normal behaviour; do not throw exceptions when a query is to be run.
*
* @param \Closure|null $callback
* @return mixed
*/
public function allowQueries($callback = null)
{
if ($callback === null) {
$this->preventQueries = false;

return;
}

$preventQueriesCache = $this->preventQueries;

$this->preventQueries = false;

try {
return $callback();
} finally {
$this->preventQueries = $preventQueriesCache;
}
}

/**
* Register a callback to be invoked when the connection queries for longer than a given amount of time.
*
Expand Down Expand Up @@ -1494,6 +1555,16 @@ public function logging()
return $this->loggingQueries;
}

/**
* Determine if the connection is preventing queries.
*
* @return bool
*/
public function preventingQueries()
{
return $this->preventQueries;
}

/**
* Get the name of the connected database.
*
Expand Down
30 changes: 30 additions & 0 deletions src/Illuminate/Foundation/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Illuminate\Foundation\Events\LocaleUpdated;
use Illuminate\Http\Request;
use Illuminate\Log\LogServiceProvider;
use Illuminate\Routing\Events\PreparingResponse;
use Illuminate\Routing\Events\ResponsePrepared;
use Illuminate\Routing\RoutingServiceProvider;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -1428,4 +1430,32 @@ public function getNamespace()

throw new RuntimeException('Unable to detect application namespace.');
}

/**
* Prevent database queries while preparing the request response.
*
* @param string|null $connection
* @return $this
*/
public function preventQueriesWhilePreparingResponse($connection = null)
{
$this['events']->listen(PreparingResponse::class, fn () => $this['db']->connection($connection)->preventQueries());

$this['events']->listen(ResponsePrepared::class, fn () => $this['db']->connection($connection)->allowQueries());

return $this;
}

/**
* Restores ability for database to query while preparing the request response.
*
* @param string|null $connection
* @return $this
*/
public function allowQueriesWhilePreparingResponse($connection = null)
{
$this['events']->listen(PreparingResponse::class, fn () => $this['db']->connection($connection)->allowQueries());

return $this;
}
}
33 changes: 33 additions & 0 deletions src/Illuminate/Routing/Events/PreparingResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Illuminate\Routing\Events;

class PreparingResponse
{
/**
* The request object.
*
* @var \Illuminate\Http\Request
*/
public $request;

/**
* The response object being resolved.
*
* @var mixed
*/
public $response;

/**
* Create a new event instance.
*
* @param \Illuminate\Http\Request $request
* @param mixed $response
*/
public function __construct($request, $response)
{
$this->request = $request;

$this->response = $response;
}
}
33 changes: 33 additions & 0 deletions src/Illuminate/Routing/Events/ResponsePrepared.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Illuminate\Routing\Events;

class ResponsePrepared
{
/**
* The request object.
*
* @var \Illuminate\Http\Request
*/
public $request;

/**
* The response object being resolved.
*
* @var \Symfony\Component\HttpFoundation\Response
*/
public $response;

/**
* Create a new event instance.
*
* @param \Illuminate\Http\Request $request
* @param \Symfony\Component\HttpFoundation\Response $response
*/
public function __construct($request, $response)
{
$this->request = $request;

$this->response = $response;
}
}
8 changes: 7 additions & 1 deletion src/Illuminate/Routing/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Events\PreparingResponse;
use Illuminate\Routing\Events\ResponsePrepared;
use Illuminate\Routing\Events\RouteMatched;
use Illuminate\Routing\Events\Routing;
use Illuminate\Support\Arr;
Expand Down Expand Up @@ -872,7 +874,11 @@ protected function sortMiddleware(Collection $middlewares)
*/
public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
$this->events->dispatch(new PreparingResponse($request, $response));

return tap(static::toResponse($request, $response), function ($response) use ($request) {
$this->events->dispatch(new ResponsePrepared($request, $response));
});
}

/**
Expand Down
57 changes: 57 additions & 0 deletions tests/Database/DatabaseConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PDOStatement;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use RuntimeException;
use stdClass;

class DatabaseConnectionTest extends TestCase
Expand Down Expand Up @@ -472,6 +473,62 @@ public function testSchemaBuilderCanBeCreated()
$this->assertSame($connection, $schema->getConnection());
}

public function testItCanPreventQueries()
{
$connection = new Connection(new DatabaseConnectionTestMockPDO);

$this->assertFalse($connection->preventingQueries());
$connection->preventQueries();
$this->assertTrue($connection->preventingQueries());

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Queries are being prevented. Attempting to run query [select * from users].');

$connection->statement('select * from users', []);
}

public function testItAcceptsClosure()
{
$connection = new Connection(new DatabaseConnectionTestMockPDO);

$result = $connection->preventQueries(function () use ($connection) {
$this->assertTrue($connection->preventingQueries());

try {
$connection->statement('select * from users', []);
} catch (RuntimeException $exception) {
return $exception->getMessage();
}
});

$this->assertFalse($connection->preventingQueries());
$this->assertSame('Queries are being prevented. Attempting to run query [select * from users].', $result);
}

public function testItCanBeNested()
{
$connection = new Connection(new DatabaseConnectionTestMockPDO);
$isActive = fn () => $connection->preventingQueries() ? 'prevent:' : 'allow:';

$result = $connection->preventQueries(function () use ($connection, $isActive) {
return $isActive().$connection->allowQueries(function () use ($connection, $isActive) {
return $isActive().$connection->preventQueries(function () use ($connection, $isActive) {
return $isActive().$connection->allowQueries(function () use ($connection, $isActive) {
return $isActive().$connection->preventQueries(function () use ($connection, $isActive) {
return $isActive().$connection->preventQueries(function () use ($connection, $isActive) {
return $isActive().$connection->allowQueries(function () use ($isActive) {
return $isActive();
}).$isActive();
}).$isActive();
}).$isActive();
}).$isActive();
}).$isActive();
}).$isActive();
});

$this->assertSame('prevent:allow:prevent:allow:prevent:prevent:allow:prevent:prevent:allow:prevent:allow:prevent:', $result);
}

protected function getMockConnection($methods = [], $pdo = null)
{
$pdo = $pdo ?: new DatabaseConnectionTestMockPDO;
Expand Down

0 comments on commit d733414

Please sign in to comment.