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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -41,6 +43,8 @@
"fakerphp/faker": "^1.23",
"friendsofphp/php-cs-fixer": "^3.54",
"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",
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
97 changes: 97 additions & 0 deletions docs/src/general/http_client.md
Original file line number Diff line number Diff line change
@@ -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.
150 changes: 149 additions & 1 deletion src/BigBlueButton.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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.
Expand Down Expand Up @@ -96,6 +100,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<string, mixed> $opts
*/
Expand All @@ -117,6 +136,33 @@ public function __construct(
$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.
*
* @see docs/src/general/http_client.md for usage examples
*/
public static function createWithHttpClient(
ClientInterface $httpClient,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
string $baseUrl,
string $secret,

@donquixote donquixote Jun 11, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have the UrlBuilder injected instead of passing base url and 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
*/
Expand Down Expand Up @@ -552,6 +598,10 @@ public function setJSessionId(string $jSessionId): void
}

/**
* Sets curl options.
*
* This has no effect if the instance has an http client.
*
* @param array<int, mixed> $curlOpts
*/
public function setCurlOpts(array $curlOpts): void
Expand All @@ -561,6 +611,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
{
Expand Down Expand Up @@ -600,6 +652,102 @@ public function getUrlBuilder(): UrlBuilder

// ____________________ INTERNAL CLASS METHODS ___________________

/**
* A private utility method used by other public methods to request HTTP responses.
*
* Uses the injected PSR http client, or falls back to curl if no client is
* injected.
*
* @param array<string, mixed>|string $payload
*
* @throws BadResponseException|\RuntimeException
*/
private function sendRequest(string $url, array|string $payload = '', string $contentType = 'application/xml'): string
{
if (null === $this->httpClient
|| null === $this->requestFactory
|| null === $this->streamFactory
) {
return $this->sendRequestWithCurl($url, $payload, $contentType);
}

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);

// 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 (null !== $sessionId) {
$this->setJSessionId($sessionId);
}
}
}

$httpCode = $response->getStatusCode();

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<string, \CURLFile|string> $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.
*
Expand All @@ -611,7 +759,7 @@ public function getUrlBuilder(): UrlBuilder
*
* @throws BadResponseException|\RuntimeException
*/
private function sendRequest(string $url, array|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.');
Expand Down
Loading