Skip to content
Open
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
5 changes: 3 additions & 2 deletions examples/peek.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 5 additions & 0 deletions src/Queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/Storage/Adapters/BeanstalkdStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Comment on lines +56 to +65
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peek() uses useTube() + peekReady(), but dequeue() uses watch($this->tube). peekReady() is not tube-scoped in the Beanstalk protocol, so this can return a job that this queue instance would never dequeue. Either implement peek in a tube-consistent way (e.g., watch+reserve+release with care) or document/rename this behavior as a global peek.

Copilot uses AI. Check for mistakes.

public function exist(string $value): bool
{
throw new \Exception('Not implemented yet');
Expand Down
22 changes: 22 additions & 0 deletions src/Storage/Adapters/FileStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/Storage/Adapters/RedisStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
6 changes: 6 additions & 0 deletions src/Storage/Adapters/SqliteStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$data ?: null will incorrectly convert valid queue items like the string "0" into null. Use a strict null check (e.g., return null only when querySingle() returns null) to keep peek() consistent with dequeue()’s $data ?? null behavior.

Suggested change
return $data ?: null;
return $data === null ? null : $data;

Copilot uses AI. Check for mistakes.
}

public function exist(string $value): bool
{
$result = $this->connection->querySingle("SELECT COUNT(*) FROM queue WHERE data = '$value'");
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SqliteStorage::exist method builds an SQL query by interpolating the untrusted $value directly into the string: "SELECT COUNT(*) FROM queue WHERE data = '$value'". If exist is ever called with attacker-controlled input, an attacker can inject arbitrary SQL (e.g. using a value like 'foo' OR 1=1 --) to read or manipulate data in the same SQLite database. Use parameterized queries with bound parameters (as done in enqueue) rather than string concatenation for the WHERE clause to prevent SQL injection.

Copilot uses AI. Check for mistakes.
Expand Down
2 changes: 2 additions & 0 deletions src/Storage/StorageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
168 changes: 168 additions & 0 deletions tests/StorageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

use Slowmove\SimplePhpQueue\Storage\Adapters\FileStorage;
use Slowmove\SimplePhpQueue\Storage\Adapters\SqliteStorage;

// ─── FileStorage ─────────────────────────────────────────────────────────────

describe('FileStorage', function () {
beforeEach(function () {
$this->file = tempnam(sys_get_temp_dir(), 'queue_file_') . '.txt';
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tempnam() already creates a real temp file. Appending an extension creates a second path and leaves the original temp file behind (leaking files in /tmp). Use the path returned by tempnam() directly, or rename the temp file and ensure both the renamed file and the original are cleaned up.

Suggested change
$this->file = tempnam(sys_get_temp_dir(), 'queue_file_') . '.txt';
$this->file = tempnam(sys_get_temp_dir(), 'queue_file_');

Copilot uses AI. Check for mistakes.
$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);
});
Comment on lines +91 to +94
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same tempnam() issue as above: appending ".db" leaks the original temp file created by tempnam(). Prefer using the returned path directly or explicitly rename+cleanup so the test suite doesn't leave stray temp files.

Copilot uses AI. Check for mistakes.

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();
});
});