Skip to content

Commit d733414

Browse files
committed
introduce affordances to restrict queries during response preparation
1 parent 3dff584 commit d733414

File tree

8 files changed

+369
-1
lines changed

8 files changed

+369
-1
lines changed

src/Illuminate/Database/Connection.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,13 @@ class Connection implements ConnectionInterface
210210
*/
211211
protected static $resolvers = [];
212212

213+
/**
214+
* Indicate that queries should be prevented.
215+
*
216+
* @var bool
217+
*/
218+
protected $preventQueries = false;
219+
213220
/**
214221
* Create a new database connection instance.
215222
*
@@ -705,6 +712,10 @@ public function prepareBindings(array $bindings)
705712
*/
706713
protected function run($query, $bindings, Closure $callback)
707714
{
715+
if ($this->preventQueries) {
716+
throw new RuntimeException("Queries are being prevented. Attempting to run query [{$query}].");
717+
}
718+
708719
foreach ($this->beforeExecutingCallbacks as $beforeExecutingCallback) {
709720
$beforeExecutingCallback($query, $bindings, $this);
710721
}
@@ -793,6 +804,56 @@ protected function getElapsedTime($start)
793804
return round((microtime(true) - $start) * 1000, 2);
794805
}
795806

807+
/**
808+
* Indicate that an exception should be thrown when a query is to be run.
809+
*
810+
* @param \Closure|null $callback
811+
* @return mixed
812+
*/
813+
public function preventQueries($callback = null)
814+
{
815+
if ($callback === null) {
816+
$this->preventQueries = true;
817+
818+
return;
819+
}
820+
821+
$preventQueriesCache = $this->preventQueries;
822+
823+
$this->preventQueries = true;
824+
825+
try {
826+
return $callback();
827+
} finally {
828+
$this->preventQueries = $preventQueriesCache;
829+
}
830+
}
831+
832+
/**
833+
* Restore normal behaviour; do not throw exceptions when a query is to be run.
834+
*
835+
* @param \Closure|null $callback
836+
* @return mixed
837+
*/
838+
public function allowQueries($callback = null)
839+
{
840+
if ($callback === null) {
841+
$this->preventQueries = false;
842+
843+
return;
844+
}
845+
846+
$preventQueriesCache = $this->preventQueries;
847+
848+
$this->preventQueries = false;
849+
850+
try {
851+
return $callback();
852+
} finally {
853+
$this->preventQueries = $preventQueriesCache;
854+
}
855+
}
856+
796857
/**
797858
* Register a callback to be invoked when the connection queries for longer than a given amount of time.
798859
*
@@ -1494,6 +1555,16 @@ public function logging()
14941555
return $this->loggingQueries;
14951556
}
14961557

1558+
/**
1559+
* Determine if the connection is preventing queries.
1560+
*
1561+
* @return bool
1562+
*/
1563+
public function preventingQueries()
1564+
{
1565+
return $this->preventQueries;
1566+
}
1567+
14971568
/**
14981569
* Get the name of the connected database.
14991570
*

src/Illuminate/Foundation/Application.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Illuminate\Foundation\Events\LocaleUpdated;
1616
use Illuminate\Http\Request;
1717
use Illuminate\Log\LogServiceProvider;
18+
use Illuminate\Routing\Events\PreparingResponse;
19+
use Illuminate\Routing\Events\ResponsePrepared;
1820
use Illuminate\Routing\RoutingServiceProvider;
1921
use Illuminate\Support\Arr;
2022
use Illuminate\Support\Collection;
@@ -1428,4 +1430,32 @@ public function getNamespace()
14281430

14291431
throw new RuntimeException('Unable to detect application namespace.');
14301432
}
1433+
1434+
/**
1435+
* Prevent database queries while preparing the request response.
1436+
*
1437+
* @param string|null $connection
1438+
* @return $this
1439+
*/
1440+
public function preventQueriesWhilePreparingResponse($connection = null)
1441+
{
1442+
$this['events']->listen(PreparingResponse::class, fn () => $this['db']->connection($connection)->preventQueries());
1443+
1444+
$this['events']->listen(ResponsePrepared::class, fn () => $this['db']->connection($connection)->allowQueries());
1445+
1446+
return $this;
1447+
}
1448+
1449+
/**
1450+
* Restores ability for database to query while preparing the request response.
1451+
*
1452+
* @param string|null $connection
1453+
* @return $this
1454+
*/
1455+
public function allowQueriesWhilePreparingResponse($connection = null)
1456+
{
1457+
$this['events']->listen(PreparingResponse::class, fn () => $this['db']->connection($connection)->allowQueries());
1458+
1459+
return $this;
1460+
}
14311461
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Illuminate\Routing\Events;
4+
5+
class PreparingResponse
6+
{
7+
/**
8+
* The request object.
9+
*
10+
* @var \Illuminate\Http\Request
11+
*/
12+
public $request;
13+
14+
/**
15+
* The response object being resolved.
16+
*
17+
* @var mixed
18+
*/
19+
public $response;
20+
21+
/**
22+
* Create a new event instance.
23+
*
24+
* @param \Illuminate\Http\Request $request
25+
* @param mixed $response
26+
*/
27+
public function __construct($request, $response)
28+
{
29+
$this->request = $request;
30+
31+
$this->response = $response;
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Illuminate\Routing\Events;
4+
5+
class ResponsePrepared
6+
{
7+
/**
8+
* The request object.
9+
*
10+
* @var \Illuminate\Http\Request
11+
*/
12+
public $request;
13+
14+
/**
15+
* The response object being resolved.
16+
*
17+
* @var \Symfony\Component\HttpFoundation\Response
18+
*/
19+
public $response;
20+
21+
/**
22+
* Create a new event instance.
23+
*
24+
* @param \Illuminate\Http\Request $request
25+
* @param \Symfony\Component\HttpFoundation\Response $response
26+
*/
27+
public function __construct($request, $response)
28+
{
29+
$this->request = $request;
30+
31+
$this->response = $response;
32+
}
33+
}

src/Illuminate/Routing/Router.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Illuminate\Http\JsonResponse;
1616
use Illuminate\Http\Request;
1717
use Illuminate\Http\Response;
18+
use Illuminate\Routing\Events\PreparingResponse;
19+
use Illuminate\Routing\Events\ResponsePrepared;
1820
use Illuminate\Routing\Events\RouteMatched;
1921
use Illuminate\Routing\Events\Routing;
2022
use Illuminate\Support\Arr;
@@ -872,7 +874,11 @@ protected function sortMiddleware(Collection $middlewares)
872874
*/
873875
public function prepareResponse($request, $response)
874876
{
875-
return static::toResponse($request, $response);
877+
$this->events->dispatch(new PreparingResponse($request, $response));
878+
879+
return tap(static::toResponse($request, $response), function ($response) use ($request) {
880+
$this->events->dispatch(new ResponsePrepared($request, $response));
881+
});
876882
}
877883

878884
/**

tests/Database/DatabaseConnectionTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PDOStatement;
2525
use PHPUnit\Framework\TestCase;
2626
use ReflectionClass;
27+
use RuntimeException;
2728
use stdClass;
2829

2930
class DatabaseConnectionTest extends TestCase
@@ -472,6 +473,62 @@ public function testSchemaBuilderCanBeCreated()
472473
$this->assertSame($connection, $schema->getConnection());
473474
}
474475

476+
public function testItCanPreventQueries()
477+
{
478+
$connection = new Connection(new DatabaseConnectionTestMockPDO);
479+
480+
$this->assertFalse($connection->preventingQueries());
481+
$connection->preventQueries();
482+
$this->assertTrue($connection->preventingQueries());
483+
484+
$this->expectException(RuntimeException::class);
485+
$this->expectExceptionMessage('Queries are being prevented. Attempting to run query [select * from users].');
486+
487+
$connection->statement('select * from users', []);
488+
}
489+
490+
public function testItAcceptsClosure()
491+
{
492+
$connection = new Connection(new DatabaseConnectionTestMockPDO);
493+
494+
$result = $connection->preventQueries(function () use ($connection) {
495+
$this->assertTrue($connection->preventingQueries());
496+
497+
try {
498+
$connection->statement('select * from users', []);
499+
} catch (RuntimeException $exception) {
500+
return $exception->getMessage();
501+
}
502+
});
503+
504+
$this->assertFalse($connection->preventingQueries());
505+
$this->assertSame('Queries are being prevented. Attempting to run query [select * from users].', $result);
506+
}
507+
508+
public function testItCanBeNested()
509+
{
510+
$connection = new Connection(new DatabaseConnectionTestMockPDO);
511+
$isActive = fn () => $connection->preventingQueries() ? 'prevent:' : 'allow:';
512+
513+
$result = $connection->preventQueries(function () use ($connection, $isActive) {
514+
return $isActive().$connection->allowQueries(function () use ($connection, $isActive) {
515+
return $isActive().$connection->preventQueries(function () use ($connection, $isActive) {
516+
return $isActive().$connection->allowQueries(function () use ($connection, $isActive) {
517+
return $isActive().$connection->preventQueries(function () use ($connection, $isActive) {
518+
return $isActive().$connection->preventQueries(function () use ($connection, $isActive) {
519+
return $isActive().$connection->allowQueries(function () use ($isActive) {
520+
return $isActive();
521+
}).$isActive();
522+
}).$isActive();
523+
}).$isActive();
524+
}).$isActive();
525+
}).$isActive();
526+
}).$isActive();
527+
});
528+
529+
$this->assertSame('prevent:allow:prevent:allow:prevent:prevent:allow:prevent:prevent:allow:prevent:allow:prevent:', $result);
530+
}
531+
475532
protected function getMockConnection($methods = [], $pdo = null)
476533
{
477534
$pdo = $pdo ?: new DatabaseConnectionTestMockPDO;

0 commit comments

Comments
 (0)