Skip to content
Open
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,35 @@ $response = BandwidthLib\Voice\Bxml\Response::make()
echo $response->toBxml();
```

### Create A Refer BXML

```php

$sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:alice@atlanta.example.com");
$refer = new BandwidthLib\Voice\Bxml\Refer();
$refer->referCompleteUrl("https://example.com/handleRefer");
$refer->referCompleteMethod("POST");
$refer->sipUri($sipUri);

$response = new BandwidthLib\Voice\Bxml\Response();
$response->addVerb($refer);
echo $response->toBxml();
```

> **Note:** On success, the call is terminated — the remote SIP endpoint redirects away from Bandwidth entirely. Use `referCompleteUrl` only for failure recovery.

```php
// Failure recovery example in your referCompleteUrl handler:
$requestBody = json_decode(file_get_contents('php://input'), true);
if ($requestBody['referCallStatus'] === 'failure') {
// Handle failure: play a message or redirect
$speakSentence = new BandwidthLib\Voice\Bxml\SpeakSentence("The transfer failed. Please try again.");
$response = new BandwidthLib\Voice\Bxml\Response();
$response->addVerb($speakSentence);
echo $response->toBxml();
}
```

### Create A MFA Request

```php
Expand Down
97 changes: 97 additions & 0 deletions src/Voice/Bxml/Refer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* Refer.php
*
* Implementation of the BXML Refer tag
*
* * @copyright Bandwidth INC
*/

namespace BandwidthLib\Voice\Bxml;

use DOMDocument;
use DOMElement;

require_once "Verb.php";

class Refer extends Verb {
/**
* @var string
*/
private $referCompleteUrl;
/**
* @var string
*/
private $referCompleteMethod;
/**
* @var string
*/
private $tag;
/**
* @var SipUri
*/
private $sipUri;

/**
* Sets the referCompleteUrl attribute for Refer
*
* @param string $referCompleteUrl The URL to receive the refer complete callback
*/
public function referCompleteUrl(string $referCompleteUrl): Refer {
$this->referCompleteUrl = $referCompleteUrl;
return $this;
}

/**
* Sets the referCompleteMethod attribute for Refer
*
* @param string $referCompleteMethod The HTTP method for the refer complete callback (GET or POST)
*/
public function referCompleteMethod(string $referCompleteMethod): Refer {
$this->referCompleteMethod = $referCompleteMethod;
return $this;
}

/**
* Sets the tag attribute for Refer
*
* @param string $tag A custom string to be included in callbacks
*/
public function tag(string $tag): Refer {
$this->tag = $tag;
return $this;
}

/**
* Sets the SipUri child element for Refer
*
* @param SipUri $sipUri The SipUri destination for the REFER
*/
public function sipUri(SipUri $sipUri): Refer {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MINOR] SipUri carries Transfer-specific attributes

The SipUri class exposes transferAnswerUrl, transferAnswerFallbackUrl, uui, username, password, etc. as setters. Any of those set on a SipUri passed here will serialize into the <SipUri> element inside <Refer>, producing malformed BXML — with no warning.

Python introduced ReferSipUri (accepts only uri); C# uses a nested Refer.SipUri class. PHP should follow the same pattern to prevent misuse.

$this->sipUri = $sipUri;
return $this;
}

public function toBxml(DOMDocument $doc): DOMElement {
$element = $doc->createElement("Refer");

if(isset($this->referCompleteUrl)) {
$element->setAttribute("referCompleteUrl", $this->referCompleteUrl);
}

if(isset($this->referCompleteMethod)) {
$element->setAttribute("referCompleteMethod", $this->referCompleteMethod);
}

if(isset($this->tag)) {
$element->setAttribute("tag", $this->tag);
}

if(isset($this->sipUri)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MINOR] No guard for missing SipUri — produces invalid BXML silently

<SipUri> is required by the spec ("exactly one"). With the current builder pattern, (new Refer())->toBxml($doc) produces <Refer/> — valid PHP, invalid BXML. Consider throwing in toBxml() when $this->sipUri is not set:

if (!isset($this->sipUri)) {
    throw new \InvalidArgumentException('Refer requires a SipUri child element.');
}

$element->appendChild($this->sipUri->toBxml($doc));
}

