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
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
* text=auto eol=lf
*.php text eol=lf
*.md text eol=lf
*.json text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf

/tests export-ignore
/examples export-ignore
/CHANGELOG.md export-ignore
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,28 @@ $queue->retry($failed[0]['id']);
$queue->flush();
```

### Inspecting Pending Jobs

```php
// List all pending jobs (including delayed ones not yet available)
$pending = $queue->getPending();

foreach ($pending as $queuedJob) {
echo sprintf(
"ID: %s | Priority: %d | Attempts: %d | Available: %s\n",
$queuedJob->getId(),
$queuedJob->getPriority(),
$queuedJob->getAttempts(),
date('Y-m-d H:i:s', $queuedJob->getAvailableAt())
);
}

// Quick count (works with any storage backend)
$count = $queue->getPendingCount();
```

> **Note:** `getPending()` requires a storage backend that implements `ListableQueueStorage` (e.g. `FileQueueStorage`). Backends like SQS that don't support listing will throw a `LogicException`.

## API

### `Job` (interface)
Expand All @@ -133,6 +155,7 @@ $queue->flush();
| `process(int $limit = 10): int` | Process pending jobs, returns count processed |
| `retry(string $id): void` | Retry a failed job |
| `getPendingCount(): int` | Number of pending jobs |
| `getPending(): array` | All pending jobs (requires `ListableQueueStorage`) |
| `getFailed(): array` | All failed jobs |
| `flush(): void` | Remove all failed jobs |
| `getStorage(): QueueStorage` | Get the storage backend |
Expand All @@ -151,6 +174,14 @@ $queue->flush();
| `getFailed(): array` | Get all failed jobs |
| `flush(): void` | Clear failed jobs |

### `ListableQueueStorage` (interface, extends `QueueStorage`)

| Method | Description |
|--------|-------------|
| `getPending(): array` | Returns all pending jobs (including delayed), sorted by priority desc |

Backends that support listing (files, database, Redis) implement `ListableQueueStorage`. Backends that only support push/pop semantics (SQS, RabbitMQ) implement the base `QueueStorage`.

### `QueueFacade`

Static wrapper. Same methods as `Queue` plus `getInstance()`, `setInstance()`, `reset()`.
Expand Down
57 changes: 48 additions & 9 deletions WebFiori/Queue/FileQueueStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* Stores each job as a JSON file. Pending jobs are in the 'pending' subdirectory,
* failed jobs in the 'failed' subdirectory.
*/
class FileQueueStorage implements QueueStorage {
class FileQueueStorage implements ListableQueueStorage {
private string $baseDir;

/**
Expand All @@ -31,7 +31,7 @@ public function __construct(string $baseDir) {
$this->ensureDir($this->getFailedDir());
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function flush(): void {
$files = glob($this->getFailedDir().DIRECTORY_SEPARATOR.'*.json');
Expand All @@ -49,7 +49,7 @@ public function getBaseDir(): string {
return $this->baseDir;
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function getFailed(): array {
$files = glob($this->getFailedDir().DIRECTORY_SEPARATOR.'*.json');
Expand All @@ -74,13 +74,52 @@ public function getFailed(): array {
return $failed;
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function getPendingCount(): int {
return count(glob($this->getPendingDir().DIRECTORY_SEPARATOR.'*.json'));
}
/**
* {@inheritDoc}
* Returns all pending jobs, including those not yet available due to delay.
*
* Jobs are sorted by priority descending, then createdAt ascending.
* This method does not remove jobs from the queue.
*
* @return QueuedJob[] Array of all pending queued jobs.
*/
public function getPending(): array {
$files = glob($this->getPendingDir().DIRECTORY_SEPARATOR.'*.json');
$jobs = [];

foreach ($files as $file) {
$data = json_decode(file_get_contents($file), true);

if ($data === null) {
continue;
}

$jobs[] = new QueuedJob(
$data['id'],
$data['payload'] ?? '',
$data['priority'] ?? 0,
$data['attempts'] ?? 0,
$data['available_at'] ?? 0,
$data['created_at'] ?? 0
);
}

usort($jobs, function (QueuedJob $a, QueuedJob $b) {
if ($a->getPriority() !== $b->getPriority()) {
return $b->getPriority() - $a->getPriority();
}

return $a->getCreatedAt() - $b->getCreatedAt();
});

return $jobs;
}
/**
* @see QueueStorage
*/
public function markComplete(string $id): void {
$file = $this->getPendingDir().DIRECTORY_SEPARATOR.$id.'.json';
Expand All @@ -90,7 +129,7 @@ public function markComplete(string $id): void {
}
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function markFailed(QueuedJob $job): void {
$pendingFile = $this->getPendingDir().DIRECTORY_SEPARATOR.$job->getId().'.json';
Expand All @@ -114,7 +153,7 @@ public function markFailed(QueuedJob $job): void {
);
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function pop(int $limit = 10): array {
$files = glob($this->getPendingDir().DIRECTORY_SEPARATOR.'*.json');
Expand Down Expand Up @@ -153,7 +192,7 @@ public function pop(int $limit = 10): array {
return array_slice($jobs, 0, $limit);
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function push(QueuedJob $job): void {
$data = [
Expand All @@ -171,7 +210,7 @@ public function push(QueuedJob $job): void {
);
}
/**
* {@inheritDoc}
* @see QueueStorage
*/
public function retry(string $id): void {
$failedFile = $this->getFailedDir().DIRECTORY_SEPARATOR.$id.'.json';
Expand Down
36 changes: 36 additions & 0 deletions WebFiori/Queue/ListableQueueStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/**
* This file is licensed under MIT License.
*
* Copyright (c) 2026 WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
*
*/
namespace WebFiori\Queue;

/**
* Extended storage interface for backends that support listing pending jobs.
*
* Not all queue backends can enumerate their contents (e.g. SQS, RabbitMQ).
* Backends that support random access and listing (files, database, Redis)
* should implement this interface to enable admin dashboards and inspection.
*
* Backends that only support push/pop semantics should implement the base
* QueueStorage interface instead.
*/
interface ListableQueueStorage extends QueueStorage {
/**
* Returns all pending jobs, including those not yet available due to delay.
*
* Unlike pop(), this method:
* - Does NOT filter by availableAt (delayed jobs are included)
* - Does NOT remove jobs from the queue
* - Returns jobs sorted by priority descending, then createdAt ascending
*
* @return QueuedJob[] Array of all pending queued jobs.
*/
public function getPending(): array;
}
20 changes: 20 additions & 0 deletions WebFiori/Queue/Queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ public function getFailed(): array {
public function getPendingCount(): int {
return $this->storage->getPendingCount();
}
/**
* Returns all pending jobs, including delayed ones not yet available.
*
* This method requires a storage backend that implements ListableQueueStorage.
* If the backend does not support listing, a LogicException is thrown.
*
* @return QueuedJob[] Array of all pending queued jobs.
*
* @throws \LogicException If the storage backend does not implement ListableQueueStorage.
*/
public function getPending(): array {
if (!($this->storage instanceof ListableQueueStorage)) {
throw new \LogicException(
'The configured storage backend does not support listing pending jobs. '
. 'Use a ListableQueueStorage implementation (e.g. FileQueueStorage).'
);
}

return $this->storage->getPending();
}
/**
* Returns the storage backend.
*
Expand Down
14 changes: 14 additions & 0 deletions WebFiori/Queue/QueueFacade.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ public static function getInstance(): Queue {
public static function getPendingCount(): int {
return self::getInstance()->getPendingCount();
}
/**
* Returns all pending jobs, including delayed ones not yet available.
*
* Requires the queue's storage backend to implement ListableQueueStorage.
*
* @return QueuedJob[] Array of all pending queued jobs.
*
* @throws \LogicException If the storage backend does not support listing.
*
* @see Queue::getPending()
*/
public static function getPending(): array {
return self::getInstance()->getPending();
}
/**
* @see Queue::process()
*/
Expand Down
20 changes: 19 additions & 1 deletion examples/01-basic-queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,25 @@ public function handle(): void {

echo "Pending jobs: ".$queue->getPendingCount()."\n\n";

// --- Step 3: Process pending jobs ---
// --- Step 3: Inspect pending jobs ---
// getPending() returns all pending jobs including delayed ones.
// It does NOT remove them from the queue — it's read-only.
// Jobs are sorted by priority (highest first), then creation time.
echo "--- Pending Jobs ---\n";
$pending = $queue->getPending();

foreach ($pending as $job) {
echo sprintf(
" [%s] priority=%d, available=%s\n",
substr($job->getId(), 0, 8).'...',
$job->getPriority(),
$job->isAvailable() ? 'now' : date('H:i:s', $job->getAvailableAt())
);
}

echo "\n";

// --- Step 4: Process pending jobs ---
// process() picks up available jobs (sorted by priority), deserializes them,
// and calls handle() on each one.
// The $limit parameter controls how many jobs to process in one call.
Expand Down
23 changes: 23 additions & 0 deletions tests/QueueFacadeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ public function testResetCreatesNewInstance() {
$second = QueueFacade::getInstance();
$this->assertNotSame($first, $second);
}
/**
* @test
*/
public function testFacadeGetPending() {
QueueFacade::reset();
$storageDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'wf_facade_getpending_test';
$queue = new \WebFiori\Queue\Queue(new \WebFiori\Queue\FileQueueStorage($storageDir));
QueueFacade::setInstance($queue);

QueueFacade::dispatch(new \WebFiori\Queue\Tests\SuccessJob());
QueueFacade::dispatch(new \WebFiori\Queue\Tests\SuccessJob());

$pending = QueueFacade::getPending();
$this->assertCount(2, $pending);

foreach ($pending as $job) {
$this->assertInstanceOf(\WebFiori\Queue\QueuedJob::class, $job);
}

// Cleanup
QueueFacade::reset();
$this->removeDir($storageDir);
}

private function removeDir(string $dir): void {
if (!is_dir($dir)) {
Expand Down
Loading
Loading