diff --git a/composer.json b/composer.json index 1e5b80c3..013f7839 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", @@ -40,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", diff --git a/src/BigBlueButton.php b/src/BigBlueButton.php index 24ebec9c..b8e266fc 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,31 @@ public function __construct(?string $baseUrl = null, ?string $secret = null, ?ar $this->curlOpts = $opts['curl'] ?? []; } + /** + * 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 static function createWithHttpClient( + ClientInterface $httpClient, + RequestFactoryInterface $requestFactory, + StreamFactoryInterface $streamFactory, + string $baseUrl, + string $secret, + ): static { + // 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; + } + /** * @throws BadResponseException|\RuntimeException */ @@ -480,6 +524,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 +537,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 +584,39 @@ 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); + + $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.'); 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. 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;