diff --git a/.editorconfig b/.editorconfig
index 0dd272d..7fbab54 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,7 +8,7 @@ indent_style = space
indent_size = 4
trim_trailing_whitespace = true
-[*.{yml, yaml, sh, conf, neon*}]
+[*.{yml,yaml,sh,conf,neon*}]
indent_size = 2
[Makefile]
diff --git a/.gitattributes b/.gitattributes
index b35bb1f..6cf3d8c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,9 +1,10 @@
* text=auto
-/.github export-ignore
+/.* export-ignore
/tests export-ignore
/[Dd]ocker* export-ignore
-/.* export-ignore
-/phpunit.xml* export-ignore
+/*.xml export-ignore
+/*.xml.dist export-ignore
/phpstan.* export-ignore
/Makefile export-ignore
+/rector.php export-ignore
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 99aea44..0a1c52b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -2,7 +2,6 @@
name: Bug report
about: Create a report to help us improve
labels: type:bug
-assignees: tarampampam
---
## Describe the bug
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 1f76be3..b534773 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -2,7 +2,6 @@
name: Feature request
about: Suggest an idea for this package
labels: type:enhancement
-assignees: tarampampam
---
### Is your feature request related to a problem?
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index 14ff0c6..da4617f 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -2,7 +2,6 @@
name: Question
about: Ask anything
labels: type:question
-assignees: tarampampam
---
> If you have any questions feel free to ask
diff --git a/.github/workflows/cs-fix.yml b/.github/workflows/cs-fix.yml
new file mode 100644
index 0000000..0395b27
--- /dev/null
+++ b/.github/workflows/cs-fix.yml
@@ -0,0 +1,12 @@
+on:
+ push:
+ branches:
+ - '*'
+
+name: Fix Code Style
+
+jobs:
+ cs-fix:
+ permissions:
+ contents: write
+ uses: spiral/gh-actions/.github/workflows/cs-fix.yml@master
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 9fd31dc..4d3b491 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -10,4 +10,5 @@
->include(__DIR__ . '/config')
->include(__DIR__ . '/tests')
->include(__DIR__ . '/rector.php')
+ ->allowRisky(false)
->build();
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59007c3..167c72c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver].
+### Added
+
+- gRPC client support
+- Added support to set queue options globally or per job [#158]
+
+## Unreleased
+
+### Fixed
+
+- Check pipeline stats on message push [#147]
+- Edit the $ttl calculation for the RoadRunnerStore, the time calculation takes place inside the spiral/roadrunner-kv package
+- Tasks were stuck in case of an error, the "release" method did not return them to the queue.
+- The "calculateBackoff" method incorrectly took the index "$job->attempts()"
+- The "withHeader" method of the "\Spiral\RoadRunner\Jobs\Task\WritableHeadersInterface" interface expects the type "string|iterable", "int" is passed
+
+[#147]:https://github.com/roadrunner-php/laravel-bridge/issues/147
+
## v5.12.0
### Added
+- Method `setPrefix` on the `\Spiral\RoadRunnerLaravel\Cache\RoadRunnerStore` class
- Support for `v3.x` of `spiral/roadrunner-http` package [#125]
[#125]:https://github.com/roadrunner-php/laravel-bridge/issues/125
diff --git a/Makefile b/Makefile
index d10e286..d38f564 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ test: ## Execute php tests and linters
docker-compose run $(RUN_APP_ARGS) app composer test
test-cover: ## Execute php tests with coverage
- docker-compose run --rm --user "0:0" -e 'XDEBUG_MODE=coverage' app sh -c 'docker-php-ext-enable xdebug && su $(shell whoami) -s /bin/sh -c "composer phpunit-cover"'
+ docker-compose run --rm --user "0:0" -e 'XDEBUG_MODE=coverage' app sh -c 'docker-php-ext-enable xdebug && su $(shell whoami) -s /bin/sh -c "composer phpunit:cover"'
shell: ## Start shell into container with php
docker-compose run $(RUN_APP_ARGS) app sh
diff --git a/README.md b/README.md
index 6a00836..abbb585 100644
--- a/README.md
+++ b/README.md
@@ -12,49 +12,57 @@ Easy way for connecting [RoadRunner][roadrunner] and [Laravel][laravel] applicat
## Why Use This Package?
-Laravel provides the [Octane](https://laravel.com/docs/12.x/octane) package which partially supports RoadRunner as an
-application server, but RoadRunner offers much more than just HTTP capabilities. It also includes Jobs, Temporal, gRPC,
-and other plugins.
+This package provides complete Laravel integration with RoadRunner, offering:
+
+- Support for HTTP and other RoadRunner plugins like gRPC, Queue, KeyValue, and more.
+- [Temporal](https://temporal.io/) integration
+- Full RoadRunner configuration control

-> **Note:** There is an article that explains all the RoadRunner
-> plugins: https://butschster.medium.com/roadrunner-an-underrated-powerhouse-for-php-applications-46410b0abc
+> [!TIP]
+> [There is an article][rr-plugins-article] that explains all the RoadRunner plugins.
-The main limitation of Octane is that it has a built-in worker only for the HTTP plugin and doesn't provide the ability
-to create additional workers for other RoadRunner plugins, restricting its use to just the HTTP plugin.
+## Table of Contents
-Our **Laravel Bridge** solves this problem by taking a different approach:
+- [Get Started](#get-started)
+ - [Installation](#installation)
+ - [Configuration](#configuration)
+ - [Starting the Server](#starting-the-server)
+- [How It Works](#how-it-works)
+- [Supported Plugins](#supported-plugins)
+ - [HTTP Plugin](#http-plugin)
+ - [Jobs (Queue) Plugin](#jobs-queue-plugin)
+ - [gRPC Plugin](#grpc-plugin)
+ - [gRPC Client](#grpc-client)
+ - [Temporal](#temporal)
+- [Custom Workers](#custom-workers)
+- [Support](#support)
+- [License](#license)
-1. We include `laravel/octane` in our package and reuse its **SDK** for clearing the state of Laravel applications
-2. We add support for running and configuring multiple workers for different RoadRunner plugins
-3. By reusing Octane's functionality for state clearing, we automatically support all third-party packages that are
- compatible with Octane
+## Get Started
-**This way, you get the best of both worlds:** Octane's state management and RoadRunner's full plugin ecosystem.
+### Installation
-## Installation
+First, install the Laravel Bridge package via Composer:
-```shell script
+```shell
composer require roadrunner-php/laravel-bridge
```
-After that you can "publish" package configuration file (`./config/roadrunner.php`) using next command:
+Publish the configuration file:
-```shell script
+```shell
php artisan vendor:publish --provider='Spiral\RoadRunnerLaravel\ServiceProvider' --tag=config
```
-## Usage
-
-After package installation, you can download and install [RoadRunner][roadrunner] binary
-using Composer script:
+Download and install RoadRunner binary using DLoad:
-```bash
-composer get:rr
+```shell
+./vendor/bin/dload get rr
```
-### Basic Configuration (.rr.yaml)
+### Configuration
Create a `.rr.yaml` configuration file in your project root:
@@ -65,7 +73,6 @@ rpc:
server:
command: 'php vendor/bin/rr-worker start'
- relay: pipes
http:
address: 0.0.0.0:8080
@@ -82,49 +89,30 @@ http:
forbid: [ ".php" ]
```
-## RoadRunner Worker Configuration
-
-You can configure workers in `config/roadrunner.php` file in the `workers` section:
-
-```php
-use Spiral\RoadRunner\Environment\Mode;
-use Spiral\RoadRunnerLaravel\Grpc\GrpcWorker;
-use Spiral\RoadRunnerLaravel\Http\HttpWorker;
-use Spiral\RoadRunnerLaravel\Queue\QueueWorker;
-use Spiral\RoadRunnerLaravel\Temporal\TemporalWorker;
+### Starting the Server
-return [
- // ... other configuration options ...
+Start the RoadRunner server with:
- 'workers' => [
- Mode::MODE_HTTP => HttpWorker::class,
- Mode::MODE_JOBS => QueueWorker::class,
- Mode::MODE_GRPC => GrpcWorker::class,
- Mode::MODE_TEMPORAL => TemporalWorker::class,
- ],
-];
+```shell
+./rr serve
```
-As you can see, there are several predefined workers for HTTP, Jobs, gRPC, and Temporal. Feel free to replace any of
-them with your implementation if needed. Or create your own worker, for example,
-for [Centrifugo](https://docs.roadrunner.dev/docs/plugins/centrifuge), [TCP](https://docs.roadrunner.dev/docs/plugins/tcp)
-or any other plugin.
-
## How It Works
-In the server section of the RoadRunner config, we specify the command to start our worker:
+RoadRunner creates a worker pool by executing the command specified in the server configuration:
```yaml
server:
command: 'php vendor/bin/rr-worker start'
- relay: pipes
```
-When RoadRunner server creates a worker pool for a specific plugin, it exposes an environment variable `RR_MODE` that
-indicates which plugin is being used. Our worker checks this variable to determine which Worker class should handle the
-request based on the configuration in `roadrunner.php`.
+When RoadRunner creates a worker pool for a specific plugin,
+it sets the `RR_MODE` environment variable to indicate which plugin is being used.
+The Laravel Bridge checks this variable to determine
+which Worker class should handle the request based on your configuration.
-The selected worker starts listening for requests from the RoadRunner server and handles them using the Octane worker,
+The selected worker then listens for requests from the RoadRunner server
+and handles them using the [Octane][octane] worker,
which clears the application state after each task (request, command, etc.).
## Supported Plugins
@@ -148,16 +136,17 @@ http:
forbid: [ ".php" ]
```
-> **Note:** Read more about the HTTP plugin in
-> the [RoadRunner documentation][https://docs.roadrunner.dev/docs/http/http].
+> [!TIP]
+> Read more about the HTTP plugin in the [RoadRunner documentation][roadrunner-docs-http].
### Jobs (Queue) Plugin
-The Queue plugin allows you to use RoadRunner as a queue driver for Laravel.
+The Queue plugin allows you to use RoadRunner as a queue driver for Laravel
+without additional services like Redis or a database.
#### Configuration
-First, add the Queue Service Provider in your `config/app.php`:
+First, add the Queue Service Provider in `config/app.php`:
```php
'providers' => [
@@ -166,7 +155,7 @@ First, add the Queue Service Provider in your `config/app.php`:
],
```
-Then, configure a new connection in your `config/queue.php`:
+Then, configure a new connection in `config/queue.php`:
```php
'connections' => [
@@ -192,10 +181,7 @@ jobs:
config: { }
```
-> **Note:** Read more about the Jobs plugin in
-> the [RoadRunner documentation][https://docs.roadrunner.dev/docs/queues-and-jobs/overview-queues].
-
-Don't forget to set the `QUEUE_CONNECTION` environment variable in your `.env` file:
+Set the `QUEUE_CONNECTION` environment variable in your `.env` file:
```dotenv
QUEUE_CONNECTION=roadrunner
@@ -203,6 +189,9 @@ QUEUE_CONNECTION=roadrunner
That's it! You can now dispatch jobs to the RoadRunner queue without any additional services like Redis or Database.
+> [!TIP]
+> Read more about the Jobs plugin in the [RoadRunner documentation][roadrunner-docs-jobs].
+
### gRPC Plugin
The gRPC plugin enables serving gRPC services with your Laravel application.
@@ -231,6 +220,66 @@ return [
];
```
+#### gRPC Client Usage
+
+The package also allows your Laravel application to act as a gRPC client, making requests to external gRPC services.
+
+##### Client Configuration
+
+Add your gRPC client configuration to `config/roadrunner.php`:
+
+```php
+return [
+ // ... other configuration
+ 'grpc' => [
+ // ... server config
+ 'clients' => [
+ 'services' => [
+ [
+ 'connection' => '127.0.0.1:9001', // gRPC server address
+ 'interfaces' => [
+ \App\Grpc\EchoServiceInterface::class,
+ ],
+ // 'tls' => [ ... ] // Optional TLS configuration
+ ],
+ ],
+ // 'interceptors' => [ ... ] // Optional interceptors
+ ],
+ ],
+];
+```
+
+##### Using the gRPC Client in Laravel
+
+You can inject `Spiral\Grpc\Client\ServiceClientProvider` into your services or controllers to obtain a gRPC client instance:
+
+```php
+use Spiral\Grpc\Client\ServiceClientProvider;
+use App\Grpc\EchoServiceInterface;
+use App\Grpc\EchoRequest;
+
+class GrpcController extends Controller
+{
+ public function callService(ServiceClientProvider $provider)
+ {
+ /** @var EchoServiceInterface $client */
+ $client = $provider->get(EchoServiceInterface::class);
+
+ $request = new EchoRequest();
+ $request->setMessage('Hello from client!');
+
+ $response = $client->Echo($request);
+
+ return $response->getMessage();
+ }
+}
+```
+
+> **Note:**
+> - Make sure you have generated the PHP classes from your `.proto` files (using `protoc`).
+> - The `connection` and `interfaces` must match the service you want to call.
+> - You can configure multiple gRPC client services as needed.
+
### Temporal
Temporal is a workflow engine that enables orchestration of microservices and provides sophisticated workflow
@@ -263,36 +312,54 @@ return [
];
```
-Download Temporal binary for development purposes using the following command:
+Download Temporal binary for development:
```bash
-composer get:temporal
+./vendor/bin/dload get temporal
```
-To start the Temporal server, you can use the following command:
+Start the Temporal dev server:
```bash
./temporal server start-dev --log-level error --color always
```
-#### Useful links
+#### Useful Links
- [PHP SDK on GitHub](https://github.com/temporalio/sdk-php)
- [PHP SDK docs](https://docs.temporal.io/develop/php/)
- [Code samples](https://github.com/temporalio/samples-php)
- [Taxi service sample](https://github.com/butschster/podlodka-taxi-service)
-## Starting RoadRunner Server
+## Custom Workers
-To start the RoadRunner server:
+The RoadRunner Laravel Bridge comes with several predefined workers for common plugins,
+but you can easily create your own custom workers for any RoadRunner plugin.
+This section explains how to create and register custom workers in your application.
-```shell script
-./rr serve
+### Understanding Workers
+
+Workers are responsible for handling requests from the RoadRunner server
+and processing them in your Laravel application.
+The predefined workers are configured in the `config/roadrunner.php` file:
+
+```php
+return [
+ // ... other configuration options ...
+
+ 'workers' => [
+ Mode::MODE_HTTP => HttpWorker::class,
+ Mode::MODE_JOBS => QueueWorker::class,
+ Mode::MODE_GRPC => GrpcWorker::class,
+ Mode::MODE_TEMPORAL => TemporalWorker::class,
+ ],
+];
```
-## Custom Workers
+### Creating Custom Workers
-You can create your own custom workers by implementing the `Spiral\RoadRunnerLaravel\WorkerInterface`:
+To create a custom worker, you need to implement the `Spiral\RoadRunnerLaravel\WorkerInterface`.
+This interface has a single method, `start()`, which is called when the worker is started by the RoadRunner server:
```php
namespace App\Workers;
@@ -304,30 +371,46 @@ class CustomWorker implements WorkerInterface
{
public function start(WorkerOptionsInterface $options): void
{
- // Your custom worker implementation
+ // Your worker implementation goes here
+ // This method should handle requests from the RoadRunner server
}
}
```
-Then register it in the `config/roadrunner.php`:
+### Registering Custom Workers
+
+After creating your custom worker, you need to register it in the `config/roadrunner.php` file:
```php
return [
+ // ... other configuration options ...
+
'workers' => [
- 'custom' => \App\Workers\CustomWorker::class,
+ // Existing workers
+ Mode::MODE_HTTP => HttpWorker::class,
+ Mode::MODE_JOBS => QueueWorker::class,
+
+ // Your custom worker for a custom or built-in plugin
+ 'custom_plugin' => \App\Workers\CustomWorker::class,
],
];
```
+The key in the `workers` array should match the value of the `RR_MODE` environment variable
+set by the RoadRunner server for your plugin.
+
## Support
-If you find this package helpful, please consider giving it a star on GitHub. Your support helps make the project more visible to other developers who might benefit from it!
+If you find this package helpful, please consider giving it a star on GitHub.
+Your support helps make the project more visible to other developers who might benefit from it!
[![Issues][badge_issues]][link_issues]
[![Issues][badge_pulls]][link_pulls]
If you find any package errors, please, [make an issue][link_create_issue] in a current repository.
+You can also [sponsor this project][link_sponsor] to help ensure its continued development and maintenance.
+
## License
MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
@@ -374,10 +457,14 @@ MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
[link_pulls]:https://github.com/roadrunner-php/laravel-bridge/pulls
+[link_sponsor]:https://github.com/sponsors/roadrunner-server
+
[link_license]:https://github.com/roadrunner-php/laravel-bridge/blob/master/LICENSE
[getcomposer]:https://getcomposer.org/download/
+[dload]:https://github.com/php-internal/dload
+
[roadrunner]:https://github.com/roadrunner-server/roadrunner
[roadrunner_config]:https://github.com/roadrunner-server/roadrunner/blob/master/.rr.yaml
@@ -387,3 +474,11 @@ MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
[laravel_events]:https://laravel.com/docs/events
[roadrunner-binary-releases]:https://github.com/roadrunner-server/roadrunner/releases
+
+[roadrunner-docs-jobs]:https://docs.roadrunner.dev/docs/queues-and-jobs/overview-queues
+
+[roadrunner-docs-http]:https://docs.roadrunner.dev/docs/http/http
+
+[octane]:https://laravel.com/docs/12.x/octane
+
+[rr-plugins-article]:https://butschster.medium.com/roadrunner-an-underrated-powerhouse-for-php-applications-46410b0abc
diff --git a/composer.json b/composer.json
index 61e412b..35a0f59 100644
--- a/composer.json
+++ b/composer.json
@@ -1,103 +1,104 @@
{
- "name": "roadrunner-php/laravel-bridge",
- "type": "library",
- "description": "Laravel integration for RoadRunner with support for HTTP, Jobs, gRPC, and Temporal plugins - going beyond Octane's capabilities",
- "keywords": [
- "laravel",
- "bridge",
- "roadrunner",
- "temporal",
- "grpc",
- "queue",
- "cache",
- "http"
- ],
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/roadrunner-server"
- }
- ],
- "license": "MIT",
- "authors": [
- {
- "name": "butschster",
- "homepage": "https://github.com/butschster"
+ "name": "roadrunner-php/laravel-bridge",
+ "type": "library",
+ "description": "Laravel integration for RoadRunner with support for HTTP, Jobs, gRPC, and Temporal plugins - going beyond Octane's capabilities",
+ "keywords": [
+ "laravel",
+ "bridge",
+ "roadrunner",
+ "temporal",
+ "grpc",
+ "queue",
+ "cache",
+ "http"
+ ],
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/roadrunner-server"
+ }
+ ],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "butschster",
+ "homepage": "https://github.com/butschster"
+ },
+ {
+ "name": "roxblnfk",
+ "homepage": "https://github.com/roxblnfk"
+ },
+ {
+ "name": "tarampampam",
+ "homepage": "https://github.com/tarampampam"
+ }
+ ],
+ "require": {
+ "php": "^8.2",
+ "spiral/roadrunner-kv": "^4.0",
+ "spiral/roadrunner-jobs": "^4.0",
+ "spiral/roadrunner-grpc": "^3.5",
+ "laravel/octane": "^2.9",
+ "spiral/roadrunner-http": "^3.0",
+ "spiral/roadrunner-worker": "^3.0",
+ "temporal/sdk": "^2.0",
+ "internal/dload": "^1.1",
+ "spiral/grpc-client": "^1.0.0-rc1"
},
- {
- "name": "roxblnfk",
- "homepage": "https://github.com/roxblnfk"
+ "require-dev": {
+ "laravel/framework": "^12.0",
+ "spiral/code-style": "^2.2.2",
+ "rector/rector": "^2.0",
+ "guzzlehttp/guzzle": "^7.0",
+ "mockery/mockery": "^1.6",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^10.0"
},
- {
- "name": "tarampampam",
- "homepage": "https://github.com/tarampampam"
- }
- ],
- "require": {
- "php": "^8.2",
- "spiral/roadrunner-kv": "^4.0",
- "spiral/roadrunner-jobs": "^4.0",
- "spiral/roadrunner-grpc": "^3.5",
- "laravel/octane": "^2.9",
- "spiral/roadrunner-http": "^3.0",
- "spiral/roadrunner-worker": "^3.0",
- "temporal/sdk": "^2.0",
- "internal/dload": "^1.1"
- },
- "require-dev": {
- "laravel/framework": "^12.0",
- "spiral/code-style": "^2.2.2",
- "rector/rector": "^2.0",
- "guzzlehttp/guzzle": "^7.0",
- "mockery/mockery": "^1.6",
- "phpstan/phpstan": "^2.1",
- "phpunit/phpunit": "^10.0"
- },
- "autoload": {
- "psr-4": {
- "Spiral\\RoadRunnerLaravel\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "Spiral\\RoadRunnerLaravel\\Tests\\": "tests/"
- }
- },
- "bin": [
- "bin/rr-worker"
- ],
- "scripts": {
- "get:rr": "dload get rr",
- "get:temporal": "dload get temporal",
- "get:protoc": "dload get protoc protoc-gen-php-grpc",
- "cs-check": "vendor/bin/php-cs-fixer fix --dry-run",
- "cs-fix": "vendor/bin/php-cs-fixer fix",
- "refactor": "rector process --config=rector.php",
- "refactor:ci": "rector process --config=rector.php --dry-run --ansi",
- "phpunit": "@php ./vendor/bin/phpunit --no-coverage",
- "phpunit-cover": "@php ./vendor/bin/phpunit",
- "phpstan": "@php ./vendor/bin/phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi",
- "test": [
- "@phpstan",
- "@phpunit"
+ "autoload": {
+ "psr-4": {
+ "Spiral\\RoadRunnerLaravel\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Spiral\\RoadRunnerLaravel\\Tests\\": "tests/"
+ }
+ },
+ "bin": [
+ "bin/rr-worker"
],
- "test-cover": [
- "@phpstan",
- "@phpunit-cover"
- ]
- },
- "extra": {
- "laravel": {
- "providers": [
- "Spiral\\RoadRunnerLaravel\\ServiceProvider",
- "Spiral\\RoadRunnerLaravel\\Queue\\QueueServiceProvider",
- "Spiral\\RoadRunnerLaravel\\Cache\\CacheServiceProvider",
- "Spiral\\RoadRunnerLaravel\\Temporal\\TemporalServiceProvider"
- ]
+ "scripts": {
+ "get:rr": "dload get rr",
+ "get:temporal": "dload get temporal",
+ "get:protoc": "dload get protoc protoc-gen-php-grpc",
+ "cs:check": "php-cs-fixer fix --dry-run",
+ "cs:fix": "php-cs-fixer fix",
+ "refactor": "rector process --config=rector.php",
+ "refactor:ci": "rector process --config=rector.php --dry-run --ansi",
+ "phpunit": "phpunit --no-coverage",
+ "phpunit-cover": "phpunit",
+ "phpstan": "phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi",
+ "test": "phpunit --color=always --testdox",
+ "test:unit": "phpunit --color=always --testsuite=Unit",
+ "test:feat": "phpunit --color=always --testsuite=Feature",
+ "test-cover": [
+ "phpstan",
+ "@putenv XDEBUG_MODE=coverage",
+ "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always"
+ ]
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Spiral\\RoadRunnerLaravel\\ServiceProvider",
+ "Spiral\\RoadRunnerLaravel\\Queue\\QueueServiceProvider",
+ "Spiral\\RoadRunnerLaravel\\Cache\\CacheServiceProvider",
+ "Spiral\\RoadRunnerLaravel\\Temporal\\TemporalServiceProvider"
+ ]
+ }
+ },
+ "support": {
+ "issues": "https://github.com/roadrunner-php/laravel-bridge/issues",
+ "source": "https://github.com/roadrunner-php/laravel-bridge"
}
- },
- "support": {
- "issues": "https://github.com/roadrunner-php/laravel-bridge/issues",
- "source": "https://github.com/roadrunner-php/laravel-bridge"
- }
}
diff --git a/config/roadrunner.php b/config/roadrunner.php
index 14bb231..77837ae 100644
--- a/config/roadrunner.php
+++ b/config/roadrunner.php
@@ -18,6 +18,31 @@
'services' => [
// GreeterInterface::class => new Greeter::class,
],
+ 'clients' => [
+ 'interceptors' => [
+ // LoggingInterceptor::class,
+ ],
+ 'services' => [
+ // [
+ // 'connection' => 'my-grpc-server:9002',
+ // 'interfaces' => [
+ // GreeterInterface::class,
+ // ],
+ // ],
+ // [
+ // 'connection' => 'my-secure-grpc-server:9002',
+ // 'interfaces' => [
+ // GreeterInterface::class,
+ // ],
+ // 'tls' => [
+ // 'rootCerts' => '/path/to/ca.pem',
+ // 'privateKey' => '/path/to/client.key',
+ // 'certChain' => '/path/to/client.crt',
+ // 'serverName' => 'my.grpc.server',
+ // ],
+ // ],
+ ],
+ ],
],
'temporal' => [
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 1680c26..5cb43d4 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -2,8 +2,12 @@
-
+ cacheResultFile="runtime/phpunit/result.cache"
+ colors="true"
+ stderr="true"
+ displayDetailsOnTestsThatTriggerWarnings="true"
+ displayDetailsOnTestsThatTriggerDeprecations="true"
+>
./tests/Unit
@@ -12,25 +16,22 @@
./tests/Feature
-
-
+
- ./src
+ src
- ./vendor
- ./tests
+ tests
+
+
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
diff --git a/src/Cache/RoadRunnerStore.php b/src/Cache/RoadRunnerStore.php
index 9239e52..90af3fd 100644
--- a/src/Cache/RoadRunnerStore.php
+++ b/src/Cache/RoadRunnerStore.php
@@ -6,14 +6,16 @@
use Illuminate\Cache\TaggableStore;
use Illuminate\Contracts\Cache\LockProvider;
-use Illuminate\Support\InteractsWithTime;
use Spiral\RoadRunner\KeyValue\StorageInterface;
final class RoadRunnerStore extends TaggableStore implements LockProvider
{
- use InteractsWithTime;
+ private string $prefix;
- public function __construct(private StorageInterface $storage, private string $prefix = '') {}
+ public function __construct(private StorageInterface $storage, string $prefix = '')
+ {
+ $this->setPrefix($prefix);
+ }
public function get($key)
{
@@ -39,7 +41,7 @@ public function many(array $keys)
public function put($key, $value, $seconds)
{
- return $this->storage->set($this->prefix . $key, $value, $this->calculateExpiration($seconds));
+ return $this->storage->set($this->prefix . $key, $value, $seconds);
}
public function putMany(array $values, $seconds)
@@ -52,7 +54,7 @@ public function putMany(array $values, $seconds)
return $this->storage->setMultiple(
$prefixedValues,
- $this->calculateExpiration($seconds),
+ $seconds,
);
}
@@ -86,32 +88,13 @@ public function flush()
return $this->storage->clear();
}
- public function getPrefix()
+ public function getPrefix(): string
{
return $this->prefix;
}
- /**
- * Set the cache key prefix.
- */
public function setPrefix(string $prefix): void
{
- $this->prefix = !empty($prefix) ? $prefix . ':' : '';
- }
-
- /**
- * Get the expiration time of the key.
- */
- protected function calculateExpiration(int $seconds): int
- {
- return $this->toTimestamp($seconds);
- }
-
- /**
- * Get the UNIX timestamp for the given number of seconds.
- */
- protected function toTimestamp(int $seconds): int
- {
- return $seconds > 0 ? $this->availableAt($seconds) : 0;
+ $this->prefix = $prefix;
}
}
diff --git a/src/Queue/Contract/HasQueueOptions.php b/src/Queue/Contract/HasQueueOptions.php
new file mode 100644
index 0000000..85f7c64
--- /dev/null
+++ b/src/Queue/Contract/HasQueueOptions.php
@@ -0,0 +1,10 @@
+resolveName(
- ) . ' has been attempted too many times or run too long. The job may have previously timed out.',
+ $job->resolveName() . ' has been attempted too many times or run too long. The job may have previously timed out.',
);
}
@@ -267,9 +266,9 @@ protected function calculateBackoff(RoadRunnerJob $job, WorkerOptions $options):
',',
\method_exists($job, 'backoff') && !\is_null($job->backoff())
? $job->backoff()
- : $options->backoff,
+ : (string) $options->backoff,
);
- return (int) ($backoff[$job->attempts() - 1] ?? last($backoff));
+ return (int) ($backoff[$job->attempts()] ?? last($backoff));
}
}
diff --git a/src/Queue/RoadRunnerConnector.php b/src/Queue/RoadRunnerConnector.php
index 86cffbd..ef12a7d 100644
--- a/src/Queue/RoadRunnerConnector.php
+++ b/src/Queue/RoadRunnerConnector.php
@@ -26,6 +26,7 @@ public function connect(array $config): Queue
new Jobs($rpc),
$rpc,
$config['queue'],
+ $config['options'] ?? [],
);
}
}
diff --git a/src/Queue/RoadRunnerJob.php b/src/Queue/RoadRunnerJob.php
index 24364d5..8a4d8ba 100644
--- a/src/Queue/RoadRunnerJob.php
+++ b/src/Queue/RoadRunnerJob.php
@@ -48,12 +48,24 @@ public function fire(): void
$this->task->complete();
}
+ public function release($delay = 0): void
+ {
+ $attempts = $this->attempts();
+
+ $this->task
+ ->withDelay($delay)
+ ->withHeader('attempts', (string) ++$attempts)
+ ->requeue('release');
+
+ parent::release($delay);
+ }
+
protected function failed($e): void
{
$attempts = $this->attempts();
$this->task
- ->withHeader('attempts', $attempts + 1)
+ ->withHeader('attempts', (string) ++$attempts)
->fail($e->getMessage());
parent::failed($e);
diff --git a/src/Queue/RoadRunnerQueue.php b/src/Queue/RoadRunnerQueue.php
index 27424b7..6f5f388 100644
--- a/src/Queue/RoadRunnerQueue.php
+++ b/src/Queue/RoadRunnerQueue.php
@@ -12,7 +12,12 @@
use RoadRunner\Jobs\DTO\V1\Stats;
use Spiral\Goridge\RPC\RPCInterface;
use Spiral\RoadRunner\Jobs\Jobs;
+use Spiral\RoadRunner\Jobs\KafkaOptions;
+use Spiral\RoadRunner\Jobs\Options;
+use Spiral\RoadRunner\Jobs\OptionsInterface;
+use Spiral\RoadRunner\Jobs\Queue\Driver;
use Spiral\RoadRunner\Jobs\QueueInterface;
+use Spiral\RoadRunnerLaravel\Queue\Contract\HasQueueOptions;
final class RoadRunnerQueue extends Queue implements QueueContract
{
@@ -20,6 +25,7 @@ public function __construct(
private readonly Jobs $jobs,
private readonly RPCInterface $rpc,
private readonly string $default = 'default',
+ private readonly array $defaultOptions = [],
) {}
public function push($job, $data = '', $queue = null): string
@@ -29,13 +35,13 @@ public function push($job, $data = '', $queue = null): string
$this->createPayload($job, $queue, $data),
$queue,
null,
- fn($payload, $queue) => $this->pushRaw($payload, $queue),
+ fn($payload, $queue) => $this->pushRaw($payload, $queue, $this->getJobOverrideOptions($job)),
);
}
public function pushRaw($payload, $queue = null, array $options = []): string
{
- $queue = $this->getQueue($queue);
+ $queue = $this->getQueue($queue, $options);
$task = $queue->dispatch(
$queue
@@ -52,7 +58,7 @@ public function later($delay, $job, $data = '', $queue = null): string
$this->createPayload($job, $queue, $data),
$queue,
$delay,
- fn($payload, $queue) => $this->laterRaw($delay, $payload, $queue),
+ fn($payload, $queue) => $this->laterRaw($delay, $payload, $queue, $this->getJobOverrideOptions($job)),
);
}
@@ -81,29 +87,9 @@ protected function availableAt($delay = 0): int
: $delay;
}
- /**
- * Push a raw job onto the queue after a delay.
- */
- private function laterRaw(
- \DateTimeInterface|\DateInterval|int $delay,
- array $payload,
- ?string $queue = null,
- ): string {
- $queue = $this->getQueue($queue);
-
- $task = $queue->dispatch(
- $queue
- ->create($payload['displayName'] ?? Uuid::uuid4()->toString())
- ->withValue($payload)
- ->withDelay($this->availableAt($delay)),
- );
-
- return $task->getId();
- }
-
- private function getQueue(?string $queue = null): QueueInterface
+ private function getQueue(?string $queue = null, array $options = []): QueueInterface
{
- $queue = $this->jobs->connect($queue ?? $this->default);
+ $queue = $this->jobs->connect($queue ?? $this->default, $this->getQueueOptions($options));
if (!$this->getStats($queue->getName())->getReady()) {
$queue->resume();
@@ -112,6 +98,22 @@ private function getQueue(?string $queue = null): QueueInterface
return $queue;
}
+ private function getQueueOptions(array $overrides = []): OptionsInterface
+ {
+ $config = array_merge($this->defaultOptions, $overrides);
+ $options = new Options(
+ $config['delay'] ?? OptionsInterface::DEFAULT_DELAY,
+ $config['priority'] ?? OptionsInterface::DEFAULT_PRIORITY,
+ $config['auto_ack'] ?? OptionsInterface::DEFAULT_AUTO_ACK,
+ );
+
+ return match ($config['driver'] ?? null) {
+ Driver::Kafka => KafkaOptions::from($options)
+ ->withTopic($config['topic'] ?? ($this->defaultOptions['topic'] ?? '')),
+ default => $options,
+ };
+ }
+
private function getStats(?string $queue = null): Stat
{
$queue ??= $this->default;
@@ -120,11 +122,48 @@ private function getStats(?string $queue = null): Stat
/** @var Stat $stat */
foreach ($stats as $stat) {
- if ($stat->getQueue() === $queue) {
+ if ($stat->getPipeline() === $queue) {
return $stat;
}
}
return new Stat();
}
+
+ private function getJobOverrideOptions(string|object $job): array
+ {
+ if (is_string($job) && class_exists($job)) {
+ $job = app($job);
+ }
+
+ if ($job instanceof HasQueueOptions) {
+ $options = $job->queueOptions();
+ if ($options instanceof Options) {
+ return $options->toArray();
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Push a raw job onto the queue after a delay.
+ */
+ private function laterRaw(
+ \DateTimeInterface|\DateInterval|int $delay,
+ array $payload,
+ ?string $queue = null,
+ array $options = [],
+ ): string {
+ $queue = $this->getQueue($queue, $options);
+
+ $task = $queue->dispatch(
+ $queue
+ ->create($payload['displayName'] ?? Uuid::uuid4()->toString())
+ ->withValue($payload)
+ ->withDelay($this->availableAt($delay)),
+ );
+
+ return $task->getId();
+ }
}
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
index 8bf06ec..54d21e7 100644
--- a/src/ServiceProvider.php
+++ b/src/ServiceProvider.php
@@ -4,8 +4,15 @@
namespace Spiral\RoadRunnerLaravel;
+use Illuminate\Contracts\Container\Container;
use Spiral\Attributes\AttributeReader;
use Spiral\Attributes\ReaderInterface;
+use Spiral\Core\FactoryInterface;
+use Spiral\Grpc\Client\Config\ConnectionConfig;
+use Spiral\Grpc\Client\Config\GrpcClientConfig;
+use Spiral\Grpc\Client\Config\ServiceConfig;
+use Spiral\Grpc\Client\Config\TlsConfig;
+use Spiral\Grpc\Client\ServiceClientProvider;
final class ServiceProvider extends \Illuminate\Support\ServiceProvider
{
@@ -23,6 +30,7 @@ public function register(): void
{
$this->app->singleton(ReaderInterface::class, AttributeReader::class);
$this->initializeConfigs();
+ $this->initializeGrpcClientServices();
}
protected function initializeConfigs(): void
@@ -33,4 +41,70 @@ protected function initializeConfigs(): void
\realpath(self::getConfigPath()) => config_path(\basename(self::getConfigPath())),
], 'config');
}
+
+ protected function initializeGrpcClientServices(): void
+ {
+ $this->app->singleton(FactoryInterface::class, fn() => new class($this->app) implements FactoryInterface {
+ public function __construct(
+ private readonly Container $container,
+ ) {}
+
+ /**
+ * @param class-string $class
+ * @param array $parameters
+ */
+ public function make(string $class, array $parameters = []): object
+ {
+ return $this->container->make($class, $parameters);
+ }
+ });
+ $this->app->singleton(ServiceClientProvider::class, function () {
+ $toNonEmptyStringOrNull = static fn($value): ?string => (is_string($value) && $value !== '') ? $value : null;
+ /**
+ * @var array,
+ * tls?: array{
+ * rootCerts?: non-empty-string|null,
+ * privateKey?: non-empty-string|null,
+ * certChain?: non-empty-string|null,
+ * serverName?: non-empty-string|null
+ * }
+ * }>
+ */
+ $rawServices = config('roadrunner.grpc.clients.services', []);
+ $services = collect($rawServices);
+ $serviceConfigs = [];
+ foreach ($services as $service) {
+ $tls = null;
+ if (isset($service['tls'])) {
+ $tlsConfig = $service['tls'];
+ $tls = new TlsConfig(
+ $toNonEmptyStringOrNull($tlsConfig['rootCerts'] ?? null),
+ $toNonEmptyStringOrNull($tlsConfig['privateKey'] ?? null),
+ $toNonEmptyStringOrNull($tlsConfig['certChain'] ?? null),
+ $toNonEmptyStringOrNull($tlsConfig['serverName'] ?? null),
+ );
+ }
+ /** @var non-empty-string $connection */
+ $connection = $service['connection'];
+ /** @var list $interfaces */
+ $interfaces = $service['interfaces'];
+ $serviceConfigs[] = new ServiceConfig(
+ connections: new ConnectionConfig($connection, $tls),
+ interfaces: $interfaces,
+ );
+ }
+
+ /** @var array|\Spiral\Core\Container\Autowire<\Spiral\Interceptors\InterceptorInterface>|\Spiral\Interceptors\InterceptorInterface> $interceptors */
+ $interceptors = config('roadrunner.grpc.clients.interceptors', []);
+ $config = new GrpcClientConfig(
+ interceptors: $interceptors,
+ services: $serviceConfigs,
+ );
+
+ return new ServiceClientProvider($config, $this->app->make(FactoryInterface::class));
+ });
+
+ }
}
diff --git a/src/Temporal/Attribute/AssignWorker.php b/src/Temporal/Attribute/AssignWorker.php
index be1b594..5870f8e 100644
--- a/src/Temporal/Attribute/AssignWorker.php
+++ b/src/Temporal/Attribute/AssignWorker.php
@@ -4,7 +4,9 @@
namespace Spiral\RoadRunnerLaravel\Temporal\Attribute;
-#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
+use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
+
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor]
final readonly class AssignWorker
{
/**