Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions src/Cache/Adapter/RedisCacheAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Infocyph\CacheLayer\Cache\Item\RedisCacheItem;
use Infocyph\CacheLayer\Exceptions\CacheInvalidArgumentException;
use Infocyph\CacheLayer\Support\RedisConnection;
use InvalidArgumentException;
use Psr\Cache\CacheItemInterface;
use RuntimeException;

Expand Down Expand Up @@ -205,23 +207,11 @@ protected function supportsItem(CacheItemInterface $item): bool

private function connect(string $dsn): \Redis
{
$r = new \Redis();
$parts = parse_url($dsn);
if (!$parts) {
throw new RuntimeException("Invalid Redis DSN: $dsn");
try {
return RedisConnection::connect($dsn);
} catch (InvalidArgumentException $exception) {
throw new RuntimeException("Invalid Redis DSN: $dsn", 0, $exception);
}
$host = is_string($parts['host'] ?? null) ? $parts['host'] : '127.0.0.1';
$port = is_int($parts['port'] ?? null) ? $parts['port'] : 6379;
$r->connect($host, $port);
if (is_string($parts['pass'] ?? null) && $parts['pass'] !== '') {
$r->auth($parts['pass']);
}
if (is_string($parts['path'] ?? null) && $parts['path'] !== '/') {
$db = (int) ltrim($parts['path'], '/');
$r->select($db);
}

return $r;
}

private function map(string $key): string
Expand Down
42 changes: 22 additions & 20 deletions src/Cluster/Cursor/SqliteCursorStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,11 @@ public function advance(string $eventId): void

public function current(): ?string
{
try {
$statement = $this->connection->prepare(
'SELECT last_event_id FROM cachelayer_cluster_cursors '
. 'WHERE cluster_name = :cluster AND node_id = :node_id LIMIT 1',
);
$statement->execute([':cluster' => $this->cluster, ':node_id' => $this->nodeId]);
$cursor = $statement->fetchColumn();
} catch (PDOException $exception) {
throw new ClusterCacheException('Unable to read the cluster cursor.', 0, $exception);
}
$cursor = $this->read(
'SELECT last_event_id FROM cachelayer_cluster_cursors '
. 'WHERE cluster_name = :cluster AND node_id = :node_id LIMIT 1',
'Unable to read the cluster cursor.',
);

return is_string($cursor) && $cursor !== '' ? $cursor : null;
}
Expand All @@ -58,16 +53,11 @@ public function reset(?string $eventId): void

public function updatedAt(): ?int
{
try {
$statement = $this->connection->prepare(
'SELECT updated_at FROM cachelayer_cluster_cursors '
. 'WHERE cluster_name = :cluster AND node_id = :node_id LIMIT 1',
);
$statement->execute([':cluster' => $this->cluster, ':node_id' => $this->nodeId]);
$updatedAt = $statement->fetchColumn();
} catch (PDOException $exception) {
throw new ClusterCacheException('Unable to read the cluster cursor update time.', 0, $exception);
}
$updatedAt = $this->read(
'SELECT updated_at FROM cachelayer_cluster_cursors '
. 'WHERE cluster_name = :cluster AND node_id = :node_id LIMIT 1',
'Unable to read the cluster cursor update time.',
);

return is_int($updatedAt) || (is_string($updatedAt) && ctype_digit($updatedAt))
? (int) $updatedAt
Expand All @@ -87,6 +77,18 @@ private function createSchemaIfMissing(): void
}
}

private function read(string $sql, string $failureMessage): mixed
{
try {
$statement = $this->connection->prepare($sql);
$statement->execute([':cluster' => $this->cluster, ':node_id' => $this->nodeId]);

return $statement->fetchColumn();
} catch (PDOException $exception) {
throw new ClusterCacheException($failureMessage, 0, $exception);
}
}