return $element;
}
}

154 changes: 154 additions & 0 deletions src/Voice/Models/ReferCompleteCallback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php
/*
* BandwidthLib
*
* This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MINOR] File claims to be auto-generated but is hand-written

The APIMATIC v3.0 header is misleading — this file was written by hand. api-specs#2142 has now merged, so the actual regen job should be run to produce the real generated file (with correct types, proper docstrings, and the APIMATIC serialization scaffolding that matches the rest of the codebase). This placeholder should be replaced by regen output before merge.

*/

namespace BandwidthLib\Voice\Models;

/**
*This object represents fields included in callbacks related to refer complete events
*/
class ReferCompleteCallback implements \JsonSerializable
{
/**
* @todo Write general description for this property

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MINOR] All property docstrings are @todo placeholders

Every property has @todo Write general description for this property. These should describe the field — at minimum mirror the spec language used in the other SDKs (e.g. The outcome of the REFER operation. Either "success" or "failure". for referCallStatus). If this file will be replaced by regen, the stubs are fine as a temporary state, but that should be called out explicitly.

* @var string|null $eventType public property
*/
public $eventType;

/**
* @todo Write general description for this property
* @var string|null $eventTime public property
*/
public $eventTime;

/**
* @todo Write general description for this property
* @var string|null $accountId public property
*/
public $accountId;

/**
* @todo Write general description for this property
* @var string|null $applicationId public property
*/
public $applicationId;

/**
* @todo Write general description for this property
* @var string|null $from public property
*/
public $from;

/**
* @todo Write general description for this property
* @var string|null $to public property
*/
public $to;

/**
* @todo Write general description for this property
* @var string|null $direction public property
*/
public $direction;

/**
* @todo Write general description for this property
* @var string|null $callId public property
*/
public $callId;

/**
* @todo Write general description for this property
* @var string|null $callUrl public property
*/
public $callUrl;

/**
* @todo Write general description for this property
* @var string|null $startTime public property
*/
public $startTime;

/**
* @todo Write general description for this property
* @var string|null $answerTime public property
*/
public $answerTime;

/**
* @todo Write general description for this property
* @var string|null $referCallStatus public property
*/
public $referCallStatus;

/**
* @todo Write general description for this property
* @var string|null $referSipResponseCode public property

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BLOCKER] referSipResponseCode typed as string|null — should be int|null

SIP response codes are integers (202, 405, 503, etc.). Deserializing a real server payload into this class will leave the value as a string, breaking any comparison like === 202. Every other SDK types this correctly: C# uses int?, Python uses Optional[StrictInt].

Fix:

/** @var int|null $referSipResponseCode The SIP response code for the REFER request (e.g. 202, 405). */
public $referSipResponseCode;

Same issue applies to notifySipResponseCode on line 95.

*/
public $referSipResponseCode;

/**
* @todo Write general description for this property
* @var string|null $notifySipResponseCode public property

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BLOCKER] notifySipResponseCode typed as string|null — should be int|null

Same issue as referSipResponseCode above. The SIP code reported via NOTIFY (e.g. 200, 404, 503) is an integer.

/** @var int|null $notifySipResponseCode The SIP response code from the NOTIFY (e.g. 200, 404). */
public $notifySipResponseCode;

*/
public $notifySipResponseCode;

/**
* @todo Write general description for this property
* @var string|null $tag public property
*/
public $tag;

/**
* Constructor to set initial or default values of member properties
*/
public function __construct()
{
if (15 == func_num_args()) {
$this->eventType = func_get_arg(0);
$this->eventTime = func_get_arg(1);
$this->accountId = func_get_arg(2);
$this->applicationId = func_get_arg(3);
$this->from = func_get_arg(4);
$this->to = func_get_arg(5);
$this->direction = func_get_arg(6);
$this->callId = func_get_arg(7);
$this->callUrl = func_get_arg(8);
$this->startTime = func_get_arg(9);
$this->answerTime = func_get_arg(10);
$this->referCallStatus = func_get_arg(11);
$this->referSipResponseCode = func_get_arg(12);
$this->notifySipResponseCode = func_get_arg(13);
$this->tag = func_get_arg(14);
}
}

/**
* Encode this object to JSON
*/
public function jsonSerialize(): array
{
$json = array();
$json['eventType'] = $this->eventType;
$json['eventTime'] = $this->eventTime;
$json['accountId'] = $this->accountId;
$json['applicationId'] = $this->applicationId;
$json['from'] = $this->from;
$json['to'] = $this->to;
$json['direction'] = $this->direction;
$json['callId'] = $this->callId;
$json['callUrl'] = $this->callUrl;
$json['startTime'] = $this->startTime;
$json['answerTime'] = $this->answerTime;
$json['referCallStatus'] = $this->referCallStatus;
$json['referSipResponseCode'] = $this->referSipResponseCode;
$json['notifySipResponseCode'] = $this->notifySipResponseCode;
$json['tag'] = $this->tag;

return array_filter($json);
}
}

