Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"./vendor/bin/php-cs-fixer fix --allow-risky=yes"
],
"pstan": [
"./vendor/bin/phpstan analyse"
"./vendor/bin/phpstan analyse --memory-limit=2G"
],
"test": [
"./vendor/bin/pest"
Expand Down
16 changes: 9 additions & 7 deletions src/Http/Middleware/NightwatchMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Saloon\Laravel\Http\Middleware;

use Saloon\Laravel\Saloon;
use Saloon\Http\PendingRequest;
use Saloon\Http\Senders\GuzzleSender;
use Saloon\Contracts\RequestMiddleware;
Expand All @@ -17,18 +18,19 @@ public function __invoke(PendingRequest $pendingRequest): void
{
$sender = $pendingRequest->getConnector()->sender();

// Check if Nightwatch is installed
if (! class_exists('Laravel\Nightwatch\Facades\Nightwatch')) {
return;
}
// Check if we're using the Guzzle Sender, Nightwatch is installed and
// if the middleware hasn't been registered yet.

// Check if we're using GuzzleSender
if ($sender instanceof GuzzleSender === false) {
if (
class_exists('Laravel\Nightwatch\Facades\Nightwatch') === false
|| $sender instanceof GuzzleSender === false
|| isset(Saloon::$registeredSenders[$senderId = spl_object_id($sender)]['nightwatch']) === true
) {
return;
}

$sender->addMiddleware(\Laravel\Nightwatch\Facades\Nightwatch::guzzleMiddleware(), 'nightwatch');

Saloon::$registeredSenders[$senderId]['nightwatch'] = true;
}

}
7 changes: 7 additions & 0 deletions src/Saloon.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class Saloon
*/
public static bool $registeredDefaults = false;

/**
* Define sender IDs that have been used before
*
* @var array<int, array<string, bool>>
*/
public static array $registeredSenders = [];

/**
* Determines if requests should be recorded.
*/
Expand Down
10 changes: 8 additions & 2 deletions src/SaloonServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,15 @@ public function boot(): void
Saloon::$registeredDefaults = true;
}

// Destroy global mock client to prevent leaky tests
$this->app->terminating(function () {
// Destroy global mock client to prevent leaky tests

BaseMockClient::destroyGlobal();
BaseMockClient::destroyGlobal();

// Clear registered senders to prevent Octane memory leaks

Saloon::$registeredSenders = [];
});
}

/**
Expand Down
44 changes: 44 additions & 0 deletions tests/Feature/NightwatchMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Saloon\Http\PendingRequest;
use Saloon\Http\Senders\GuzzleSender;
use Saloon\Laravel\Tests\Fixtures\Requests\UserRequest;
use Saloon\Laravel\Http\Middleware\NightwatchMiddleware;
use Saloon\Laravel\Tests\Fixtures\Connectors\TestConnector;
Expand Down Expand Up @@ -32,3 +33,46 @@
$middleware($pendingRequest);
})->not->toThrow(Exception::class);
});

test('nightwatch middleware is only registered once on the handler stack for long-lived connectors', function () {
if (! class_exists('Laravel\Nightwatch\Facades\Nightwatch')) {
require_once __DIR__ . '/../Fixtures/Middleware/NightwatchMock.php';
}

$connector = TestConnector::make();
$pendingRequest = new PendingRequest($connector, new UserRequest());

$sender = $connector->sender();
$this->assertInstanceOf(GuzzleSender::class, $sender);

$handlerStack = $sender->getHandlerStack();

$middleware = new NightwatchMiddleware();

// Simulate multiple requests being sent
$middleware($pendingRequest);
$middleware($pendingRequest);

/*
* Handler stack __toString() renders middleware as in > and out <.
* Example:
* > 5) Name: 'http_errors', Function: callable(00000000000004130000000000000000)
* > 4) Name: 'allow_redirects', Function: callable(00000000000004120000000000000000)
* > 3) Name: 'cookies', Function: callable(00000000000004110000000000000000)
* > 2) Name: 'prepare_body', Function: callable(00000000000004100000000000000000)
* > 1) Name: 'nightwatch', Function: callable(00000000000004440000000000000000)
* < 0) Handler: callable(00000000000004170000000000000000)
* < 1) Name: 'nightwatch', Function: callable(00000000000004440000000000000000)
* < 2) Name: 'prepare_body', Function: callable(00000000000004100000000000000000)
* < 3) Name: 'cookies', Function: callable(00000000000004110000000000000000)
* < 4) Name: 'allow_redirects', Function: callable(00000000000004120000000000000000)
* < 5) Name: 'http_errors', Function: callable(00000000000004130000000000000000)
*
* Count only the ">" section to avoid double counting
*/
$stackString = (string) $handlerStack;
$reverseSection = explode('<', $stackString)[0] ?? '';
$nightwatchCount = mb_substr_count($reverseSection, 'Name: \'nightwatch\'');

expect($nightwatchCount)->toBe(1);
});
22 changes: 22 additions & 0 deletions tests/Fixtures/Middleware/NightwatchMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Laravel\Nightwatch\Facades;

/**
* Mock Nightwatch facade for testing when Nightwatch is not installed.
* This allows us to test the handler stack manipulation with a real GuzzleSender.
* Note: our namespace needs to match the Laravel Nightwatch facade
*/
class Nightwatch
{
public static function guzzleMiddleware(): callable
{
return function (callable $handler): callable {
return function ($request, $options) use ($handler) {
return $handler($request, $options);
};
};
}
}
Loading