diff --git a/examples/peek.php b/examples/peek.php index 7c6b035..3a1e3fd 100644 --- a/examples/peek.php +++ b/examples/peek.php @@ -9,5 +9,6 @@ storage: StorageType::FILE, queueFile: "" ); -$exist = $queue->exist("test 132"); -var_dump($exist); + +$next = $queue->peek(); +echo "Next item in queue: " . ($next ?? '(empty)') . PHP_EOL; diff --git a/src/Queue.php b/src/Queue.php index 6deb018..d6b9466 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -27,6 +27,11 @@ public function dequeue(): ?string return $this->storage->dequeue(); } + public function peek(): ?string + { + return $this->storage->peek(); + } + public function exist($value): ?string { return $this->storage->exist($value); diff --git a/src/Storage/Adapters/BeanstalkdStorage.php b/src/Storage/Adapters/BeanstalkdStorage.php index 309b96f..d82b5f3 100644 --- a/src/Storage/Adapters/BeanstalkdStorage.php +++ b/src/Storage/Adapters/BeanstalkdStorage.php @@ -53,6 +53,17 @@ public function dequeue(): ?string return null; } + public function peek(): ?string + { + try { + $this->beanstalkdClient->useTube($this->tube); + $job = $this->beanstalkdClient->peekReady(); + return $job->getData(); + } catch (\Throwable $th) { + return null; + } + } + public function exist(string $value): bool { throw new \Exception('Not implemented yet'); diff --git a/src/Storage/Adapters/FileStorage.php b/src/Storage/Adapters/FileStorage.php index b231422..4cd532c 100644 --- a/src/Storage/Adapters/FileStorage.php +++ b/src/Storage/Adapters/FileStorage.php @@ -75,6 +75,28 @@ public function dequeue(): ?string return $data; } + public function peek(): ?string + { + $fileHandle = fopen($this->queueFile, 'r'); + if (!$fileHandle) { + return null; + } + + flock($fileHandle, LOCK_SH); + + $line = fgets($fileHandle); + + flock($fileHandle, LOCK_UN); + fclose($fileHandle); + + if ($line === false) { + return null; + } + + $data = rtrim($line, PHP_EOL); + return $data !== '' ? $data : null; + } + public function exist(string $value): bool { $lines = file($this->queueFile, FILE_SKIP_EMPTY_LINES); diff --git a/src/Storage/Adapters/RedisStorage.php b/src/Storage/Adapters/RedisStorage.php index 201af59..1f958ec 100644 --- a/src/Storage/Adapters/RedisStorage.php +++ b/src/Storage/Adapters/RedisStorage.php @@ -30,6 +30,11 @@ public function dequeue(): ?string return $this->redisClient->rpop(self::DEFAULT_STORAGE_NAME); } + public function peek(): ?string + { + return $this->redisClient->lindex(self::DEFAULT_STORAGE_NAME, -1); + } + public function exist(string $value): bool { $exist = $this->redisClient->executeRaw(["LPOS", self::DEFAULT_STORAGE_NAME, $value]); diff --git a/src/Storage/Adapters/SqliteStorage.php b/src/Storage/Adapters/SqliteStorage.php index a85e086..d2304e8 100644 --- a/src/Storage/Adapters/SqliteStorage.php +++ b/src/Storage/Adapters/SqliteStorage.php @@ -42,6 +42,12 @@ public function dequeue(): ?string return $data ?? null; } + public function peek(): ?string + { + $data = $this->connection->querySingle("SELECT data FROM queue ORDER BY id ASC LIMIT 1"); + return $data ?: null; + } + public function exist(string $value): bool { $result = $this->connection->querySingle("SELECT COUNT(*) FROM queue WHERE data = '$value'"); diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php index c298b87..883a8b2 100644 --- a/src/Storage/StorageInterface.php +++ b/src/Storage/StorageInterface.php @@ -10,6 +10,8 @@ public function enqueue(string $data): bool; public function dequeue(): ?string; + public function peek(): ?string; + public function exist(string $value): bool; public function length(): int; diff --git a/tests/StorageTest.php b/tests/StorageTest.php new file mode 100644 index 0000000..293249a --- /dev/null +++ b/tests/StorageTest.php @@ -0,0 +1,168 @@ +file = tempnam(sys_get_temp_dir(), 'queue_file_') . '.txt'; + $this->storage = new FileStorage($this->file); + }); + + afterEach(function () { + if (file_exists($this->file)) { + unlink($this->file); + } + }); + + it('peek returns null on an empty queue', function () { + expect($this->storage->peek())->toBeNull(); + }); + + it('peek returns the next item without removing it', function () { + $this->storage->enqueue('first'); + $this->storage->enqueue('second'); + + expect($this->storage->peek())->toBe('first'); + expect($this->storage->length())->toBe(2); + }); + + it('peek is idempotent — calling it twice returns the same item', function () { + $this->storage->enqueue('only'); + + expect($this->storage->peek())->toBe('only'); + expect($this->storage->peek())->toBe('only'); + }); + + it('dequeue after peek removes the item that was peeked', function () { + $this->storage->enqueue('alpha'); + $this->storage->enqueue('beta'); + + $peeked = $this->storage->peek(); + $dequeued = $this->storage->dequeue(); + + expect($peeked)->toBe($dequeued); + expect($this->storage->length())->toBe(1); + }); + + it('peek returns null after all items are dequeued', function () { + $this->storage->enqueue('sole'); + $this->storage->dequeue(); + + expect($this->storage->peek())->toBeNull(); + }); + + it('enqueue and dequeue work in FIFO order', function () { + $this->storage->enqueue('one'); + $this->storage->enqueue('two'); + $this->storage->enqueue('three'); + + expect($this->storage->dequeue())->toBe('one'); + expect($this->storage->dequeue())->toBe('two'); + expect($this->storage->dequeue())->toBe('three'); + expect($this->storage->dequeue())->toBeNull(); + }); + + it('length reflects the current number of items', function () { + expect($this->storage->length())->toBe(0); + $this->storage->enqueue('x'); + expect($this->storage->length())->toBe(1); + $this->storage->enqueue('y'); + expect($this->storage->length())->toBe(2); + $this->storage->dequeue(); + expect($this->storage->length())->toBe(1); + }); + + it('exist returns true for an item in the queue', function () { + $this->storage->enqueue('hello'); + expect($this->storage->exist('hello'))->toBeTrue(); + }); + + it('exist returns false for an item not in the queue', function () { + expect($this->storage->exist('ghost'))->toBeFalse(); + }); +}); + +// ─── SqliteStorage ──────────────────────────────────────────────────────────── + +describe('SqliteStorage', function () { + beforeEach(function () { + $this->file = tempnam(sys_get_temp_dir(), 'queue_sqlite_') . '.db'; + $this->storage = new SqliteStorage($this->file); + }); + + afterEach(function () { + if (file_exists($this->file)) { + unlink($this->file); + } + }); + + it('peek returns null on an empty queue', function () { + expect($this->storage->peek())->toBeNull(); + }); + + it('peek returns the next item without removing it', function () { + $this->storage->enqueue('first'); + $this->storage->enqueue('second'); + + expect($this->storage->peek())->toBe('first'); + expect($this->storage->length())->toBe(2); + }); + + it('peek is idempotent — calling it twice returns the same item', function () { + $this->storage->enqueue('only'); + + expect($this->storage->peek())->toBe('only'); + expect($this->storage->peek())->toBe('only'); + }); + + it('dequeue after peek removes the item that was peeked', function () { + $this->storage->enqueue('alpha'); + $this->storage->enqueue('beta'); + + $peeked = $this->storage->peek(); + $dequeued = $this->storage->dequeue(); + + expect($peeked)->toBe($dequeued); + expect($this->storage->length())->toBe(1); + }); + + it('peek returns null after all items are dequeued', function () { + $this->storage->enqueue('sole'); + $this->storage->dequeue(); + + expect($this->storage->peek())->toBeNull(); + }); + + it('enqueue and dequeue work in FIFO order', function () { + $this->storage->enqueue('one'); + $this->storage->enqueue('two'); + $this->storage->enqueue('three'); + + expect($this->storage->dequeue())->toBe('one'); + expect($this->storage->dequeue())->toBe('two'); + expect($this->storage->dequeue())->toBe('three'); + expect($this->storage->dequeue())->toBeNull(); + }); + + it('length reflects the current number of items', function () { + expect($this->storage->length())->toBe(0); + $this->storage->enqueue('x'); + expect($this->storage->length())->toBe(1); + $this->storage->enqueue('y'); + expect($this->storage->length())->toBe(2); + $this->storage->dequeue(); + expect($this->storage->length())->toBe(1); + }); + + it('exist returns true for an item in the queue', function () { + $this->storage->enqueue('hello'); + expect($this->storage->exist('hello'))->toBeTrue(); + }); + + it('exist returns false for an item not in the queue', function () { + expect($this->storage->exist('ghost'))->toBeFalse(); + }); +});