From 3a1eda809d753aefd08aca8ddf48c7ea27b92074 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Thu, 27 Mar 2025 13:55:24 +0100 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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;