Skip to content

Commit 020dd19

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

File tree

8 files changed

+327
-3
lines changed

8 files changed

+327
-3
lines changed

src/Illuminate/Database/Connection.php

Lines changed: 68 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,53 @@ 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 $callback
811+
* @return mixed
812+
*/
813+
public function preventQueries($callback = null)
814+
{
815+
if ($callback === null) {
816+
return $this->preventQueries = true;
817+
}
818+
819+
$preventQueriesCache = $this->preventQueries;
820+
821+
$this->preventQueries = true;
822+
823+
try {
824+
return $callback();
825+
} finally {
826+
$this->preventQueries = $preventQueriesCache;
827+
}
828+
}
829+
830+
/**
831+
* Restore normal behaviour; do not throw exceptions when a query is to be run.
832+
*
833+
* @param \Closure $callback
834+
* @return mixed
835+
*/
836+
public function allowQueries($callback = null)
837+
{
838+
if ($callback === null) {
839+
return $this->preventQueries = false;
840+
}
841+
842+
$preventQueriesCache = $this->preventQueries;
843+
844+
$this->preventQueries = false;
845+
846+
try {
847+
return $callback();
848+
} finally {
849+
$this->preventQueries = $preventQueriesCache;
850+
}
851+
}
852+
853+
796854
/**
797855
* Register a callback to be invoked when the connection queries for longer than a given amount of time.
798856
*
@@ -1494,6 +1552,16 @@ public function logging()
14941552
return $this->loggingQueries;
14951553
}
14961554

1555+
/**
1556+
* Determine if the connection is preventing queries.
1557+
*
1558+
* @return bool
1559+
*/
1560+
public function preventingQueries()
1561+
{
1562+
return $this->preventQueries;
1563+
}
1564+
14971565
/**
14981566
* Get the name of the connected database.
14991567
*

src/Illuminate/Foundation/Application.php

Lines changed: 18 additions & 2 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;
@@ -1423,9 +1425,23 @@ public function getNamespace()
14231425
if (realpath($this->path()) === realpath($this->basePath($pathChoice))) {
14241426
return $this->namespace = $namespace;
14251427
}
1426-
}
1427-
}
1428+
} }
14281429

14291430
throw new RuntimeException('Unable to detect application namespace.');
14301431
}
1432+
1433+
/**
1434+
* Prevent database queries while preparing the request response.
1435+
*
1436+
* @param string|null $connection
1437+
* @return $this
1438+
*/
1439+
public function preventQueriesWhilePreparingResponse($connection = null)
1440+
{
1441+
$this['events']->listen(PreparingResponse::class, fn () => $this['db']->connection($connection)->preventQueries());
1442+
1443+
$this['events']->listen(ResponsePrepared::class, fn () => $this['db']->connection($connection)->allowQueries());
1444+
1445+
return $this;
1446+
}
14311447
}
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 \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+
}
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 \Response
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+
}

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 ($connection, $isActive) {
520+
return $isActive();
521+
});
522+
});
523+
});
524+
});
525+
});
526+
});
527+
});
528+
529+
$this->assertSame('prevent:allow:prevent:allow:prevent:prevent:allow:', $result);
530+
}
531+
475532
protected function getMockConnection($methods = [], $pdo = null)
476533
{
477534
$pdo = $pdo ?: new DatabaseConnectionTestMockPDO;

0 commit comments

Comments
 (0)