Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,39 @@ permissions:
jobs:
phpunit:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
include:
- php: "8.3"
laravel: 11
- php: "8.3"
laravel: 12
- php: "8.3"
laravel: 13
- php: "8.4"
laravel: 13

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
php-version: "${{ matrix.php }}"
extensions: swoole
coverage: none
tools: composer:v2
cache: composer

- name: Install dependencies
run: composer install --no-interaction --prefer-dist
run: |
composer require "laravel/framework:^${{ matrix.laravel }}.0" --no-interaction --no-update
composer update --no-interaction --prefer-dist

- name: Run tests
run: vendor/bin/phpunit -c phpunit.xml
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ With the same 1-second blocking operations, this achieves **2,773+ requests/seco

## 📦 Installation

### Requirements

- PHP 8.1+ (PHP 8.3+ when running Laravel 13)
- Laravel 10, 11, 12 or 13
- The `swoole` PHP extension

Install via Composer from [Packagist](https://packagist.org/packages/modelslab/octane-coroutine):

```bash
Expand Down
14 changes: 7 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@
],
"require": {
"php": "^8.1.0",
"hyperf/context": "^3.1",
"hyperf/pool": "^3.1",
"hyperf/context": "^3.1|^3.2",
"hyperf/pool": "^3.1|^3.2",
"laminas/laminas-diactoros": "^3.0",
"laravel/framework": "^10.10.1|^11.0|^12.0",
"laravel/framework": "^10.10.1|^11.0|^12.0|^13.0",
"laravel/prompts": "^0.1.24|^0.2.0|^0.3.0",
"laravel/serializable-closure": "^1.3|^2.0",
"nesbot/carbon": "^2.66.0|^3.0",
"symfony/console": "^6.0|^7.0",
"symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0"
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0|^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.6.1",
Expand All @@ -48,9 +48,9 @@
"livewire/livewire": "^2.12.3|^3.0",
"mockery/mockery": "^1.5.1",
"nunomaduro/collision": "^6.4.0|^7.5.2|^8.0",
"orchestra/testbench": "^8.21|^9.0|^10.0",
"orchestra/testbench": "^8.21|^9.0|^10.0|^11.0",
"phpstan/phpstan": "^2.1.7",
"phpunit/phpunit": "^10.4|^11.5",
"phpunit/phpunit": "^10.4|^11.5|^12.0|^13.0",
"spiral/roadrunner-cli": "^2.6.0",
"spiral/roadrunner-http": "^3.3.0"
},
Expand Down
20 changes: 20 additions & 0 deletions src/Cache/OctaneStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,26 @@ protected function intervalShouldBeRefreshed(array $interval)
(Carbon::now()->getTimestamp() - $interval['lastRefreshedAt']) >= $interval['refreshInterval'];
}

/**
* Set the expiration of a cached item.
*
* @param string $key
* @param int $seconds
* @return bool
*/
public function touch($key, $seconds)
{
$record = $this->table->get($key);

if ($this->recordIsFalseOrExpired($record)) {
return false;
}

return $this->table->set($key, [
'expiration' => Carbon::now()->getTimestamp() + $seconds,
]);
}

/**
* Remove an item from the cache.
*
Expand Down
28 changes: 28 additions & 0 deletions src/Octane.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace Laravel\Octane;

use Exception;
use Illuminate\Container\Container;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\DevCommands;
use Laravel\Octane\Swoole\WorkerState;
use Swoole\Http\Server;
use Swoole\Table;
Expand Down Expand Up @@ -54,4 +57,29 @@ public static function writeError(string $message): void

error_log($message, 4);
}

/**
* Register the Octane dev commands.
*
* Laravel 13's "artisan dev" command runs a set of registered processes.
* Registering Octane as the "server" process replaces the default
* "artisan serve" so the dev command boots Octane instead.
*/
public static function registerDevCommands(): void
{
if (! class_exists(DevCommands::class)) {
return;
}

// DevCommands reaches for the container itself, so only register once a
// real application is bound. Octane builds sandbox containers of its
// own, and those are not always in place when this provider registers.
$app = Container::getInstance();

if (! $app instanceof Application) {
return;
}

DevCommands::artisan('octane:start --watch', 'server');
}
}
2 changes: 2 additions & 0 deletions src/OctaneServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public function register()
? new SwooleCoroutineDispatcher($app->bound('Swoole\Http\Server'))
: $app->make(SequentialCoroutineDispatcher::class);
});

Octane::registerDevCommands();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Laravel\Octane\Swoole\Actions;

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;

class ConvertSwooleRequestToIlluminateRequest
Expand Down Expand Up @@ -35,7 +34,7 @@ public function __invoke($swooleRequest, string $phpSapi): Request
in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'PATCH', 'DELETE'])) {
parse_str($request->getContent(), $data);

$request->request = new InputBag($data);
$request->request->replace($data);
}

return Request::createFromBase($request);
Expand Down
10 changes: 8 additions & 2 deletions src/Swoole/Coroutine/CoroutineApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -1724,9 +1724,15 @@ public function resolveEnvironmentUsing(?callable $callback)
return $this->getCurrentApp()->resolveEnvironmentUsing($callback);
}

public function resolveFromAttribute(\ReflectionAttribute $attribute)
public function resolveFromAttribute(\ReflectionAttribute $attribute, ?\ReflectionParameter $parameter = null)
{
return $this->getCurrentApp()->resolveFromAttribute($attribute);
$app = $this->getCurrentApp();

// Laravel 13 passes the resolving parameter to contextual attribute
// handlers, while Laravel 11 and 12 only pass the attribute itself.
return $parameter === null
? $app->resolveFromAttribute($attribute)
: $app->resolveFromAttribute($attribute, $parameter);
}

public function runningConsoleCommand(...$commands)
Expand Down
11 changes: 7 additions & 4 deletions src/Swoole/Database/MySqlStringBindingConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,16 @@ protected function octaneFlag(string $key, bool $default = true): bool
/**
* {@inheritdoc}
*/
public function select($query, $bindings = [], $useReadPdo = true)
public function select($query, $bindings = [], $useReadPdo = true, array $fetchUsing = [])
{
if (! $this->statementCacheIsEnabled()) {
return parent::select($query, $bindings, $useReadPdo);
// Laravel 13 added $fetchUsing; earlier versions reject a fourth argument.
return $fetchUsing === []
? parent::select($query, $bindings, $useReadPdo)
: parent::select($query, $bindings, $useReadPdo, $fetchUsing);
}

return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo, $fetchUsing) {
if ($this->pretending()) {
return [];
}
Expand All @@ -127,7 +130,7 @@ public function select($query, $bindings = [], $useReadPdo = true)
try {
$this->executeCached($statement, $pdo, $query, $bindings);

return $statement->fetchAll();
return $statement->fetchAll(...$fetchUsing);
} finally {
// Always drain: a cached statement no longer frees its result
// in a destructor, and on the unbuffered connections two of
Expand Down
25 changes: 25 additions & 0 deletions tests/Unit/ConsoleSmokeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Tests\Unit;

use Tests\TestCase;

class ConsoleSmokeTest extends TestCase
{
public function test_octane_commands_are_registered_and_definable(): void
{
$kernel = $this->app->make(\Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();

$all = $kernel->all();

foreach (['octane:install', 'octane:start', 'octane:reload', 'octane:status', 'octane:stop'] as $name) {
$this->assertArrayHasKey($name, $all, "Missing command [$name].");

// Touching the definition compiles every option/argument through
// Symfony Console, which Laravel 13 bumps to a new major.
$definition = $all[$name]->getDefinition();
$this->assertNotEmpty($definition->getOptions());
}
}
}
4 changes: 4 additions & 0 deletions tests/Unit/DatabasePoolConnectionConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ public function test_after_rollback_callbacks_run_on_pooled_connections(): void
{
$this->skipIfUnsupported();

if (! method_exists(\Illuminate\Database\Connection::class, 'afterRollBack')) {
$this->markTestSkipped('afterRollBack() requires Laravel 12.');
}

$base = $this->baseApplication();
$manager = $this->databaseManager($base);

Expand Down
23 changes: 23 additions & 0 deletions tests/Unit/DevCommandRegistrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Tests\Unit;

use Illuminate\Foundation\DevCommands;
use Tests\TestCase;

class DevCommandRegistrationTest extends TestCase
{
public function test_octane_replaces_the_default_dev_server_command(): void
{
if (! class_exists(DevCommands::class)) {
$this->markTestSkipped('The "dev" command requires Laravel 13.');
}

$commands = collect(DevCommands::commands());

$server = $commands->firstWhere('name', 'server');

$this->assertNotNull($server, 'Octane did not register a "server" dev command.');
$this->assertSame('php artisan octane:start --watch', $server['command']);
}
}
9 changes: 9 additions & 0 deletions tests/Unit/RequestScopeDeferredCallbackIsolationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@

class RequestScopeDeferredCallbackIsolationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

if (! class_exists(DeferredCallbackCollection::class)) {
$this->markTestSkipped('Deferred callbacks require Laravel 11.');
}
}

public function test_deferred_callback_collection_is_request_scoped(): void
{
$base = new Application(__DIR__);
Expand Down
11 changes: 9 additions & 2 deletions tests/Unit/RequestScopeQueueIsolationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,22 @@ public function test_standard_queue_drivers_resolve_with_the_sandbox_container()
$defaultConnection = $scope->resolve('queue.connection', $sandbox);
$sync = $scopedQueue->connection('sync');
$null = $scopedQueue->connection('null');
$failover = $scopedQueue->connection('failover');

$this->assertInstanceOf(RedisQueue::class, $defaultConnection);
$this->assertSame($scopedQueue->connection('redis'), $defaultConnection);
$this->assertInstanceOf(SyncQueue::class, $sync);
$this->assertInstanceOf(NullQueue::class, $null);
$this->assertInstanceOf(FailoverQueue::class, $failover);
$this->assertSame($sandbox, $sync->getContainer());
$this->assertSame($sandbox, $null->getContainer());

// The failover driver was introduced in Laravel 12.
if (! class_exists(FailoverQueue::class)) {
return;
}

$failover = $scopedQueue->connection('failover');

$this->assertInstanceOf(FailoverQueue::class, $failover);
$this->assertSame($sandbox, $failover->getContainer());
$this->assertSame($scopedQueue, $failover->manager);
$this->assertSame(['null', 'sync'], $failover->connections);
Expand Down
5 changes: 2 additions & 3 deletions tests/Unit/SwooleTableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

use Laravel\Octane\Tables\OpenSwooleTable;
use Laravel\Octane\Tables\SwooleTable;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Swoole\Table;

class SwooleTableTest extends TestCase
{
/**
* @dataProvider tableClasses
*/
#[DataProvider('tableClasses')]
public function test_set_preserves_the_requested_row_key(string $tableClass): void
{
if (! class_exists(Table::class)) {
Expand Down
Loading