diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..e9318a5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,127 @@
+name: Bug report
+
+description: Create a report to help us improve CodeIgniter4 Queue
+title: "Bug: "
+labels: ['bug']
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+
+ Before you begin, **please ensure that there are no existing issues,
+ whether still open or closed, related to your report**.
+ If there is, your report will be closed promptly.
+
+ For example, if you encounter an issue with queue processing,
+ you can search the GitHub repository with relevant keywords.
+
+ ---
+
+ - type: input
+ id: php-version
+ attributes:
+ label: PHP Version
+ description: |
+ e.g. 8.2.0
+ validations:
+ required: true
+
+ - type: input
+ id: codeigniter-version
+ attributes:
+ label: CodeIgniter4 Version
+ description: |
+ e.g. 4.5.0
+ validations:
+ required: true
+
+ - type: input
+ id: queue-version
+ attributes:
+ label: Queue Package Version
+ description: |
+ e.g. dev:develop, 1.0.0 and ...
+ If you are not using the [latest version](https://github.com/codeigniter4/queue/releases), please
+ check to see if the problem occurs with the latest version.
+ validations:
+ required: true
+
+ - type: dropdown
+ id: operating-systems
+ attributes:
+ label: Which operating systems have you tested for this bug?
+ description: You may select more than one.
+ multiple: true
+ options:
+ - macOS
+ - Windows
+ - Linux
+ validations:
+ required: true
+
+ - type: dropdown
+ id: server
+ attributes:
+ label: Which server did you use?
+ options:
+ - apache
+ - cli
+ - cli-server (PHP built-in webserver)
+ - cgi-fcgi
+ - fpm-fcgi
+ - phpdbg
+ validations:
+ required: true
+
+ - type: input
+ id: queue-driver
+ attributes:
+ label: Queue Driver
+ description: |
+ e.g. database, redis, predis
+ validations:
+ required: true
+
+ - type: textarea
+ id: queue-configuration
+ attributes:
+ label: Queue Configuration
+ description: |
+ Please provide your queue configuration settings.
+ **Important:** Before sharing, ensure that all sensitive data such as passwords, API keys, and secrets are masked or removed.
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: What happened?
+ placeholder: Tell us what you see!
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Steps to Reproduce
+ description: Steps to reproduce the behavior.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Expected Output
+ description: What do you expect to happen instead of this filed bug?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Anything else?
+ description: |
+ Links? References? Anything that will give us more context about the issue you are encountering!
+
+ Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
+ validations:
+ required: false
\ No newline at end of file
diff --git a/.github/workflows/deptrac.yml b/.github/workflows/deptrac.yml
index 2021f8d..78b7ecf 100644
--- a/.github/workflows/deptrac.yml
+++ b/.github/workflows/deptrac.yml
@@ -7,7 +7,7 @@ on:
paths:
- '**.php'
- 'composer.*'
- - 'depfile.yaml'
+ - 'deptrac.yaml'
- '.github/workflows/deptrac.yml'
push:
branches:
@@ -15,60 +15,9 @@ on:
paths:
- '**.php'
- 'composer.*'
- - 'depfile.yaml'
+ - 'deptrac.yaml'
- '.github/workflows/deptrac.yml'
jobs:
- build:
- name: Dependency Tracing
- runs-on: ubuntu-latest
- if: "!contains(github.event.head_commit.message, '[ci skip]')"
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Set up PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
- tools: phive
- extensions: intl, json, mbstring, xml
- coverage: none
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Get composer cache directory
- run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
-
- - name: Cache composer dependencies
- uses: actions/cache@v3
- with:
- path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }}
- restore-keys: ${{ runner.os }}-composer-
-
- - name: Create Deptrac cache directory
- run: mkdir -p build/
-
- - name: Cache Deptrac results
- uses: actions/cache@v3
- with:
- path: build
- key: ${{ runner.os }}-deptrac-${{ github.sha }}
- restore-keys: ${{ runner.os }}-deptrac-
-
- - name: Install dependencies
- run: |
- if [ -f composer.lock ]; then
- composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
- else
- composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader
- fi
-
- - name: Trace dependencies
- run: |
- sudo phive --no-progress install --global --trust-gpg-keys B8F640134AB1782E,A98E898BB53EB748 qossmic/deptrac
- deptrac analyze --cache-file=build/deptrac.cache
- env:
- GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ deptrac:
+ uses: codeigniter4/.github/.github/workflows/deptrac.yml@CI46
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index e52cbad..0937a92 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -5,8 +5,9 @@ on:
branches:
- develop
paths:
- - 'docs/*'
+ - 'docs/**/*'
- 'mkdocs.yml'
+ - '.github/workflows/docs.yml'
permissions:
contents: write
@@ -16,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material
diff --git a/.github/workflows/phpcsfixer.yml b/.github/workflows/phpcsfixer.yml
index 59c867c..9ac8b7f 100644
--- a/.github/workflows/phpcsfixer.yml
+++ b/.github/workflows/phpcsfixer.yml
@@ -37,7 +37,7 @@ jobs:
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
- name: Cache composer dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }}
diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
index 9433d3a..ff29de7 100644
--- a/.github/workflows/phpstan.yml
+++ b/.github/workflows/phpstan.yml
@@ -46,7 +46,7 @@ jobs:
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
- name: Cache composer dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }}
@@ -56,7 +56,7 @@ jobs:
run: mkdir -p build/phpstan
- name: Cache PHPStan results
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: build/phpstan
key: ${{ runner.os }}-phpstan-${{ github.sha }}
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index b03028e..a63634b 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -18,16 +18,126 @@ on:
- 'phpunit*'
- '.github/workflows/phpunit.yml'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ NLS_LANG: 'AMERICAN_AMERICA.UTF8'
+ NLS_DATE_FORMAT: 'YYYY-MM-DD HH24:MI:SS'
+ NLS_TIMESTAMP_FORMAT: 'YYYY-MM-DD HH24:MI:SS'
+ NLS_TIMESTAMP_TZ_FORMAT: 'YYYY-MM-DD HH24:MI:SS'
+
jobs:
main:
- name: PHP ${{ matrix.php-versions }} Unit Tests
- runs-on: ubuntu-latest
+ name: PHP ${{ matrix.php-versions }} - ${{ matrix.db-platforms }}
+ runs-on: ubuntu-22.04
if: "!contains(github.event.head_commit.message, '[ci skip]')"
strategy:
matrix:
- php-versions: ['8.1', '8.2']
+ php-versions: ['8.1', '8.2', '8.3']
+ db-platforms: ['MySQLi', 'SQLite3']
+ include:
+ # Postgre
+ - php-versions: '8.1'
+ db-platforms: Postgre
+ # SQLSRV
+ - php-versions: '8.1'
+ db-platforms: SQLSRV
+ # OCI8
+ - php-versions: '8.1'
+ db-platforms: OCI8
+
+ services:
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ALLOW_EMPTY_PASSWORD: yes
+ MYSQL_DATABASE: test
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="mysqladmin ping"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=3
+
+ postgres:
+ image: postgres
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: test
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd=pg_isready
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=3
+
+ mssql:
+ image: mcr.microsoft.com/mssql/server:2022-latest
+ env:
+ MSSQL_SA_PASSWORD: 1Secure*Password1
+ ACCEPT_EULA: Y
+ MSSQL_PID: Developer
+ ports:
+ - 1433:1433
+ options: >-
+ --health-cmd="/opt/mssql-tools18/bin/sqlcmd -C -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q 'SELECT @@VERSION'"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=3
+
+ oracle:
+ image: gvenzl/oracle-xe:21
+ env:
+ ORACLE_RANDOM_PASSWORD: true
+ APP_USER: ORACLE
+ APP_USER_PASSWORD: ORACLE
+ ports:
+ - 1521:1521
+ options: >-
+ --health-cmd healthcheck.sh
+ --health-interval 20s
+ --health-timeout 10s
+ --health-retries 10
+
+ redis:
+ image: redis
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=3
steps:
+ - name: Free Disk Space (Ubuntu)
+ uses: jlumbroso/free-disk-space@main
+ with:
+ # this might remove tools that are actually needed,
+ # if set to "true" but frees about 6 GB
+ tool-cache: false
+
+ # all of these default to true, but feel free to set to
+ # "false" if necessary for your workflow
+ android: true
+ dotnet: true
+ haskell: true
+ large-packages: false
+ docker-images: true
+ swap-storage: true
+
+ - name: Create database for MSSQL Server
+ if: matrix.db-platforms == 'SQLSRV'
+ run: sqlcmd -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q "CREATE DATABASE test"
+
- name: Checkout
uses: actions/checkout@v4
@@ -36,7 +146,7 @@ jobs:
with:
php-version: ${{ matrix.php-versions }}
tools: composer, phive, phpunit
- extensions: intl, json, mbstring, gd, xdebug, xml, sqlite3
+ extensions: intl, json, mbstring, gd, xdebug, xml, sqlite3, sqlsrv, oci8, pgsql
coverage: xdebug
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -45,7 +155,7 @@ jobs:
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
- name: Cache composer dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }}
@@ -60,8 +170,9 @@ jobs:
fi
- name: Test with PHPUnit
- run: vendor/bin/phpunit --verbose --coverage-text
+ run: vendor/bin/phpunit --coverage-text
env:
+ DB: ${{ matrix.db-platforms }}
TERM: xterm-256color
TACHYCARDIA_MONITOR_GA: enabled
@@ -74,11 +185,12 @@ jobs:
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_PARALLEL: true
- COVERALLS_FLAG_NAME: PHP ${{ matrix.php-versions }}
+ COVERALLS_FLAG_NAME: PHP ${{ matrix.php-versions }} - ${{ matrix.db-platforms }}
coveralls:
needs: [main]
name: Coveralls Finished
+ if: github.repository_owner == 'codeigniter4'
runs-on: ubuntu-latest
steps:
- name: Upload Coveralls results
diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml
index b28b1fc..1d3c7cb 100644
--- a/.github/workflows/psalm.yml
+++ b/.github/workflows/psalm.yml
@@ -42,7 +42,7 @@ jobs:
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
- name: Cache composer dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }}
@@ -52,7 +52,7 @@ jobs:
run: mkdir -p build/psalm
- name: Cache Psalm results
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: build/psalm
key: ${{ runner.os }}-psalm-${{ github.sha }}
diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml
index f4482bf..cab67ce 100644
--- a/.github/workflows/rector.yml
+++ b/.github/workflows/rector.yml
@@ -18,15 +18,22 @@ on:
- 'rector.php'
- '.github/workflows/rector.yml'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
jobs:
build:
name: PHP ${{ matrix.php-versions }} Rector Analysis
runs-on: ubuntu-latest
- if: "!contains(github.event.head_commit.message, '[ci skip]')"
+ if: (! contains(github.event.head_commit.message, '[ci skip]'))
strategy:
fail-fast: false
matrix:
- php-versions: ['8.1', '8.2']
+ php-versions: ['8.1', '8.3']
steps:
- name: Checkout
@@ -46,7 +53,7 @@ jobs:
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
- name: Cache composer dependencies
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }}
@@ -61,6 +68,4 @@ jobs:
fi
- name: Analyze for refactoring
- run: |
- composer global require --dev rector/rector:^0.15.1
- rector process --dry-run --no-progress-bar
+ run: vendor/bin/rector process --dry-run --no-progress-bar
diff --git a/.gitignore b/.gitignore
index 74c6d0c..8a589c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -126,5 +126,5 @@ nb-configuration.xml
/results/
/phpunit*.xml
/.phpunit.*.cache
-
+/.phpunit.cache
/.php-cs-fixer.php
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 1185b9c..001fd00 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -1,5 +1,16 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
use CodeIgniter\CodingStandard\CodeIgniter4;
use Nexus\CsConfig\Factory;
use PhpCsFixer\Finder;
@@ -13,11 +24,18 @@
->exclude('build')
->append([__FILE__]);
-$overrides = [];
+$overrides = [
+ 'declare_strict_types' => true,
+ 'void_return' => true,
+];
$options = [
'finder' => $finder,
'cacheFile' => 'build/.php-cs-fixer.cache',
];
-return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects();
+return Factory::create(new CodeIgniter4(), $overrides, $options)->forLibrary(
+ 'CodeIgniter Queue',
+ 'CodeIgniter Foundation',
+ 'admin@codeigniter.com',
+);
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b712dd6
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,10 @@
+# Contributing to CodeIgniter4
+
+CodeIgniter is a community driven project and accepts contributions of
+code and documentation from the community.
+
+If you'd like to contribute, please read [Contributing to CodeIgniter](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/README.md)
+in the [main repository](https://github.com/codeigniter4/CodeIgniter4).
+
+If you are going to contribute to this repository, please report bugs or send PRs
+to this repository instead of the main repository.
diff --git a/LICENSE b/LICENSE
index 3403dc7..3cbdd9e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2023 Michal Sniatala
+Copyright (c) 2023 CodeIgniter Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index e7af099..f4c6ee0 100644
--- a/README.md
+++ b/README.md
@@ -2,17 +2,21 @@
Queues for the CodeIgniter 4 framework.
-[](https://github.com/michalsn/codeigniter-queue/actions/workflows/phpunit.yml)
-[](https://github.com/michalsn/codeigniter-queue/actions/workflows/phpstan.yml)
-[](https://github.com/michalsn/codeigniter-queue/actions/workflows/deptrac.yml)
-[](https://coveralls.io/github/michalsn/codeigniter-queue?branch=develop)
+[](https://github.com/codeigniter4/queue/actions/workflows/phpunit.yml)
+[](https://github.com/codeigniter4/queue/actions/workflows/phpstan.yml)
+[](https://github.com/codeigniter4/queue/actions/workflows/deptrac.yml)
+[](https://coveralls.io/github/codeigniter4/queue?branch=develop)


+
+
+> [!NOTE]
+> A queue system is typically used to handle resource-intensive or time-consuming tasks (e.g., image processing, sending emails) that are to be run in the background. It can also be a way to postpone certain activities that are to be executed automatically later.
## Installation
- composer require michalsn/codeigniter-queue
+ composer require codeigniter4/queue
Migrate your database:
@@ -58,4 +62,11 @@ Run the queue worker:
## Docs
-https://michalsn.github.io/codeigniter-queue/
+Read the full documentation: https://queue.codeigniter.com
+
+## Contributing
+
+We accept and encourage contributions from the community in any shape. It doesn't matter
+whether you can code, write documentation, or help find bugs, all contributions are welcome.
+See the [CONTRIBUTING.md](CONTRIBUTING.md) file for details.
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..7879188
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,26 @@
+# Security Policy
+
+The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.**
+
+## Reporting a Vulnerability
+
+Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged.
+
+**Please report security flaws by emailing the development team directly: security@codeigniter.com**.
+
+The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating
+the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the
+progress towards a fix and full announcement, and may ask for additional information or guidance.
+
+## Disclosure Policy
+
+When the security team receives a security bug report, they will assign it to a primary handler.
+This person will coordinate the fix and release process, involving the following steps:
+
+- Confirm the problem and determine the affected versions.
+- Audit code to find any potential similar problems.
+- Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible.
+
+## Comments on this Policy
+
+If you have suggestions on how this process could be improved please submit a Pull Request.
diff --git a/composer.json b/composer.json
index a1c889f..2e00ce2 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,9 @@
{
- "name": "michalsn/codeigniter-queue",
+ "name": "codeigniter4/queue",
"description": "Queues for CodeIgniter 4 framework",
"license": "MIT",
"type": "library",
- "keywords": ["codeigniter", "codeigniter4", "queue"],
+ "keywords": ["codeigniter", "codeigniter4", "queue", "database", "redis", "predis"],
"authors": [
{
"name": "michalsn",
@@ -11,27 +11,35 @@
"role": "Developer"
}
],
- "homepage": "https://github.com/michalsn/codeigniter-queue",
+ "homepage": "https://github.com/codeigniter4/queue",
"require": {
"php": "^8.1"
},
"require-dev": {
"codeigniter4/devkit": "^1.0",
- "codeigniter4/framework": "^4.4",
- "rector/rector": "0.18.6"
+ "codeigniter4/framework": "^4.3",
+ "predis/predis": "^2.0",
+ "phpstan/phpstan-strict-rules": "^1.5"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
- "Michalsn\\CodeIgniterQueue\\": "src"
- }
+ "CodeIgniter\\Queue\\": "src"
+ },
+ "exclude-from-classmap": [
+ "**/Database/Migrations/**"
+ ]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests"
}
},
+ "suggest": {
+ "ext-redis": "If you want to use RedisHandler",
+ "predis/predis": "If you want to use PredisHandler"
+ },
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
@@ -55,7 +63,7 @@
"cs": "php-cs-fixer fix --ansi --verbose --dry-run --diff",
"cs-fix": "php-cs-fixer fix --ansi --verbose --diff",
"style": "@cs-fix",
- "deduplicate": "phpcpd app/ src/",
+ "deduplicate": "phpcpd src/ tests/",
"inspect": "deptrac analyze --cache-file=build/deptrac.cache",
"mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit",
"test": "phpunit"
diff --git a/deptrac.yaml b/deptrac.yaml
index 21a7a89..3caa730 100644
--- a/deptrac.yaml
+++ b/deptrac.yaml
@@ -1,4 +1,4 @@
-parameters:
+deptrac:
paths:
- ./src/
- ./vendor/codeigniter4/framework/system/
@@ -9,90 +9,90 @@ parameters:
collectors:
- type: bool
must:
- - type: className
- regex: .*[A-Za-z]+Model$
+ - type: class
+ value: .*[A-Za-z]+Model$
must_not:
- - type: directory
- regex: vendor/.*
+ - type: directory
+ value: vendor/.*
- name: Vendor Model
collectors:
- type: bool
must:
- - type: className
- regex: .*[A-Za-z]+Model$
- - type: directory
- regex: vendor/.*
+ - type: class
+ value: .*[A-Za-z]+Model$
+ - type: directory
+ value: vendor/.*
- name: Controller
collectors:
- type: bool
must:
- - type: className
- regex: .*\/Controllers\/.*
+ - type: class
+ value: .*\/Controllers\/.*
must_not:
- - type: directory
- regex: vendor/.*
+ - type: directory
+ value: vendor/.*
- name: Vendor Controller
collectors:
- type: bool
must:
- - type: className
- regex: .*\/Controllers\/.*
- - type: directory
- regex: vendor/.*
+ - type: class
+ value: .*\/Controllers\/.*
+ - type: directory
+ value: vendor/.*
- name: Config
collectors:
- type: bool
must:
- - type: directory
- regex: app/Config/.*
+ - type: directory
+ value: app/Config/.*
must_not:
- - type: className
- regex: .*Services
- - type: directory
- regex: vendor/.*
+ - type: class
+ value: .*Services
+ - type: directory
+ value: vendor/.*
- name: Vendor Config
collectors:
- type: bool
must:
- - type: directory
- regex: vendor/.*/Config/.*
+ - type: directory
+ value: vendor/.*/Config/.*
must_not:
- - type: className
- regex: .*Services
+ - type: class
+ value: .*Services
- name: Entity
collectors:
- type: bool
must:
- - type: directory
- regex: app/Entities/.*
+ - type: directory
+ value: app/Entities/.*
must_not:
- - type: directory
- regex: vendor/.*
+ - type: directory
+ value: vendor/.*
- name: Vendor Entity
collectors:
- type: bool
must:
- - type: directory
- regex: vendor/.*/Entities/.*
+ - type: directory
+ value: vendor/.*/Entities/.*
- name: View
collectors:
- type: bool
must:
- - type: directory
- regex: app/Views/.*
+ - type: directory
+ value: app/Views/.*
must_not:
- - type: directory
- regex: vendor/.*
+ - type: directory
+ value: vendor/.*
- name: Vendor View
collectors:
- type: bool
must:
- - type: directory
- regex: vendor/.*/Views/.*
+ - type: directory
+ value: vendor/.*/Views/.*
- name: Service
collectors:
- - type: className
- regex: .*Services.*
+ - type: class
+ value: .*Services.*
ruleset:
Entity:
- Config
@@ -153,4 +153,4 @@ parameters:
- Vendor Entity
- Vendor Model
- Vendor View
- skip_violations:
+ skip_violations: []
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 0000000..ec5fa62
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+queue.codeigniter.com
diff --git a/docs/assets/css/codeigniter.css b/docs/assets/css/codeigniter.css
new file mode 100644
index 0000000..9895207
--- /dev/null
+++ b/docs/assets/css/codeigniter.css
@@ -0,0 +1,18 @@
+[data-md-color-scheme="codeigniter"] {
+ --md-primary-fg-color: #dd4814;
+ --md-primary-fg-color--light: #ECB7B7;
+ --md-primary-fg-color--dark: #90030C;
+
+ --md-default-bg-color: #fcfcfc;
+
+ --md-typeset-a-color: #e74c3c;
+ --md-accent-fg-color: #97310e;
+
+ --md-accent-fg-color--transparent: #ECB7B7;
+
+ --md-code-bg-color: #ffffff;
+
+ .md-typeset code {
+ border: 1px solid #e1e4e5;
+ }
+}
diff --git a/docs/assets/css/codeigniter_dark_mode.css b/docs/assets/css/codeigniter_dark_mode.css
new file mode 100644
index 0000000..3c708d3
--- /dev/null
+++ b/docs/assets/css/codeigniter_dark_mode.css
@@ -0,0 +1,71 @@
+[data-md-color-scheme="slate"] {
+ --md-primary-fg-color: #b13a10;
+ --md-primary-fg-color--light: #8d7474;
+ --md-primary-fg-color--dark: #6d554d;
+
+ --md-default-bg-color: #1e2129;
+
+ --md-typeset-a-color: #ed6436;
+ --md-accent-fg-color: #f18a67;
+
+ --md-accent-fg-color--transparent: #625151;
+
+ --md-code-bg-color: #282b2d;
+
+ .hljs-title,
+ .hljs-title.class_,
+ .hljs-title.class_.inherited__,
+ .hljs-title.function_ {
+ color: #c9a69b;
+ }
+
+ .hljs-meta .hljs-string,
+ .hljs-regexp,
+ .hljs-string {
+ color: #a3b4c7;
+ }
+
+ .hljs-attr,
+ .hljs-attribute,
+ .hljs-literal,
+ .hljs-meta,
+ .hljs-number,
+ .hljs-operator,
+ .hljs-selector-attr,
+ .hljs-selector-class,
+ .hljs-selector-id,
+ .hljs-variable {
+ color: #c1b79f;
+ }
+
+ .hljs-doctag,
+ .hljs-keyword,
+ .hljs-meta .hljs-keyword,
+ .hljs-template-tag,
+ .hljs-template-variable,
+ .hljs-type,
+ .hljs-variable.language_ {
+ color: #c97100;
+ }
+
+ .hljs-subst {
+ color: #ddba52
+ }
+
+ .md-typeset code {
+ border: 1px solid #3f4547;
+ }
+
+ .md-typeset .admonition.note,
+ .md-typeset details.note {
+ border-color: #2c5293;
+ }
+
+ .md-typeset .note > .admonition-title:before,
+ .md-typeset .note > summary:before {
+ background-color: #2c5293;
+ -webkit-mask-image: var(--md-admonition-icon--note);
+ mask-image: var(--md-admonition-icon--note);
+ }
+
+}
diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico
index 531feed..a725c10 100644
Binary files a/docs/assets/favicon.ico and b/docs/assets/favicon.ico differ
diff --git a/docs/assets/github-dark-dimmed.css b/docs/assets/github-dark-dimmed.css
deleted file mode 100644
index e702177..0000000
--- a/docs/assets/github-dark-dimmed.css
+++ /dev/null
@@ -1,15 +0,0 @@
-pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
- Theme: GitHub Dark Dimmed
- Description: Dark dimmed theme as seen on github.com
- Author: github.com
- Maintainer: @Hirse
- Updated: 2021-05-15
- Modified: 2022:12:27 by @michalsn
-
- Colors taken from GitHub's CSS
-*/.hljs{color:#adbac7 !important;background-color:#22272e !important}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#f47067}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#dcbdfb}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#6cb6ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#96d0ff}.hljs-built_in,.hljs-symbol{color:#f69d50}.hljs-code,.hljs-comment,.hljs-formula{color:#768390}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#8ddb8c}.hljs-subst{color:#adbac7}.hljs-section{color:#316dca;font-weight:700}.hljs-bullet{color:#eac55f}.hljs-emphasis{color:#adbac7;font-style:italic}.hljs-strong{color:#adbac7;font-weight:700}.hljs-addition{color:#b4f1b4;background-color:#1b4721}.hljs-deletion{color:#ffd8d3;background-color:#78191b}
-
-[data-md-color-scheme="default"] {
- --md-default-fg-color--lightest: #575757;
- --md-default-fg-color--light: #959595;
-}
diff --git a/docs/assets/hljs.js b/docs/assets/hljs.js
deleted file mode 100644
index 48bc360..0000000
--- a/docs/assets/hljs.js
+++ /dev/null
@@ -1,3 +0,0 @@
-document.addEventListener('DOMContentLoaded', (ev) => {
- hljs.highlightAll();
-});
diff --git a/docs/assets/js/hljs.js b/docs/assets/js/hljs.js
new file mode 100644
index 0000000..56159c4
--- /dev/null
+++ b/docs/assets/js/hljs.js
@@ -0,0 +1,3 @@
+window.document$.subscribe(() => {
+ hljs.highlightAll();
+});
diff --git a/docs/basic_usage.md b/docs/basic-usage.md
similarity index 54%
rename from docs/basic_usage.md
rename to docs/basic-usage.md
index 16f2f42..b91ae37 100644
--- a/docs/basic_usage.md
+++ b/docs/basic-usage.md
@@ -47,8 +47,8 @@ One of the most popular tasks delegated to a queue is sending email messages. Th
namespace App\Jobs;
use Exception;
-use Michalsn\CodeIgniterQueue\BaseJob;
-use Michalsn\CodeIgniterQueue\Interfaces\JobInterface;
+use CodeIgniter\Queue\BaseJob;
+use CodeIgniter\Queue\Interfaces\JobInterface;
class Email extends BaseJob implements JobInterface
{
@@ -79,6 +79,52 @@ You may be wondering what the `$this->data['message']` variable is all about. We
Throwing an exception is a way to let the queue worker know that the job has failed.
+#### Using transactions
+
+If you have to use transactions in your Job - this is a simple schema you can follow.
+
+!!! note
+
+ Due to the nature of the queue worker, [Strict Mode](https://codeigniter.com/user_guide/database/transactions.html#strict-mode) is automatically disabled for the database connection assigned to the Database handler. That's because queue worker is a long-running process, and we don't want one failed transaction to affect others.
+
+ If you use the same connection group in your Job as defined in the Database handler, then in that case, you don't need to do anything.
+
+ On the other hand, if you are using a different group to connect to the database in your Job, then if you are using transactions, you should disable Strict Mode through the method: `$db->transStrict(false)` or by setting the `transStrict` option to `false` in your connection config group - the last option will disable Strict Mode globally.
+
+```php
+// ...
+
+class Email extends BaseJob implements JobInterface
+{
+ /**
+ * @throws Exception
+ */
+ public function process(string $data):
+ {
+ try {
+ $db = db_connect();
+ // Disable Strict Mode
+ $db->transStrict(false);
+ $db->transBegin();
+
+ // Job logic goes here
+ // Your code should throw an exception on error
+
+ if ($db->transStatus() === false) {
+ $db->transRollback();
+ } else {
+ $db->transCommit();
+ }
+ } catch (Exception $e) {
+ $db->transRollback();
+ throw $e;
+ }
+ }
+}
+```
+
+#### Other options
+
We can also configure some things on the job level. It's a number of tries, when the job is failing and time after the job will be retried again after failure. We can specify these options by using variables:
```php
@@ -114,6 +160,29 @@ service('queue')->push('emails', 'email', ['message' => 'Email message goes here
We will be pushing `email` job to the `emails` queue.
+As a result of calling the `push()` method, you will receive a `QueuePushResult` object, which you can inspect if needed. It provides the following information:
+
+- `getStatus()`: Indicates whether the job was successfully added to the queue.
+- `getJobId()`: Returns the ID of the job that was added to the queue.
+- `getError()`: Returns any error that occurred if the job was not added.
+
+### Sending chained jobs to the queue
+
+Sending chained jobs is also simple and lets you specify the particular order of the job execution.
+
+```php
+service('queue')->chain(function($chain) {
+ $chain
+ ->push('reports', 'generate-report', ['userId' => 123])
+ ->push('emails', 'email', ['message' => 'Email message goes here', 'userId' => 123]);
+});
+```
+
+In the example above, we will send jobs to the `reports` and `emails` queues. First, we will generate a report for given user with the `generate-report` job, after this, we will send an email with `email` job.
+The `email` job will be executed only if the `generate-report` job was successful.
+
+As with the `push()` method, calling the `chain()` method also returns a `QueuePushResult` object.
+
### Consuming the queue
Since we sent our sample job to queue `emails`, then we need to run the worker with the appropriate queue:
diff --git a/docs/commands.md b/docs/commands.md
index b539f6d..2631459 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -6,15 +6,15 @@ Here are all the commands you can use with the Queue library.
Available options:
-- [queue:publish](#queuePublish)
-- [queue:job](#queueJob)
-- [queue:work](#queueWork)
-- [queue:stop](#queueStop)
-- [queue:clear](#queueClear)
-- [queue:failed](#queueFailed)
-- [queue:retry](#queueRetry)
-- [queue:forget](#queueForget)
-- [queue:flush](#queueFlush)
+- [queue:publish](#queuepublish)
+- [queue:job](#queuejob)
+- [queue:work](#queuework)
+- [queue:stop](#queuestop)
+- [queue:clear](#queueclear)
+- [queue:failed](#queuefailed)
+- [queue:retry](#queueretry)
+- [queue:forget](#queueforget)
+- [queue:flush](#queueflush)
### queue:publish
diff --git a/docs/configuration.md b/docs/configuration.md
index c872654..c26eb5c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -12,14 +12,16 @@ We will get our copy ready for modifications.
Available options:
-- [$defaultHandler](#defaultHandler)
+- [$defaultHandler](#defaulthandler)
- [$handlers](#handlers)
- [$database](#database)
-- [$keepDoneJobs](#keepDoneJobs)
-- [$keepFailedJobs](#keepFailedJobs)
-- [$queueDefaultPriority](#queueDefaultPriority)
-- [$queuePriorities](#queuePriorities)
-- [$jobHandlers](#jobHandlers)
+- [$redis](#redis)
+- [$predis](#predis)
+- [$keepDoneJobs](#keepdonejobs)
+- [$keepFailedJobs](#keepfailedjobs)
+- [$queueDefaultPriority](#queuedefaultpriority)
+- [$queuePriorities](#queuepriorities)
+- [$jobHandlers](#jobhandlers)
### $defaultHandler
@@ -27,7 +29,7 @@ The default handler used by the library. Default value: `database`.
### $handlers
-An array of available handlers. By now only `database` handler is implemented.
+An array of available handlers. By now only `database`, `redis` and `predis` handlers are implemented.
### $database
@@ -35,6 +37,34 @@ The configuration settings for `database` handler.
* `dbGroup` - The database group to use. Default value: `default`.
* `getShared` - Weather to use shared instance. Default value: `true`.
+* `skipLocked` - Weather to use "skip locked" feature to maintain concurrency calls. Default to `true`.
+
+!!! note
+
+ The [Strict Mode](https://codeigniter.com/user_guide/database/transactions.html#strict-mode) for the given `dbGroup` is automatically disabled - due to the nature of the queue worker.
+
+### $redis
+
+The configuration settings for `redis` handler. You need to have a [ext-redis](https://github.com/phpredis/phpredis) installed to use it.
+
+* `host` - The host name or unix socket. Default value: `127.0.0.1`.
+* `password` - The password. Default value: `null`.
+* `port` - The port number. Default value: `6379`.
+* `timeout` - The timeout for connection. Default value: `0`.
+* `database` - The database number. Default value: `0`.
+* `prefix` - The default key prefix. Default value: `''` (not set).
+
+### $predis
+
+The configuration settings for `predis` handler. You need to have [Predis](https://github.com/predis/predis) installed to use it.
+
+* `scheme` - The scheme to use: `tcp`, `tls` or `unix`. Default value: `tcp`.
+* `host` - The host name. Default value: `127.0.0.1`.
+* `password` - The password. Default value: `null`.
+* `port` - The port number (when `tcp`). Default value: `6379`.
+* `timeout` - The timeout for connection. Default value: `5`.
+* `database` - The database number. Default value: `0`.
+* `prefix` - The default key prefix. Default value: `''` (not set).
### $keepDoneJobs
diff --git a/docs/index.md b/docs/index.md
index 1434981..0b23235 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -2,7 +2,11 @@
A library that helps you handle Queues in the CodeIgniter 4 framework.
-Add job to the queue.
+!!! info "What are queues used for?"
+
+ A queue system is typically used to handle resource-intensive or time-consuming tasks (e.g., image processing, sending emails) that are to be run in the background. It can also be a way to postpone certain activities that are to be executed automatically later.
+
+Add a job to the queue.
```php
service('queue')->push('queueName', 'jobName', ['array' => 'parameters']);
@@ -14,14 +18,39 @@ Listen for queued jobs.
### Requirements
-
-
+- PHP 8.1+
+- CodeIgniter 4.3+
+
+If you use `database` handler:
+
+- MySQL 8.0.1+
+- MariaDB 10.6+
+- PostgreSQL 9.5+
+- SQL Server 2012+
+- Oracle 12.1+
+- SQLite3
+
+If you use `Redis` (you still need a relational database to store failed jobs):
+
+- PHPRedis
+- Predis
### Table of Contents
* [Installation](installation.md)
* [Configuration](configuration.md)
-* [Basic usage](basic_usage.md)
-* [Running queues](running_queues.md)
+* [Basic usage](basic-usage.md)
+* [Running queues](running-queues.md)
* [Commands](commands.md)
* [Troubleshooting](troubleshooting.md)
+
+### Acknowledgements
+
+Every open-source project depends on its contributors to be a success. The following users have
+contributed in one manner or another in making this project:
+
+
+
+
+
+Made with [contrib.rocks](https://contrib.rocks).
diff --git a/docs/installation.md b/docs/installation.md
index daf7973..0363de1 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -8,13 +8,38 @@
The only thing you have to do is to run this command, and you're ready to go.
- composer require michalsn/codeigniter-queue
+ composer require codeigniter4/queue
+
+#### A composer error occurred?
+
+If you get the following error:
+
+```console
+Could not find a version of package codeigniter4/queue matching your minimum-stability (stable).
+Require it with an explicit version constraint allowing its desired stability.
+```
+
+1. Run the following commands to change your [minimum-stability](https://getcomposer.org/doc/articles/versions.md#minimum-stability) in your project `composer.json`:
+
+ ```console
+ composer config minimum-stability dev
+ composer config prefer-stable true
+ ```
+
+2. Or specify an explicit version:
+
+ ```console
+ composer require codeigniter4/queue:dev-develop
+ ```
+
+ The above specifies `develop` branch.
+ See
## Manual Installation
In the example below we will assume, that files from this project will be located in `app/ThirdParty/queue` directory.
-Download this project and then enable it by editing the `app/Config/Autoload.php` file and adding the `Michalsn\CodeIgniterQueue` namespace to the `$psr4` array, like in the below example:
+Download this project and then enable it by editing the `app/Config/Autoload.php` file and adding the `CodeIgniter\Queue` namespace to the `$psr4` array, like in the below example:
```php
APPPATH, // For custom app namespace
'Config' => APPPATH . 'Config',
- 'Michalsn\CodeIgniterQueue' => APPPATH . 'ThirdParty/queue/src',
+ 'CodeIgniter\Queue' => APPPATH . 'ThirdParty/queue/src',
];
// ...
diff --git a/docs/running_queues.md b/docs/running-queues.md
similarity index 55%
rename from docs/running_queues.md
rename to docs/running-queues.md
index 10d4139..5bae1c0 100644
--- a/docs/running_queues.md
+++ b/docs/running-queues.md
@@ -12,7 +12,7 @@ This will cause command to check for the new jobs every 10 seconds if the queue
### With CRON
-Using queues with CRON is more challenging, but definitely doable. You can use command like this:
+Using queues with CRON is more challenging but definitely doable. You can use command like this:
php spark queue:work emails -max-jobs 20 --stop-when-empty
@@ -63,13 +63,65 @@ But we can also run the worker like this:
This way, worker will consume jobs with the `low` priority and then with `high`. The order set in the config file is override.
+### Delaying jobs
+
+Normally, when we add jobs to a queue, they are run in the order in which we added them to the queue (FIFO - first in, first out).
+Of course, there are also priorities, which we described in the previous section. But what about the scenario where we want to run a job, but not earlier than in 5 minutes?
+
+This is where job delay comes into play. We measure the delay in seconds.
+
+```php
+// This job will be run not sooner than in 5 minutes
+service('queue')->setDelay(5 * MINUTE)->push('emails', 'email', ['message' => 'Email sent no sooner than 5 minutes from now']);
+```
+
+Note that there is no guarantee that the job will run exactly in 5 minutes. If many new jobs are added to the queue (without a delay), it may take a long time before the delayed job is actually executed.
+
+We can also combine delayed jobs with priorities.
+
+### Chained jobs
+
+We can create sequences of jobs that run in a specific order. Each job in the chain will be executed after the previous job has completed successfully.
+
+```php
+service('queue')->chain(function($chain) {
+ $chain
+ ->push('reports', 'generate-report', ['userId' => 123])
+ ->setPriority('high') // optional
+ ->push('emails', 'email', ['message' => 'Email message goes here', 'userId' => 123])
+ ->setDelay(30); // optional
+});
+```
+
+As you may notice, we can use the same options as in regular `push()` - we can set priority and delay, which are optional settings.
+
+#### Important Differences from Regular `push()`
+
+When using the `chain()` method, there are a few important differences compared to the regular `push()` method:
+
+1. **Method Order**: Unlike the regular `push()` method where you set the priority and delay before pushing the job, in a chain you must set these properties after calling `push()` for each job:
+
+ ```php
+ // Regular push() - priority set before pushing
+ service('queue')->setPriority('high')->push('queue', 'job', []);
+
+ // Chain push() - priority set after pushing
+ service('queue')->chain(function($chain) {
+ $chain->push('queue', 'job', [])->setPriority('high');
+ });
+ ```
+
+2. **Configuration Scope**: Each configuration (priority, delay) only applies to the job that was just added to the chain.
+
### Running many instances of the same queue
-As mentioned above, sometimes we may want to have multiple instances of the same command running at the same time. The queue is safe to use in that scenario with all databases except `SQLite3` since it doesn't guarantee that the job will be selected only by one process.
+As mentioned above, sometimes we may want to have multiple instances of the same command running at the same time. The queue is safe to use in that scenario with all databases as long as you keep the `skipLocked` to `true` in the config file. Only for SQLite3 driver, this setting is not relevant as it provides atomicity without the need for explicit concurrency control.
+
+The PHPRedis and Predis drivers are also safe to use with multiple instances of the same command.
### Handling long-running process
-If we decide to run the long process e.g. with the command:
+If we decide to run the long process, e.g., with the command:
php spark queue:work emails -wait 10
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 58d91bc..10cd890 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -5,3 +5,7 @@
If you want to assign an object to the queue, please make sure it implements `JsonSerializable` interface. This is how CodeIgniter [Entities](https://codeigniter.com/user_guide/models/entities.html) are handled by default.
You may ask, why not just use `serialize` and `unserialize`? There are security reasons that keep us from doing so. These functions are not safe to use with user provided data.
+
+### I get an error when trying to install via composer.
+
+Please see these [instructions](installation.md/#a-composer-error-occurred).
diff --git a/mkdocs.yml b/mkdocs.yml
index 774318b..b93a433 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,30 +9,51 @@ theme:
repo: fontawesome/brands/github
palette:
- media: "(prefers-color-scheme: light)"
- scheme: default
- primary: indigo
- accent: indigo
+ scheme: codeigniter
+ primary: custom
+ accent: custom
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
- primary: indigo
- accent: indigo
+ primary: custom
+ accent: custom
toggle:
icon: material/brightness-4
name: Switch to light mode
+ features:
+ - navigation.instant
+ - content.code.copy
+ - navigation.footer
+ - content.action.edit
+ - navigation.top
+ - search.suggest
+ - search.highlight
+ - search.share
extra:
- homepage: https://michalsn.github.io/codeigniter-queue
+ homepage: https://codeigniter.com
+ generator: false
social:
- - icon: fontawesome/brands/github
- link: https://github.com/michalsn/codeigniter-queue
+ - icon: material/github
+ link: https://github.com/codeigniter4/queue
name: GitHub
+ - icon: material/twitter
+ link: https://twitter.com/CodeIgniterPhp
+ name: X
+ - icon: material/forum
+ link: https://forum.codeigniter.com
+ name: Forum Codeigniter
+ - icon: material/slack
+ link: https://join.slack.com/t/codeigniterchat/shared_invite/zt-244xrrslc-l_I69AJSi5y2a2RVN~xIdQ
+ name: Slack
-repo_url: https://github.com/michalsn/codeigniter-queue
+site_url: https://queue.codeigniter.com/
+repo_url: https://github.com/codeigniter4/queue
edit_uri: edit/develop/docs/
+copyright: Copyright © 2025 CodeIgniter Foundation.
markdown_extensions:
- admonition
@@ -41,17 +62,19 @@ markdown_extensions:
use_pygments: false
extra_css:
- - assets/github-dark-dimmed.css
+ - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/styles/github.min.css
+ - assets/css/codeigniter.css
+ - assets/css/codeigniter_dark_mode.css
extra_javascript:
- - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.6.0/build/highlight.min.js
- - assets/hljs.js
+ - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/highlight.min.js
+ - assets/js/hljs.js
nav:
- Home: index.md
- Installation: installation.md
- Configuration: configuration.md
- - Basic usage: basic_usage.md
- - Running queues: running_queues.md
+ - Basic usage: basic-usage.md
+ - Running queues: running-queues.md
- Commands: commands.md
- Troubleshooting: troubleshooting.md
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index d1d87e5..a8bbb27 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -9,45 +9,22 @@ parameters:
excludePaths:
ignoreErrors:
-
- message: '#Cannot use \+\+ on array\|bool\|float\|int\|object\|string\|null.#'
- paths:
- - src/Commands/QueueWork.php
+ message: '#Call to method PHPUnit\\Framework\\Assert::assertInstanceOf\(\) with.#'
-
- message: '#Variable \$config on left side of \?\?\= always exists and is not nullable.#'
+ message: '#Call to deprecated function random_string\(\):#'
paths:
- - src/Config/Services.php
- -
- message: '#Call to an undefined method Michalsn\\CodeIgniterQueue\\Handlers\\BaseHandler::push\(\).#'
- paths:
- - src/Handlers/BaseHandler.php
+ - src/Handlers/RedisHandler.php
+ - src/Handlers/PredisHandler.php
-
- message: '#Access to an undefined property CodeIgniter\\I18n\\Time::\$timestamp.#'
+ message: '#Call to an undefined method CodeIgniter\\Queue\\Models\\QueueJobFailedModel::affectedRows\(\).#'
paths:
- src/Handlers/BaseHandler.php
- - src/Handlers/DatabaseHandler.php
- - src/Models/QueueJobModel.php
-
- message: '#Call to an undefined method Michalsn\\CodeIgniterQueue\\Models\\QueueJobFailedModel::affectedRows\(\).#'
+ message: '#Call to an undefined method CodeIgniter\\Queue\\Models\\QueueJobFailedModel::truncate\(\).#'
paths:
- src/Handlers/BaseHandler.php
-
- message: '#Call to an undefined method Michalsn\\CodeIgniterQueue\\Models\\QueueJobFailedModel::truncate\(\).#'
- paths:
- - src/Handlers/BaseHandler.php
- -
- message: '#Parameter \#3 \$tries of method Michalsn\\CodeIgniterQueue\\Commands\\QueueWork::handleWork\(\) expects int\|null, string\|true\|null given.#'
- paths:
- - src/Commands/QueueWork.php
- -
- message: '#Parameter \#4 \$retryAfter of method Michalsn\\CodeIgniterQueue\\Commands\\QueueWork::handleWork\(\) expects int\|null, string\|true\|null given.#'
- paths:
- - src/Commands/QueueWork.php
- -
- message: '#Expression on left side of \?\? is not nullable.#'
- paths:
- - src/Commands/QueueWork.php
- -
- message: '#Variable \$job might not be defined.#'
+ message: '#If condition is always true.#'
paths:
- src/Commands/QueueWork.php
universalObjectCratesClasses:
@@ -60,3 +37,10 @@ parameters:
- APP_NAMESPACE
- CI_DEBUG
- ENVIRONMENT
+ strictRules:
+ allRules: false
+ disallowedLooseComparison: true
+ booleansInConditions: true
+ disallowedConstructs: true
+ matchingInheritedMethodNames: true
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 470e815..48fe085 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,15 +1,10 @@
+ cacheDirectory="build/.phpunit.cache"
+ beStrictAboutCoverageMetadata="true">
-
-
- ./src/
-
-
- ./src/Commands
- ./src/Config
-
+
@@ -43,27 +32,11 @@
-
-
-
-
- 0.50
-
-
- 30
-
-
- 2
-
-
- true
-
-
- true
-
-
-
-
+
+
+
+
+
@@ -99,4 +72,14 @@
-->
+
+
+ ./src/
+
+
+ ./src/Commands/Generators
+ ./src/Commands/Utils
+ ./src/Config
+
+
diff --git a/rector.php b/rector.php
index 6c3c25a..cec36f6 100644
--- a/rector.php
+++ b/rector.php
@@ -1,7 +1,10 @@
sets([
@@ -59,9 +67,10 @@
realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php',
]);
- if (is_file(__DIR__ . '/phpstan.neon.dist')) {
- $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist');
- }
+ $rectorConfig->phpstanConfigs([
+ __DIR__ . '/phpstan.neon.dist',
+ __DIR__ . '/vendor/phpstan/phpstan-strict-rules/rules.neon',
+ ]);
// Set the target version for refactoring
$rectorConfig->phpVersion(PhpVersion::PHP_81);
@@ -73,17 +82,22 @@
$rectorConfig->skip([
__DIR__ . '/app/Views',
- JsonThrowOnErrorRector::class,
StringifyStrNeedlesRector::class,
+ YieldDataProviderRector::class,
// Note: requires php 8
RemoveUnusedPromotedPropertyRector::class,
+ AnnotationWithValueToAttributeRector::class,
// May load view files directly when detecting classes
StringClassNameToClassConstantRector::class,
// Supported from PHPUnit 10
DataProviderAnnotationToAttributeRector::class,
+
+ NewInInitializerRector::class => [
+ 'src/Payloads/Payload.php',
+ ],
]);
// auto import fully qualified class names
@@ -108,10 +122,16 @@
$rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
$rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
$rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
+ $rectorConfig->rule(SimplifyEmptyCheckOnEmptyArrayRector::class);
+ $rectorConfig->rule(TernaryEmptyArrayArrayDimFetchToCoalesceRector::class);
+ $rectorConfig->rule(EmptyOnNullableObjectToInstanceOfRector::class);
+ $rectorConfig->rule(DisallowedEmptyRuleFixerRector::class);
$rectorConfig
->ruleWithConfiguration(TypedPropertyFromAssignsRector::class, [
/**
- * The INLINE_PUBLIC value is default to false to avoid BC break, if you use for libraries and want to preserve BC break, you don't need to configure it, as it included in LevelSetList::UP_TO_PHP_74
+ * The INLINE_PUBLIC value is default to false to avoid BC break,
+ * if you use for libraries and want to preserve BC break, you don't
+ * need to configure it, as it included in LevelSetList::UP_TO_PHP_74
* Set to true for projects that allow BC break
*/
TypedPropertyFromAssignsRector::INLINE_PUBLIC => false,
diff --git a/src/BaseJob.php b/src/BaseJob.php
index fd94582..0538783 100644
--- a/src/BaseJob.php
+++ b/src/BaseJob.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue;
abstract class BaseJob
{
diff --git a/src/Commands/Generators/JobGenerator.php b/src/Commands/Generators/JobGenerator.php
index 9a4ec19..4a067e2 100644
--- a/src/Commands/Generators/JobGenerator.php
+++ b/src/Commands/Generators/JobGenerator.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\GeneratorTrait;
@@ -43,7 +54,7 @@ class JobGenerator extends BaseCommand
/**
* The Command's Arguments
*
- * @var array
+ * @var array
*/
protected $arguments = [
'name' => 'The job class name.',
@@ -52,7 +63,7 @@ class JobGenerator extends BaseCommand
/**
* The Command's Options
*
- * @var array
+ * @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
diff --git a/src/Commands/Generators/Views/job.tpl.php b/src/Commands/Generators/Views/job.tpl.php
index b007789..36951f1 100644
--- a/src/Commands/Generators/Views/job.tpl.php
+++ b/src/Commands/Generators/Views/job.tpl.php
@@ -2,8 +2,8 @@
namespace {namespace};
-use Michalsn\CodeIgniterQueue\BaseJob;
-use Michalsn\CodeIgniterQueue\Interfaces\JobInterface;
+use CodeIgniter\Queue\BaseJob;
+use CodeIgniter\Queue\Interfaces\JobInterface;
class {class} extends BaseJob implements JobInterface
{
diff --git a/src/Commands/QueueClear.php b/src/Commands/QueueClear.php
index b0f7d34..c57ba24 100644
--- a/src/Commands/QueueClear.php
+++ b/src/Commands/QueueClear.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@@ -38,7 +49,7 @@ class QueueClear extends BaseCommand
/**
* The Command's Arguments
*
- * @var array
+ * @var array
*/
protected $arguments = [
'queueName' => 'Name of the queue we will work with.',
@@ -50,7 +61,8 @@ class QueueClear extends BaseCommand
public function run(array $params)
{
// Read params
- if (! $queue = array_shift($params)) {
+ $queue = array_shift($params);
+ if ($queue === null) {
CLI::error('The queueName is not specified.');
return EXIT_ERROR;
diff --git a/src/Commands/QueueFailed.php b/src/Commands/QueueFailed.php
index ecb9168..9d7d2a1 100644
--- a/src/Commands/QueueFailed.php
+++ b/src/Commands/QueueFailed.php
@@ -1,10 +1,21 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
-use Michalsn\CodeIgniterQueue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
class QueueFailed extends BaseCommand
{
@@ -39,7 +50,7 @@ class QueueFailed extends BaseCommand
/**
* The Command's Options
*
- * @var array
+ * @var array
*/
protected $options = [
'-queue' => 'Queue name.',
@@ -54,7 +65,7 @@ public function run(array $params)
$queue = $params['queue'] ?? CLI::getOption('queue');
/** @var QueueConfig $config */
- $config = config('queue');
+ $config = config('Queue');
$results = service('queue')->listFailed($queue);
@@ -62,7 +73,13 @@ public function run(array $params)
$tbody = [];
foreach ($results as $result) {
- $tbody[] = [$result->id, $result->connection, $result->queue, $this->getClassName($result->payload['job'], $config), $result->failed_at];
+ $tbody[] = [
+ $result->id,
+ $result->connection,
+ $result->queue,
+ $this->getClassName($result->payload['job'], $config),
+ $result->failed_at,
+ ];
}
CLI::table($tbody, $thead);
diff --git a/src/Commands/QueueFlush.php b/src/Commands/QueueFlush.php
index b87f971..446dc11 100644
--- a/src/Commands/QueueFlush.php
+++ b/src/Commands/QueueFlush.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@@ -38,7 +49,7 @@ class QueueFlush extends BaseCommand
/**
* The Command's Options
*
- * @var array
+ * @var array
*/
protected $options = [
'-hours' => 'Number of hours.',
@@ -54,6 +65,10 @@ public function run(array $params)
$hours = $params['hours'] ?? CLI::getOption('hours');
$queue = $params['queue'] ?? CLI::getOption('queue');
+ if ($hours !== null) {
+ $hours = (int) $hours;
+ }
+
service('queue')->flush($hours, $queue);
if ($hours === null) {
diff --git a/src/Commands/QueueForget.php b/src/Commands/QueueForget.php
index 523c40d..9f930fb 100644
--- a/src/Commands/QueueForget.php
+++ b/src/Commands/QueueForget.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@@ -38,7 +49,7 @@ class QueueForget extends BaseCommand
/**
* The Command's Arguments
*
- * @var array
+ * @var array
*/
protected $arguments = [
'id' => 'ID of the failed job.',
@@ -50,13 +61,14 @@ class QueueForget extends BaseCommand
public function run(array $params)
{
// Read params
- if (! $id = array_shift($params)) {
+ $id = array_shift($params);
+ if ($id === null) {
CLI::error('The ID of the failed job is not specified.');
return EXIT_ERROR;
}
- if (service('queue')->forget($id)) {
+ if (service('queue')->forget((int) $id)) {
CLI::write(sprintf('Failed job with ID %s has been removed.', $id), 'green');
} else {
CLI::write(sprintf('Could not find the failed job with ID %s', $id), 'red');
diff --git a/src/Commands/QueuePublish.php b/src/Commands/QueuePublish.php
index 606cb60..6e4e7bb 100644
--- a/src/Commands/QueuePublish.php
+++ b/src/Commands/QueuePublish.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@@ -11,14 +22,11 @@ class QueuePublish extends BaseCommand
{
protected $group = 'Queue';
protected $name = 'queue:publish';
- protected $description = 'Publish QueueJob config file into the current application.';
+ protected $description = 'Publish Queue config file into the current application.';
- /**
- * @return void
- */
- public function run(array $params)
+ public function run(array $params): void
{
- $source = service('autoloader')->getNamespace('Michalsn\\CodeIgniterQueue')[0];
+ $source = service('autoloader')->getNamespace('CodeIgniter\\Queue')[0];
$publisher = new Publisher($source, APPPATH);
@@ -34,8 +42,8 @@ public function run(array $params)
foreach ($publisher->getPublished() as $file) {
$contents = file_get_contents($file);
- $contents = str_replace('namespace Michalsn\\CodeIgniterQueue\\Config', 'namespace Config', $contents);
- $contents = str_replace('use CodeIgniter\\Config\\BaseConfig', 'use Michalsn\\CodeIgniterQueue\\Config\\Queue as BaseQueue', $contents);
+ $contents = str_replace('namespace CodeIgniter\\Queue\\Config', 'namespace Config', $contents);
+ $contents = str_replace('use CodeIgniter\\Config\\BaseConfig', 'use CodeIgniter\\Queue\\Config\\Queue as BaseQueue', $contents);
$contents = str_replace('class Queue extends BaseConfig', 'class Queue extends BaseQueue', $contents);
$method = <<<'EOT'
@@ -50,6 +58,8 @@ public function __construct()
/**
* Resolve job class name.
+ *
+ * @return class-string
*/
public function resolveJobClass(string $name): string
{
diff --git a/src/Commands/QueueRetry.php b/src/Commands/QueueRetry.php
index 8a81290..c269d53 100644
--- a/src/Commands/QueueRetry.php
+++ b/src/Commands/QueueRetry.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@@ -38,7 +49,7 @@ class QueueRetry extends BaseCommand
/**
* The Command's Arguments
*
- * @var array
+ * @var array
*/
protected $arguments = [
'id' => 'ID of the failed job or "all" for all failed jobs.',
@@ -47,7 +58,7 @@ class QueueRetry extends BaseCommand
/**
* The Command's Options
*
- * @var array
+ * @var array
*/
protected $options = [
'-queue' => 'Queue name.',
@@ -59,15 +70,14 @@ class QueueRetry extends BaseCommand
public function run(array $params)
{
// Read params
- if (! $id = array_shift($params)) {
+ $id = array_shift($params);
+ if ($id === null) {
CLI::error('The ID of the failed job is not specified.');
return EXIT_ERROR;
}
- if ($id === 'all') {
- $id = null;
- }
+ $id = $id === 'all' ? null : (int) $id;
$queue = $params['queue'] ?? CLI::getOption('queue');
diff --git a/src/Commands/QueueStop.php b/src/Commands/QueueStop.php
index 9d5d1f8..219db56 100644
--- a/src/Commands/QueueStop.php
+++ b/src/Commands/QueueStop.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@@ -38,7 +49,7 @@ class QueueStop extends BaseCommand
/**
* The Command's Arguments
*
- * @var array
+ * @var array
*/
protected $arguments = [
'queueName' => 'Name of the queue we will work with.',
@@ -47,7 +58,7 @@ class QueueStop extends BaseCommand
/**
* The Command's Options
*
- * @var array
+ * @var array
*/
protected $options = [
];
@@ -58,7 +69,8 @@ class QueueStop extends BaseCommand
public function run(array $params)
{
// Read params
- if (! $queue = array_shift($params)) {
+ $queue = array_shift($params);
+ if ($queue === null) {
CLI::error('The queueName is not specified.');
return EXIT_ERROR;
@@ -69,7 +81,7 @@ public function run(array $params)
cache()->save($cacheName, $startTime, MINUTE * 10);
- CLI::write('QueueJob will be stopped after the current job finish', 'yellow');
+ CLI::write('Queue will be stopped after the current job finish', 'yellow');
return EXIT_SUCCESS;
}
diff --git a/src/Commands/QueueWork.php b/src/Commands/QueueWork.php
index 5053993..4f2bf22 100644
--- a/src/Commands/QueueWork.php
+++ b/src/Commands/QueueWork.php
@@ -1,12 +1,24 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
use Exception;
-use Michalsn\CodeIgniterQueue\Config\Queue as QueueConfig;
-use Michalsn\CodeIgniterQueue\Entities\QueueJob;
use Throwable;
class QueueWork extends BaseCommand
@@ -42,7 +54,7 @@ class QueueWork extends BaseCommand
/**
* The Command's Arguments
*
- * @var array
+ * @var array
*/
protected $arguments = [
'queueName' => 'Name of the queue we will work with.',
@@ -51,7 +63,7 @@ class QueueWork extends BaseCommand
/**
* The Command's Options
*
- * @var array
+ * @var array
*/
protected $options = [
'-sleep' => 'Wait time between the next check for available job when the queue is empty. Default value: 10 (seconds).',
@@ -79,23 +91,34 @@ public function run(array $params)
$stopWhenEmpty = false;
$waiting = false;
- // Read params
- if (! $queue = array_shift($params)) {
+ // Read queue name from params
+ $queue = array_shift($params);
+ if ($queue === null) {
CLI::error('The queueName is not specified.');
return EXIT_ERROR;
}
// Read options
- $sleep = $params['sleep'] ?? CLI::getOption('sleep') ?? 10;
- $rest = $params['rest'] ?? CLI::getOption('rest') ?? 0;
- $maxJobs = $params['max-jobs'] ?? CLI::getOption('max-jobs') ?? 0;
- $maxTime = $params['max-time'] ?? CLI::getOption('max-time') ?? 0;
- $memory = $params['memory'] ?? CLI::getOption('memory') ?? 128;
- $priority = $params['priority'] ?? CLI::getOption('priority') ?? $config->getQueuePriorities($queue) ?? 'default';
- $tries = $params['tries'] ?? CLI::getOption('tries');
- $retryAfter = $params['retry-after'] ?? CLI::getOption('retry-after');
- $countJobs = 0;
+ [
+ $error,
+ $sleep,
+ $rest,
+ $maxJobs,
+ $maxTime,
+ $memory,
+ $priority,
+ $tries,
+ $retryAfter,
+ ] = $this->readOptions($params, $config, $queue);
+
+ if ($error !== null) {
+ CLI::write($error, 'red');
+
+ return EXIT_ERROR;
+ }
+
+ $countJobs = 0;
if (array_key_exists('stop-when-empty', $params) || CLI::getOption('stop-when-empty')) {
$stopWhenEmpty = true;
@@ -111,7 +134,7 @@ public function run(array $params)
CLI::write(PHP_EOL);
- $priority = array_map('trim', explode(',', $priority));
+ $priority = array_map('trim', explode(',', (string) $priority));
while (true) {
$work = service('queue')->pop($queue, $priority);
@@ -148,7 +171,7 @@ public function run(array $params)
CLI::print('Starting a new job: ', 'cyan');
CLI::print($work->payload['job'], 'light_cyan');
CLI::print(', with ID: ', 'cyan');
- CLI::print($work->id, 'light_cyan');
+ CLI::print((string) $work->id, 'light_cyan');
$this->handleWork($work, $config, $tries, $retryAfter);
@@ -175,12 +198,56 @@ public function run(array $params)
}
}
+ private function readOptions(array $params, QueueConfig $config, string $queue): array
+ {
+ $options = [
+ 'error' => null,
+ 'sleep' => $params['sleep'] ?? CLI::getOption('sleep') ?? 10,
+ 'rest' => $params['rest'] ?? CLI::getOption('rest') ?? 0,
+ 'maxJobs' => $params['max-jobs'] ?? CLI::getOption('max-jobs') ?? 0,
+ 'maxTime' => $params['max-time'] ?? CLI::getOption('max-time') ?? 0,
+ 'memory' => $params['memory'] ?? CLI::getOption('memory') ?? 128,
+ 'priority' => $params['priority'] ?? CLI::getOption('priority') ?? $config->getQueuePriorities($queue) ?? 'default',
+ 'tries' => $params['tries'] ?? CLI::getOption('tries'),
+ 'retryAfter' => $params['retry-after'] ?? CLI::getOption('retry-after'),
+ ];
+
+ // Options that, being defined, cannot be `true`
+ $keys = ['sleep', 'rest', 'maxJobs', 'maxTime', 'memory', 'priority', 'tries', 'retryAfter'];
+
+ foreach ($keys as $key) {
+ if ($options[$key] === true) {
+ $options['error'] = sprintf('Option: "-%s" must have a defined value.', $key);
+
+ return array_values($options);
+ }
+ }
+ // Options that, being defined, have to be `int`
+ $keys = array_diff($keys, ['priority']);
+
+ foreach ($keys as $key) {
+ if ($options[$key] !== null && ! is_int($options[$key])) {
+ $options[$key] = (int) $options[$key];
+ }
+ }
+
+ return array_values($options);
+ }
+
private function handleWork(QueueJob $work, QueueConfig $config, ?int $tries, ?int $retryAfter): void
{
timer()->start('work');
$payload = $work->payload;
+ $payloadMetadata = null;
+
try {
+ // Load payload metadata
+ $payloadMetadata = PayloadMetadata::fromArray($payload['metadata'] ?? []);
+
+ // Renew lock if needed
+ $this->renewLock($payloadMetadata);
+
$class = $config->resolveJobClass($payload['job']);
$job = new $class($payload['data']);
$job->process();
@@ -189,8 +256,11 @@ private function handleWork(QueueJob $work, QueueConfig $config, ?int $tries, ?i
service('queue')->done($work, $config->keepDoneJobs);
CLI::write('The processing of this job was successful', 'green');
+
+ // Check chained jobs
+ $this->processNextJobInChain($payloadMetadata);
} catch (Throwable $err) {
- if (isset($job) && ++$work->attempts < $tries ?? $job->getTries()) {
+ if (isset($job) && ++$work->attempts < ($tries ?? $job->getTries())) {
// Schedule for later
service('queue')->later($work, $retryAfter ?? $job->getRetryAfter());
} else {
@@ -199,11 +269,83 @@ private function handleWork(QueueJob $work, QueueConfig $config, ?int $tries, ?i
}
CLI::write('The processing of this job failed', 'red');
} finally {
+ // Remove lock if needed
+ $this->clearLock($payloadMetadata);
+
timer()->stop('work');
CLI::write(sprintf('It took: %s sec', timer()->getElapsedTime('work')) . PHP_EOL, 'cyan');
}
}
+ /**
+ * Process the next job in the chain
+ */
+ private function processNextJobInChain(PayloadMetadata $payloadMetadata): void
+ {
+ if (! $payloadMetadata->hasChainedJobs()) {
+ return;
+ }
+
+ $nextPayload = $payloadMetadata->getChainedJobs()->shift();
+ $priority = $nextPayload->getPriority();
+ $delay = $nextPayload->getDelay();
+
+ if ($priority !== null) {
+ service('queue')->setPriority($priority);
+ }
+
+ if ($delay !== null) {
+ service('queue')->setDelay($delay);
+ }
+
+ if ($payloadMetadata->hasChainedJobs()) {
+ $nextPayload->setChainedJobs($payloadMetadata->getChainedJobs());
+ }
+
+ service('queue')->push(
+ $nextPayload->getQueue(),
+ $nextPayload->getJob(),
+ $nextPayload->getData(),
+ $nextPayload->getMetadata(),
+ );
+
+ CLI::write(sprintf('Chained job: %s has been placed in the queue: %s', $nextPayload->getJob(), $nextPayload->getQueue()), 'green');
+ }
+
+ /**
+ * Renew task lock
+ */
+ private function renewLock(PayloadMetadata $payloadMetadata): void
+ {
+ if (! $payloadMetadata->has('taskLockTTL') || ! $payloadMetadata->has('taskLockKey')) {
+ return;
+ }
+
+ $ttl = $payloadMetadata->get('taskLockTTL');
+ $key = $payloadMetadata->get('taskLockKey');
+
+ // Permanent lock, no need to renew
+ if ($ttl === 0) {
+ return;
+ }
+
+ cache()->save($key, [], $ttl);
+ }
+
+ /**
+ * Remove task lock
+ */
+ private function clearLock(PayloadMetadata $payloadMetadata): void
+ {
+ if (! $payloadMetadata->has('taskLockKey')) {
+ return;
+ }
+
+ $key = $payloadMetadata->get('taskLockKey');
+
+ cache()->delete($key);
+ }
+
private function maxJobsCheck(int $maxJobs, int $countJobs): bool
{
if ($maxJobs > 0 && $countJobs >= $maxJobs) {
diff --git a/src/Config/Queue.php b/src/Config/Queue.php
index fc8a37b..793ea85 100644
--- a/src/Config/Queue.php
+++ b/src/Config/Queue.php
@@ -1,10 +1,25 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Config;
use CodeIgniter\Config\BaseConfig;
-use Michalsn\CodeIgniterQueue\Exceptions\QueueException;
-use Michalsn\CodeIgniterQueue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Handlers\PredisHandler;
+use CodeIgniter\Queue\Handlers\RedisHandler;
+use CodeIgniter\Queue\Interfaces\JobInterface;
+use CodeIgniter\Queue\Interfaces\QueueInterface;
class Queue extends BaseConfig
{
@@ -15,9 +30,13 @@ class Queue extends BaseConfig
/**
* Available handlers.
+ *
+ * @var array>
*/
public array $handlers = [
'database' => DatabaseHandler::class,
+ 'redis' => RedisHandler::class,
+ 'predis' => PredisHandler::class,
];
/**
@@ -26,6 +45,34 @@ class Queue extends BaseConfig
public array $database = [
'dbGroup' => 'default',
'getShared' => true,
+ // use skip locked feature to maintain concurrency calls
+ // this is not relevant for the SQLite3 database driver
+ 'skipLocked' => true,
+ ];
+
+ /**
+ * Redis handler config.
+ */
+ public array $redis = [
+ 'host' => '127.0.0.1',
+ 'password' => null,
+ 'port' => 6379,
+ 'timeout' => 0,
+ 'database' => 0,
+ 'prefix' => '',
+ ];
+
+ /**
+ * Predis handler config.
+ */
+ public array $predis = [
+ 'scheme' => 'tcp',
+ 'host' => '127.0.0.1',
+ 'password' => null,
+ 'port' => 6379,
+ 'timeout' => 5,
+ 'database' => 0,
+ 'prefix' => '',
];
/**
@@ -52,6 +99,8 @@ class Queue extends BaseConfig
/**
* Your jobs handlers.
+ *
+ * @var array>
*/
public array $jobHandlers = [];
@@ -66,6 +115,8 @@ public function __construct()
/**
* Resolve job class name.
+ *
+ * @return class-string
*/
public function resolveJobClass(string $name): string
{
diff --git a/src/Config/Registrar.php b/src/Config/Registrar.php
index 7b5956b..634837b 100644
--- a/src/Config/Registrar.php
+++ b/src/Config/Registrar.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Config;
class Registrar
{
@@ -8,7 +19,7 @@ public static function Generators(): array
{
return [
'views' => [
- 'queue:job' => 'Michalsn\CodeIgniterQueue\Commands\Generators\Views\job.tpl.php',
+ 'queue:job' => 'CodeIgniter\Queue\Commands\Generators\Views\job.tpl.php',
],
];
}
diff --git a/src/Config/Services.php b/src/Config/Services.php
index fe5e158..1ca828f 100644
--- a/src/Config/Services.php
+++ b/src/Config/Services.php
@@ -1,22 +1,33 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Config;
use CodeIgniter\Config\BaseService;
-use Michalsn\CodeIgniterQueue\Config\Queue as QueueConfig;
-use Michalsn\CodeIgniterQueue\Interfaces\QueueInterface;
-use Michalsn\CodeIgniterQueue\Queue;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Interfaces\QueueInterface;
+use CodeIgniter\Queue\Queue;
class Services extends BaseService
{
- public static function queue(?QueueConfig $config = null, $getShared = true): QueueInterface
+ public static function queue($getShared = true): QueueInterface
{
if ($getShared) {
- return static::getSharedInstance('queue', $config);
+ return static::getSharedInstance('queue');
}
/** @var QueueConfig $config */
- $config ??= config('Queue');
+ $config = config('Queue');
return (new Queue($config))->init();
}
diff --git a/src/Database/Migrations/2023-10-12-112040_AddQueueTables.php b/src/Database/Migrations/2023-10-12-112040_AddQueueTables.php
index c53fd20..e593047 100644
--- a/src/Database/Migrations/2023-10-12-112040_AddQueueTables.php
+++ b/src/Database/Migrations/2023-10-12-112040_AddQueueTables.php
@@ -1,12 +1,23 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddQueueTables extends Migration
{
- public function up()
+ public function up(): void
{
$this->forge->addField([
'id' => ['type' => 'bigint', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
@@ -34,7 +45,7 @@ public function up()
$this->forge->createTable('queue_jobs_failed', true);
}
- public function down()
+ public function down(): void
{
$this->forge->dropTable('queue_jobs', true);
$this->forge->dropTable('queue_jobs_failed', true);
diff --git a/src/Database/Migrations/2023-11-05-064053_AddPriorityField.php b/src/Database/Migrations/2023-11-05-064053_AddPriorityField.php
index 1f9fe82..82089b0 100644
--- a/src/Database/Migrations/2023-11-05-064053_AddPriorityField.php
+++ b/src/Database/Migrations/2023-11-05-064053_AddPriorityField.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Database\Migrations;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Migration;
@@ -10,7 +21,7 @@
*/
class AddPriorityField extends Migration
{
- public function up()
+ public function up(): void
{
$fields = [
'priority' => [
@@ -40,7 +51,7 @@ public function up()
$this->forge->processIndexes('queue_jobs');
}
- public function down()
+ public function down(): void
{
// Ugly fix for dropping the correct index
$keys = $this->db->getIndexData('queue_jobs');
diff --git a/src/Database/Migrations/2024-12-27-110712_ChangePayloadFieldTypeInSqlsrv.php b/src/Database/Migrations/2024-12-27-110712_ChangePayloadFieldTypeInSqlsrv.php
new file mode 100644
index 0000000..c77c4e0
--- /dev/null
+++ b/src/Database/Migrations/2024-12-27-110712_ChangePayloadFieldTypeInSqlsrv.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Database\Migrations;
+
+use CodeIgniter\Database\BaseConnection;
+use CodeIgniter\Database\Migration;
+
+/**
+ * @property BaseConnection $db
+ */
+class ChangePayloadFieldTypeInSqlsrv extends Migration
+{
+ public function up(): void
+ {
+ if ($this->db->DBDriver === 'SQLSRV') {
+ $fields = [
+ 'payload' => [
+ 'name' => 'payload',
+ 'type' => 'NVARCHAR',
+ 'constraint' => 'MAX',
+ 'null' => false,
+ ],
+ ];
+ $this->forge->modifyColumn('queue_jobs', $fields);
+ }
+ }
+
+ public function down(): void
+ {
+ if ($this->db->DBDriver === 'SQLSRV') {
+ $fields = [
+ 'payload' => [
+ 'name' => 'payload',
+ 'type' => 'TEXT', // already deprecated
+ 'null' => false,
+ ],
+ ];
+ $this->forge->modifyColumn('queue_jobs', $fields);
+ }
+ }
+}
diff --git a/src/Entities/QueueJob.php b/src/Entities/QueueJob.php
index 59beb3a..670a9f8 100644
--- a/src/Entities/QueueJob.php
+++ b/src/Entities/QueueJob.php
@@ -1,9 +1,31 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Entities;
use CodeIgniter\Entity\Entity;
+use CodeIgniter\I18n\Time;
+/**
+ * @property int $attempts
+ * @property Time $available_at
+ * @property Time $created_at
+ * @property int $id
+ * @property array $payload
+ * @property string $priority
+ * @property string $queue
+ * @property int $status
+ */
class QueueJob extends Entity
{
protected $dates = ['available_at', 'created_at'];
diff --git a/src/Entities/QueueJobFailed.php b/src/Entities/QueueJobFailed.php
index 06c1a97..6b296d9 100644
--- a/src/Entities/QueueJobFailed.php
+++ b/src/Entities/QueueJobFailed.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Entities;
use CodeIgniter\Entity\Entity;
diff --git a/src/Enums/Status.php b/src/Enums/Status.php
index 1631cc1..eaacfc9 100644
--- a/src/Enums/Status.php
+++ b/src/Enums/Status.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Enums;
enum Status: int
{
diff --git a/src/Exceptions/QueueException.php b/src/Exceptions/QueueException.php
index 93b240c..542de45 100644
--- a/src/Exceptions/QueueException.php
+++ b/src/Exceptions/QueueException.php
@@ -1,6 +1,17 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Exceptions;
use RuntimeException;
@@ -11,6 +22,16 @@ public static function forIncorrectHandler(): static
return new self(lang('Queue.incorrectHandler'));
}
+ public static function forIncorrectQueueFormat(): static
+ {
+ return new self(lang('Queue.incorrectQueueFormat'));
+ }
+
+ public static function forTooLongQueueName(): static
+ {
+ return new self(lang('Queue.tooLongQueueName'));
+ }
+
public static function forIncorrectJobHandler(): static
{
return new self(lang('Queue.incorrectJobHandler'));
@@ -30,4 +51,9 @@ public static function forIncorrectQueuePriority(string $priority, string $queue
{
return new self(lang('Queue.incorrectQueuePriority', [$priority, $queue]));
}
+
+ public static function forIncorrectDelayValue(): static
+ {
+ return new self(lang('Queue.incorrectDelayValue'));
+ }
}
diff --git a/src/Handlers/BaseHandler.php b/src/Handlers/BaseHandler.php
index aad22b4..f364802 100644
--- a/src/Handlers/BaseHandler.php
+++ b/src/Handlers/BaseHandler.php
@@ -1,13 +1,29 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Handlers;
+
+use Closure;
use CodeIgniter\I18n\Time;
-use Michalsn\CodeIgniterQueue\Config\Queue as QueueConfig;
-use Michalsn\CodeIgniterQueue\Entities\QueueJob;
-use Michalsn\CodeIgniterQueue\Entities\QueueJobFailed;
-use Michalsn\CodeIgniterQueue\Exceptions\QueueException;
-use Michalsn\CodeIgniterQueue\Models\QueueJobFailedModel;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Entities\QueueJobFailed;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use CodeIgniter\Queue\Payloads\ChainBuilder;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use CodeIgniter\Queue\QueuePushResult;
+use CodeIgniter\Queue\Traits\HasQueueValidation;
use ReflectionException;
use Throwable;
@@ -16,26 +32,25 @@
*/
abstract class BaseHandler
{
+ use HasQueueValidation;
+
protected QueueConfig $config;
protected ?string $priority = null;
+ protected ?int $delay = null;
- /**
- * Set priority for job queue.
- */
- public function setPriority(string $priority): static
- {
- if (! preg_match('/^[a-z_-]+$/', $priority)) {
- throw QueueException::forIncorrectPriorityFormat();
- }
+ abstract public function name(): string;
- if (strlen($priority) > 64) {
- throw QueueException::forTooLongPriorityName();
- }
+ abstract public function push(string $queue, string $job, array $data, ?PayloadMetadata $metadata = null): QueuePushResult;
- $this->priority = $priority;
+ abstract public function pop(string $queue, array $priorities): ?QueueJob;
- return $this;
- }
+ abstract public function later(QueueJob $queueJob, int $seconds): bool;
+
+ abstract public function failed(QueueJob $queueJob, Throwable $err, bool $keepJob): bool;
+
+ abstract public function done(QueueJob $queueJob, bool $keepJob): bool;
+
+ abstract public function clear(?string $queue = null): bool;
/**
* Retry failed job.
@@ -47,11 +62,11 @@ public function retry(?int $id, ?string $queue): int
$jobs = model(QueueJobFailedModel::class)
->when(
$id !== null,
- static fn ($query) => $query->where('id', $id)
+ static fn ($query) => $query->where('id', $id),
)
->when(
$queue !== null,
- static fn ($query) => $query->where('queue', $queue)
+ static fn ($query) => $query->where('queue', $queue),
)
->findAll();
@@ -64,7 +79,7 @@ public function retry(?int $id, ?string $queue): int
}
/**
- * Delete failed job by ID.
+ * Delete a failed job by ID.
*/
public function forget(int $id): bool
{
@@ -87,11 +102,11 @@ public function flush(?int $hours, ?string $queue): bool
return model(QueueJobFailedModel::class)
->when(
$hours !== null,
- static fn ($query) => $query->where('failed_at <=', Time::now()->subHours($hours)->timestamp)
+ static fn ($query) => $query->where('failed_at <=', Time::now()->subHours($hours)->timestamp),
)
->when(
$queue !== null,
- static fn ($query) => $query->where('queue', $queue)
+ static fn ($query) => $query->where('queue', $queue),
)
->delete();
}
@@ -104,12 +119,49 @@ public function listFailed(?string $queue): array
return model(QueueJobFailedModel::class)
->when(
$queue !== null,
- static fn ($query) => $query->where('queue', $queue)
+ static fn ($query) => $query->where('queue', $queue),
)
->orderBy('failed_at', 'desc')
->findAll();
}
+ /**
+ * Set delay for job queue (in seconds).
+ */
+ public function setDelay(int $delay): static
+ {
+ $this->validateDelay($delay);
+
+ $this->delay = $delay;
+
+ return $this;
+ }
+
+ /**
+ * Set priority for job queue.
+ */
+ public function setPriority(string $priority): static
+ {
+ $this->validatePriority($priority);
+
+ $this->priority = $priority;
+
+ return $this;
+ }
+
+ /**
+ * Create a job chain on the specified queue
+ *
+ * @param Closure $callback Chain definition callback
+ */
+ public function chain(Closure $callback): QueuePushResult
+ {
+ $chainBuilder = new ChainBuilder($this);
+ $callback($chainBuilder);
+
+ return $chainBuilder->dispatch();
+ }
+
/**
* Log failed job.
*
@@ -121,7 +173,7 @@ protected function logFailed(QueueJob $queueJob, Throwable $err): bool
"file: {$err->getFile()}:{$err->getLine()}";
$queueJobFailed = new QueueJobFailed([
- 'connection' => 'database',
+ 'connection' => $this->name(),
'queue' => $queueJob->queue,
'payload' => $queueJob->payload,
'priority' => $queueJob->priority,
@@ -136,6 +188,9 @@ protected function logFailed(QueueJob $queueJob, Throwable $err): bool
*/
protected function validateJobAndPriority(string $queue, string $job): void
{
+ // Validate queue
+ $this->validateQueue($queue);
+
// Validate jobHandler.
if (! in_array($job, array_keys($this->config->jobHandlers), true)) {
throw QueueException::forIncorrectJobHandler();
@@ -150,4 +205,18 @@ protected function validateJobAndPriority(string $queue, string $job): void
throw QueueException::forIncorrectQueuePriority($this->priority, $queue);
}
}
+
+ /**
+ * Validate queue name.
+ */
+ protected function validateQueue(string $queue): void
+ {
+ if (! preg_match('/^[a-z0-9_-]+$/', $queue)) {
+ throw QueueException::forIncorrectQueueFormat();
+ }
+
+ if (strlen($queue) > 64) {
+ throw QueueException::forTooLongQueueName();
+ }
+ }
}
diff --git a/src/Handlers/DatabaseHandler.php b/src/Handlers/DatabaseHandler.php
index bf74dc5..403e7cd 100644
--- a/src/Handlers/DatabaseHandler.php
+++ b/src/Handlers/DatabaseHandler.php
@@ -1,14 +1,27 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Handlers;
use CodeIgniter\I18n\Time;
-use Michalsn\CodeIgniterQueue\Config\Queue as QueueConfig;
-use Michalsn\CodeIgniterQueue\Entities\QueueJob;
-use Michalsn\CodeIgniterQueue\Enums\Status;
-use Michalsn\CodeIgniterQueue\Interfaces\QueueInterface;
-use Michalsn\CodeIgniterQueue\Models\QueueJobModel;
-use Michalsn\CodeIgniterQueue\Payload;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Queue\Interfaces\QueueInterface;
+use CodeIgniter\Queue\Models\QueueJobModel;
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use CodeIgniter\Queue\QueuePushResult;
use ReflectionException;
use Throwable;
@@ -22,27 +35,43 @@ public function __construct(protected QueueConfig $config)
$this->jobModel = model(QueueJobModel::class, true, $connection);
}
+ /**
+ * Name of the handler.
+ */
+ public function name(): string
+ {
+ return 'database';
+ }
+
/**
* Add job to the queue.
- *
- * @throws ReflectionException
*/
- public function push(string $queue, string $job, array $data): bool
+ public function push(string $queue, string $job, array $data, ?PayloadMetadata $metadata = null): QueuePushResult
{
$this->validateJobAndPriority($queue, $job);
$queueJob = new QueueJob([
'queue' => $queue,
- 'payload' => new Payload($job, $data),
+ 'payload' => new Payload($job, $data, $metadata),
'priority' => $this->priority,
'status' => Status::PENDING->value,
'attempts' => 0,
- 'available_at' => Time::now()->timestamp,
+ 'available_at' => Time::now()->addSeconds($this->delay ?? 0),
]);
- $this->priority = null;
+ $this->priority = $this->delay = null;
+
+ try {
+ $jobId = $this->jobModel->insert($queueJob);
+ } catch (Throwable $e) {
+ return QueuePushResult::failure($e->getMessage());
+ }
+
+ if ($jobId === 0) {
+ return QueuePushResult::failure('Failed to insert job into the database.');
+ }
- return $this->jobModel->insert($queueJob, false);
+ return QueuePushResult::success($jobId);
}
/**
@@ -73,7 +102,7 @@ public function pop(string $queue, array $priorities): ?QueueJob
public function later(QueueJob $queueJob, int $seconds): bool
{
$queueJob->status = Status::PENDING->value;
- $queueJob->available_at = Time::now()->addSeconds($seconds)->timestamp;
+ $queueJob->available_at = Time::now()->addSeconds($seconds);
return $this->jobModel->save($queueJob);
}
diff --git a/src/Handlers/PredisHandler.php b/src/Handlers/PredisHandler.php
new file mode 100644
index 0000000..22be7cf
--- /dev/null
+++ b/src/Handlers/PredisHandler.php
@@ -0,0 +1,194 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Handlers;
+
+use CodeIgniter\Autoloader\FileLocator;
+use CodeIgniter\Exceptions\CriticalError;
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Queue\Interfaces\QueueInterface;
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use CodeIgniter\Queue\QueuePushResult;
+use Exception;
+use Predis\Client;
+use Throwable;
+
+class PredisHandler extends BaseHandler implements QueueInterface
+{
+ private readonly Client $predis;
+ private readonly string $luaScript;
+
+ public function __construct(protected QueueConfig $config)
+ {
+ try {
+ $this->predis = new Client($config->predis, ['prefix' => $config->predis['prefix']]);
+ $this->predis->time();
+
+ $locator = new FileLocator(service('autoloader'));
+ $luaScript = $locator->locateFile('CodeIgniter\Queue\Lua\pop_task', null, 'lua');
+ if ($luaScript === false) {
+ throw new CriticalError('Queue: LUA script for Predis is not available.');
+ }
+ $this->luaScript = file_get_contents($luaScript);
+ } catch (Exception $e) {
+ throw new CriticalError('Queue: Predis connection refused (' . $e->getMessage() . ').');
+ }
+ }
+
+ /**
+ * Name of the handler.
+ */
+ public function name(): string
+ {
+ return 'predis';
+ }
+
+ /**
+ * Add job to the queue.
+ */
+ public function push(string $queue, string $job, array $data, ?PayloadMetadata $metadata = null): QueuePushResult
+ {
+ $this->validateJobAndPriority($queue, $job);
+
+ helper('text');
+
+ $jobId = (int) random_string('numeric', 16);
+ $availableAt = Time::now()->addSeconds($this->delay ?? 0);
+
+ $queueJob = new QueueJob([
+ 'id' => $jobId,
+ 'queue' => $queue,
+ 'payload' => new Payload($job, $data, $metadata),
+ 'priority' => $this->priority,
+ 'status' => Status::PENDING->value,
+ 'attempts' => 0,
+ 'available_at' => $availableAt,
+ ]);
+
+ try {
+ $result = $this->predis->zadd("queues:{$queue}:{$this->priority}", [json_encode($queueJob) => $availableAt->timestamp]);
+ } catch (Throwable $e) {
+ return QueuePushResult::failure('Unexpected Redis error: ' . $e->getMessage());
+ } finally {
+ $this->priority = $this->delay = null;
+ }
+
+ $this->priority = $this->delay = null;
+
+ return $result > 0
+ ? QueuePushResult::success($jobId)
+ : QueuePushResult::failure('Job already exists in the queue.');
+ }
+
+ /**
+ * Get job from the queue.
+ */
+ public function pop(string $queue, array $priorities): ?QueueJob
+ {
+ $now = (string) Time::now()->timestamp;
+
+ // Prepare the arguments for the Lua script
+ $args = [
+ 'queues:' . $queue, // KEYS[1]
+ $now, // ARGV[2]
+ json_encode($priorities), // ARGV[3]
+ ];
+
+ // Execute the Lua script
+ $task = $this->predis->eval($this->luaScript, 1, ...$args);
+
+ if ($task === null) {
+ return null;
+ }
+
+ $queueJob = new QueueJob(json_decode((string) $task, true));
+
+ // Set the actual status as in DB.
+ $queueJob->status = Status::RESERVED->value;
+ $queueJob->syncOriginal();
+
+ $this->predis->hset("queues:{$queue}::reserved", (string) $queueJob->id, json_encode($queueJob));
+
+ return $queueJob;
+ }
+
+ /**
+ * Schedule job for later
+ */
+ public function later(QueueJob $queueJob, int $seconds): bool
+ {
+ $queueJob->status = Status::PENDING->value;
+ $queueJob->available_at = Time::now()->addSeconds($seconds);
+
+ $result = $this->predis->zadd(
+ "queues:{$queueJob->queue}:{$queueJob->priority}",
+ [json_encode($queueJob) => $queueJob->available_at->timestamp],
+ );
+ if ($result !== 0) {
+ $this->predis->hdel("queues:{$queueJob->queue}::reserved", [$queueJob->id]);
+ }
+
+ return $result > 0;
+ }
+
+ /**
+ * Move job to failed table or move and delete.
+ */
+ public function failed(QueueJob $queueJob, Throwable $err, bool $keepJob): bool
+ {
+ if ($keepJob) {
+ $this->logFailed($queueJob, $err);
+ }
+
+ return (bool) $this->predis->hdel("queues:{$queueJob->queue}::reserved", [$queueJob->id]);
+ }
+
+ /**
+ * Change job status to DONE or delete it.
+ */
+ public function done(QueueJob $queueJob, bool $keepJob): bool
+ {
+ if ($keepJob) {
+ $queueJob->status = Status::DONE->value;
+ $this->predis->lpush("queues:{$queueJob->queue}::done", [json_encode($queueJob)]);
+ }
+
+ return (bool) $this->predis->hdel("queues:{$queueJob->queue}::reserved", [$queueJob->id]);
+ }
+
+ /**
+ * Delete queue jobs
+ */
+ public function clear(?string $queue = null): bool
+ {
+ if ($queue !== null) {
+ $keys = $this->predis->keys("queues:{$queue}:*");
+ if ($keys !== []) {
+ return $this->predis->del($keys) > 0;
+ }
+
+ return true;
+ }
+
+ $keys = $this->predis->keys('queues:*');
+ if ($keys !== []) {
+ return $this->predis->del($keys) > 0;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Handlers/RedisHandler.php b/src/Handlers/RedisHandler.php
new file mode 100644
index 0000000..4676436
--- /dev/null
+++ b/src/Handlers/RedisHandler.php
@@ -0,0 +1,220 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Handlers;
+
+use CodeIgniter\Autoloader\FileLocator;
+use CodeIgniter\Exceptions\CriticalError;
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Queue\Interfaces\QueueInterface;
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use CodeIgniter\Queue\QueuePushResult;
+use Redis;
+use RedisException;
+use Throwable;
+
+class RedisHandler extends BaseHandler implements QueueInterface
+{
+ private readonly Redis $redis;
+ private readonly string $luaScript;
+
+ public function __construct(protected QueueConfig $config)
+ {
+ $this->redis = new Redis();
+
+ try {
+ if (! $this->redis->connect($config->redis['host'], ($config->redis['host'][0] === '/' ? 0 : $config->redis['port']), $config->redis['timeout'])) {
+ throw new CriticalError('Queue: Redis connection failed. Check your configuration.');
+ }
+
+ if (isset($config->redis['password']) && ! $this->redis->auth($config->redis['password'])) {
+ throw new CriticalError('Queue: Redis authentication failed.');
+ }
+
+ if (isset($config->redis['database']) && ! $this->redis->select($config->redis['database'])) {
+ throw new CriticalError('Queue: Redis select database failed.');
+ }
+
+ if (isset($config->redis['prefix']) && ! $this->redis->setOption(Redis::OPT_PREFIX, $config->redis['prefix'])) {
+ throw new CriticalError('Queue: Redis setting prefix failed.');
+ }
+
+ $locator = new FileLocator(service('autoloader'));
+ $luaScript = $locator->locateFile('CodeIgniter\Queue\Lua\pop_task', null, 'lua');
+ if ($luaScript === false) {
+ throw new CriticalError('Queue: LUA script for Redis is not available.');
+ }
+ $this->luaScript = file_get_contents($luaScript);
+ } catch (RedisException $e) {
+ throw new CriticalError('Queue: RedisException occurred with message (' . $e->getMessage() . ').');
+ }
+ }
+
+ /**
+ * Name of the handler.
+ */
+ public function name(): string
+ {
+ return 'redis';
+ }
+
+ /**
+ * Add job to the queue.
+ *
+ * @throws RedisException
+ */
+ public function push(string $queue, string $job, array $data, ?PayloadMetadata $metadata = null): QueuePushResult
+ {
+ $this->validateJobAndPriority($queue, $job);
+
+ helper('text');
+
+ $availableAt = Time::now()->addSeconds($this->delay ?? 0);
+ $jobId = (int) random_string('numeric', 16);
+
+ $queueJob = new QueueJob([
+ 'id' => $jobId,
+ 'queue' => $queue,
+ 'payload' => new Payload($job, $data, $metadata),
+ 'priority' => $this->priority,
+ 'status' => Status::PENDING->value,
+ 'attempts' => 0,
+ 'available_at' => $availableAt,
+ ]);
+
+ try {
+ $result = $this->redis->zAdd("queues:{$queue}:{$this->priority}", $availableAt->timestamp, json_encode($queueJob));
+ } catch (Throwable $e) {
+ return QueuePushResult::failure('Unexpected Redis error: ' . $e->getMessage());
+ } finally {
+ $this->priority = $this->delay = null;
+ }
+
+ if ($result === false) {
+ return QueuePushResult::failure('Failed to add job to Redis.');
+ }
+
+ return (int) $result > 0
+ ? QueuePushResult::success($jobId)
+ : QueuePushResult::failure('Job already exists in the queue.');
+ }
+
+ /**
+ * Get job from the queue.
+ *
+ * @throws RedisException
+ */
+ public function pop(string $queue, array $priorities): ?QueueJob
+ {
+ $now = Time::now()->timestamp;
+
+ // Prepare the arguments for the Lua script
+ $args = [
+ 'queues:' . $queue, // KEYS[1]
+ $now, // ARGV[2]
+ json_encode($priorities), // ARGV[3]
+ ];
+
+ // Execute the Lua script
+ $task = $this->redis->eval($this->luaScript, $args, 1);
+
+ if ($task === false) {
+ return null;
+ }
+
+ $queueJob = new QueueJob(json_decode((string) $task, true));
+
+ // Set the actual status as in DB.
+ $queueJob->status = Status::RESERVED->value;
+ $queueJob->syncOriginal();
+
+ $this->redis->hSet("queues:{$queue}::reserved", (string) $queueJob->id, json_encode($queueJob));
+
+ return $queueJob;
+ }
+
+ /**
+ * Schedule job for later
+ *
+ * @throws RedisException
+ */
+ public function later(QueueJob $queueJob, int $seconds): bool
+ {
+ $queueJob->status = Status::PENDING->value;
+ $queueJob->available_at = Time::now()->addSeconds($seconds);
+
+ $result = (int) $this->redis->zAdd(
+ "queues:{$queueJob->queue}:{$queueJob->priority}",
+ $queueJob->available_at->timestamp,
+ json_encode($queueJob),
+ );
+ if ($result !== 0) {
+ $this->redis->hDel("queues:{$queueJob->queue}::reserved", (string) $queueJob->id);
+ }
+
+ return $result > 0;
+ }
+
+ /**
+ * Move job to failed table or move and delete.
+ */
+ public function failed(QueueJob $queueJob, Throwable $err, bool $keepJob): bool
+ {
+ if ($keepJob) {
+ $this->logFailed($queueJob, $err);
+ }
+
+ return (bool) $this->redis->hDel("queues:{$queueJob->queue}::reserved", (string) $queueJob->id);
+ }
+
+ /**
+ * Change job status to DONE or delete it.
+ *
+ * @throws RedisException
+ */
+ public function done(QueueJob $queueJob, bool $keepJob): bool
+ {
+ if ($keepJob) {
+ $queueJob->status = Status::DONE->value;
+ $this->redis->lPush("queues:{$queueJob->queue}::done", json_encode($queueJob));
+ }
+
+ return (bool) $this->redis->hDel("queues:{$queueJob->queue}::reserved", (string) $queueJob->id);
+ }
+
+ /**
+ * Delete queue jobs
+ *
+ * @throws RedisException
+ */
+ public function clear(?string $queue = null): bool
+ {
+ if ($queue !== null) {
+ if ($keys = $this->redis->keys("queues:{$queue}:*")) {
+ return (int) $this->redis->del($keys) > 0;
+ }
+
+ return true;
+ }
+
+ if ($keys = $this->redis->keys('queues:*')) {
+ return (int) $this->redis->del($keys) > 0;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Interfaces/JobInterface.php b/src/Interfaces/JobInterface.php
index 92564e4..85bed8d 100644
--- a/src/Interfaces/JobInterface.php
+++ b/src/Interfaces/JobInterface.php
@@ -1,10 +1,25 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Interfaces;
interface JobInterface
{
public function __construct(array $data);
public function process();
+
+ public function getRetryAfter(): int;
+
+ public function getTries(): int;
}
diff --git a/src/Interfaces/QueueInterface.php b/src/Interfaces/QueueInterface.php
index 2f6be86..30ab6c9 100644
--- a/src/Interfaces/QueueInterface.php
+++ b/src/Interfaces/QueueInterface.php
@@ -1,8 +1,19 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Interfaces;
+
+use CodeIgniter\Queue\Entities\QueueJob;
use Throwable;
interface QueueInterface
diff --git a/src/Language/en/Queue.php b/src/Language/en/Queue.php
index d7c1a09..2336197 100644
--- a/src/Language/en/Queue.php
+++ b/src/Language/en/Queue.php
@@ -1,5 +1,16 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
return [
'generator' => [
'className' => [
@@ -7,8 +18,11 @@
],
],
'incorrectHandler' => 'This queue handler is incorrect.',
+ 'incorrectQueueFormat' => 'The queue name should consists only lowercase letters or numbers.',
+ 'tooLongQueueName' => 'The queue name is too long. It should be no longer than 64 letters.',
'incorrectJobHandler' => 'This job name is not defined in the $jobHandlers array.',
'incorrectPriorityFormat' => 'The priority name should consists only lowercase letters.',
'tooLongPriorityName' => 'The priority name is too long. It should be no longer than 64 letters.',
'incorrectQueuePriority' => 'This queue has incorrectly defined priority: "{0}" for the queue: "{1}".',
+ 'incorrectDelayValue' => 'The number of seconds of delay must be a positive integer.',
];
diff --git a/src/Lua/pop_task.lua b/src/Lua/pop_task.lua
new file mode 100644
index 0000000..8ddeb79
--- /dev/null
+++ b/src/Lua/pop_task.lua
@@ -0,0 +1,17 @@
+local queue = KEYS[1]
+local now = tonumber(ARGV[1])
+local priorities = cjson.decode(ARGV[2])
+local task = nil
+
+for _, priority in ipairs(priorities) do
+ local key = queue .. ':' .. priority
+ local tasks = redis.call('ZRANGEBYSCORE', key, '-inf', tostring(now), 'LIMIT', 0, 1)
+
+ if #tasks > 0 then
+ redis.call('ZREM', key, tasks[1])
+ task = tasks[1]
+ break
+ end
+end
+
+return task
diff --git a/src/Models/QueueJobFailedModel.php b/src/Models/QueueJobFailedModel.php
index 25932d2..e98b5f3 100644
--- a/src/Models/QueueJobFailedModel.php
+++ b/src/Models/QueueJobFailedModel.php
@@ -1,9 +1,24 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Models;
+
+use CodeIgniter\Database\BaseConnection;
+use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Model;
-use Michalsn\CodeIgniterQueue\Entities\QueueJobFailed;
+use CodeIgniter\Queue\Entities\QueueJobFailed;
+use CodeIgniter\Validation\ValidationInterface;
+use Config\Database;
class QueueJobFailedModel extends Model
{
@@ -26,4 +41,19 @@ class QueueJobFailedModel extends Model
// Callbacks
protected $allowCallbacks = false;
+
+ public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
+ {
+ $this->DBGroup = config('Queue')->database['dbGroup'];
+
+ /**
+ * @var BaseConnection|null $db
+ */
+ $db ??= Database::connect($this->DBGroup);
+
+ // Turn off the Strict Mode
+ $db->transStrict(false);
+
+ parent::__construct($db, $validation);
+ }
}
diff --git a/src/Models/QueueJobModel.php b/src/Models/QueueJobModel.php
index 58d2fb0..4df5bd1 100644
--- a/src/Models/QueueJobModel.php
+++ b/src/Models/QueueJobModel.php
@@ -1,13 +1,27 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Models;
use CodeIgniter\Database\BaseBuilder;
-use CodeIgniter\Database\RawSql;
+use CodeIgniter\Database\BaseConnection;
+use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\I18n\Time;
use CodeIgniter\Model;
-use Michalsn\CodeIgniterQueue\Entities\QueueJob;
-use Michalsn\CodeIgniterQueue\Enums\Status;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Validation\ValidationInterface;
+use Config\Database;
use ReflectionException;
class QueueJobModel extends Model
@@ -32,6 +46,21 @@ class QueueJobModel extends Model
// Callbacks
protected $allowCallbacks = false;
+ public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
+ {
+ $this->DBGroup = config('Queue')->database['dbGroup'];
+
+ /**
+ * @var BaseConnection|null $db
+ */
+ $db ??= Database::connect($this->DBGroup);
+
+ // Turn off the Strict Mode
+ $db->transStrict(false);
+
+ parent::__construct($db, $validation);
+ }
+
/**
* Get the oldest item from the queue.
*
@@ -41,7 +70,7 @@ public function getFromQueue(string $name, array $priority): ?QueueJob
{
// For SQLite3 memory database this will cause problems
// so check if we're not in the testing environment first.
- if ($this->db->database !== ':memory:') {
+ if ($this->db->database !== ':memory:' && $this->db->connID !== false) {
// Make sure we still have the connection
$this->db->reconnect();
}
@@ -80,7 +109,7 @@ public function getFromQueue(string $name, array $priority): ?QueueJob
*/
private function skipLocked(string $sql): string
{
- if ($this->db->DBDriver === 'SQLite3') {
+ if ($this->db->DBDriver === 'SQLite3' || config('Queue')->database['skipLocked'] === false) {
return $sql;
}
@@ -90,7 +119,13 @@ private function skipLocked(string $sql): string
return str_replace('WHERE', $replace, $sql);
}
- return $sql .= ' FOR UPDATE SKIP LOCKED';
+ if ($this->db->DBDriver === 'OCI8') {
+ $sql = str_replace('SELECT *', 'SELECT "id"', $sql);
+ // prepare final query
+ $sql = sprintf('SELECT * FROM "%s" WHERE "id" = (%s)', $this->db->prefixTable($this->table), $sql);
+ }
+
+ return $sql . ' FOR UPDATE SKIP LOCKED';
}
/**
@@ -101,10 +136,28 @@ private function setPriority(BaseBuilder $builder, array $priority): BaseBuilder
$builder->whereIn('priority', $priority);
if ($priority !== ['default']) {
- if ($this->db->DBDriver === 'SQLite3') {
- $builder->orderBy(new RawSql('CASE priority ' . implode(' ', array_map(static fn ($value, $key) => "WHEN '{$value}' THEN {$key}", $priority, array_keys($priority))) . ' END'));
+ if ($this->db->DBDriver !== 'MySQLi') {
+ $builder->orderBy(
+ sprintf('CASE %s ', $this->db->protectIdentifiers('priority'))
+ . implode(
+ ' ',
+ array_map(static fn ($value, $key) => "WHEN '{$value}' THEN {$key}", $priority, array_keys($priority)),
+ )
+ . ' END',
+ '',
+ false,
+ );
} else {
- $builder->orderBy(new RawSql('FIELD(priority, ' . implode(',', array_map(static fn ($value) => "'{$value}'", $priority)) . ')'));
+ $builder->orderBy(
+ 'FIELD(priority, '
+ . implode(
+ ',',
+ array_map(static fn ($value) => "'{$value}'", $priority),
+ )
+ . ')',
+ '',
+ false,
+ );
}
}
diff --git a/src/Payload.php b/src/Payload.php
deleted file mode 100644
index bdc4b8f..0000000
--- a/src/Payload.php
+++ /dev/null
@@ -1,20 +0,0 @@
- $this->job,
- 'data' => $this->data,
- ];
- }
-}
diff --git a/src/Payloads/ChainBuilder.php b/src/Payloads/ChainBuilder.php
new file mode 100644
index 0000000..772e376
--- /dev/null
+++ b/src/Payloads/ChainBuilder.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Payloads;
+
+use CodeIgniter\Queue\Handlers\BaseHandler;
+use CodeIgniter\Queue\QueuePushResult;
+
+class ChainBuilder
+{
+ /**
+ * Collection of jobs in the chain
+ */
+ protected PayloadCollection $payloads;
+
+ public function __construct(protected BaseHandler $handler)
+ {
+ $this->payloads = new PayloadCollection();
+ }
+
+ /**
+ * Add a job to the chain
+ */
+ public function push(string $queue, string $jobName, array $data = []): ChainElement
+ {
+ $payload = new Payload($jobName, $data);
+
+ $payload->setQueue($queue);
+
+ $this->payloads->add($payload);
+
+ return new ChainElement($payload, $this);
+ }
+
+ /**
+ * Dispatch the chain of jobs
+ */
+ public function dispatch(): QueuePushResult
+ {
+ if ($this->payloads->count() === 0) {
+ return QueuePushResult::failure('No jobs to dispatch.');
+ }
+
+ $current = $this->payloads->shift();
+ $priority = $current->getPriority();
+ $delay = $current->getDelay();
+
+ if ($priority !== null) {
+ $this->handler->setPriority($priority);
+ }
+
+ if ($delay !== null) {
+ $this->handler->setDelay($delay);
+ }
+
+ // Set chained jobs for the next job
+ if ($this->payloads->count() > 0) {
+ $current->setChainedJobs($this->payloads);
+ }
+
+ // Push to the queue with the specified queue name
+ return $this->handler->push(
+ $current->getQueue(),
+ $current->getJob(),
+ $current->getData(),
+ $current->getMetadata(),
+ );
+ }
+}
diff --git a/src/Payloads/ChainElement.php b/src/Payloads/ChainElement.php
new file mode 100644
index 0000000..d8d7bc3
--- /dev/null
+++ b/src/Payloads/ChainElement.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Payloads;
+
+class ChainElement
+{
+ public function __construct(protected Payload $payload, protected ChainBuilder $chainBuilder)
+ {
+ }
+
+ /**
+ * Set priority for this specific job
+ */
+ public function setPriority(string $priority): self
+ {
+ $this->payload->setPriority($priority);
+
+ return $this;
+ }
+
+ /**
+ * Set delay for this specific job
+ */
+ public function setDelay(int $delay): self
+ {
+ $this->payload->setDelay($delay);
+
+ return $this;
+ }
+
+ /**
+ * Push the next job in the chain (method chaining)
+ */
+ public function push(string $queue, string $jobName, array $data = []): ChainElement
+ {
+ return $this->chainBuilder->push($queue, $jobName, $data);
+ }
+}
diff --git a/src/Payloads/Payload.php b/src/Payloads/Payload.php
new file mode 100644
index 0000000..8539a87
--- /dev/null
+++ b/src/Payloads/Payload.php
@@ -0,0 +1,150 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Payloads;
+
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Traits\HasQueueValidation;
+use JsonSerializable;
+
+class Payload implements JsonSerializable
+{
+ use HasQueueValidation;
+
+ /**
+ * Job metadata
+ */
+ protected PayloadMetadata $metadata;
+
+ public function __construct(protected string $job, protected array $data, ?PayloadMetadata $metadata = null)
+ {
+ $this->metadata = $metadata ?? new PayloadMetadata();
+ }
+
+ public function getJob(): string
+ {
+ return $this->job;
+ }
+
+ public function getData(): array
+ {
+ return $this->data;
+ }
+
+ public function getMetadata(): PayloadMetadata
+ {
+ return $this->metadata;
+ }
+
+ public function setMetadata(PayloadMetadata $metadata): self
+ {
+ $this->metadata = $metadata;
+
+ return $this;
+ }
+
+ /**
+ * Set the queue name
+ *
+ * @throws QueueException
+ */
+ public function setQueue(string $queue): self
+ {
+ $this->validateQueue($queue);
+
+ $this->metadata->set('queue', $queue);
+
+ return $this;
+ }
+
+ public function getQueue(): ?string
+ {
+ return $this->metadata->get('queue');
+ }
+
+ /**
+ * Set the priority
+ *
+ * @throws QueueException
+ */
+ public function setPriority(string $priority): self
+ {
+ $this->validatePriority($priority);
+
+ $this->metadata->set('priority', $priority);
+
+ return $this;
+ }
+
+ public function getPriority(): ?string
+ {
+ return $this->metadata->get('priority');
+ }
+
+ /**
+ * Set the delay
+ *
+ * @throws QueueException
+ */
+ public function setDelay(int $delay): self
+ {
+ $this->validateDelay($delay);
+
+ $this->metadata->set('delay', $delay);
+
+ return $this;
+ }
+
+ public function getDelay(): ?int
+ {
+ return $this->metadata->get('delay');
+ }
+
+ public function setChainedJobs(PayloadCollection $payloads): self
+ {
+ $this->metadata->setChainedJobs($payloads);
+
+ return $this;
+ }
+
+ public function getChainedJobs(): ?PayloadCollection
+ {
+ return $this->metadata->getChainedJobs();
+ }
+
+ public function hasChainedJobs(): bool
+ {
+ return $this->metadata->hasChainedJobs();
+ }
+
+ public function jsonSerialize(): array
+ {
+ return [
+ 'job' => $this->job,
+ 'data' => $this->data,
+ 'metadata' => $this->metadata,
+ ];
+ }
+
+ /**
+ * Create a Payload from an array
+ */
+ public static function fromArray(array $data): self
+ {
+ $job = $data['job'] ?? '';
+ $jobData = $data['data'] ?? [];
+ $metadata = isset($data['metadata']) ? PayloadMetadata::fromArray($data['metadata']) : null;
+
+ return new self($job, $jobData, $metadata);
+ }
+}
diff --git a/src/Payloads/PayloadCollection.php b/src/Payloads/PayloadCollection.php
new file mode 100644
index 0000000..871c96a
--- /dev/null
+++ b/src/Payloads/PayloadCollection.php
@@ -0,0 +1,106 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Payloads;
+
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use JsonSerializable;
+
+/**
+ * @template T
+ *
+ * @implements IteratorAggregate
+ */
+class PayloadCollection implements IteratorAggregate, Countable, JsonSerializable
+{
+ /**
+ * Create a new payload collection
+ *
+ * @param list $items
+ */
+ public function __construct(protected array $items = [])
+ {
+ }
+
+ /**
+ * Add a payload to the collection
+ */
+ public function add(Payload $payload): self
+ {
+ $this->items[] = $payload;
+
+ return $this;
+ }
+
+ /**
+ * Get the first payload and remove it.
+ */
+ public function shift(): ?Payload
+ {
+ if ($this->count() === 0) {
+ return null;
+ }
+
+ return array_shift($this->items);
+ }
+
+ /**
+ * Convert the collection to an array
+ */
+ public function toArray(): array
+ {
+ $result = [];
+
+ foreach ($this->items as $payload) {
+ $result[] = $payload->jsonSerialize();
+ }
+
+ return $result;
+ }
+
+ public function jsonSerialize(): array
+ {
+ return $this->toArray();
+ }
+
+ public function count(): int
+ {
+ return count($this->items);
+ }
+
+ /**
+ * @return ArrayIterator
+ */
+ public function getIterator(): ArrayIterator
+ {
+ return new ArrayIterator($this->items);
+ }
+
+ /**
+ * Create a new PayloadCollection from an array
+ */
+ public static function fromArray(array $payloads): self
+ {
+ $collection = new self();
+
+ foreach ($payloads as $payload) {
+ if (isset($payload['job'], $payload['data'])) {
+ $collection->add(Payload::fromArray($payload));
+ }
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/Payloads/PayloadMetadata.php b/src/Payloads/PayloadMetadata.php
new file mode 100644
index 0000000..17c8833
--- /dev/null
+++ b/src/Payloads/PayloadMetadata.php
@@ -0,0 +1,133 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Payloads;
+
+use JsonSerializable;
+
+class PayloadMetadata implements JsonSerializable
+{
+ public function __construct(protected array $data = [])
+ {
+ }
+
+ /**
+ * Set chained jobs
+ */
+ public function setChainedJobs(?PayloadCollection $payloads): self
+ {
+ if ($payloads !== null) {
+ $this->data['chainedJobs'] = $payloads;
+ } else {
+ unset($this->data['chainedJobs']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get chained jobs
+ */
+ public function getChainedJobs(): ?PayloadCollection
+ {
+ return $this->data['chainedJobs'] ?? null;
+ }
+
+ /**
+ * Check if has chained jobs
+ */
+ public function hasChainedJobs(): bool
+ {
+ return isset($this->data['chainedJobs']) && $this->data['chainedJobs']->count() > 0;
+ }
+
+ /**
+ * Set a generic metadata value
+ */
+ public function set(string $key, mixed $value): self
+ {
+ $this->data[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get a generic metadata value
+ *
+ * @param mixed|null $default
+ */
+ public function get(string $key, $default = null)
+ {
+ return $this->data[$key] ?? $default;
+ }
+
+ /**
+ * Check if a metadata key exists
+ */
+ public function has(string $key): bool
+ {
+ return isset($this->data[$key]);
+ }
+
+ /**
+ * Remove a metadata key
+ */
+ public function remove(string $key): self
+ {
+ unset($this->data[$key]);
+
+ return $this;
+ }
+
+ /**
+ * Get all metadata as an array
+ */
+ public function toArray(): array
+ {
+ return $this->data;
+ }
+
+ /**
+ * JSON serialize implementation
+ */
+ public function jsonSerialize(): array
+ {
+ return $this->data;
+ }
+
+ public static function fromArray(array $data): PayloadMetadata
+ {
+ $metadata = new self();
+
+ foreach ($data as $key => $value) {
+ // Handle chainedJobs specially
+ if ($key === 'chainedJobs' && is_array($value)) {
+ $payloadCollection = new PayloadCollection();
+
+ foreach ($value as $jobData) {
+ if (isset($jobData['job'], $jobData['data'])) {
+ $payload = Payload::fromArray($jobData);
+ $payloadCollection->add($payload);
+ }
+ }
+
+ $metadata->setChainedJobs($payloadCollection);
+ } else {
+ // Regular metadata
+ $metadata->set($key, $value);
+ }
+ }
+
+ return $metadata;
+ }
+}
diff --git a/src/Queue.php b/src/Queue.php
index 5f2b9ad..6819586 100644
--- a/src/Queue.php
+++ b/src/Queue.php
@@ -1,10 +1,21 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue;
+
+use CodeIgniter\Queue\Config\Queue as QueueConfig;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Interfaces\QueueInterface;
class Queue
{
diff --git a/src/QueuePushResult.php b/src/QueuePushResult.php
new file mode 100644
index 0000000..efe7456
--- /dev/null
+++ b/src/QueuePushResult.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue;
+
+/**
+ * Represents the result of a queue push operation.
+ */
+class QueuePushResult
+{
+ public function __construct(
+ protected readonly bool $success,
+ protected readonly ?int $jobId = null,
+ protected readonly ?string $error = null,
+ ) {
+ }
+
+ /**
+ * Creates a successful push result.
+ */
+ public static function success(int $jobId): self
+ {
+ return new self(true, $jobId);
+ }
+
+ /**
+ * Creates a failed push result.
+ */
+ public static function failure(?string $error = null): self
+ {
+ return new self(false, null, $error);
+ }
+
+ /**
+ * Returns whether the push operation was successful.
+ */
+ public function getStatus(): bool
+ {
+ return $this->success;
+ }
+
+ /**
+ * Returns the job ID if the push was successful, null otherwise.
+ */
+ public function getJobId(): ?int
+ {
+ return $this->jobId;
+ }
+
+ /**
+ * Returns the error message if the push failed, null otherwise.
+ */
+ public function getError(): ?string
+ {
+ return $this->error;
+ }
+}
diff --git a/src/Traits/HasQueueValidation.php b/src/Traits/HasQueueValidation.php
new file mode 100644
index 0000000..aa66593
--- /dev/null
+++ b/src/Traits/HasQueueValidation.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Queue\Traits;
+
+use CodeIgniter\Queue\Exceptions\QueueException;
+
+trait HasQueueValidation
+{
+ /**
+ * Validate priority value.
+ *
+ * @throws QueueException
+ */
+ protected function validatePriority(string $priority): void
+ {
+ if (! preg_match('/^[a-z_-]+$/', $priority)) {
+ throw QueueException::forIncorrectPriorityFormat();
+ }
+
+ if (strlen($priority) > 64) {
+ throw QueueException::forTooLongPriorityName();
+ }
+ }
+
+ /**
+ * Validate delay value.
+ *
+ * @throws QueueException
+ */
+ protected function validateDelay(int $delay): void
+ {
+ if ($delay < 0) {
+ throw QueueException::forIncorrectDelayValue();
+ }
+ }
+
+ /**
+ * Validate queue name.
+ *
+ * @throws QueueException
+ */
+ protected function validateQueue(string $queue): void
+ {
+ if (! preg_match('/^[a-z0-9_-]+$/', $queue)) {
+ throw QueueException::forIncorrectQueueFormat();
+ }
+
+ if (strlen($queue) > 64) {
+ throw QueueException::forTooLongQueueName();
+ }
+ }
+}
diff --git a/tests/Commands/QueueClearTest.php b/tests/Commands/QueueClearTest.php
new file mode 100644
index 0000000..6024837
--- /dev/null
+++ b/tests/Commands/QueueClearTest.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueClearTest extends CLITestCase
+{
+ public function testRunWithNoQueueName(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addErrorFilter();
+
+ $this->assertNotFalse(command('queue:clear'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeErrorFilter();
+
+ $this->assertSame('The queueName is not specified.', $output);
+ }
+
+ public function testRun(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:clear test'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Queue test has been cleared.', $output);
+ }
+}
diff --git a/tests/Commands/QueueFailedTest.php b/tests/Commands/QueueFailedTest.php
new file mode 100644
index 0000000..fed02df
--- /dev/null
+++ b/tests/Commands/QueueFailedTest.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Exception;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueFailedTest extends CLITestCase
+{
+ /**
+ * @throws Exception
+ */
+ public function testRun(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobFailedModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'exception' => 'Exception: Test error',
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:failed'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $expect = <<<'EOT'
+ +----+------------+-------+----------------------------+---------------------+
+ | ID | Connection | Queue | Class | Failed At |
+ +----+------------+-------+----------------------------+---------------------+
+ | 1 | database | test | Tests\Support\Jobs\Failure | 2023-12-19 14:15:16 |
+ +----+------------+-------+----------------------------+---------------------+
+ EOT;
+
+ $this->assertSame($expect, $output);
+ }
+}
diff --git a/tests/Commands/QueueFlushTest.php b/tests/Commands/QueueFlushTest.php
new file mode 100644
index 0000000..a6395f2
--- /dev/null
+++ b/tests/Commands/QueueFlushTest.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Exception;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueFlushTest extends CLITestCase
+{
+ /**
+ * @throws Exception
+ */
+ public function testRun(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobFailedModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'exception' => 'Exception: Test error',
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:flush'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('All failed jobs has been removed from the queue ', $output);
+ }
+
+ public function testRunWithQueue(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobFailedModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'exception' => 'Exception: Test error',
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:flush -queue default'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('All failed jobs has been removed from the queue default', $output);
+ }
+
+ public function testRunWithQueueAndHour(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobFailedModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'exception' => 'Exception: Test error',
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:flush -queue default -hours 2'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('All failed jobs older than 2 hours has been removed from the queue default', $output);
+ }
+}
diff --git a/tests/Commands/QueueForgetTest.php b/tests/Commands/QueueForgetTest.php
new file mode 100644
index 0000000..388cadc
--- /dev/null
+++ b/tests/Commands/QueueForgetTest.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueForgetTest extends CLITestCase
+{
+ public function testRunWithNoQueueName(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addErrorFilter();
+
+ $this->assertNotFalse(command('queue:forget'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeErrorFilter();
+
+ $this->assertSame('The ID of the failed job is not specified.', $output);
+ }
+
+ public function testRunFailed(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:forget 123'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Could not find the failed job with ID 123', $output);
+ }
+
+ public function testRun(): void
+ {
+ fake(QueueJobFailedModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'exception' => 'Exception: Test error',
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:forget 1'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Failed job with ID 1 has been removed.', $output);
+ }
+}
diff --git a/tests/Commands/QueuePublishTest.php b/tests/Commands/QueuePublishTest.php
new file mode 100644
index 0000000..dd456ea
--- /dev/null
+++ b/tests/Commands/QueuePublishTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueuePublishTest extends CLITestCase
+{
+ public function testRun(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:publish'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame(' Published! You can customize the configuration by editing the "app/Config/Queue.php" file.', $output);
+ }
+}
diff --git a/tests/Commands/QueueRetryTest.php b/tests/Commands/QueueRetryTest.php
new file mode 100644
index 0000000..fe0da19
--- /dev/null
+++ b/tests/Commands/QueueRetryTest.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueRetryTest extends CLITestCase
+{
+ public function testRunWithNoQueueName(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addErrorFilter();
+
+ $this->assertNotFalse(command('queue:retry'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeErrorFilter();
+
+ $this->assertSame('The ID of the failed job is not specified.', $output);
+ }
+
+ public function testRunFailed(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:retry all -queue test'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('No failed jobs has been restored to the queue test', $output);
+ }
+
+ public function testRun(): void
+ {
+ fake(QueueJobFailedModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'exception' => 'Exception: Test error',
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:retry 1'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('1 failed job(s) has been restored to the queue ', $output);
+ }
+}
diff --git a/tests/Commands/QueueStopTest.php b/tests/Commands/QueueStopTest.php
new file mode 100644
index 0000000..94e2631
--- /dev/null
+++ b/tests/Commands/QueueStopTest.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueStopTest extends CLITestCase
+{
+ public function testRunWithNoQueueName(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addErrorFilter();
+
+ $this->assertNotFalse(command('queue:stop'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeErrorFilter();
+
+ $this->assertSame('The queueName is not specified.', $output);
+ }
+
+ public function testRun(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:stop test'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Queue will be stopped after the current job finish', $output);
+ }
+}
diff --git a/tests/Commands/QueueWorkTest.php b/tests/Commands/QueueWorkTest.php
new file mode 100644
index 0000000..af39092
--- /dev/null
+++ b/tests/Commands/QueueWorkTest.php
@@ -0,0 +1,337 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Commands;
+
+use CodeIgniter\Cache\CacheInterface;
+use CodeIgniter\Config\Services;
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Models\QueueJobModel;
+use CodeIgniter\Test\Filters\CITestStreamFilter;
+use Tests\Support\CLITestCase;
+
+/**
+ * @internal
+ */
+final class QueueWorkTest extends CLITestCase
+{
+ public function testRunWithNoQueueName(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addErrorFilter();
+
+ $this->assertNotFalse(command('queue:work'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeErrorFilter();
+
+ $this->assertSame('The queueName is not specified.', $output);
+ }
+
+ public function testRunWithEmptyQueue(): void
+ {
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test --stop-when-empty'));
+ $output = $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $expect = <<<'EOT'
+ Listening for the jobs with the queue: test
+
+
+ No job available. Stopping.
+ EOT;
+
+ $this->assertSame($expect, $output);
+ }
+
+ public function testRunWithQueueFailed(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'failure', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'status' => 0,
+ 'attempts' => 0,
+ 'available_at' => 1_702_977_074,
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test sleep 1 --stop-when-empty'));
+ $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Listening for the jobs with the queue: test', $this->getLine(0));
+ $this->assertSame('Starting a new job: failure, with ID: 1', $this->getLine(3));
+ $this->assertSame('The processing of this job failed', $this->getLine(4));
+ $this->assertSame('No job available. Stopping.', $this->getLine(7));
+ }
+
+ public function testRunWithQueueSucceed(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => ['job' => 'success', 'data' => ['key' => 'value']],
+ 'priority' => 'default',
+ 'status' => 0,
+ 'attempts' => 0,
+ 'available_at' => 1_702_977_074,
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test sleep 1 --stop-when-empty'));
+ $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Listening for the jobs with the queue: test', $this->getLine(0));
+ $this->assertSame('Starting a new job: success, with ID: 1', $this->getLine(3));
+ $this->assertSame('The processing of this job was successful', $this->getLine(4));
+ $this->assertSame('No job available. Stopping.', $this->getLine(7));
+ }
+
+ public function testRunWithChainedQueueSucceed(): void
+ {
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ fake(QueueJobModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => [
+ 'job' => 'success',
+ 'data' => ['key' => 'value'],
+ 'metadata' => [
+ 'queue' => 'test',
+ 'chainedJobs' => [
+ [
+ 'job' => 'success',
+ 'data' => [
+ 'key3' => 'value3',
+ ],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'priority' => 'high',
+ 'delay' => 30,
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'priority' => 'default',
+ 'status' => 0,
+ 'attempts' => 0,
+ 'available_at' => 1_702_977_074,
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test sleep 1 --stop-when-empty'));
+ $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Listening for the jobs with the queue: test', $this->getLine(0));
+ $this->assertSame('Starting a new job: success, with ID: 1', $this->getLine(3));
+ $this->assertSame('The processing of this job was successful', $this->getLine(4));
+ $this->assertSame('Chained job: success has been placed in the queue: queue', $this->getLine(5));
+ $this->assertSame('No job available. Stopping.', $this->getLine(8));
+
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode([
+ 'job' => 'success',
+ 'data' => ['key3' => 'value3'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'priority' => 'high',
+ 'delay' => 30,
+ ],
+ ]),
+ ]);
+ }
+
+ public function testRunWithTaskLock(): void
+ {
+ $lockKey = 'test_lock_key';
+ $lockTTL = 300; // 5 minutes
+
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ $cache = $this->createMock(CacheInterface::class);
+
+ // Set up expectations
+ $cache->expects($this->once())
+ ->method('save')
+ ->with($lockKey, $this->anything(), $lockTTL)
+ ->willReturn(true);
+
+ $cache->expects($this->once())
+ ->method('delete')
+ ->with($lockKey)
+ ->willReturn(true);
+
+ // Replace the cache service
+ Services::injectMock('cache', $cache);
+
+ fake(QueueJobModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => [
+ 'job' => 'success',
+ 'data' => ['key' => 'value'],
+ 'metadata' => [
+ 'taskLockKey' => $lockKey,
+ 'taskLockTTL' => $lockTTL,
+ 'queue' => 'test',
+ ],
+ ],
+ 'priority' => 'default',
+ 'status' => 0,
+ 'attempts' => 0,
+ 'available_at' => 1_702_977_074,
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test sleep 1 --stop-when-empty'));
+ $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Listening for the jobs with the queue: test', $this->getLine(0));
+ $this->assertSame('Starting a new job: success, with ID: 1', $this->getLine(3));
+ $this->assertSame('The processing of this job was successful', $this->getLine(4));
+ }
+
+ public function testRunWithPermanentTaskLock(): void
+ {
+ $lockKey = 'permanent_lock_key';
+ $lockTTL = 0; // Permanent lock
+
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ $cache = $this->createMock(CacheInterface::class);
+
+ // For permanent lock (TTL=0), save should NOT be called
+ $cache->expects($this->never())
+ ->method('save');
+
+ $cache->expects($this->once())
+ ->method('delete')
+ ->with($lockKey)
+ ->willReturn(true);
+
+ // Replace the cache service
+ Services::injectMock('cache', $cache);
+
+ fake(QueueJobModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => [
+ 'job' => 'success',
+ 'data' => ['key4' => 'value4'],
+ 'metadata' => [
+ 'taskLockKey' => $lockKey,
+ 'taskLockTTL' => $lockTTL,
+ 'queue' => 'test',
+ ],
+ ],
+ 'priority' => 'default',
+ 'status' => 0,
+ 'attempts' => 0,
+ 'available_at' => 1_702_977_074,
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test sleep 1 --stop-when-empty'));
+ $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Listening for the jobs with the queue: test', $this->getLine(0));
+ $this->assertSame('Starting a new job: success, with ID: 1', $this->getLine(3));
+ $this->assertSame('The processing of this job was successful', $this->getLine(4));
+ }
+
+ public function testLockClearedOnFailure(): void
+ {
+ $lockKey = 'failure_lock_key';
+ $lockTTL = 300;
+
+ Time::setTestNow('2023-12-19 14:15:16');
+
+ $cache = $this->createMock(CacheInterface::class);
+
+ // Set up expectations
+ $cache->expects($this->once())
+ ->method('save')
+ ->with($lockKey, $this->anything(), $lockTTL)
+ ->willReturn(true);
+
+ $cache->expects($this->once())
+ ->method('delete')
+ ->with($lockKey)
+ ->willReturn(true);
+
+ // Replace the cache service
+ Services::injectMock('cache', $cache);
+
+ fake(QueueJobModel::class, [
+ 'connection' => 'database',
+ 'queue' => 'test',
+ 'payload' => [
+ 'job' => 'failure',
+ 'data' => ['key' => 'value'],
+ 'metadata' => [
+ 'taskLockKey' => $lockKey,
+ 'taskLockTTL' => $lockTTL,
+ 'queue' => 'test',
+ ],
+ ],
+ 'priority' => 'default',
+ 'status' => 0,
+ 'attempts' => 0,
+ 'available_at' => 1_702_977_074,
+ ]);
+
+ CITestStreamFilter::registration();
+ CITestStreamFilter::addOutputFilter();
+
+ $this->assertNotFalse(command('queue:work test sleep 1 --stop-when-empty'));
+ $this->parseOutput(CITestStreamFilter::$buffer);
+
+ CITestStreamFilter::removeOutputFilter();
+
+ $this->assertSame('Listening for the jobs with the queue: test', $this->getLine(0));
+ $this->assertSame('Starting a new job: failure, with ID: 1', $this->getLine(3));
+ $this->assertSame('The processing of this job failed', $this->getLine(4));
+ }
+}
diff --git a/tests/DatabaseHandlerTest.php b/tests/DatabaseHandlerTest.php
index 26eb20a..f189f78 100644
--- a/tests/DatabaseHandlerTest.php
+++ b/tests/DatabaseHandlerTest.php
@@ -1,17 +1,29 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests;
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
use CodeIgniter\Test\ReflectionHelper;
use Exception;
-use Michalsn\CodeIgniterQueue\Entities\QueueJob;
-use Michalsn\CodeIgniterQueue\Enums\Status;
-use Michalsn\CodeIgniterQueue\Exceptions\QueueException;
-use Michalsn\CodeIgniterQueue\Handlers\DatabaseHandler;
-use Michalsn\CodeIgniterQueue\Models\QueueJobFailedModel;
use ReflectionException;
use Tests\Support\Config\Queue as QueueConfig;
-use Tests\Support\Database\Seeds\TestQueueSeeder;
+use Tests\Support\Database\Seeds\TestDatabaseQueueSeeder;
use Tests\Support\TestCase;
/**
@@ -21,7 +33,7 @@ final class DatabaseHandlerTest extends TestCase
{
use ReflectionHelper;
- protected $seed = TestQueueSeeder::class;
+ protected $seed = TestDatabaseQueueSeeder::class;
private QueueConfig $config;
protected function setUp(): void
@@ -31,13 +43,13 @@ protected function setUp(): void
$this->config = config(QueueConfig::class);
}
- public function testDatabaseHandler()
+ public function testDatabaseHandler(): void
{
$handler = new DatabaseHandler($this->config);
$this->assertInstanceOf(DatabaseHandler::class, $handler);
}
- public function testPriority()
+ public function testPriority(): void
{
$handler = new DatabaseHandler($this->config);
$handler->setPriority('high');
@@ -45,7 +57,7 @@ public function testPriority()
$this->assertSame('high', self::getPrivateProperty($handler, 'priority'));
}
- public function testPriorityNameException()
+ public function testPriorityNameException(): void
{
$this->expectException(QueueException::class);
$this->expectExceptionMessage('The priority name should consists only lowercase letters.');
@@ -54,7 +66,7 @@ public function testPriorityNameException()
$handler->setPriority('high_:');
}
- public function testPriorityNameLengthException()
+ public function testPriorityNameLengthException(): void
{
$this->expectException(QueueException::class);
$this->expectExceptionMessage('The priority name is too long. It should be no longer than 64 letters.');
@@ -66,38 +78,189 @@ public function testPriorityNameLengthException()
/**
* @throws ReflectionException
*/
- public function testPush()
+ public function testPush(): void
{
+ Time::setTestNow('2023-12-29 14:15:16');
+
$handler = new DatabaseHandler($this->config);
$result = $handler->push('queue', 'success', ['key' => 'value']);
- $this->assertTrue($result);
+ $this->assertTrue($result->getStatus());
$this->seeInDatabase('queue_jobs', [
- 'queue' => 'queue',
- 'payload' => json_encode(['job' => 'success', 'data' => ['key' => 'value']]),
+ 'queue' => 'queue',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key' => 'value'], 'metadata' => []]),
+ 'available_at' => 1703859316,
]);
}
/**
* @throws ReflectionException
*/
- public function testPushWithPriority()
+ public function testPushWithPriority(): void
{
+ Time::setTestNow('2023-12-29 14:15:16');
+
$handler = new DatabaseHandler($this->config);
$result = $handler->setPriority('high')->push('queue', 'success', ['key' => 'value']);
- $this->assertTrue($result);
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key' => 'value'], 'metadata' => []]),
+ 'priority' => 'high',
+ 'available_at' => 1703859316,
+ ]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPushAndPopWithPriority(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->push('queue', 'success', ['key1' => 'value1']);
+
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key1' => 'value1'], 'metadata' => []]),
+ 'priority' => 'low',
+ 'available_at' => 1703859316,
+ ]);
+
+ $result = $handler->setPriority('high')->push('queue', 'success', ['key2' => 'value2']);
+
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key2' => 'value2'], 'metadata' => []]),
+ 'priority' => 'high',
+ 'available_at' => 1703859316,
+ ]);
+
+ $result = $handler->pop('queue', ['high', 'low']);
+ $this->assertInstanceOf(QueueJob::class, $result);
+ $payload = ['job' => 'success', 'data' => ['key2' => 'value2'], 'metadata' => []];
+ $this->assertSame($payload, $result->payload);
+
+ $result = $handler->pop('queue', ['high', 'low']);
+ $this->assertInstanceOf(QueueJob::class, $result);
+ $payload = ['job' => 'success', 'data' => ['key1' => 'value1'], 'metadata' => []];
+ $this->assertSame($payload, $result->payload);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testPushWithDelay(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->setDelay(MINUTE)->push('queue-delay', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $availableAt = 1703859376;
+
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue-delay',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key' => 'value'], 'metadata' => []]),
+ 'available_at' => $availableAt,
+ ]);
+
+ $this->assertEqualsWithDelta(MINUTE, $availableAt - Time::now()->getTimestamp(), 1);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testChain(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->push('queue', 'success', ['key2' => 'value2']);
+ });
+
+ $this->assertTrue($result->getStatus());
$this->seeInDatabase('queue_jobs', [
- 'queue' => 'queue',
- 'payload' => json_encode(['job' => 'success', 'data' => ['key' => 'value']]),
- 'priority' => 'high',
+ 'queue' => 'queue',
+ 'payload' => json_encode([
+ 'job' => 'success',
+ 'data' => ['key1' => 'value1'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'chainedJobs' => [
+ [
+ 'job' => 'success', 'data' => ['key2' => 'value2'], 'metadata' => ['queue' => 'queue']],
+ ],
+ ],
+ ]),
+ 'available_at' => 1703859316,
]);
}
+ public function testChainWithPriorityAndDelay(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->setPriority('high')
+ ->setDelay(60)
+ ->push('queue', 'success', ['key2' => 'value2'])
+ ->setPriority('low')
+ ->setDelay(120);
+ });
+
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode([
+ 'job' => 'success',
+ 'data' => ['key1' => 'value1'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'priority' => 'high',
+ 'delay' => 60,
+ 'chainedJobs' => [
+ [
+ 'job' => 'success',
+ 'data' => ['key2' => 'value2'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'priority' => 'low',
+ 'delay' => 120,
+ ],
+ ],
+ ],
+ ],
+ ]),
+ 'available_at' => 1703859316 + 60, // Adding delay to available_at
+ ]);
+ }
+
+ public function testPushWithDelayException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('The number of seconds of delay must be a positive integer.');
+
+ $handler = new DatabaseHandler($this->config);
+ $handler->setDelay(-60);
+ }
+
/**
* @throws ReflectionException
*/
- public function testPushException()
+ public function testPushException(): void
{
$this->expectException(QueueException::class);
$this->expectExceptionMessage('This job name is not defined in the $jobHandlers array.');
@@ -109,7 +272,7 @@ public function testPushException()
/**
* @throws ReflectionException
*/
- public function testPushWithPriorityException()
+ public function testPushWithPriorityException(): void
{
$this->expectException(QueueException::class);
$this->expectExceptionMessage('This queue has incorrectly defined priority: "invalid" for the queue: "queue".');
@@ -121,7 +284,31 @@ public function testPushWithPriorityException()
/**
* @throws ReflectionException
*/
- public function testPop()
+ public function testPushWithIncorrectQueueFormatException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('The queue name should consists only lowercase letters or numbers.');
+
+ $handler = new DatabaseHandler($this->config);
+ $handler->push('queue*', 'success', ['key' => 'value']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPushWithTooLongQueueNameException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('The queue name is too long. It should be no longer than 64 letters.');
+
+ $handler = new DatabaseHandler($this->config);
+ $handler->push(str_repeat('a', 65), 'success', ['key' => 'value']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPop(): void
{
$handler = new DatabaseHandler($this->config);
$result = $handler->pop('queue1', ['default']);
@@ -136,7 +323,7 @@ public function testPop()
/**
* @throws ReflectionException
*/
- public function testPopEmpty()
+ public function testPopEmpty(): void
{
$handler = new DatabaseHandler($this->config);
$result = $handler->pop('queue123', ['default']);
@@ -147,8 +334,10 @@ public function testPopEmpty()
/**
* @throws ReflectionException
*/
- public function testLater()
+ public function testLater(): void
{
+ Time::setTestNow('2023-12-29 14:15:16');
+
$handler = new DatabaseHandler($this->config);
$queueJob = $handler->pop('queue1', ['default']);
@@ -161,16 +350,19 @@ public function testLater()
$this->assertTrue($result);
$this->seeInDatabase('queue_jobs', [
- 'id' => 2,
- 'status' => Status::PENDING->value,
+ 'id' => 2,
+ 'status' => Status::PENDING->value,
+ 'available_at' => Time::now()->addSeconds(60)->timestamp,
]);
}
/**
* @throws ReflectionException
*/
- public function testFailedAndKeepJob()
+ public function testFailedAndKeepJob(): void
{
+ Time::setTestNow('2023-12-29 14:15:16');
+
$handler = new DatabaseHandler($this->config);
$queueJob = $handler->pop('queue1', ['default']);
@@ -185,10 +377,11 @@ public function testFailedAndKeepJob()
'id' => 2,
'connection' => 'database',
'queue' => 'queue1',
+ 'failed_at' => 1703859316,
]);
}
- public function testFailedAndDontKeepJob()
+ public function testFailedAndDontKeepJob(): void
{
$handler = new DatabaseHandler($this->config);
$queueJob = $handler->pop('queue1', ['default']);
@@ -210,7 +403,7 @@ public function testFailedAndDontKeepJob()
/**
* @throws ReflectionException
*/
- public function testDoneAndKeepJob()
+ public function testDoneAndKeepJob(): void
{
$handler = new DatabaseHandler($this->config);
$queueJob = $handler->pop('queue1', ['default']);
@@ -227,7 +420,7 @@ public function testDoneAndKeepJob()
/**
* @throws ReflectionException
*/
- public function testDoneAndDontKeepJob()
+ public function testDoneAndDontKeepJob(): void
{
$handler = new DatabaseHandler($this->config);
$queueJob = $handler->pop('queue1', ['default']);
@@ -240,7 +433,7 @@ public function testDoneAndDontKeepJob()
]);
}
- public function testClear()
+ public function testClear(): void
{
$handler = new DatabaseHandler($this->config);
$result = $handler->clear('queue1');
@@ -255,24 +448,24 @@ public function testClear()
]);
}
- public function testRetry()
+ public function testRetry(): void
{
$handler = new DatabaseHandler($this->config);
$count = $handler->retry(1, 'queue1');
- $this->assertSame($count, 1);
+ $this->assertSame(1, $count);
$this->seeInDatabase('queue_jobs', [
'id' => 3,
'queue' => 'queue1',
- 'payload' => json_encode(['job' => 'failure', 'data' => []]),
+ 'payload' => json_encode(['job' => 'failure', 'data' => [], 'metadata' => []]),
]);
$this->dontSeeInDatabase('queue_jobs_failed', [
'id' => 1,
]);
}
- public function testForget()
+ public function testForget(): void
{
$handler = new DatabaseHandler($this->config);
$result = $handler->forget(1);
@@ -284,10 +477,18 @@ public function testForget()
]);
}
+ public function testForgetFalse(): void
+ {
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->forget(1111);
+
+ $this->assertFalse($result);
+ }
+
/**
* @throws ReflectionException
*/
- public function testFlush()
+ public function testFlush(): void
{
$handler = new DatabaseHandler($this->config);
$queueJob = $handler->pop('queue1', ['default']);
@@ -309,7 +510,7 @@ public function testFlush()
]);
}
- public function testFlushAll()
+ public function testFlushAll(): void
{
$handler = new DatabaseHandler($this->config);
$handler->flush(null, null);
@@ -318,7 +519,7 @@ public function testFlushAll()
]);
}
- public function testListFailed()
+ public function testListFailed(): void
{
$handler = new DatabaseHandler($this->config);
$list = $handler->listFailed('queue1');
diff --git a/tests/Models/QueueJobModelTest.php b/tests/Models/QueueJobModelTest.php
new file mode 100644
index 0000000..cf829d3
--- /dev/null
+++ b/tests/Models/QueueJobModelTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Models;
+
+use CodeIgniter\Queue\Models\QueueJobModel;
+use CodeIgniter\Test\ReflectionHelper;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class QueueJobModelTest extends TestCase
+{
+ use ReflectionHelper;
+
+ public function testQueueJobModel(): void
+ {
+ $model = model(QueueJobModel::class);
+ $this->assertInstanceOf(QueueJobModel::class, $model);
+ }
+
+ public function testSkipLocked(): void
+ {
+ $model = model(QueueJobModel::class);
+ $method = $this->getPrivateMethodInvoker($model, 'skipLocked');
+
+ $sql = 'SELECT * FROM queue_jobs WHERE queue = "test" AND status = 0 AND available_at < 123456 LIMIT 1';
+ $result = $method($sql);
+
+ if ($model->db->DBDriver === 'SQLite3') {
+ $this->assertSame($sql, $result);
+ } elseif ($model->db->DBDriver === 'SQLSRV') {
+ $this->assertStringContainsString('WITH (ROWLOCK,UPDLOCK,READPAST) WHERE', $result);
+ } else {
+ $this->assertStringContainsString('FOR UPDATE SKIP LOCKED', $result);
+ }
+ }
+
+ public function testSkipLockedFalse(): void
+ {
+ config('Queue')->database['skipLocked'] = false;
+
+ $model = model(QueueJobModel::class);
+ $method = $this->getPrivateMethodInvoker($model, 'skipLocked');
+
+ $sql = 'SELECT * FROM queue_jobs WHERE queue = "test" AND status = 0 AND available_at < 123456 LIMIT 1';
+ $result = $method($sql);
+
+ $this->assertSame($sql, $result);
+ }
+}
diff --git a/tests/Payloads/ChainBuilderTest.php b/tests/Payloads/ChainBuilderTest.php
new file mode 100644
index 0000000..6aa91ba
--- /dev/null
+++ b/tests/Payloads/ChainBuilderTest.php
@@ -0,0 +1,160 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Payloads;
+
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Payloads\ChainBuilder;
+use CodeIgniter\Queue\Payloads\ChainElement;
+use Tests\Support\Config\Queue as QueueConfig;
+use Tests\Support\Database\Seeds\TestDatabaseQueueSeeder;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class ChainBuilderTest extends TestCase
+{
+ protected $seed = TestDatabaseQueueSeeder::class;
+ private QueueConfig $config;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->config = config(QueueConfig::class);
+ }
+
+ public function testChainBuilder(): void
+ {
+ $handler = new DatabaseHandler($this->config);
+ $chainBuilder = new ChainBuilder($handler);
+
+ $this->assertInstanceOf(ChainBuilder::class, $chainBuilder);
+ }
+
+ public function testPush(): void
+ {
+ $handler = new DatabaseHandler($this->config);
+ $chainBuilder = new ChainBuilder($handler);
+
+ $chainElement = $chainBuilder->push('queue', 'job', ['data' => 'value']);
+
+ $this->assertInstanceOf(ChainElement::class, $chainElement);
+ }
+
+ public function testChainWithSingleJob(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain->push('queue', 'success', ['key' => 'value']);
+ });
+
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode([
+ 'job' => 'success',
+ 'data' => ['key' => 'value'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ ],
+ ]),
+ 'available_at' => 1703859316,
+ ]);
+ }
+
+ public function testEmptyChain(): void
+ {
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ // No jobs added
+ });
+
+ $this->assertFalse($result->getStatus());
+ $this->seeInDatabase('queue_jobs', []);
+ }
+
+ public function testMultipleDifferentQueues(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue1', 'success', ['key1' => 'value1'])
+ ->push('queue2', 'success', ['key2' => 'value2']);
+ });
+
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue1',
+ 'payload' => json_encode([
+ 'job' => 'success',
+ 'data' => ['key1' => 'value1'],
+ 'metadata' => [
+ 'queue' => 'queue1',
+ 'chainedJobs' => [
+ [
+ 'job' => 'success',
+ 'data' => ['key2' => 'value2'],
+ 'metadata' => ['queue' => 'queue2'],
+ ],
+ ],
+ ],
+ ]),
+ 'available_at' => 1703859316,
+ ]);
+ }
+
+ public function testChainWithManyJobs(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new DatabaseHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->push('queue', 'success', ['key2' => 'value2'])
+ ->push('queue', 'success', ['key3' => 'value3']);
+ });
+
+ $this->assertTrue($result->getStatus());
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue',
+ 'payload' => json_encode([
+ 'job' => 'success',
+ 'data' => ['key1' => 'value1'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'chainedJobs' => [
+ [
+ 'job' => 'success',
+ 'data' => ['key2' => 'value2'],
+ 'metadata' => ['queue' => 'queue'],
+ ],
+ [
+ 'job' => 'success',
+ 'data' => ['key3' => 'value3'],
+ 'metadata' => ['queue' => 'queue'],
+ ],
+ ],
+ ],
+ ]),
+ 'available_at' => 1703859316,
+ ]);
+ }
+}
diff --git a/tests/Payloads/ChainElementTest.php b/tests/Payloads/ChainElementTest.php
new file mode 100644
index 0000000..2bb232a
--- /dev/null
+++ b/tests/Payloads/ChainElementTest.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace App\ThirdParty\queue\tests\Payloads;
+
+use CodeIgniter\Queue\Payloads\ChainBuilder;
+use CodeIgniter\Queue\Payloads\ChainElement;
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class ChainElementTest extends TestCase
+{
+ private Payload $payload;
+ private ChainBuilder $chainBuilder;
+ private ChainElement $chainElement;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ // Create a payload object
+ $this->payload = new Payload('job', ['key' => 'value']);
+ $this->payload->setQueue('queue');
+
+ // Create a mock ChainBuilder
+ $this->chainBuilder = $this->createMock(ChainBuilder::class);
+
+ // Create the ChainElement to test
+ $this->chainElement = new ChainElement($this->payload, $this->chainBuilder);
+ }
+
+ public function testSetPriority(): void
+ {
+ $result = $this->chainElement->setPriority('high');
+
+ $this->assertInstanceOf(ChainElement::class, $result);
+ $this->assertSame('high', $this->payload->getPriority());
+ }
+
+ public function testSetDelay(): void
+ {
+ $result = $this->chainElement->setDelay(60);
+
+ $this->assertInstanceOf(ChainElement::class, $result);
+ $this->assertSame(60, $this->payload->getDelay());
+ }
+
+ public function testPush(): void
+ {
+ $nextPayload = new Payload('nextJob', ['nextKey' => 'nextValue']);
+ $nextElement = new ChainElement($nextPayload, $this->chainBuilder);
+
+ /** @phpstan-ignore-next-line */
+ $this->chainBuilder->expects($this->once())
+ ->method('push')
+ ->with('queue2', 'job2', ['data' => 'value2'])
+ ->willReturn($nextElement);
+
+ $result = $this->chainElement->push('queue2', 'job2', ['data' => 'value2']);
+
+ $this->assertInstanceOf(ChainElement::class, $result);
+ $this->assertSame($nextElement, $result);
+ }
+
+ public function testMultipleMethodChaining(): void
+ {
+ $chainBuilder = $this->createMock(ChainBuilder::class);
+ $chainBuilder->method('push')->willReturnSelf();
+
+ $payload = new Payload('job', ['key' => 'value']);
+
+ $chainElement = new ChainElement($payload, $chainBuilder);
+
+ $chainElement
+ ->setPriority('medium')
+ ->setDelay(30);
+
+ $this->assertSame('medium', $payload->getPriority());
+ $this->assertSame(30, $payload->getDelay());
+ }
+
+ public function testCorrectMetadataModification(): void
+ {
+ $metadata = new PayloadMetadata();
+ $payload = new Payload('job', ['key' => 'value'], $metadata);
+
+ $chainElement = new ChainElement($payload, $this->chainBuilder);
+
+ $chainElement->setPriority('low');
+ $chainElement->setDelay(120);
+
+ $this->assertSame('low', $payload->getMetadata()->get('priority'));
+ $this->assertSame(120, $payload->getMetadata()->get('delay'));
+ }
+}
diff --git a/tests/Payloads/PayloadCollectionTest.php b/tests/Payloads/PayloadCollectionTest.php
new file mode 100644
index 0000000..13ef3d6
--- /dev/null
+++ b/tests/Payloads/PayloadCollectionTest.php
@@ -0,0 +1,185 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace App\ThirdParty\queue\tests\Payloads;
+
+use ArrayIterator;
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadCollection;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class PayloadCollectionTest extends TestCase
+{
+ private PayloadCollection $collection;
+ private Payload $payload1;
+ private Payload $payload2;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ // Create sample payloads
+ $this->payload1 = new Payload('job1', ['key1' => 'value1']);
+ $this->payload1->setQueue('queue1');
+
+ $this->payload2 = new Payload('job2', ['key2' => 'value2']);
+ $this->payload2->setQueue('queue2');
+
+ // Create an empty collection
+ $this->collection = new PayloadCollection();
+ }
+
+ public function testEmptyCollectionCount(): void
+ {
+ $this->assertCount(0, $this->collection);
+ }
+
+ public function testAddPayload(): void
+ {
+ $result = $this->collection->add($this->payload1);
+
+ $this->assertInstanceOf(PayloadCollection::class, $result);
+ $this->assertCount(1, $this->collection);
+ }
+
+ public function testAddMultiplePayloads(): void
+ {
+ $this->collection->add($this->payload1);
+ $this->collection->add($this->payload2);
+
+ $this->assertCount(2, $this->collection);
+ }
+
+ public function testShiftPayload(): void
+ {
+ $this->collection->add($this->payload1);
+ $this->collection->add($this->payload2);
+
+ $first = $this->collection->shift();
+
+ $this->assertSame($this->payload1, $first);
+ $this->assertCount(1, $this->collection);
+ }
+
+ public function testShiftFromEmptyCollection(): void
+ {
+ $result = $this->collection->shift();
+
+ $this->assertNull($result);
+ }
+
+ public function testGetIterator(): void
+ {
+ $this->collection->add($this->payload1);
+ $this->collection->add($this->payload2);
+
+ $iterator = $this->collection->getIterator();
+
+ $this->assertInstanceOf(ArrayIterator::class, $iterator);
+ $this->assertCount(2, $iterator);
+ }
+
+ public function testToArray(): void
+ {
+ $this->collection->add($this->payload1);
+ $this->collection->add($this->payload2);
+
+ $array = $this->collection->toArray();
+
+ $this->assertCount(2, $array);
+
+ // Check array structure
+ $this->assertArrayHasKey('job', $array[0]);
+ $this->assertArrayHasKey('data', $array[0]);
+ $this->assertArrayHasKey('metadata', $array[0]);
+
+ $this->assertSame('job1', $array[0]['job']);
+ $this->assertSame(['key1' => 'value1'], $array[0]['data']);
+ }
+
+ public function testJsonSerialize(): void
+ {
+ $this->collection->add($this->payload1);
+ $this->collection->add($this->payload2);
+
+ $json = json_encode($this->collection);
+ $decoded = json_decode($json, true);
+
+ $this->assertIsArray($decoded);
+ $this->assertCount(2, $decoded);
+ $this->assertSame('job1', $decoded[0]['job']);
+ $this->assertSame('job2', $decoded[1]['job']);
+ }
+
+ public function testFromArray(): void
+ {
+ $arrayData = [
+ [
+ 'job' => 'job1',
+ 'data' => ['key1' => 'value1'],
+ 'metadata' => ['queue' => 'queue1'],
+ ],
+ [
+ 'job' => 'job2',
+ 'data' => ['key2' => 'value2'],
+ 'metadata' => ['queue' => 'queue2'],
+ ],
+ ];
+
+ $collection = PayloadCollection::fromArray($arrayData);
+
+ $this->assertInstanceOf(PayloadCollection::class, $collection);
+ $this->assertCount(2, $collection);
+
+ $first = $collection->shift();
+ $this->assertInstanceOf(Payload::class, $first);
+ $this->assertSame('job1', $first->getJob());
+ $this->assertSame(['key1' => 'value1'], $first->getData());
+ }
+
+ public function testInvalidDataInFromArray(): void
+ {
+ $arrayData = [
+ ['invalid' => 'data'], // Missing job and data
+ [
+ 'job' => 'job2',
+ 'data' => ['key2' => 'value2'],
+ ],
+ ];
+
+ $collection = PayloadCollection::fromArray($arrayData);
+
+ // Should only have created one valid payload
+ $this->assertCount(1, $collection);
+ }
+
+ public function testIteration(): void
+ {
+ $this->collection->add($this->payload1);
+ $this->collection->add($this->payload2);
+
+ $count = 0;
+ $jobs = [];
+
+ foreach ($this->collection as $payload) {
+ $count++;
+ $jobs[] = $payload->getJob();
+ }
+
+ $this->assertSame(2, $count);
+ $this->assertSame(['job1', 'job2'], $jobs);
+ }
+}
diff --git a/tests/Payloads/PayloadMetadataTest.php b/tests/Payloads/PayloadMetadataTest.php
new file mode 100644
index 0000000..e0af535
--- /dev/null
+++ b/tests/Payloads/PayloadMetadataTest.php
@@ -0,0 +1,233 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace App\ThirdParty\queue\tests\Payloads;
+
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadCollection;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class PayloadMetadataTest extends TestCase
+{
+ private PayloadMetadata $metadata;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->metadata = new PayloadMetadata();
+ }
+
+ public function testEmptyMetadata(): void
+ {
+ $this->assertSame([], $this->metadata->toArray());
+ }
+
+ public function testSetAndGetGenericValue(): void
+ {
+ $this->metadata->set('key', 'value');
+
+ $this->assertSame('value', $this->metadata->get('key'));
+ }
+
+ public function testGetWithDefault(): void
+ {
+ $this->assertSame('default', $this->metadata->get('nonexistent', 'default'));
+ }
+
+ public function testHasKey(): void
+ {
+ $this->metadata->set('key', 'value');
+
+ $this->assertTrue($this->metadata->has('key'));
+ $this->assertFalse($this->metadata->has('nonexistent'));
+ }
+
+ public function testRemoveKey(): void
+ {
+ $this->metadata->set('key', 'value');
+ $this->metadata->remove('key');
+
+ $this->assertFalse($this->metadata->has('key'));
+ }
+
+ public function testSetAndGetChainedJobs(): void
+ {
+ $payload1 = new Payload('job1', ['key1' => 'value1']);
+ $payload2 = new Payload('job2', ['key2' => 'value2']);
+
+ $payloads = new PayloadCollection();
+ $payloads->add($payload1);
+ $payloads->add($payload2);
+
+ $this->metadata->setChainedJobs($payloads);
+
+ $result = $this->metadata->getChainedJobs();
+
+ $this->assertInstanceOf(PayloadCollection::class, $result);
+ $this->assertCount(2, $result);
+ }
+
+ public function testSetChainedJobsToNull(): void
+ {
+ $payload = new Payload('job', ['key' => 'value']);
+ $payloads = new PayloadCollection();
+ $payloads->add($payload);
+
+ $this->metadata->setChainedJobs($payloads);
+
+ // Then set to null
+ $this->metadata->setChainedJobs(null);
+
+ $this->assertNull($this->metadata->getChainedJobs());
+ $this->assertFalse($this->metadata->hasChainedJobs());
+ }
+
+ public function testHasChainedJobs(): void
+ {
+ $this->assertFalse($this->metadata->hasChainedJobs());
+
+ $payload = new Payload('job', ['key' => 'value']);
+ $payloads = new PayloadCollection();
+ $payloads->add($payload);
+
+ $this->metadata->setChainedJobs($payloads);
+
+ $this->assertTrue($this->metadata->hasChainedJobs());
+ }
+
+ public function testHasChainedJobsWithEmptyCollection(): void
+ {
+ $emptyCollection = new PayloadCollection();
+ $this->metadata->setChainedJobs($emptyCollection);
+
+ $this->assertFalse($this->metadata->hasChainedJobs());
+ }
+
+ public function testJsonSerialize(): void
+ {
+ $this->metadata->set('queue', 'default');
+ $this->metadata->set('priority', 'high');
+
+ $json = json_encode($this->metadata);
+ $decoded = json_decode($json, true);
+
+ $this->assertIsArray($decoded);
+ $this->assertArrayHasKey('queue', $decoded);
+ $this->assertArrayHasKey('priority', $decoded);
+ $this->assertSame('default', $decoded['queue']);
+ $this->assertSame('high', $decoded['priority']);
+ }
+
+ public function testJsonSerializeWithChainedJobs(): void
+ {
+ $payload = new Payload('job', ['key' => 'value']);
+ $payload->setQueue('queue');
+
+ $payloads = new PayloadCollection();
+ $payloads->add($payload);
+
+ $this->metadata->setChainedJobs($payloads);
+
+ $json = json_encode($this->metadata);
+ $decoded = json_decode($json, true);
+
+ $this->assertIsArray($decoded);
+ $this->assertArrayHasKey('chainedJobs', $decoded);
+ $this->assertIsArray($decoded['chainedJobs']);
+ $this->assertCount(1, $decoded['chainedJobs']);
+ $this->assertSame('job', $decoded['chainedJobs'][0]['job']);
+ }
+
+ public function testFromArray(): void
+ {
+ $data = [
+ 'queue' => 'default',
+ 'priority' => 'high',
+ 'delay' => 60,
+ ];
+
+ $metadata = PayloadMetadata::fromArray($data);
+
+ $this->assertSame('default', $metadata->get('queue'));
+ $this->assertSame('high', $metadata->get('priority'));
+ $this->assertSame(60, $metadata->get('delay'));
+ }
+
+ public function testFromArrayWithChainedJobs(): void
+ {
+ $data = [
+ 'queue' => 'default',
+ 'chainedJobs' => [
+ [
+ 'job' => 'job1',
+ 'data' => ['key1' => 'value1'],
+ 'metadata' => ['queue' => 'queue1'],
+ ],
+ [
+ 'job' => 'job2',
+ 'data' => ['key2' => 'value2'],
+ 'metadata' => ['queue' => 'queue2'],
+ ],
+ ],
+ ];
+
+ $metadata = PayloadMetadata::fromArray($data);
+
+ $this->assertSame('default', $metadata->get('queue'));
+ $this->assertTrue($metadata->hasChainedJobs());
+
+ $chainedJobs = $metadata->getChainedJobs();
+ $this->assertInstanceOf(PayloadCollection::class, $chainedJobs);
+ $this->assertCount(2, $chainedJobs);
+
+ $job1 = $chainedJobs->shift();
+ $this->assertSame('job1', $job1->getJob());
+ $this->assertSame(['key1' => 'value1'], $job1->getData());
+ $this->assertSame('queue1', $job1->getQueue());
+ }
+
+ public function testFromArrayWithInvalidChainedJobs(): void
+ {
+ $data = [
+ 'chainedJobs' => [
+ ['invalid' => 'data'], // Missing job and data
+ [
+ 'job' => 'job2',
+ 'data' => ['key2' => 'value2'],
+ ],
+ ],
+ ];
+
+ $metadata = PayloadMetadata::fromArray($data);
+
+ $this->assertTrue($metadata->hasChainedJobs());
+ $this->assertSame(1, $metadata->getChainedJobs()->count());
+ }
+
+ public function testToArray(): void
+ {
+ $this->metadata->set('queue', 'default');
+ $this->metadata->set('priority', 'high');
+
+ $array = $this->metadata->toArray();
+
+ $this->assertArrayHasKey('queue', $array);
+ $this->assertArrayHasKey('priority', $array);
+ $this->assertSame('default', $array['queue']);
+ $this->assertSame('high', $array['priority']);
+ }
+}
diff --git a/tests/Payloads/PayloadTest.php b/tests/Payloads/PayloadTest.php
new file mode 100644
index 0000000..570cff2
--- /dev/null
+++ b/tests/Payloads/PayloadTest.php
@@ -0,0 +1,295 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace App\ThirdParty\queue\tests\Payloads;
+
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Payloads\Payload;
+use CodeIgniter\Queue\Payloads\PayloadCollection;
+use CodeIgniter\Queue\Payloads\PayloadMetadata;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class PayloadTest extends TestCase
+{
+ private Payload $payload;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->payload = new Payload('job', ['key' => 'value']);
+ }
+
+ public function testConstructor(): void
+ {
+ $this->assertSame('job', $this->payload->getJob());
+ $this->assertSame(['key' => 'value'], $this->payload->getData());
+ }
+
+ public function testConstructorWithMetadata(): void
+ {
+ $metadata = new PayloadMetadata();
+ $metadata->set('priority', 'high');
+
+ $payload = new Payload('job', ['key' => 'value'], $metadata);
+
+ $this->assertSame('high', $payload->getMetadata()->get('priority'));
+ }
+
+ public function testGetJob(): void
+ {
+ $this->assertSame('job', $this->payload->getJob());
+ }
+
+ public function testGetData(): void
+ {
+ $this->assertSame(['key' => 'value'], $this->payload->getData());
+ }
+
+ public function testGetMetadata(): void
+ {
+ $metadata = $this->payload->getMetadata();
+
+ $this->assertInstanceOf(PayloadMetadata::class, $metadata);
+ }
+
+ public function testSetMetadata(): void
+ {
+ $metadata = new PayloadMetadata();
+ $metadata->set('priority', 'high');
+
+ $result = $this->payload->setMetadata($metadata);
+
+ $this->assertInstanceOf(Payload::class, $result);
+ $this->assertSame('high', $this->payload->getMetadata()->get('priority'));
+ }
+
+ public function testSetQueue(): void
+ {
+ $result = $this->payload->setQueue('queue');
+
+ $this->assertInstanceOf(Payload::class, $result);
+ $this->assertSame('queue', $this->payload->getQueue());
+ }
+
+ public function testSetQueueWithInvalidFormat(): void
+ {
+ $this->expectException(QueueException::class);
+
+ $this->payload->setQueue('invalid queue name!');
+ }
+
+ public function testSetQueueWithTooLongName(): void
+ {
+ $this->expectException(QueueException::class);
+
+ $this->payload->setQueue(str_repeat('a', 65)); // 65 characters, too long
+ }
+
+ public function testGetQueue(): void
+ {
+ $this->payload->setQueue('queue');
+
+ $this->assertSame('queue', $this->payload->getQueue());
+ }
+
+ public function testSetPriority(): void
+ {
+ $result = $this->payload->setPriority('high');
+
+ $this->assertInstanceOf(Payload::class, $result);
+ $this->assertSame('high', $this->payload->getPriority());
+ }
+
+ public function testSetPriorityWithInvalidFormat(): void
+ {
+ $this->expectException(QueueException::class);
+
+ $this->payload->setPriority('invalid priority!');
+ }
+
+ public function testSetPriorityWithTooLongName(): void
+ {
+ $this->expectException(QueueException::class);
+
+ $this->payload->setPriority(str_repeat('a', 65)); // 65 characters, too long
+ }
+
+ public function testGetPriority(): void
+ {
+ $this->payload->setPriority('high');
+
+ $this->assertSame('high', $this->payload->getPriority());
+ }
+
+ public function testSetDelay(): void
+ {
+ $result = $this->payload->setDelay(60);
+
+ $this->assertInstanceOf(Payload::class, $result);
+ $this->assertSame(60, $this->payload->getDelay());
+ }
+
+ public function testSetDelayWithNegativeValue(): void
+ {
+ $this->expectException(QueueException::class);
+
+ $this->payload->setDelay(-1);
+ }
+
+ public function testGetDelay(): void
+ {
+ $this->payload->setDelay(60);
+
+ $this->assertSame(60, $this->payload->getDelay());
+ }
+
+ public function testSetChainedJobs(): void
+ {
+ $payloads = new PayloadCollection();
+ $payloads->add(new Payload('nextJob', ['nextKey' => 'nextValue']));
+
+ $result = $this->payload->setChainedJobs($payloads);
+
+ $this->assertInstanceOf(Payload::class, $result);
+ $this->assertTrue($this->payload->hasChainedJobs());
+ }
+
+ public function testGetChainedJobs(): void
+ {
+ $payloads = new PayloadCollection();
+ $payloads->add(new Payload('nextJob', ['nextKey' => 'nextValue']));
+
+ $this->payload->setChainedJobs($payloads);
+ $chainedJobs = $this->payload->getChainedJobs();
+
+ $this->assertInstanceOf(PayloadCollection::class, $chainedJobs);
+ $this->assertCount(1, $chainedJobs);
+ }
+
+ public function testHasChainedJobs(): void
+ {
+ $this->assertFalse($this->payload->hasChainedJobs());
+
+ $payloads = new PayloadCollection();
+ $payloads->add(new Payload('nextJob', ['nextKey' => 'nextValue']));
+
+ $this->payload->setChainedJobs($payloads);
+
+ $this->assertTrue($this->payload->hasChainedJobs());
+ }
+
+ public function testJsonSerialize(): void
+ {
+ $this->payload->setQueue('queue');
+ $this->payload->setPriority('high');
+
+ $json = json_encode($this->payload);
+ $decoded = json_decode($json, true);
+
+ $this->assertIsArray($decoded);
+ $this->assertSame('job', $decoded['job']);
+ $this->assertSame(['key' => 'value'], $decoded['data']);
+ $this->assertIsArray($decoded['metadata']);
+ $this->assertSame('queue', $decoded['metadata']['queue']);
+ $this->assertSame('high', $decoded['metadata']['priority']);
+ }
+
+ public function testJsonSerializeWithChainedJobs(): void
+ {
+ $this->payload->setQueue('queue');
+
+ $nextPayload = new Payload('nextJob', ['nextKey' => 'nextValue']);
+ $nextPayload->setQueue('queue');
+
+ $payloads = new PayloadCollection();
+ $payloads->add($nextPayload);
+
+ $this->payload->setChainedJobs($payloads);
+
+ $json = json_encode($this->payload);
+ $decoded = json_decode($json, true);
+
+ $this->assertIsArray($decoded);
+ $this->assertArrayHasKey('metadata', $decoded);
+ $this->assertArrayHasKey('chainedJobs', $decoded['metadata']);
+ $this->assertIsArray($decoded['metadata']['chainedJobs']);
+ $this->assertCount(1, $decoded['metadata']['chainedJobs']);
+ $this->assertSame('nextJob', $decoded['metadata']['chainedJobs'][0]['job']);
+ $this->assertSame(['nextKey' => 'nextValue'], $decoded['metadata']['chainedJobs'][0]['data']);
+ }
+
+ public function testFromArray(): void
+ {
+ $data = [
+ 'job' => 'job',
+ 'data' => ['key' => 'value'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'priority' => 'high',
+ ],
+ ];
+
+ $payload = Payload::fromArray($data);
+
+ $this->assertSame('job', $payload->getJob());
+ $this->assertSame(['key' => 'value'], $payload->getData());
+ $this->assertSame('queue', $payload->getQueue());
+ $this->assertSame('high', $payload->getPriority());
+ }
+
+ public function testFromArrayWithChainedJobs(): void
+ {
+ $data = [
+ 'job' => 'job',
+ 'data' => ['key' => 'value'],
+ 'metadata' => [
+ 'queue' => 'queue',
+ 'chainedJobs' => [
+ [
+ 'job' => 'nextJob',
+ 'data' => ['nextKey' => 'nextValue'],
+ 'metadata' => ['queue' => 'nextQueue'],
+ ],
+ ],
+ ],
+ ];
+
+ $payload = Payload::fromArray($data);
+
+ $this->assertTrue($payload->hasChainedJobs());
+ $chainedJobs = $payload->getChainedJobs();
+ $this->assertCount(1, $chainedJobs);
+
+ $nextJob = $chainedJobs->shift();
+ $this->assertSame('nextJob', $nextJob->getJob());
+ $this->assertSame(['nextKey' => 'nextValue'], $nextJob->getData());
+ $this->assertSame('nextQueue', $nextJob->getQueue());
+ }
+
+ public function testMultipleValidations(): void
+ {
+ $payload = new Payload('job', ['key' => 'value']);
+
+ // Test that all validations pass
+ $payload->setQueue('valid-queue');
+ $payload->setPriority('valid-priority');
+ $payload->setDelay(30);
+
+ $this->assertSame('valid-queue', $payload->getQueue());
+ $this->assertSame('valid-priority', $payload->getPriority());
+ $this->assertSame(30, $payload->getDelay());
+ }
+}
diff --git a/tests/PredisHandlerTest.php b/tests/PredisHandlerTest.php
new file mode 100644
index 0000000..05d79d0
--- /dev/null
+++ b/tests/PredisHandlerTest.php
@@ -0,0 +1,414 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace ThirdParty\queue\tests;
+
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Handlers\PredisHandler;
+use CodeIgniter\Test\ReflectionHelper;
+use Exception;
+use ReflectionException;
+use Tests\Support\Config\Queue as QueueConfig;
+use Tests\Support\Database\Seeds\TestRedisQueueSeeder;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class PredisHandlerTest extends TestCase
+{
+ use ReflectionHelper;
+
+ protected $seed = TestRedisQueueSeeder::class;
+ private QueueConfig $config;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->config = config(QueueConfig::class);
+ }
+
+ public function testPredisHandler(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $this->assertInstanceOf(PredisHandler::class, $handler);
+ }
+
+ public function testPriority(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $handler->setPriority('high');
+
+ $this->assertSame('high', self::getPrivateProperty($handler, 'priority'));
+ }
+
+ public function testPriorityException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('The priority name should consists only lowercase letters.');
+
+ $handler = new PredisHandler($this->config);
+ $handler->setPriority('high_:');
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPush(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $result = $handler->push('queue', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(1, $predis->zcard('queues:queue:low'));
+
+ $task = $predis->zrangebyscore('queues:queue:low', '-inf', Time::now()->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key' => 'value'], $queueJob->payload['data']);
+ $this->assertSame([], $queueJob->payload['metadata']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPushWithPriority(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $result = $handler->setPriority('high')->push('queue', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(1, $predis->zcard('queues:queue:high'));
+
+ $task = $predis->zrangebyscore('queues:queue:high', '-inf', Time::now()->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key' => 'value'], $queueJob->payload['data']);
+ $this->assertSame([], $queueJob->payload['metadata']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPushWithDelay(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new PredisHandler($this->config);
+ $result = $handler->setDelay(MINUTE)->push('queue-delay', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(1, $predis->zcard('queues:queue-delay:default'));
+
+ $task = $predis->zrangebyscore('queues:queue-delay:default', '-inf', Time::now()->addSeconds(MINUTE)->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key' => 'value'], $queueJob->payload['data']);
+ $this->assertSame([], $queueJob->payload['metadata']);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testChain(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new PredisHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->push('queue', 'success', ['key2' => 'value2']);
+ });
+
+ $this->assertTrue($result->getStatus());
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(1, $predis->zcard('queues:queue:low'));
+
+ $task = $predis->zrangebyscore('queues:queue:low', '-inf', Time::now()->timestamp, ['limit' => [0, 1]]);
+ $job = new QueueJob(json_decode((string) $task[0], true));
+
+ $this->assertSame('success', $job->payload['job']);
+ $this->assertSame(['key1' => 'value1'], $job->payload['data']);
+ $this->assertArrayHasKey('metadata', $job->payload);
+ $this->assertArrayHasKey('queue', $job->payload['metadata']);
+ $this->assertSame('queue', $job->payload['metadata']['queue']);
+ $this->assertArrayHasKey('chainedJobs', $job->payload['metadata']);
+
+ $chainedJobs = $job->payload['metadata']['chainedJobs'];
+ $this->assertCount(1, $chainedJobs);
+ $this->assertSame('success', $chainedJobs[0]['job']);
+ $this->assertSame(['key2' => 'value2'], $chainedJobs[0]['data']);
+ $this->assertSame('queue', $chainedJobs[0]['metadata']['queue']);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testChainWithPriorityAndDelay(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new PredisHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->setPriority('high')
+ ->setDelay(60)
+ ->push('queue', 'success', ['key2' => 'value2'])
+ ->setPriority('low')
+ ->setDelay(120);
+ });
+
+ $this->assertTrue($result->getStatus());
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ // Should be in high priority queue
+ $this->assertSame(1, $predis->zcard('queues:queue:high'));
+
+ // Check with delay
+ $task = $predis->zrangebyscore('queues:queue:high', '-inf', Time::now()->addSeconds(61)->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key1' => 'value1'], $queueJob->payload['data']);
+ $this->assertArrayHasKey('metadata', $queueJob->payload);
+
+ // Check metadata
+ $meta = $queueJob->payload['metadata'];
+ $this->assertSame('queue', $meta['queue']);
+ $this->assertSame('high', $meta['priority']);
+ $this->assertSame(60, $meta['delay']);
+
+ // Check a chained job with its priority and delay
+ $this->assertArrayHasKey('chainedJobs', $meta);
+ $chainedJobs = $meta['chainedJobs'];
+ $this->assertCount(1, $chainedJobs);
+ $this->assertSame('success', $chainedJobs[0]['job']);
+ $this->assertSame(['key2' => 'value2'], $chainedJobs[0]['data']);
+ $this->assertSame('queue', $chainedJobs[0]['metadata']['queue']);
+ $this->assertSame('low', $chainedJobs[0]['metadata']['priority']);
+ $this->assertSame(120, $chainedJobs[0]['metadata']['delay']);
+ }
+
+ public function testPushException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('This job name is not defined in the $jobHandlers array.');
+
+ $handler = new PredisHandler($this->config);
+ $handler->push('queue', 'not-exists', ['key' => 'value']);
+ }
+
+ public function testPushWithPriorityException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('This queue has incorrectly defined priority: "invalid" for the queue: "queue".');
+
+ $handler = new PredisHandler($this->config);
+ $handler->setPriority('invalid')->push('queue', 'success', ['key' => 'value']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPop(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $result = $handler->pop('queue1', ['default']);
+
+ $this->assertInstanceOf(QueueJob::class, $result);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(1_234_567_890_654_321, $result->id);
+ $this->assertSame(0, $predis->zcard('queues:queue1:default'));
+ $this->assertSame(1, $predis->hexists('queues:queue1::reserved', $result->id));
+ }
+
+ public function testPopEmpty(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $result = $handler->pop('queue123', ['default']);
+
+ $this->assertNull($result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testLater(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(1, $predis->hexists('queues:queue1::reserved', $queueJob->id));
+ $this->assertSame(0, $predis->zcard('queues:queue1:default'));
+
+ $result = $handler->later($queueJob, 60);
+
+ $this->assertTrue($result);
+ $this->assertSame(0, $predis->hexists('queues:queue1::reserved', $queueJob->id));
+ $this->assertSame(1, $predis->zcard('queues:queue1:default'));
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testFailedAndKeepJob(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $err = new Exception('Sample exception');
+ $result = $handler->failed($queueJob, $err, true);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+
+ $this->assertTrue($result);
+ $this->assertSame(0, $predis->hexists('queues:queue1::reserved', $queueJob->id));
+ $this->assertSame(0, $predis->zcard('queues:queue1:default'));
+
+ $this->seeInDatabase('queue_jobs_failed', [
+ 'id' => 2,
+ 'connection' => 'predis',
+ 'queue' => 'queue1',
+ ]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testFailedAndDontKeepJob(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $err = new Exception('Sample exception');
+ $result = $handler->failed($queueJob, $err, false);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+
+ $this->assertTrue($result);
+ $this->assertSame(0, $predis->hexists('queues:queue1::reserved', $queueJob->id));
+ $this->assertSame(0, $predis->zcard('queues:queue1:default'));
+
+ $this->dontSeeInDatabase('queue_jobs_failed', [
+ 'id' => 2,
+ 'connection' => 'predis',
+ 'queue' => 'queue1',
+ ]);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testDoneAndKeepJob(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $result = $handler->done($queueJob, true);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+
+ $this->assertTrue($result);
+ $this->assertSame(0, $predis->hexists('queues:queue1::reserved', $queueJob->id));
+ $this->assertSame(1, $predis->llen('queues:queue1::done'));
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testDoneAndDontKeepJob(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(0, $predis->zcard('queues:queue1:default'));
+
+ $result = $handler->done($queueJob, false);
+
+ $this->assertTrue($result);
+ $this->assertSame(0, $predis->hexists('queues:queue1::reserved', $queueJob->id));
+ $this->assertSame(0, $predis->llen('queues:queue1::done'));
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testClear(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $result = $handler->clear('queue1');
+
+ $this->assertTrue($result);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(0, $predis->zcard('queues:queue1:default'));
+
+ $result = $handler->clear('queue1');
+ $this->assertTrue($result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testClearAll(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $result = $handler->clear();
+
+ $this->assertTrue($result);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertCount(0, $predis->keys('queues:*'));
+
+ $result = $handler->clear();
+ $this->assertTrue($result);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testRetry(): void
+ {
+ $handler = new PredisHandler($this->config);
+ $count = $handler->retry(1, 'queue1');
+
+ $this->assertSame(1, $count);
+
+ $predis = self::getPrivateProperty($handler, 'predis');
+ $this->assertSame(2, $predis->zcard('queues:queue1:default'));
+
+ $task = $predis->zrangebyscore('queues:queue1:default', '-inf', Time::now()->timestamp, ['limit' => [0, 2]]);
+ $queueJob = new QueueJob(json_decode((string) $task[1], true));
+ $this->assertSame('failure', $queueJob->payload['job']);
+ $this->assertSame(['failed' => true], $queueJob->payload['data']);
+
+ $this->dontSeeInDatabase('queue_jobs_failed', [
+ 'id' => 1,
+ ]);
+ }
+}
diff --git a/tests/PushAndPopWithDelayTest.php b/tests/PushAndPopWithDelayTest.php
new file mode 100644
index 0000000..f06b0f9
--- /dev/null
+++ b/tests/PushAndPopWithDelayTest.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests;
+
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Test\ReflectionHelper;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Tests\Support\Config\Queue as QueueConfig;
+use Tests\Support\Database\Seeds\TestDatabaseQueueSeeder;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class PushAndPopWithDelayTest extends TestCase
+{
+ use ReflectionHelper;
+
+ protected $seed = TestDatabaseQueueSeeder::class;
+ private QueueConfig $config;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->config = config(QueueConfig::class);
+ }
+
+ #[DataProvider('providePushAndPopWithDelay')]
+ public function testPushAndPopWithDelay(string $name, string $class): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new $class($this->config);
+ $result = $handler->setDelay(MINUTE)->push('queue-delay', 'success', ['key1' => 'value1']);
+
+ $this->assertNotNull($result);
+
+ $result = $handler->push('queue-delay', 'success', ['key2' => 'value2']);
+
+ $this->assertNotNull($result);
+
+ if ($name === 'database') {
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue-delay',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key1' => 'value1'], 'metadata' => []]),
+ 'available_at' => 1703859376,
+ ]);
+
+ $this->seeInDatabase('queue_jobs', [
+ 'queue' => 'queue-delay',
+ 'payload' => json_encode(['job' => 'success', 'data' => ['key2' => 'value2'], 'metadata' => []]),
+ 'available_at' => 1703859316,
+ ]);
+ }
+
+ $result = $handler->pop('queue-delay', ['default']);
+ $this->assertInstanceOf(QueueJob::class, $result);
+ $payload = ['job' => 'success', 'data' => ['key2' => 'value2'], 'metadata' => []];
+ $this->assertSame($payload, $result->payload);
+
+ $result = $handler->pop('queue-delay', ['default']);
+ $this->assertNull($result);
+
+ // add 1 minute
+ Time::setTestNow('2023-12-29 14:16:16');
+
+ $result = $handler->pop('queue-delay', ['default']);
+ $this->assertInstanceOf(QueueJob::class, $result);
+ $payload = ['job' => 'success', 'data' => ['key1' => 'value1'], 'metadata' => []];
+ $this->assertSame($payload, $result->payload);
+ }
+
+ public static function providePushAndPopWithDelay(): iterable
+ {
+ return [
+ [
+ 'database', // name
+ 'CodeIgniter\Queue\Handlers\DatabaseHandler', // class
+ ],
+ [
+ 'redis',
+ 'CodeIgniter\Queue\Handlers\RedisHandler',
+ ],
+ [
+ 'predis',
+ 'CodeIgniter\Queue\Handlers\PredisHandler',
+ ],
+ ];
+ }
+}
diff --git a/tests/QueuePushResultTest.php b/tests/QueuePushResultTest.php
new file mode 100644
index 0000000..f5a6dfa
--- /dev/null
+++ b/tests/QueuePushResultTest.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests;
+
+use CodeIgniter\Queue\QueuePushResult;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class QueuePushResultTest extends TestCase
+{
+ public function testConstructorSuccess(): void
+ {
+ $result = new QueuePushResult(true, 123456, null);
+
+ $this->assertTrue($result->getStatus());
+ $this->assertSame(123456, $result->getJobId());
+ $this->assertNull($result->getError());
+ }
+
+ public function testConstructorFailure(): void
+ {
+ $result = new QueuePushResult(false, null, 'Something went wrong');
+
+ $this->assertFalse($result->getStatus());
+ $this->assertNull($result->getJobId());
+ $this->assertSame('Something went wrong', $result->getError());
+ }
+
+ public function testStaticSuccess(): void
+ {
+ $result = QueuePushResult::success(999888);
+
+ $this->assertTrue($result->getStatus());
+ $this->assertSame(999888, $result->getJobId());
+ $this->assertNull($result->getError());
+ }
+
+ public function testStaticFailure(): void
+ {
+ $result = QueuePushResult::failure('Redis error');
+
+ $this->assertFalse($result->getStatus());
+ $this->assertNull($result->getJobId());
+ $this->assertSame('Redis error', $result->getError());
+ }
+
+ public function testStaticFailureWithoutError(): void
+ {
+ $result = QueuePushResult::failure();
+
+ $this->assertFalse($result->getStatus());
+ $this->assertNull($result->getJobId());
+ $this->assertNull($result->getError());
+ }
+}
diff --git a/tests/QueueTest.php b/tests/QueueTest.php
index f9fe243..7daa4d4 100644
--- a/tests/QueueTest.php
+++ b/tests/QueueTest.php
@@ -1,12 +1,23 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests;
-use Michalsn\CodeIgniterQueue\Exceptions\QueueException;
-use Michalsn\CodeIgniterQueue\Handlers\DatabaseHandler;
-use Michalsn\CodeIgniterQueue\Queue;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Queue;
use Tests\Support\Config\Queue as QueueConfig;
-use Tests\Support\Database\Seeds\TestQueueSeeder;
+use Tests\Support\Database\Seeds\TestDatabaseQueueSeeder;
use Tests\Support\TestCase;
/**
@@ -14,7 +25,7 @@
*/
final class QueueTest extends TestCase
{
- protected $seed = TestQueueSeeder::class;
+ protected $seed = TestDatabaseQueueSeeder::class;
private QueueConfig $config;
protected function setUp(): void
@@ -24,13 +35,13 @@ protected function setUp(): void
$this->config = config(QueueConfig::class);
}
- public function testQueue()
+ public function testQueue(): void
{
$queue = new Queue($this->config);
$this->assertInstanceOf(Queue::class, $queue);
}
- public function testQueueException()
+ public function testQueueException(): void
{
$this->expectException(QueueException::class);
$this->expectExceptionMessage('This queue handler is incorrect.');
@@ -41,7 +52,7 @@ public function testQueueException()
$this->assertInstanceOf(Queue::class, $queue);
}
- public function testQueueInit()
+ public function testQueueInit(): void
{
$queue = new Queue($this->config);
$this->assertInstanceOf(DatabaseHandler::class, $queue->init());
diff --git a/tests/RedisHandlerTest.php b/tests/RedisHandlerTest.php
new file mode 100644
index 0000000..7707cd4
--- /dev/null
+++ b/tests/RedisHandlerTest.php
@@ -0,0 +1,381 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests;
+
+use CodeIgniter\I18n\Time;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Exceptions\QueueException;
+use CodeIgniter\Queue\Handlers\RedisHandler;
+use CodeIgniter\Test\ReflectionHelper;
+use Exception;
+use ReflectionException;
+use Tests\Support\Config\Queue as QueueConfig;
+use Tests\Support\Database\Seeds\TestRedisQueueSeeder;
+use Tests\Support\TestCase;
+
+/**
+ * @internal
+ */
+final class RedisHandlerTest extends TestCase
+{
+ use ReflectionHelper;
+
+ protected $seed = TestRedisQueueSeeder::class;
+ private QueueConfig $config;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->config = config(QueueConfig::class);
+ }
+
+ public function testRedisHandler(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $this->assertInstanceOf(RedisHandler::class, $handler);
+ }
+
+ public function testPriority(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $handler->setPriority('high');
+
+ $this->assertSame('high', self::getPrivateProperty($handler, 'priority'));
+ }
+
+ public function testPriorityException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('The priority name should consists only lowercase letters.');
+
+ $handler = new RedisHandler($this->config);
+ $handler->setPriority('high_:');
+ }
+
+ public function testPush(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $result = $handler->push('queue', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(1, $redis->zCard('queues:queue:low'));
+
+ $task = $redis->zRangeByScore('queues:queue:low', '-inf', Time::now()->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key' => 'value'], $queueJob->payload['data']);
+ $this->assertSame([], $queueJob->payload['metadata']);
+ }
+
+ public function testPushWithPriority(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $result = $handler->setPriority('high')->push('queue', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(1, $redis->zCard('queues:queue:high'));
+
+ $task = $redis->zRangeByScore('queues:queue:high', '-inf', Time::now()->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key' => 'value'], $queueJob->payload['data']);
+ $this->assertSame([], $queueJob->payload['metadata']);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testPushWithDelay(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new RedisHandler($this->config);
+ $result = $handler->setDelay(MINUTE)->push('queue-delay', 'success', ['key' => 'value']);
+
+ $this->assertTrue($result->getStatus());
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(1, $redis->zCard('queues:queue-delay:default'));
+
+ $task = $redis->zRangeByScore('queues:queue-delay:default', '-inf', Time::now()->addSeconds(MINUTE)->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key' => 'value'], $queueJob->payload['data']);
+ $this->assertSame([], $queueJob->payload['metadata']);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testChain(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new RedisHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->push('queue', 'success', ['key2' => 'value2']);
+ });
+
+ $this->assertTrue($result->getStatus());
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(1, $redis->zCard('queues:queue:low'));
+
+ $task = $redis->zRangeByScore('queues:queue:low', '-inf', Time::now()->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key1' => 'value1'], $queueJob->payload['data']);
+ $this->assertArrayHasKey('metadata', $queueJob->payload);
+ $this->assertArrayHasKey('queue', $queueJob->payload['metadata']);
+ $this->assertSame('queue', $queueJob->payload['metadata']['queue']);
+ $this->assertArrayHasKey('chainedJobs', $queueJob->payload['metadata']);
+
+ $chainedJobs = $queueJob->payload['metadata']['chainedJobs'];
+ $this->assertCount(1, $chainedJobs);
+ $this->assertSame('success', $chainedJobs[0]['job']);
+ $this->assertSame(['key2' => 'value2'], $chainedJobs[0]['data']);
+ $this->assertSame('queue', $chainedJobs[0]['metadata']['queue']);
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testChainWithPriorityAndDelay(): void
+ {
+ Time::setTestNow('2023-12-29 14:15:16');
+
+ $handler = new RedisHandler($this->config);
+ $result = $handler->chain(static function ($chain): void {
+ $chain
+ ->push('queue', 'success', ['key1' => 'value1'])
+ ->setPriority('high')
+ ->setDelay(60)
+ ->push('queue', 'success', ['key2' => 'value2'])
+ ->setPriority('low')
+ ->setDelay(120);
+ });
+
+ $this->assertTrue($result->getStatus());
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ // Should be in high priority queue
+ $this->assertSame(1, $redis->zCard('queues:queue:high'));
+
+ // Check with delay
+ $task = $redis->zRangeByScore('queues:queue:high', '-inf', Time::now()->addSeconds(61)->timestamp, ['limit' => [0, 1]]);
+ $queueJob = new QueueJob(json_decode((string) $task[0], true));
+
+ $this->assertSame('success', $queueJob->payload['job']);
+ $this->assertSame(['key1' => 'value1'], $queueJob->payload['data']);
+ $this->assertArrayHasKey('metadata', $queueJob->payload);
+
+ // Check metadata
+ $metadata = $queueJob->payload['metadata'];
+ $this->assertSame('queue', $metadata['queue']);
+ $this->assertSame('high', $metadata['priority']);
+ $this->assertSame(60, $metadata['delay']);
+
+ // Check a chained job with its priority and delay
+ $this->assertArrayHasKey('chainedJobs', $metadata);
+ $chainedJobs = $metadata['chainedJobs'];
+ $this->assertCount(1, $chainedJobs);
+ $this->assertSame('success', $chainedJobs[0]['job']);
+ $this->assertSame(['key2' => 'value2'], $chainedJobs[0]['data']);
+ $this->assertSame('queue', $chainedJobs[0]['metadata']['queue']);
+ $this->assertSame('low', $chainedJobs[0]['metadata']['priority']);
+ $this->assertSame(120, $chainedJobs[0]['metadata']['delay']);
+ }
+
+ public function testPushException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('This job name is not defined in the $jobHandlers array.');
+
+ $handler = new RedisHandler($this->config);
+ $handler->push('queue', 'not-exists', ['key' => 'value']);
+ }
+
+ public function testPushWithPriorityException(): void
+ {
+ $this->expectException(QueueException::class);
+ $this->expectExceptionMessage('This queue has incorrectly defined priority: "invalid" for the queue: "queue".');
+
+ $handler = new RedisHandler($this->config);
+ $handler->setPriority('invalid')->push('queue', 'success', ['key' => 'value']);
+ }
+
+ public function testPop(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $result = $handler->pop('queue1', ['default']);
+
+ $this->assertInstanceOf(QueueJob::class, $result);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(1_234_567_890_654_321, $result->id);
+ $this->assertSame(0, $redis->zCard('queues:queue1:default'));
+ $this->assertTrue($redis->hExists('queues:queue1::reserved', (string) $result->id));
+ }
+
+ public function testPopEmpty(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $result = $handler->pop('queue123', ['default']);
+
+ $this->assertNull($result);
+ }
+
+ public function testLater(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertTrue($redis->hExists('queues:queue1::reserved', (string) $queueJob->id));
+ $this->assertSame(0, $redis->zCard('queues:queue1:default'));
+
+ $result = $handler->later($queueJob, 60);
+
+ $this->assertTrue($result);
+ $this->assertFalse($redis->hExists('queues:queue1::reserved', (string) $queueJob->id));
+ $this->assertSame(1, $redis->zCard('queues:queue1:default'));
+ }
+
+ public function testFailedAndKeepJob(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $err = new Exception('Sample exception');
+ $result = $handler->failed($queueJob, $err, true);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+
+ $this->assertTrue($result);
+ $this->assertFalse($redis->hExists('queues:queue1::reserved', (string) $queueJob->id));
+ $this->assertSame(0, $redis->zCard('queues:queue1:default'));
+
+ $this->seeInDatabase('queue_jobs_failed', [
+ 'id' => 2,
+ 'connection' => 'redis',
+ 'queue' => 'queue1',
+ ]);
+ }
+
+ public function testFailedAndDontKeepJob(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $err = new Exception('Sample exception');
+ $result = $handler->failed($queueJob, $err, false);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+
+ $this->assertTrue($result);
+ $this->assertFalse($redis->hExists('queues:queue1::reserved', (string) $queueJob->id));
+ $this->assertSame(0, $redis->zCard('queues:queue1:default'));
+
+ $this->dontSeeInDatabase('queue_jobs_failed', [
+ 'id' => 2,
+ 'connection' => 'redis',
+ 'queue' => 'queue1',
+ ]);
+ }
+
+ public function testDoneAndKeepJob(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $result = $handler->done($queueJob, true);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+
+ $this->assertTrue($result);
+ $this->assertFalse($redis->hExists('queues:queue1::reserved', (string) $queueJob->id));
+ $this->assertSame(1, $redis->lLen('queues:queue1::done'));
+ }
+
+ public function testDoneAndDontKeepJob(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $queueJob = $handler->pop('queue1', ['default']);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(0, $redis->zCard('queues:queue1:default'));
+
+ $result = $handler->done($queueJob, false);
+
+ $this->assertTrue($result);
+ $this->assertFalse($redis->hExists('queues:queue1::reserved', (string) $queueJob->id));
+ $this->assertSame(0, $redis->lLen('queues:queue1::done'));
+ }
+
+ public function testClear(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $result = $handler->clear('queue1');
+
+ $this->assertTrue($result);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(0, $redis->zCard('queues:queue1:default'));
+
+ $result = $handler->clear('queue1');
+ $this->assertTrue($result);
+ }
+
+ public function testClearAll(): void
+ {
+ $handler = new RedisHandler($this->config);
+
+ $result = $handler->clear();
+ $this->assertTrue($result);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertCount(0, $redis->keys('queues:*'));
+
+ $result = $handler->clear();
+ $this->assertTrue($result);
+ }
+
+ public function testRetry(): void
+ {
+ $handler = new RedisHandler($this->config);
+ $count = $handler->retry(1, 'queue1');
+
+ $this->assertSame(1, $count);
+
+ $redis = self::getPrivateProperty($handler, 'redis');
+ $this->assertSame(2, $redis->zCard('queues:queue1:default'));
+
+ $task = $redis->zRangeByScore('queues:queue1:default', '-inf', Time::now()->timestamp, ['limit' => [0, 2]]);
+ $queueJob = new QueueJob(json_decode((string) $task[1], true));
+ $this->assertSame('failure', $queueJob->payload['job']);
+ $this->assertSame(['failed' => true], $queueJob->payload['data']);
+
+ $this->dontSeeInDatabase('queue_jobs_failed', [
+ 'id' => 1,
+ ]);
+ }
+}
diff --git a/tests/_support/CLITestCase.php b/tests/_support/CLITestCase.php
new file mode 100644
index 0000000..f0d405d
--- /dev/null
+++ b/tests/_support/CLITestCase.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Support;
+
+use CodeIgniter\CLI\CLI;
+use CodeIgniter\Test\ReflectionHelper;
+
+abstract class CLITestCase extends TestCase
+{
+ use ReflectionHelper;
+
+ private array $lines = [];
+
+ protected function parseOutput(string $output): string
+ {
+ $this->lines = [];
+ $output = $this->removeColorCodes($output);
+ $this->lines = explode("\n", $output);
+
+ return $output;
+ }
+
+ protected function getLine(int $line = 0): ?string
+ {
+ return $this->lines[$line] ?? null;
+ }
+
+ protected function getLines(): string
+ {
+ return implode('', $this->lines);
+ }
+
+ protected function removeColorCodes(string $output): string
+ {
+ $colors = $this->getPrivateProperty(CLI::class, 'foreground_colors');
+ $colors = array_values(array_map(static fn ($color) => "\033[" . $color . 'm', $colors));
+ $colors = array_merge(["\033[0m"], $colors);
+
+ $output = str_replace($colors, '', trim($output));
+
+ if (is_windows()) {
+ $output = str_replace("\r\n", "\n", $output);
+ }
+
+ return $output;
+ }
+}
diff --git a/tests/_support/Config/Queue.php b/tests/_support/Config/Queue.php
index 57f7256..cea913e 100644
--- a/tests/_support/Config/Queue.php
+++ b/tests/_support/Config/Queue.php
@@ -1,9 +1,22 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests\Support\Config;
-use Michalsn\CodeIgniterQueue\Config\Queue as BaseQueue;
-use Michalsn\CodeIgniterQueue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Config\Queue as BaseQueue;
+use CodeIgniter\Queue\Handlers\DatabaseHandler;
+use CodeIgniter\Queue\Handlers\PredisHandler;
+use CodeIgniter\Queue\Handlers\RedisHandler;
use Tests\Support\Jobs\Failure;
use Tests\Support\Jobs\Success;
@@ -19,14 +32,41 @@ class Queue extends BaseQueue
*/
public array $handlers = [
'database' => DatabaseHandler::class,
+ 'redis' => RedisHandler::class,
+ 'predis' => PredisHandler::class,
];
/**
* Database handler config.
*/
public array $database = [
- 'dbGroup' => 'default',
- 'getShared' => true,
+ 'dbGroup' => 'default',
+ 'getShared' => true,
+ 'skipLocked' => true,
+ ];
+
+ /**
+ * Redis and Predis handler config.
+ */
+ public array $redis = [
+ 'host' => '127.0.0.1',
+ 'password' => null,
+ 'port' => 6379,
+ 'timeout' => 0,
+ 'database' => 0,
+ ];
+
+ /**
+ * Predis handler config.
+ */
+ public array $predis = [
+ 'scheme' => 'tcp',
+ 'host' => '127.0.0.1',
+ 'password' => null,
+ 'port' => 6379,
+ 'timeout' => 5,
+ 'database' => 0,
+ 'prefix' => '',
];
/**
diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php
new file mode 100644
index 0000000..86a303e
--- /dev/null
+++ b/tests/_support/Config/Registrar.php
@@ -0,0 +1,143 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Support\Config;
+
+/**
+ * Class Registrar
+ *
+ * Provides a basic registrar class for testing BaseConfig registration functions.
+ */
+class Registrar
+{
+ /**
+ * DB config array for testing purposes.
+ *
+ * @var array|bool|int|string>>
+ */
+ protected static array $dbConfig = [
+ 'MySQLi' => [
+ 'DSN' => '',
+ 'hostname' => '127.0.0.1',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'test',
+ 'DBDriver' => 'MySQLi',
+ 'DBPrefix' => 'db_',
+ 'pConnect' => false,
+ 'DBDebug' => true,
+ 'charset' => 'utf8mb4',
+ 'DBCollat' => 'utf8mb4_general_ci',
+ 'swapPre' => '',
+ 'encrypt' => false,
+ 'compress' => false,
+ 'strictOn' => false,
+ 'failover' => [],
+ 'port' => 3306,
+ ],
+ 'Postgre' => [
+ 'DSN' => '',
+ 'hostname' => 'localhost',
+ 'username' => 'postgres',
+ 'password' => 'postgres',
+ 'database' => 'test',
+ 'DBDriver' => 'Postgre',
+ 'DBPrefix' => 'db_',
+ 'pConnect' => false,
+ 'DBDebug' => true,
+ 'charset' => 'utf8',
+ 'DBCollat' => '',
+ 'swapPre' => '',
+ 'encrypt' => false,
+ 'compress' => false,
+ 'strictOn' => false,
+ 'failover' => [],
+ 'port' => 5432,
+ ],
+ 'SQLite3' => [
+ 'DSN' => '',
+ 'hostname' => 'localhost',
+ 'username' => '',
+ 'password' => '',
+ 'database' => 'database.db',
+ 'DBDriver' => 'SQLite3',
+ 'DBPrefix' => 'db_',
+ 'pConnect' => false,
+ 'DBDebug' => true,
+ 'charset' => 'utf8',
+ 'DBCollat' => '',
+ 'swapPre' => '',
+ 'encrypt' => false,
+ 'compress' => false,
+ 'strictOn' => false,
+ 'failover' => [],
+ 'port' => 3306,
+ 'foreignKeys' => true,
+ ],
+ 'SQLSRV' => [
+ 'DSN' => '',
+ 'hostname' => 'localhost',
+ 'username' => 'sa',
+ 'password' => '1Secure*Password1',
+ 'database' => 'test',
+ 'DBDriver' => 'SQLSRV',
+ 'DBPrefix' => 'db_',
+ 'pConnect' => false,
+ 'DBDebug' => true,
+ 'charset' => 'utf8',
+ 'DBCollat' => '',
+ 'swapPre' => '',
+ 'encrypt' => false,
+ 'compress' => false,
+ 'strictOn' => false,
+ 'failover' => [],
+ 'port' => 1433,
+ ],
+ 'OCI8' => [
+ 'DSN' => 'localhost:1521/XEPDB1',
+ 'hostname' => '',
+ 'username' => 'ORACLE',
+ 'password' => 'ORACLE',
+ 'database' => '',
+ 'DBDriver' => 'OCI8',
+ 'DBPrefix' => 'db_',
+ 'pConnect' => false,
+ 'DBDebug' => true,
+ 'charset' => 'AL32UTF8',
+ 'DBCollat' => '',
+ 'swapPre' => '',
+ 'encrypt' => false,
+ 'compress' => false,
+ 'strictOn' => false,
+ 'failover' => [],
+ ],
+ ];
+
+ /**
+ * Override database config
+ *
+ * @return array|bool|int|string>
+ */
+ public static function Database(): array
+ {
+ $config = [];
+
+ // Under GitHub Actions, we can set an ENV var named 'DB'
+ // so that we can test against multiple databases.
+ if (($group = getenv('DB')) && isset(self::$dbConfig[$group])) {
+ $config['tests'] = self::$dbConfig[$group];
+ }
+
+ return $config;
+ }
+}
diff --git a/tests/_support/Database/Seeds/TestQueueSeeder.php b/tests/_support/Database/Seeds/TestDatabaseQueueSeeder.php
similarity index 68%
rename from tests/_support/Database/Seeds/TestQueueSeeder.php
rename to tests/_support/Database/Seeds/TestDatabaseQueueSeeder.php
index 5d3b315..1b04659 100644
--- a/tests/_support/Database/Seeds/TestQueueSeeder.php
+++ b/tests/_support/Database/Seeds/TestDatabaseQueueSeeder.php
@@ -1,15 +1,26 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests\Support\Database\Seeds;
use CodeIgniter\Database\Seeder;
-use Michalsn\CodeIgniterQueue\Entities\QueueJob;
-use Michalsn\CodeIgniterQueue\Entities\QueueJobFailed;
-use Michalsn\CodeIgniterQueue\Enums\Status;
-use Michalsn\CodeIgniterQueue\Models\QueueJobFailedModel;
-use Michalsn\CodeIgniterQueue\Models\QueueJobModel;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Entities\QueueJobFailed;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use CodeIgniter\Queue\Models\QueueJobModel;
-class TestQueueSeeder extends Seeder
+class TestDatabaseQueueSeeder extends Seeder
{
public function run(): void
{
diff --git a/tests/_support/Database/Seeds/TestRedisQueueSeeder.php b/tests/_support/Database/Seeds/TestRedisQueueSeeder.php
new file mode 100644
index 0000000..4fe4233
--- /dev/null
+++ b/tests/_support/Database/Seeds/TestRedisQueueSeeder.php
@@ -0,0 +1,75 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Tests\Support\Database\Seeds;
+
+use CodeIgniter\Database\Seeder;
+use CodeIgniter\Exceptions\CriticalError;
+use CodeIgniter\Queue\Entities\QueueJob;
+use CodeIgniter\Queue\Entities\QueueJobFailed;
+use CodeIgniter\Queue\Enums\Status;
+use CodeIgniter\Queue\Models\QueueJobFailedModel;
+use Redis;
+use RedisException;
+use ReflectionException;
+use Tests\Support\Config\Queue as QueueConfig;
+
+class TestRedisQueueSeeder extends Seeder
+{
+ /**
+ * @throws RedisException|ReflectionException
+ */
+ public function run(): void
+ {
+ $redis = new Redis();
+
+ try {
+ $config = config(QueueConfig::class);
+ $redis->connect($config->redis['host'], ($config->redis['host'][0] === '/' ? 0 : $config->redis['port']), $config->redis['timeout']);
+ } catch (RedisException $e) {
+ throw new CriticalError('Queue: RedisException occurred with message (' . $e->getMessage() . ').');
+ }
+
+ $redis->flushDB();
+
+ $jobQueue = new QueueJob([
+ 'id' => '1234567890123456',
+ 'queue' => 'queue1',
+ 'payload' => ['job' => 'success', 'data' => []],
+ 'priority' => 'default',
+ 'status' => Status::RESERVED->value,
+ 'attempts' => 0,
+ 'available_at' => 1_697_269_864,
+ ]);
+ $redis->hSet("queues:{$jobQueue->queue}::reserved", (string) $jobQueue->id, json_encode($jobQueue));
+
+ $jobQueue = new QueueJob([
+ 'id' => '1234567890654321',
+ 'queue' => 'queue1',
+ 'payload' => ['job' => 'failure', 'data' => []],
+ 'priority' => 'default',
+ 'status' => Status::PENDING->value,
+ 'attempts' => 0,
+ 'available_at' => 1_697_269_860,
+ ]);
+ $redis->zAdd("queues:{$jobQueue->queue}:{$jobQueue->priority}", $jobQueue->available_at->timestamp, json_encode($jobQueue));
+
+ model(QueueJobFailedModel::class)->insert(new QueueJobFailed([
+ 'connection' => 'database',
+ 'queue' => 'queue1',
+ 'payload' => ['job' => 'failure', 'data' => ['failed' => true]],
+ 'priority' => 'default',
+ 'exception' => 'Exception info',
+ ]));
+ }
+}
diff --git a/tests/_support/Jobs/Failure.php b/tests/_support/Jobs/Failure.php
index 8ebdcf6..7cad334 100644
--- a/tests/_support/Jobs/Failure.php
+++ b/tests/_support/Jobs/Failure.php
@@ -1,10 +1,21 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests\Support\Jobs;
+use CodeIgniter\Queue\BaseJob;
+use CodeIgniter\Queue\Interfaces\JobInterface;
use Exception;
-use Michalsn\CodeIgniterQueue\BaseJob;
-use Michalsn\CodeIgniterQueue\Interfaces\JobInterface;
class Failure extends BaseJob implements JobInterface
{
diff --git a/tests/_support/Jobs/Success.php b/tests/_support/Jobs/Success.php
index 90b4ee4..e32f192 100644
--- a/tests/_support/Jobs/Success.php
+++ b/tests/_support/Jobs/Success.php
@@ -1,14 +1,25 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests\Support\Jobs;
-use Michalsn\CodeIgniterQueue\BaseJob;
-use Michalsn\CodeIgniterQueue\Interfaces\JobInterface;
+use CodeIgniter\Queue\BaseJob;
+use CodeIgniter\Queue\Interfaces\JobInterface;
class Success extends BaseJob implements JobInterface
{
protected int $retryAfter = 6;
- protected int $retries = 3;
+ protected int $tries = 3;
public function process(): bool
{
diff --git a/tests/_support/TestCase.php b/tests/_support/TestCase.php
index 48f2d40..0b956db 100644
--- a/tests/_support/TestCase.php
+++ b/tests/_support/TestCase.php
@@ -1,9 +1,22 @@
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
namespace Tests\Support;
+use CodeIgniter\I18n\Time;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
+use Exception;
abstract class TestCase extends CIUnitTestCase
{
@@ -17,4 +30,15 @@ protected function setUp(): void
parent::setUp();
}
+
+ /**
+ * @throws Exception
+ */
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ // Reset the current time.
+ Time::setTestNow();
+ }
}