private function write(?string $eventId): void
{
try {
Expand Down
28 changes: 28 additions & 0 deletions src/Cluster/Transport/EventField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Infocyph\CacheLayer\Cluster\Transport;

use Infocyph\CacheLayer\Cluster\Exception\ClusterTransportException;

final class EventField
{
public static function requiredString(
mixed $fields,
string $name,
string $invalidFieldsMessage,
string $invalidValueMessage,
): string {
if (!is_array($fields)) {
throw new ClusterTransportException($invalidFieldsMessage);
}

$value = $fields[$name] ?? null;
if (!is_string($value) || $value === '') {
throw new ClusterTransportException($invalidValueMessage);
}

return $value;
}
}
17 changes: 7 additions & 10 deletions src/Cluster/Transport/Pdo/PdoInvalidationTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Infocyph\CacheLayer\Cluster\Event\InvalidationEvent;
use Infocyph\CacheLayer\Cluster\Event\InvalidationEventType;
use Infocyph\CacheLayer\Cluster\Exception\ClusterTransportException;
use Infocyph\CacheLayer\Cluster\Transport\EventField;
use Infocyph\CacheLayer\Cluster\Transport\InvalidationTransportInspectorInterface;
use Infocyph\CacheLayer\Cluster\Transport\TransactionalInvalidationTransportInterface;
use PDO;
Expand Down Expand Up @@ -329,16 +330,12 @@ private function pruneSql(): string

private function requiredString(mixed $row, string $key): string
{
if (!is_array($row)) {
throw new ClusterTransportException('Invalid PDO transport event row.');
}

$value = $row[$key] ?? null;
if (!is_string($value) || $value === '') {
throw new ClusterTransportException("Invalid {$key} returned by PDO transport.");
}

return $value;
return EventField::requiredString(
$row,
$key,
'Invalid PDO transport event row.',
"Invalid {$key} returned by PDO transport.",
);
}

private function timestampFromValue(mixed $value): int
Expand Down
16 changes: 6 additions & 10 deletions src/Cluster/Transport/RedisStreamInvalidationTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,12 @@ private function eventFromFields(string $id, array $fields): InvalidationEvent

private function field(mixed $fields, string $name): string
{
if (!is_array($fields)) {
throw new ClusterTransportException('Redis Stream event fields must be an array.');
}

$value = $fields[$name] ?? null;
if (!is_string($value) || $value === '') {
throw new ClusterTransportException("Redis Stream event has an invalid {$name} field.");
}

return $value;
return EventField::requiredString(
$fields,
$name,
'Redis Stream event fields must be an array.',
"Redis Stream event has an invalid {$name} field.",
);
}

/**
Expand Down
22 changes: 6 additions & 16 deletions src/Counter/AtomicCounters.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Infocyph\CacheLayer\Counter;

use Infocyph\CacheLayer\Counter\Exception\AtomicCounterException;
use Infocyph\CacheLayer\Support\RedisConnection;
use InvalidArgumentException;

final class AtomicCounters
{
Expand All @@ -30,22 +32,10 @@ public static function valkey(

private static function connect(string $dsn): \Redis
{
$parts = parse_url($dsn);
if (!is_array($parts)) {
throw new AtomicCounterException("Invalid Redis-compatible DSN: {$dsn}");
try {
return RedisConnection::connect($dsn);
} catch (InvalidArgumentException $exception) {
throw new AtomicCounterException("Invalid Redis-compatible DSN: {$dsn}", 0, $exception);
}

$client = new \Redis();
$host = is_string($parts['host'] ?? null) ? $parts['host'] : '127.0.0.1';
$port = is_int($parts['port'] ?? null) ? $parts['port'] : 6379;
$client->connect($host, $port);
if (is_string($parts['pass'] ?? null) && $parts['pass'] !== '') {
$client->auth($parts['pass']);
}
if (is_string($parts['path'] ?? null) && $parts['path'] !== '/') {
$client->select((int) ltrim($parts['path'], '/'));
}

return $client;
}
}
31 changes: 31 additions & 0 deletions src/Support/RedisConnection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Infocyph\CacheLayer\Support;

use InvalidArgumentException;

final class RedisConnection
{
public static function connect(string $dsn): \Redis
{
$parts = parse_url($dsn);
if (!is_array($parts)) {
throw new InvalidArgumentException('Invalid Redis-compatible DSN.');
}

$connection = new \Redis();
$host = is_string($parts['host'] ?? null) ? $parts['host'] : '127.0.0.1';
$port = is_int($parts['port'] ?? null) ? $parts['port'] : 6379;
$connection->connect($host, $port);
if (is_string($parts['pass'] ?? null) && $parts['pass'] !== '') {
$connection->auth($parts['pass']);
}
if (is_string($parts['path'] ?? null) && $parts['path'] !== '/') {
$connection->select((int) ltrim($parts['path'], '/'));
}

return $connection;
}
}
10 changes: 10 additions & 0 deletions tests/Support/RedisConnectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Infocyph\CacheLayer\Support\RedisConnection;

test('redis connection rejects malformed DSNs before creating a client', function () {
expect(fn () => RedisConnection::connect('redis://'))
->toThrow(\InvalidArgumentException::class, 'Invalid Redis-compatible DSN.');
});