Skip to content

Commit

Permalink
Merge branch '11.x' into fix-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald authored Jul 22, 2024
2 parents 21b7de5 + a13b726 commit 65823de
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 44 deletions.
116 changes: 95 additions & 21 deletions src/Illuminate/Cache/DatabaseStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\QueryException;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;

class DatabaseStore implements LockProvider, Store
{
use InteractsWithTime, RetrievesMultipleKeys;
use InteractsWithTime;

/**
* The database connection instance.
Expand Down Expand Up @@ -98,29 +99,56 @@ public function __construct(ConnectionInterface $connection,
*/
public function get($key)
{
$prefixed = $this->prefix.$key;

$cache = $this->table()->where('key', '=', $prefixed)->first();
return $this->many([$key])[$key];
}

// If we have a cache record we will check the expiration time against current
// time on the system and see if the record has expired. If it has, we will
// remove the records from the database table so it isn't returned again.
if (is_null($cache)) {
return;
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @return array
*/
public function many(array $keys)
{
if (count($keys) === 0) {
return [];
}

$cache = is_array($cache) ? (object) $cache : $cache;
$results = array_fill_keys($keys, null);

// First we will retrieve all of the items from the cache using their keys and
// the prefix value. Then we will need to iterate through each of the items
// and convert them to an object when they are currently in array format.
$values = $this->table()
->whereIn('key', array_map(function ($key) {
return $this->prefix.$key;
}, $keys))
->get()
->map(function ($value) {
return is_array($value) ? (object) $value : $value;
});

$currentTime = $this->currentTime();

// If this cache expiration date is past the current time, we will remove this
// item from the cache. Then we will return a null value since the cache is
// expired. We will use "Carbon" to make this comparison with the column.
if ($this->currentTime() >= $cache->expiration) {
$this->forgetIfExpired($key);
[$values, $expired] = $values->partition(function ($cache) use ($currentTime) {
return $cache->expiration > $currentTime;
});

return;
if ($expired->isNotEmpty()) {
$this->forgetManyIfExpired($expired->pluck('key')->all(), prefixed: true);
}

return $this->unserialize($cache->value);
return Arr::map($results, function ($value, $key) use ($values) {
if ($cache = $values->firstWhere('key', $this->prefix.$key)) {
return $this->unserialize($cache->value);
}

return $value;
});
}

/**
Expand All @@ -133,11 +161,30 @@ public function get($key)
*/
public function put($key, $value, $seconds)
{
$key = $this->prefix.$key;
$value = $this->serialize($value);
return $this->putMany([$key => $value], $seconds);
}

/**
* Store multiple items in the cache for a given number of seconds.
*
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
$serializedValues = [];

$expiration = $this->getTime() + $seconds;

return $this->table()->upsert(compact('key', 'value', 'expiration'), 'key') > 0;
foreach ($values as $key => $value) {
$serializedValues[] = [
'key' => $this->prefix.$key,
'value' => $this->serialize($value),
'expiration' => $expiration,
];
}

return $this->table()->upsert($serializedValues, 'key') > 0;
}

/**
Expand Down Expand Up @@ -309,9 +356,7 @@ public function restoreLock($name, $owner)
*/
public function forget($key)
{
$this->table()->where('key', '=', $this->prefix.$key)->delete();

return true;
return $this->forgetMany([$key]);
}

/**
Expand All @@ -321,9 +366,38 @@ public function forget($key)
* @return bool
*/
public function forgetIfExpired($key)
{
return $this->forgetManyIfExpired([$key]);
}

/**
* Remove all items from the cache.
*
* @param array $keys
* @return bool
*/
protected function forgetMany(array $keys)
{
$this->table()->whereIn('key', array_map(function ($key) {
return $this->prefix.$key;
}, $keys))->delete();

return true;
}

/**
* Remove all expired items from the given set from the cache.
*
* @param array $keys
* @param bool $prefixed
* @return bool
*/
protected function forgetManyIfExpired(array $keys, bool $prefixed = false)
{
$this->table()
->where('key', '=', $this->prefix.$key)
->whereIn('key', $prefixed ? $keys : array_map(function ($key) {
return $this->prefix.$key;
}, $keys))
->where('expiration', '<=', $this->getTime())
->delete();

Expand Down
13 changes: 13 additions & 0 deletions src/Illuminate/Database/Events/QueryExecuted.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,17 @@ public function __construct($sql, $bindings, $time, $connection)
$this->connection = $connection;
$this->connectionName = $connection->getName();
}

/**
* Get the raw SQL representation of the query with embedded bindings.
*
* @return string
*/
public function toRawSql()
{
return $this->connection
->query()
->getGrammar()
->substituteBindingsIntoRawSql($this->sql, $this->connection->prepareBindings($this->bindings));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Closure;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Testing\Fakes\ExceptionHandlerFake;
use Illuminate\Support\Traits\ReflectsClosures;
use Illuminate\Testing\Assert;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Console\Application as ConsoleApplication;
Expand All @@ -13,6 +14,8 @@

trait InteractsWithExceptionHandling
{
use ReflectsClosures;

/**
* The original exception handler.
*
Expand Down Expand Up @@ -169,18 +172,22 @@ public function renderForConsole($output, Throwable $e)
* Assert that the given callback throws an exception with the given message when invoked.
*
* @param \Closure $test
* @param class-string<\Throwable> $expectedClass
* @param \Closure|class-string<\Throwable> $expectedClass
* @param string|null $expectedMessage
* @return $this
*/
protected function assertThrows(Closure $test, string $expectedClass = Throwable::class, ?string $expectedMessage = null)
protected function assertThrows(Closure $test, string|Closure $expectedClass = Throwable::class, ?string $expectedMessage = null)
{
[$expectedClass, $expectedClassCallback] = $expectedClass instanceof Closure
? [$this->firstClosureParameterType($expectedClass), $expectedClass]
: [$expectedClass, null];

try {
$test();

$thrown = false;
} catch (Throwable $exception) {
$thrown = $exception instanceof $expectedClass;
$thrown = $exception instanceof $expectedClass && ($expectedClassCallback === null || $expectedClassCallback($exception));

$actualMessage = $exception->getMessage();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ public function get(string $key, mixed $default = null): mixed
public function json($key = null, $default = null)
{
if (! isset($this->json)) {
$this->json = new InputBag((array) json_decode($this->getContent(), true));
$this->json = new InputBag((array) json_decode($this->getContent() ?: '[]', true));
}

if (is_null($key)) {
Expand Down
34 changes: 20 additions & 14 deletions tests/Cache/CacheDatabaseStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,26 @@ public function testNullIsReturnedWhenItemNotFound()
$store = $this->getStore();
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
$table->shouldReceive('first')->once()->andReturn(null);
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
$table->shouldReceive('get')->once()->andReturn(collect([]));

$this->assertNull($store->get('foo'));
}

public function testNullIsReturnedAndItemDeletedWhenItemIsExpired()
{
$store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['forgetIfExpired'])->setConstructorArgs($this->getMocks())->getMock();
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
$table->shouldReceive('first')->once()->andReturn((object) ['expiration' => 1]);
$store->expects($this->once())->method('forgetIfExpired')->with($this->equalTo('foo'))->willReturn(null);

$getQuery = m::mock(stdClass::class);
$getQuery->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($getQuery);
$getQuery->shouldReceive('get')->once()->andReturn(collect([(object) ['key' => 'prefixfoo', 'expiration' => 1]]));

$deleteQuery = m::mock(stdClass::class);
$deleteQuery->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($deleteQuery);
$deleteQuery->shouldReceive('where')->once()->with('expiration', '<=', m::any())->andReturn($deleteQuery);
$deleteQuery->shouldReceive('delete')->once()->andReturnNull();

$store->getConnection()->shouldReceive('table')->twice()->with('table')->andReturn($getQuery, $deleteQuery);

$this->assertNull($store->get('foo'));
}
Expand All @@ -45,8 +51,8 @@ public function testDecryptedValueIsReturnedWhenItemIsValid()
$store = $this->getStore();
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
$table->shouldReceive('first')->once()->andReturn((object) ['value' => serialize('bar'), 'expiration' => 999999999999999]);
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
$table->shouldReceive('get')->once()->andReturn(collect([(object) ['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 999999999999999]]));

$this->assertSame('bar', $store->get('foo'));
}
Expand All @@ -56,8 +62,8 @@ public function testValueIsReturnedOnPostgres()
$store = $this->getPostgresStore();
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
$table->shouldReceive('first')->once()->andReturn((object) ['value' => base64_encode(serialize('bar')), 'expiration' => 999999999999999]);
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
$table->shouldReceive('get')->once()->andReturn(collect([(object) ['key' => 'prefixfoo', 'value' => base64_encode(serialize('bar')), 'expiration' => 999999999999999]]));

$this->assertSame('bar', $store->get('foo'));
}
Expand All @@ -68,7 +74,7 @@ public function testValueIsUpserted()
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$store->expects($this->once())->method('getTime')->willReturn(1);
$table->shouldReceive('upsert')->once()->with(['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61], 'key')->andReturnTrue();
$table->shouldReceive('upsert')->once()->with([['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61]], 'key')->andReturnTrue();

$result = $store->put('foo', 'bar', 60);
$this->assertTrue($result);
Expand All @@ -80,7 +86,7 @@ public function testValueIsUpsertedOnPostgres()
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$store->expects($this->once())->method('getTime')->willReturn(1);
$table->shouldReceive('upsert')->once()->with(['key' => 'prefixfoo', 'value' => base64_encode(serialize("\0")), 'expiration' => 61], 'key')->andReturn(1);
$table->shouldReceive('upsert')->once()->with([['key' => 'prefixfoo', 'value' => base64_encode(serialize("\0")), 'expiration' => 61]], 'key')->andReturn(1);

$result = $store->put('foo', "\0", 60);
$this->assertTrue($result);
Expand All @@ -99,7 +105,7 @@ public function testItemsMayBeRemovedFromCache()
$store = $this->getStore();
$table = m::mock(stdClass::class);
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
$table->shouldReceive('delete')->once();

$store->forget('foo');
Expand Down
38 changes: 38 additions & 0 deletions tests/Database/DatabaseIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Events\Dispatcher;
use PHPUnit\Framework\TestCase;

class DatabaseIntegrationTest extends TestCase
{
protected function setUp(): void
{
$db = new DB;
$db->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
]);
$db->setAsGlobal();
$db->setEventDispatcher(new Dispatcher);
}

public function testQueryExecutedToRawSql(): void
{
$connection = DB::connection();

$connection->listen(function (QueryExecuted $query) use (&$queryExecuted): void {
$queryExecuted = $query;
});

$connection->select('select ?', [true]);

$this->assertInstanceOf(QueryExecuted::class, $queryExecuted);
$this->assertSame('select ?', $queryExecuted->sql);
$this->assertSame([true], $queryExecuted->bindings);
$this->assertSame('select 1', $queryExecuted->toRawSql());
}
}
Loading

0 comments on commit 65823de

Please sign in to comment.