Skip to content

Commit

Permalink
Merge pull request #7 from DirectoryTree/array-repository
Browse files Browse the repository at this point in the history
Add ability to set array repository for testing
  • Loading branch information
stevebauman authored Nov 19, 2024
2 parents 55f6201 + 9670d62 commit 91af444
Show file tree
Hide file tree
Showing 5 changed files with 388 additions and 2 deletions.
21 changes: 20 additions & 1 deletion src/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use DirectoryTree\ActiveRedis\Exceptions\DuplicateKeyException;
use DirectoryTree\ActiveRedis\Exceptions\InvalidKeyException;
use DirectoryTree\ActiveRedis\Exceptions\JsonEncodingException;
use DirectoryTree\ActiveRedis\Repositories\ArrayRepository;
use DirectoryTree\ActiveRedis\Repositories\RedisRepository;
use DirectoryTree\ActiveRedis\Repositories\Repository;
use Illuminate\Contracts\Redis\Connection;
Expand All @@ -26,6 +27,7 @@
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
use InvalidArgumentException;
use JsonException;
use Stringable;

Expand Down Expand Up @@ -84,6 +86,11 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, Stringable, Ur
*/
protected ?string $connection = null;

/**
* The repository for the model.
*/
protected static string $repository = 'redis';

/**
* Handle dynamic method calls into the model.
*/
Expand Down Expand Up @@ -411,12 +418,24 @@ public function newBuilder(): Query
return new Query($this, $this->newRepository());
}

/**
* Set the repository for the model.
*/
public static function setRepository(string $repository): void
{
static::$repository = $repository;
}

/**
* Create a new repository instance.
*/
protected function newRepository(): Repository
{
return new RedisRepository($this->getConnection());
return match ($repository = static::$repository) {
'array' => new ArrayRepository,
'redis' => new RedisRepository($this->getConnection()),
default => throw new InvalidArgumentException("Repository [{$repository}] is not supported."),
};
}

/**
Expand Down
148 changes: 148 additions & 0 deletions src/Repositories/ArrayRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace DirectoryTree\ActiveRedis\Repositories;

use Closure;
use Generator;
use Illuminate\Support\Str;

class ArrayRepository implements Repository
{
/**
* Constructor.
*/
public function __construct(
protected array $data = [],
protected array $expiry = [],
) {}

/**
* Determine if the given hash exists.
*/
public function exists(string $hash): bool
{
return isset($this->data[$hash]);
}

/**
* Chunk through the hashes matching the given pattern.
*/
public function chunk(string $pattern, int $count): Generator
{
$matches = [];

foreach (array_keys($this->data) as $key) {
if (Str::is($pattern, $key)) {
$matches[] = $key;
}
}

foreach (array_chunk($matches, $count) as $chunk) {
yield $chunk;
}
}

/**
* Set the hash field's value.
*/
public function setAttribute(string $hash, string $attribute, string $value): void
{
$this->data[$hash][$attribute] = $value;
}

/**
* Set the hash field's value.
*/
public function setAttributes(string $hash, array $attributes): void
{
foreach ($attributes as $attribute => $value) {
$this->setAttribute($hash, $attribute, $value);
}
}

/**
* Get the hash field's value.
*/
public function getAttribute(string $hash, string $field): mixed
{
if ($this->isExpired($hash)) {
$this->delete($hash);

return null;
}

return $this->data[$hash][$field] ?? null;
}

/**
* Get all the attributes in the hash.
*/
public function getAttributes(string $hash): array
{
if ($this->isExpired($hash)) {
$this->delete($hash);

return [];
}

return $this->data[$hash] ?? [];
}

/**
* Set a time-to-live on a hash key.
*
* Not supported in ArrayRepository.
*/
public function setExpiry(string $hash, int $seconds): void
{
$this->expiry[$hash] = time() + $seconds;
}

/**
* Get the time-to-live of a hash key.
*/
public function getExpiry(string $hash): ?int
{
if (! isset($this->expiry[$hash])) {
return null;
}

$remaining = $this->expiry[$hash] - time();

return $remaining > 0 ? $remaining : null;
}

