From 3a1eda809d753aefd08aca8ddf48c7ea27b92074 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Thu, 27 Mar 2025 13:55:24 +0100 Subject: [PATCH 01/11] Require psr/http-client and psr/http-client-factory. --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1e5b80c3..3a403ea8 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,9 @@ "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "ext-simplexml": "*" + "ext-simplexml": "*", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.1" }, "require-dev": { "bmitch/churn-php": "^1.7", From 85d5cde1ab1a99d5b3e35fdd201d350beef126ce Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Fri, 28 Mar 2025 15:53:51 +0100 Subject: [PATCH 02/11] Inject psr http interface objects into BigBlueButton using immutable setter. --- src/BigBlueButton.php | 84 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index 24ebec9c..fbb93ae5 100644 --- a/src/BigBlueButton.php +++ b/src/BigBlueButton.php @@ -56,6 +56,10 @@ use BigBlueButton\Responses\SendChatMessageResponse; use BigBlueButton\Responses\UpdateRecordingsResponse; use BigBlueButton\Util\UrlBuilder; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\StreamFactoryInterface; /** * Class BigBlueButton. @@ -89,6 +93,21 @@ class BigBlueButton protected UrlBuilder $urlBuilder; + /** + * An http client, or NULL to fall back to curl. + */ + private ?ClientInterface $httpClient = null; + + /** + * An http request factory, or NULL to fall back to curl. + */ + private ?RequestFactoryInterface $requestFactory = null; + + /** + * A stream factory, or NULL to fall back to curl. + */ + private ?StreamFactoryInterface $streamFactory = null; + /** * @param null|array $opts */ @@ -124,6 +143,26 @@ public function __construct(?string $baseUrl = null, ?string $secret = null, ?ar $this->curlOpts = $opts['curl'] ?? []; } + /** + * Immutable setter. Sets a http client and factories. + * + * It is recommended for the http client to have a timeout of e.g. 10 + * seconds, to avoid hanging requests. The timeout from ->setTimeout() will + * have no effect on an instance created in this way. + */ + public function withHttpClient( + ClientInterface $httpClient, + RequestFactoryInterface $requestFactory, + StreamFactoryInterface $streamFactory, + ): static { + $clone = clone $this; + $clone->httpClient = $httpClient; + $clone->requestFactory = $requestFactory; + $clone->streamFactory = $streamFactory; + + return $clone; + } + /** * @throws BadResponseException|\RuntimeException */ @@ -480,6 +519,10 @@ public function setJSessionId(string $jSessionId): void } /** + * Sets curl options. + * + * This has no effect if the instance has an http client. + * * @param array $curlOpts */ public function setCurlOpts(array $curlOpts): void @@ -489,6 +532,8 @@ public function setCurlOpts(array $curlOpts): void /** * Set Curl Timeout (Optional), Default 10 Seconds. + * + * This has no effect if the instance has an http client. */ public function setTimeOut(int $TimeOutInSeconds): self { @@ -534,6 +579,45 @@ public function getUrlBuilder(): UrlBuilder * @throws BadResponseException|\RuntimeException */ private function sendRequest(string $url, string $payload = '', string $contentType = 'application/xml'): string + { + if (null === $this->httpClient + || null === $this->requestFactory + || null === $this->streamFactory + ) { + return $this->sendRequestWithCurl($url, $payload, $contentType); + } + + $request = $this->requestFactory->createRequest('GET', $url); + + $request = $request->withHeader('Content-type', $contentType); + + if ($payload) { + $payloadStream = $this->streamFactory->createStream($payload); + $request = $request->withBody($payloadStream); + assert($request instanceof RequestInterface); + $request = $request->withMethod('POST'); + } + assert($request instanceof RequestInterface); + + // @todo Handle cookies. + // @todo Set UTF-8? + // @todo Follow redirect location? + // @todo Recommend timeout. + // @todo Check if clients verify the peer's certificate. + + $response = $this->httpClient->sendRequest($request); + + // @todo Handle failed requests. + + return (string) $response->getBody(); + } + + /** + * A private utility method used by other public methods to request HTTP responses. + * + * @throws BadResponseException|\RuntimeException + */ + private function sendRequestWithCurl(string $url, string $payload = '', string $contentType = 'application/xml'): string { if (!extension_loaded('curl')) { throw new \RuntimeException('Post XML data set but curl PHP module is not installed or not enabled.'); From d4ac1760275bd2439bc050955748b64258d746f3 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Fri, 28 Mar 2025 16:02:29 +0100 Subject: [PATCH 03/11] Use a static factory instead of the immutable setter. --- src/BigBlueButton.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index fbb93ae5..7a729452 100644 --- a/src/BigBlueButton.php +++ b/src/BigBlueButton.php @@ -144,23 +144,28 @@ public function __construct(?string $baseUrl = null, ?string $secret = null, ?ar } /** - * Immutable setter. Sets a http client and factories. + * Creates an instance with http client and factories. * * It is recommended for the http client to have a timeout of e.g. 10 * seconds, to avoid hanging requests. The timeout from ->setTimeout() will * have no effect on an instance created in this way. */ - public function withHttpClient( + public static function createWithHttpClient( ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, + ?string $baseUrl = null, + ?string $secret = null, ): static { - $clone = clone $this; - $clone->httpClient = $httpClient; - $clone->requestFactory = $requestFactory; - $clone->streamFactory = $streamFactory; - - return $clone; + // Extending classes need to override this method, if they change the + // constructor signature. + // @phpstan-ignore new.static + $instance = new static($baseUrl, $secret); + $instance->httpClient = $httpClient; + $instance->requestFactory = $requestFactory; + $instance->streamFactory = $streamFactory; + + return $instance; } /** From c47b6349a84a96f9418d16242818aed88ee25af4 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Fri, 28 Mar 2025 16:53:56 +0100 Subject: [PATCH 04/11] Make $baseUrl and $secret required in the static factory. --- src/BigBlueButton.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index 7a729452..6615d884 100644 --- a/src/BigBlueButton.php +++ b/src/BigBlueButton.php @@ -154,8 +154,8 @@ public static function createWithHttpClient( ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, - ?string $baseUrl = null, - ?string $secret = null, + string $baseUrl, + string $secret, ): static { // Extending classes need to override this method, if they change the // constructor signature. From 43ce402ff4e8aac55d333cc886159c5111c3adae Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Thu, 12 Jun 2025 15:57:44 +0200 Subject: [PATCH 05/11] Remove todos in BigBlueButton::sendRequest(), which are now responsibility of the client. --- src/BigBlueButton.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index 6615d884..b8e266fc 100644 --- a/src/BigBlueButton.php +++ b/src/BigBlueButton.php @@ -604,12 +604,6 @@ private function sendRequest(string $url, string $payload = '', string $contentT } assert($request instanceof RequestInterface); - // @todo Handle cookies. - // @todo Set UTF-8? - // @todo Follow redirect location? - // @todo Recommend timeout. - // @todo Check if clients verify the peer's certificate. - $response = $this->httpClient->sendRequest($request); // @todo Handle failed requests. From 9f658c120eebbfb00f7e5ab9ef40a7b32d513870 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Thu, 12 Jun 2025 15:45:45 +0200 Subject: [PATCH 06/11] Require guzzle in require-dev. --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 3a403ea8..013f7839 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "captainhook/hook-installer": "^1.0", "fakerphp/faker": "^1.23", "friendsofphp/php-cs-fixer": "^3.54", + "guzzlehttp/guzzle": "^7.9", "nunomaduro/phpinsights": "^2.11", "phpstan/phpstan": "^1.10", "phpunit/php-code-coverage": "^10.1", From 6d268b22ccbf16d7bfa2b95e924d18a1312838cb Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Thu, 12 Jun 2025 15:53:58 +0200 Subject: [PATCH 07/11] Add BigBlueButtonGuzzleTest. --- tests/BigBlueButtonGuzzleTest.php | 53 +++++++++++++++++++++++++++++++ tests/BigBlueButtonTest.php | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/BigBlueButtonGuzzleTest.php diff --git a/tests/BigBlueButtonGuzzleTest.php b/tests/BigBlueButtonGuzzleTest.php new file mode 100644 index 00000000..af7e8648 --- /dev/null +++ b/tests/BigBlueButtonGuzzleTest.php @@ -0,0 +1,53 @@ +. + */ + +namespace BigBlueButton; + +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; + +/** + * Class BigBlueButtonGuzzleTest. + * + * This test verifies that all the functionality that works with curl also works + * with an injected http client. In this case, we use Guzzle. + * + * @internal + */ +class BigBlueButtonGuzzleTest extends BigBlueButtonTest +{ + /** + * Setup test class. + */ + public function setUp(): void + { + parent::setUp(); + + $client = new Client(); + $factory = new HttpFactory(); + $this->bbb = BigBlueButton::createWithHttpClient( + $client, + $factory, + $factory, + getenv('BBB_SERVER_BASE_URL') ?: $this->fail(), + getenv('BBB_SECRET') ?: getenv('BBB_SECURITY_SALT') ?: $this->fail(), + ); + } +} diff --git a/tests/BigBlueButtonTest.php b/tests/BigBlueButtonTest.php index 45ea9122..2e393c1b 100644 --- a/tests/BigBlueButtonTest.php +++ b/tests/BigBlueButtonTest.php @@ -46,7 +46,7 @@ */ class BigBlueButtonTest extends TestCase { - private BigBlueButton $bbb; + protected BigBlueButton $bbb; /** * Setup test class. From edee2eb938064d8d82c9da945c72c9fb9d2116b8 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Thu, 12 Jun 2025 19:44:56 +0200 Subject: [PATCH 08/11] Add FixturesGuzzleTest. --- tests/Util/FixturesGuzzleTest.php | 49 +++++++++++++++++++++++++++++++ tests/Util/FixturesTest.php | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/Util/FixturesGuzzleTest.php diff --git a/tests/Util/FixturesGuzzleTest.php b/tests/Util/FixturesGuzzleTest.php new file mode 100644 index 00000000..94793d06 --- /dev/null +++ b/tests/Util/FixturesGuzzleTest.php @@ -0,0 +1,49 @@ +. + */ + +namespace BigBlueButton\Util; + +use BigBlueButton\BigBlueButton; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; + +/** + * This test verifies that all the functionality that works with curl also works + * with an injected http client. In this case, we use Guzzle. + * + * @internal + */ +class FixturesGuzzleTest extends FixturesTest +{ + public function setUp(): void + { + $client = new Client(); + $factory = new HttpFactory(); + $this->bbb = BigBlueButton::createWithHttpClient( + $client, + $factory, + $factory, + getenv('BBB_SERVER_BASE_URL') ?: $this->fail(), + getenv('BBB_SECRET') ?: getenv('BBB_SECURITY_SALT') ?: $this->fail(), + ); + + parent::setUp(); + } +} diff --git a/tests/Util/FixturesTest.php b/tests/Util/FixturesTest.php index d4e5aa05..dd6820fc 100644 --- a/tests/Util/FixturesTest.php +++ b/tests/Util/FixturesTest.php @@ -43,7 +43,7 @@ */ class FixturesTest extends TestCase { - private BigBlueButton $bbb; + protected BigBlueButton $bbb; private Fixtures $fixtures; private static Generator $faker; From 420bf5f2cc5de252375698cd9e24a47d5f6d45f0 Mon Sep 17 00:00:00 2001 From: Ghazi Triki Date: Mon, 17 Aug 2026 10:17:12 +0100 Subject: [PATCH 09/11] Fix merge semantics and complete PSR client support - sendRequestWithCurl keeps the array|string payload (multipart upload) - PSR path: multipart/form-data bodies, BadResponseException on non-2xx, JSESSIONID capture from Set-Cookie - FixturesGuzzleTest: load env before using it - DocumentUrlTest: use BBB test server instead of third-party hosts --- src/BigBlueButton.php | 91 ++++++++++++++++++++++++++----- tests/BigBlueButtonGuzzleTest.php | 2 +- tests/Core/DocumentUrlTest.php | 15 +++-- tests/Util/FixturesGuzzleTest.php | 6 +- 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index 80397765..ef3157b8 100644 --- a/src/BigBlueButton.php +++ b/src/BigBlueButton.php @@ -653,9 +653,8 @@ public function getUrlBuilder(): UrlBuilder /** * A private utility method used by other public methods to request HTTP responses. * - * A string payload is sent as POST body with the given content type, an array - * payload is sent as multipart/form-data (the Content-type header incl. boundary - * is then set by cURL itself). + * Uses the injected PSR http client, or falls back to curl if no client is + * injected. * * @param array|string $payload * @@ -670,31 +669,95 @@ private function sendRequest(string $url, array|string $payload = '', string $co return $this->sendRequestWithCurl($url, $payload, $contentType); } - $request = $this->requestFactory->createRequest('GET', $url); + if (\is_array($payload)) { + $request = $this->buildMultipartRequest($url, $payload); + } else { + $request = $this->requestFactory->createRequest('GET', $url); + + if ('' !== $payload) { + $request = $request + ->withBody($this->streamFactory->createStream($payload)) + ->withMethod('POST') + ->withHeader('Content-type', $contentType) + ; + } + } + + $response = $this->httpClient->sendRequest($request); - $request = $request->withHeader('Content-type', $contentType); + // JSESSIONID - capture from the Set-Cookie headers with the same + // validation as the curl transport + foreach ($response->getHeader('Set-Cookie') as $cookie) { + if ($this->isValidCookieFormat($cookie)) { + $sessionId = $this->extractJSessionIdSafely($cookie); - if ($payload) { - $payloadStream = $this->streamFactory->createStream($payload); - $request = $request->withBody($payloadStream); - assert($request instanceof RequestInterface); - $request = $request->withMethod('POST'); + if (null !== $sessionId) { + $this->setJSessionId($sessionId); + } + } } - assert($request instanceof RequestInterface); - $response = $this->httpClient->sendRequest($request); + $httpCode = $response->getStatusCode(); - // @todo Handle failed requests. + if ($httpCode < 200 || $httpCode >= 300) { + throw new BadResponseException('Bad response, HTTP code: ' . $httpCode . ', url: ' . $url); + } return (string) $response->getBody(); } + /** + * Builds a multipart/form-data request, e.g. for the caption track upload. + * + * @param array $payload + */ + private function buildMultipartRequest(string $url, array $payload): RequestInterface + { + if (null === $this->requestFactory || null === $this->streamFactory) { + throw new \RuntimeException('A request factory and a stream factory are required to build a multipart request.'); + } + + $boundary = 'bbb-' . bin2hex(random_bytes(16)); + $body = $this->streamFactory->createStream(''); + + foreach ($payload as $name => $value) { + $body->write("--{$boundary}\r\n"); + + if ($value instanceof \CURLFile) { + $filename = str_replace(["\r", "\n", '"'], '', $value->getPostFilename()); + $body->write(sprintf( + "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n", + $name, + $filename, + $value->getMimeType() + )); + $body->write((string) $this->streamFactory->createStreamFromFile($value->getFilename())); + $body->write("\r\n"); + } else { + $body->write(sprintf("Content-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", $name, $value)); + } + } + + $body->write("--{$boundary}--\r\n"); + + return $this->requestFactory->createRequest('POST', $url) + ->withBody($body) + ->withHeader('Content-type', 'multipart/form-data; boundary=' . $boundary) + ; + } + /** * A private utility method used by other public methods to request HTTP responses. * + * A string payload is sent as POST body with the given content type, an array + * payload is sent as multipart/form-data (the Content-type header incl. boundary + * is then set by cURL itself). + * + * @param array|string $payload + * * @throws BadResponseException|\RuntimeException */ - private function sendRequestWithCurl(string $url, string $payload = '', string $contentType = 'application/xml'): string + private function sendRequestWithCurl(string $url, array|string $payload = '', string $contentType = 'application/xml'): string { if (!extension_loaded('curl')) { throw new \RuntimeException('Post XML data set but curl PHP module is not installed or not enabled.'); diff --git a/tests/BigBlueButtonGuzzleTest.php b/tests/BigBlueButtonGuzzleTest.php index af7e8648..32442b5c 100644 --- a/tests/BigBlueButtonGuzzleTest.php +++ b/tests/BigBlueButtonGuzzleTest.php @@ -3,7 +3,7 @@ /* * BigBlueButton open source conferencing system - https://www.bigbluebutton.org/. * - * Copyright (c) 2016-2025 BigBlueButton Inc. and by respective authors (see below). + * Copyright (c) 2016-2026 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software diff --git a/tests/Core/DocumentUrlTest.php b/tests/Core/DocumentUrlTest.php index 831d4f2f..a9568e22 100644 --- a/tests/Core/DocumentUrlTest.php +++ b/tests/Core/DocumentUrlTest.php @@ -21,6 +21,7 @@ namespace BigBlueButton\Core; use BigBlueButton\TestCase; +use BigBlueButton\TestServices\EnvLoader; /** * Class DocumentUrlTest. @@ -78,10 +79,11 @@ public function testSetAndGetTimeout(): void public function testIsValidWithValidUrl(): void { - // Use a reliable URL that should be accessible - $documentUrl = new DocumentUrl('https://example.com/'); + // Use the BBB-Server of the test environment (same server all other live tests use) + EnvLoader::loadEnvironmentVariables(); + $baseUrl = mb_rtrim((string) getenv('BBB_SERVER_BASE_URL'), '/'); + $documentUrl = new DocumentUrl($baseUrl . '/'); - // This test might be slow due to network call $isValid = $documentUrl->isValid(); $this->assertTrue($isValid); @@ -89,10 +91,11 @@ public function testIsValidWithValidUrl(): void public function testIsValidWithInvalidUrl(): void { - // Use a URL that should return 404 - $documentUrl = new DocumentUrl('https://example.com/nonexistent'); + // Use a path that is answered with 404 by the BBB-Server + EnvLoader::loadEnvironmentVariables(); + $baseUrl = mb_rtrim((string) getenv('BBB_SERVER_BASE_URL'), '/'); + $documentUrl = new DocumentUrl($baseUrl . '/nonexistent-path-for-404'); - // This test might be slow due to network call $isValid = $documentUrl->isValid(); $this->assertFalse($isValid); diff --git a/tests/Util/FixturesGuzzleTest.php b/tests/Util/FixturesGuzzleTest.php index 94793d06..9fcdb68f 100644 --- a/tests/Util/FixturesGuzzleTest.php +++ b/tests/Util/FixturesGuzzleTest.php @@ -3,7 +3,7 @@ /* * BigBlueButton open source conferencing system - https://www.bigbluebutton.org/. * - * Copyright (c) 2016-2025 BigBlueButton Inc. and by respective authors (see below). + * Copyright (c) 2016-2026 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -34,6 +34,8 @@ class FixturesGuzzleTest extends FixturesTest { public function setUp(): void { + parent::setUp(); + $client = new Client(); $factory = new HttpFactory(); $this->bbb = BigBlueButton::createWithHttpClient( @@ -43,7 +45,5 @@ public function setUp(): void getenv('BBB_SERVER_BASE_URL') ?: $this->fail(), getenv('BBB_SECRET') ?: getenv('BBB_SECURITY_SALT') ?: $this->fail(), ); - - parent::setUp(); } } From 3ea2422105a9f811f4123ea5320ec544401d47d6 Mon Sep 17 00:00:00 2001 From: Ghazi Triki Date: Mon, 17 Aug 2026 10:32:44 +0100 Subject: [PATCH 10/11] Replace Guzzle with lightweight PSR-18 test client --- composer.json | 3 ++- ...st.php => BigBlueButtonHttpClientTest.php} | 23 +++++++++++------- ...zleTest.php => FixturesHttpClientTest.php} | 24 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) rename tests/{BigBlueButtonGuzzleTest.php => BigBlueButtonHttpClientTest.php} (68%) rename tests/Util/{FixturesGuzzleTest.php => FixturesHttpClientTest.php} (67%) diff --git a/composer.json b/composer.json index 4fc77df9..9da423f8 100644 --- a/composer.json +++ b/composer.json @@ -42,8 +42,9 @@ "captainhook/hook-installer": "^1.0", "fakerphp/faker": "^1.23", "friendsofphp/php-cs-fixer": "^3.54", - "guzzlehttp/guzzle": "^7.9", "nunomaduro/phpinsights": "^2.11", + "nyholm/psr7": "^1.8", + "php-http/curl-client": "^2.4", "phpstan/phpstan": "^1.10", "phpunit/php-code-coverage": "^10.1", "phpunit/phpunit": "^10.5", diff --git a/tests/BigBlueButtonGuzzleTest.php b/tests/BigBlueButtonHttpClientTest.php similarity index 68% rename from tests/BigBlueButtonGuzzleTest.php rename to tests/BigBlueButtonHttpClientTest.php index 32442b5c..20e74220 100644 --- a/tests/BigBlueButtonGuzzleTest.php +++ b/tests/BigBlueButtonHttpClientTest.php @@ -20,18 +20,19 @@ namespace BigBlueButton; -use GuzzleHttp\Client; -use GuzzleHttp\Psr7\HttpFactory; +use Http\Client\Curl\Client; +use Nyholm\Psr7\Factory\Psr17Factory; /** - * Class BigBlueButtonGuzzleTest. + * Class BigBlueButtonHttpClientTest. * * This test verifies that all the functionality that works with curl also works - * with an injected http client. In this case, we use Guzzle. + * with an injected PSR-18 http client. In this case, the lightweight + * php-http/curl-client is used with nyholm/psr7 as PSR-17 factory. * * @internal */ -class BigBlueButtonGuzzleTest extends BigBlueButtonTest +class BigBlueButtonHttpClientTest extends BigBlueButtonTest { /** * Setup test class. @@ -40,12 +41,16 @@ public function setUp(): void { parent::setUp(); - $client = new Client(); - $factory = new HttpFactory(); + $psr17Factory = new Psr17Factory(); + $client = new Client($psr17Factory, $psr17Factory, [ + CURLOPT_FOLLOWLOCATION => 1, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 20, + ]); $this->bbb = BigBlueButton::createWithHttpClient( $client, - $factory, - $factory, + $psr17Factory, + $psr17Factory, getenv('BBB_SERVER_BASE_URL') ?: $this->fail(), getenv('BBB_SECRET') ?: getenv('BBB_SECURITY_SALT') ?: $this->fail(), ); diff --git a/tests/Util/FixturesGuzzleTest.php b/tests/Util/FixturesHttpClientTest.php similarity index 67% rename from tests/Util/FixturesGuzzleTest.php rename to tests/Util/FixturesHttpClientTest.php index 9fcdb68f..cacda8e0 100644 --- a/tests/Util/FixturesGuzzleTest.php +++ b/tests/Util/FixturesHttpClientTest.php @@ -21,27 +21,33 @@ namespace BigBlueButton\Util; use BigBlueButton\BigBlueButton; -use GuzzleHttp\Client; -use GuzzleHttp\Psr7\HttpFactory; +use Http\Client\Curl\Client; +use Nyholm\Psr7\Factory\Psr17Factory; /** - * This test verifies that all the functionality that works with curl also works - * with an injected http client. In this case, we use Guzzle. + * Class FixturesHttpClientTest. + * + * Runs the fixture validation of the FixturesTest with an injected PSR-18 http + * client (php-http/curl-client with nyholm/psr7) instead of curl. * * @internal */ -class FixturesGuzzleTest extends FixturesTest +class FixturesHttpClientTest extends FixturesTest { public function setUp(): void { parent::setUp(); - $client = new Client(); - $factory = new HttpFactory(); + $psr17Factory = new Psr17Factory(); + $client = new Client($psr17Factory, $psr17Factory, [ + CURLOPT_FOLLOWLOCATION => 1, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 20, + ]); $this->bbb = BigBlueButton::createWithHttpClient( $client, - $factory, - $factory, + $psr17Factory, + $psr17Factory, getenv('BBB_SERVER_BASE_URL') ?: $this->fail(), getenv('BBB_SECRET') ?: getenv('BBB_SECURITY_SALT') ?: $this->fail(), ); From 499a16e1bad0cecb143d0e3ccb6f46145ec3c817 Mon Sep 17 00:00:00 2001 From: Ghazi Triki Date: Mon, 17 Aug 2026 10:38:41 +0100 Subject: [PATCH 11/11] Document PSR-18 http client usage with examples --- README.md | 21 +++++++ docs/src/SUMMARY.md | 1 + docs/src/general/http_client.md | 97 +++++++++++++++++++++++++++++++++ src/BigBlueButton.php | 4 +- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 docs/src/general/http_client.md diff --git a/README.md b/README.md index 1de066d7..8eed968f 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,27 @@ BBB_SERVER_BASE_URL=https://your-bbb-server.example.com/bigbluebutton/ BBB_SECRET=your-secret ``` +### 5. HTTP Client + +The library uses curl by default and has no HTTP client dependency. Alternatively, inject any PSR-18 client with its PSR-17 factories: + +```php +use BigBlueButton\BigBlueButton; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; + +$factory = new HttpFactory(); +$bbb = BigBlueButton::createWithHttpClient( + new Client(['timeout' => 10]), + $factory, + $factory, + 'https://your-bbb-server.example.com/bigbluebutton/', + 'your-secret', +); +``` + +See the [HTTP Client documentation](docs/src/general/http_client.md) for more examples (Guzzle, Symfony HttpClient, php-http) and behavioral notes. + --- ## ✅ Pre-Commit Checks (CaptainHook) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a22135b2..f495efd0 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,6 +4,7 @@ - [General]() - [Welcome](./general/home.md) - [Getting Started](./general/getting_started.md) + - [HTTP Client](./general/http_client.md) - [Executing API Calls]() - [Meetings](./api_calls/meetings.md) diff --git a/docs/src/general/http_client.md b/docs/src/general/http_client.md new file mode 100644 index 00000000..6d3e1572 --- /dev/null +++ b/docs/src/general/http_client.md @@ -0,0 +1,97 @@ +{{#include ../header.md}} + +# HTTP Client + +By default, this library sends all requests with PHP's curl extension — no HTTP client package is required: + +```php +use BigBlueButton\BigBlueButton; + +$bbb = new BigBlueButton('https://your-server.example.com/bigbluebutton/', 'your-secret'); +``` + +## Injecting a PSR-18 http client + +Alternatively, you can inject any [PSR-18](https://www.php-fig.org/psr/psr-18/) http client together with the [PSR-17](https://www.php-fig.org/psr/psr-17/) request and stream factories. This makes the library independent of curl and lets you reuse the client, its configuration and its logging/middleware stack from your application: + +```php +use BigBlueButton\BigBlueButton; + +$bbb = BigBlueButton::createWithHttpClient( + $httpClient, // Psr\Http\Client\ClientInterface + $requestFactory, // Psr\Http\Message\RequestFactoryInterface + $streamFactory, // Psr\Http\Message\StreamFactoryInterface + 'https://your-server.example.com/bigbluebutton/', + 'your-secret', +); +``` + +The library itself only requires the two interface packages (`psr/http-client`, `psr/http-factory`) — bring your own client. + +### Example: Guzzle + +```php +use BigBlueButton\BigBlueButton; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; + +$client = new Client(['timeout' => 10]); +$factory = new HttpFactory(); // implements all PSR-17 interfaces + +$bbb = BigBlueButton::createWithHttpClient( + $client, + $factory, + $factory, + 'https://your-server.example.com/bigbluebutton/', + 'your-secret', +); +``` + +### Example: Symfony HttpClient + +```php +use BigBlueButton\BigBlueButton; +use Nyholm\Psr7\Factory\Psr17Factory; +use Symfony\Component\HttpClient\HttplugClient; + +$client = new HttplugClient(); // PSR-18 compatible +$factory = new Psr17Factory(); + +$bbb = BigBlueButton::createWithHttpClient( + $client, + $factory, + $factory, + 'https://your-server.example.com/bigbluebutton/', + 'your-secret', +); +``` + +### Example: lightweight php-http/curl-client + +```php +use BigBlueButton\BigBlueButton; +use Http\Client\Curl\Client; +use Nyholm\Psr7\Factory\Psr17Factory; + +$factory = new Psr17Factory(); +$client = new Client($factory, $factory, [ + CURLOPT_FOLLOWLOCATION => 1, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 20, +]); + +$bbb = BigBlueButton::createWithHttpClient( + $client, + $factory, + $factory, + 'https://your-server.example.com/bigbluebutton/', + 'your-secret', +); +``` + +## Behavior with an injected client + +- **Timeouts and transport options are the responsibility of your client.** `setTimeOut()` and `setCurlOpts()` have no effect on an instance created with `createWithHttpClient()`. Configure timeouts, SSL verification and proxies on the http client you pass in. +- **Redirects:** the built-in curl transport follows redirects. If your client does not follow redirects by default, enable it if you rely on redirecting calls (e.g. `join` with `redirect=true`). +- **Multipart uploads** (e.g. uploading a caption track via `putRecordingTextTrack`) are fully supported with an injected client — the library builds the `multipart/form-data` request itself. +- **Error handling stays the same:** non-2xx responses throw a `BadResponseException` regardless of the transport used. diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index ef3157b8..4fdf8373 100644 --- a/src/BigBlueButton.php +++ b/src/BigBlueButton.php @@ -140,8 +140,10 @@ public function __construct( * Creates an instance with http client and factories. * * It is recommended for the http client to have a timeout of e.g. 10 - * seconds, to avoid hanging requests. The timeout from ->setTimeout() will + * seconds, to avoid hanging requests. The timeout from ->setTimeOut() will * have no effect on an instance created in this way. + * + * @see docs/src/general/http_client.md for usage examples */ public static function createWithHttpClient( ClientInterface $httpClient,