2 changes: 0 additions & 2 deletions tests/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,6 @@ public function testSyncTnLookup() {
$body->phoneNumbers = [getenv("USER_NUMBER")];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BLOCKER] Two assertions silently removed from testSyncTnLookup — unrelated to this PR

The deleted lines:

$this->assertIsArray($response->getResult()->links);
$this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\Link::class, $response->getResult()->links[0]);

…have nothing to do with <Refer>. Removing assertions from an existing integration test to make it pass is a hidden regression. What broke here? If links no longer exists on the response model, that should be a separate PR with an explanation.

$response = self::$bandwidthClient->getPhoneNumberLookup()->getClient()->createSyncLookupRequest(getenv("BW_ACCOUNT_ID"), $body);
$this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\LookupResponse::class, $response->getResult());
$this->assertIsArray($response->getResult()->links);
$this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\Link::class, $response->getResult()->links[0]);
$this->assertInstanceOf(BandwidthLib\PhoneNumberLookup\Models\LookupResponseData::class, $response->getResult()->data);
$this->assertIsString($response->getResult()->data->requestId);
$this->assertIsString($response->getResult()->data->status);
Expand Down
29 changes: 29 additions & 0 deletions tests/BxmlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -540,4 +540,33 @@ public function testStopTranscription() {
$responseXml = $response->toBxml();
$this->assertEquals($expectedXml, $responseXml);
}

public function testRefer() {
$sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:alice@atlanta.example.com");
$refer = new BandwidthLib\Voice\Bxml\Refer();
$refer->referCompleteUrl("https://example.com/handleRefer");
$refer->referCompleteMethod("POST");
$refer->tag("my-tag");
$refer->sipUri($sipUri);

$response = new BandwidthLib\Voice\Bxml\Response();
$response->addVerb($refer);

$expectedXml = '<?xml version="1.0" encoding="UTF-8"?><Response><Refer referCompleteUrl="https://example.com/handleRefer" referCompleteMethod="POST" tag="my-tag"><SipUri>sip:alice@atlanta.example.com</SipUri></Refer></Response>';
$responseXml = $response->toBxml();
$this->assertEquals($expectedXml, $responseXml);
}

public function testReferNoOptionalAttributes() {
$sipUri = new BandwidthLib\Voice\Bxml\SipUri("sip:bob@biloxi.example.com");
$refer = new BandwidthLib\Voice\Bxml\Refer();
$refer->sipUri($sipUri);

$response = new BandwidthLib\Voice\Bxml\Response();
$response->addVerb($refer);

$expectedXml = '<?xml version="1.0" encoding="UTF-8"?><Response><Refer><SipUri>sip:bob@biloxi.example.com</SipUri></Refer></Response>';
$responseXml = $response->toBxml();
$this->assertEquals($expectedXml, $responseXml);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MINOR] No tests for ReferCompleteCallback and no test for Refer without SipUri

Two gaps:

  1. ReferCompleteCallback has no test coverage at all — no round-trip JSON test, no scenario tests for the four outcomes (success, REFER rejected, destination unreachable, NOTIFY timeout). Every other SDK has these (Python, Ruby).
  2. No test documenting what (new Refer())->toBxml($doc) produces when SipUri is not set. If a guard is added (see Refer.php comment), this becomes a expectException test.