/**
* Delete the attributes from the hash.
*/
public function deleteAttributes(string $hash, array|string $attributes): void
{
foreach ((array) $attributes as $attribute) {
unset($this->data[$hash][$attribute]);
}
}

/**
* Delete the given hash.
*/
public function delete(string $hash): void
{
unset($this->data[$hash]);
}

/**
* Perform a transaction.
*/
public function transaction(Closure $operation): void
{
$operation($this);
}

/**
* Determine if the given hash has expired.
*/
protected function isExpired(string $hash): bool
{
return isset($this->expiry[$hash]) && $this->expiry[$hash] <= time();
}
}
4 changes: 3 additions & 1 deletion src/Repositories/RedisRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ public function getExpiry(string $hash): ?int
{
// The number of seconds until the key will expire, or
// null if the key does not exist or has no timeout.
return $this->redis->ttl($hash);
$ttl = $this->redis->ttl($hash);

return $ttl > 0 ? $ttl : null;
}

/**
Expand Down
99 changes: 99 additions & 0 deletions tests/Repositories/ArrayRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

use DirectoryTree\ActiveRedis\Repositories\ArrayRepository;

it('can check if a hash exists', function () {
$repository = new ArrayRepository(['foo' => ['bar' => 'baz']]);

expect($repository->exists('foo'))->toBeTrue();
expect($repository->exists('bar'))->toBeFalse();
});

it('can chunk through hashes matching a pattern', function () {
$repository = new ArrayRepository([
'user:1' => [],
'user:2' => [],
'post:1' => [],
'user:3' => [],
]);

$chunks = iterator_to_array($repository->chunk('user:*', 2));

expect($chunks)->toHaveCount(2);
expect($chunks[0])->toEqual(['user:1', 'user:2']);
expect($chunks[1])->toEqual(['user:3']);
});

it('can set and get a single attribute', function () {
$repository = new ArrayRepository;

$repository->setAttribute('foo', 'bar', 'baz');

expect($repository->getAttribute('foo', 'bar'))->toBe('baz');
});

it('can set and get multiple attributes', function () {
$repository = new ArrayRepository;

$repository->setAttributes('foo', ['bar' => 'baz', 'qux' => 'quux']);

expect($repository->getAttributes('foo'))->toEqual(['bar' => 'baz', 'qux' => 'quux']);
});

it('can delete attributes', function () {
$repository = new ArrayRepository(['foo' => ['bar' => 'baz', 'qux' => 'quux']]);

$repository->deleteAttributes('foo', 'bar');

expect($repository->getAttributes('foo'))->toEqual(['qux' => 'quux']);

$repository->deleteAttributes('foo', ['qux']);

expect($repository->getAttributes('foo'))->toEqual([]);
});

it('can delete a hash', function () {
$repository = new ArrayRepository(['foo' => ['bar' => 'baz']]);

$repository->delete('foo');

expect($repository->exists('foo'))->toBeFalse();
});

it('can set and get expiry', function () {
$repository = new ArrayRepository;

$repository->setExpiry('foo', 10);

expect($repository->getExpiry('foo'))->toBeGreaterThanOrEqual(9);
});

it('returns null for expiry of non-existent key', function () {
$repository = new ArrayRepository;

expect($repository->getExpiry('foo'))->toBeNull();
});

it('deletes expired hashes', function () {
$repository = new ArrayRepository;

$repository->setAttributes('foo', ['bar' => 'baz']);

$repository->setExpiry('foo', 1);

sleep(2);

expect($repository->getAttributes('foo'))->toEqual([]);
});

it('handles getting attributes from expired hash', function () {
$repository = new ArrayRepository;

$repository->setAttributes('foo', ['bar' => 'baz']);

$repository->setExpiry('foo', 1);

sleep(2);

expect($repository->getAttribute('foo', 'bar'))->toBeNull();
});
Loading

0 comments on commit 91af444

Please sign in to comment.