diff --git a/src/GovTalk.php b/src/GovTalk.php
index 5e41cda..ae60e56 100644
--- a/src/GovTalk.php
+++ b/src/GovTalk.php
@@ -140,7 +140,7 @@ class GovTalk implements LoggerAwareInterface
*
* @var array
*/
- private $messageChannelRouting = array();
+ private $messageChannelRouting = [];
/* Target details related variables. */
@@ -218,6 +218,12 @@ class GovTalk implements LoggerAwareInterface
*/
private LoggerInterface $logger;
+ /**
+ * @var bool Requested by HMRC that this does *not* happen, but not prohibited by the
+ * general GovTalk-Envelope spec. Potentially useful for other departments?
+ */
+ private bool $autoAppendOwnChannelRouting = true;
+
/**
* Instance constructor.
*
@@ -609,29 +615,28 @@ public function getTestFlag()
* @param string $xmlSchema The URL of an XML schema to check the XML body against.
* @return boolean True if the body is valid and set, false if it's invalid (and therefore not set).
*/
- public function setMessageBody($messageBody, $xmlSchema = null)
+ public function setMessageBody($messageBody, $xmlSchema = null): bool
{
- if (is_string($messageBody) || is_a($messageBody, 'XMLWriter')) {
- if ($xmlSchema !== null) {
- $validate = new DOMDocument();
- if (is_string($messageBody)) {
- $validate->loadXML($messageBody);
- } else {
- $validate->loadXML($messageBody->outputMemory());
- }
- if ($validate->schemaValidate($xmlSchema)) {
- $this->messageBody = $messageBody;
- return true;
- } else {
- return false;
- }
+ if (!is_string($messageBody) && !($messageBody instanceof \XMLWriter)) {
+ return false;
+ }
+
+ if ($xmlSchema !== null) {
+ $validate = new DOMDocument();
+ if (is_string($messageBody)) {
+ $validate->loadXML($messageBody);
} else {
+ $validate->loadXML($messageBody->outputMemory());
+ }
+ if ($validate->schemaValidate($xmlSchema)) {
$this->messageBody = $messageBody;
return true;
}
- } else {
return false;
}
+
+ $this->messageBody = $messageBody;
+ return true;
}
@@ -805,6 +810,34 @@ public function getMessageAuthentication()
/* Channel routing related methods. */
+ /**
+ * Sets a channel route, replacing any that exist including this library's default. Implicitly
+ * turns off this library's feature of adding its own element, as this is
+ * incompatible with e.g. HMRC's preferred submission format according to HMRC Software
+ * Developer Support Team as of November '21.
+ *
+ * @param string $uri 'URI' is a misnomer for at least some departments. For example
+ * HMRC expect this to be your 4 digit Vendor ID and nothing else.
+ * @param string|null $softwareName
+ * @param string|null $softwareVersion
+ * @param array|null $id Seems to require an assoc array which 'type' and 'value' keys, if set.
+ * @param mixed $timestamp
+ * @return bool Whether route was valid and set. If not, routes will be left empty.
+ */
+ public function setChannelRoute(
+ string $uri,
+ ?string $softwareName = null,
+ ?string $softwareVersion = null,
+ array $id = null,
+ $timestamp = null
+ ): bool {
+ $this->setAutoAppendOwnChannelRouting(false);
+
+ $this->messageChannelRouting = [];
+
+ return $this->addChannelRoute($uri, $softwareName, $softwareVersion, $id, $timestamp);
+ }
+
/**
* Adds a channel routing element to the message. Channel routes should be
* added in order by every application which the message has passed through
@@ -813,10 +846,15 @@ public function getMessageAuthentication()
* automatically be added at the moment the route is added. Any optional
* arguments may be skipped by passing null as that argument.
*
- * Applications using php-govtalk should always add at least one
+ * Applications using php-govtalk may add at least one
* additional channel route before sending a message to the Gateway.
+ * However, contrary to the guidance in previous versions of this library
+ * and the XML spec, HMRC have stated that for their submissions they expect
+ * and prefer only a single element. You should therefore
+ * check in with the department you are sending data to and choose between
+ * this method and {@see setChannelRoute()} accordingly.
*
- * Note: php-govtalk will always add itself as the last route in the chain.
+ * Note: When using *this* method, php-govtalk will add itself as the last route in the chain.
* This is to identify the library to the Gateway and to assist in tracking
* down issues caused by the library itself.
*
@@ -867,17 +905,17 @@ public function addChannelRoute(
break;
}
}
- if ($matchedChannel == false) {
+ if (!$matchedChannel) {
$this->messageChannelRouting[] = $newRoute;
}
return true;
- } else {
- $this->messageChannelRouting[] = $newRoute;
- return true;
}
- } else {
- return false;
+
+ $this->messageChannelRouting[] = $newRoute;
+ return true;
}
+
+ return false;
}
@@ -1205,6 +1243,14 @@ public function setTimestamp(?\DateTime $timestamp): void
$this->timestamp = $timestamp;
}
+ /**
+ * @param bool $autoAppendOwnChannelRouting
+ */
+ public function setAutoAppendOwnChannelRouting(bool $autoAppendOwnChannelRouting): void
+ {
+ $this->autoAppendOwnChannelRouting = $autoAppendOwnChannelRouting;
+ }
+
/* Protected methods. */
/**
@@ -1270,184 +1316,199 @@ protected function packageGovTalkEnvelope()
$this->govTalkPassword,
$this->messageAuthType
);
- if ($allSet) {
- // Generate the transaction ID...
- $this->generateTransactionId();
- if (isset($this->messageBody)) {
- // Create the XML document (in memory)...
- $package = new XMLWriter();
- $package->openMemory();
- $package->setIndent(true);
-
- // Packaging...
- $package->startElement('GovTalkMessage');
- $xsiSchemaName = 'http://www.govtalk.gov.uk/CM/envelope';
- $xsiSchemaLocation = $xsiSchemaName.' http://www.govtalk.gov.uk/documents/envelope-v2-0.xsd';
- if ($this->additionalXsiSchemaLocation !== null) {
- $xsiSchemaLocation .= ' '.$this->additionalXsiSchemaLocation;
- }
- $package->writeAttribute('xmlns', $xsiSchemaName);
- $package->writeAttributeNS(
- 'xsi',
- 'schemaLocation',
- 'http://www.w3.org/2001/XMLSchema-instance',
- $xsiSchemaLocation
- );
- $package->writeElement('EnvelopeVersion', '2.0');
+ if (!$allSet) {
+ $this->logError(
+ 'ENVELOPE_PROPERTIES_MISSING',
+ 'Essential information to build envelope missing',
+ 'GovTalk::packageGovTalkEnvelope',
+ );
+ return false;
+ }
- // Header...
- $package->startElement('Header');
+ if (!isset($this->messageBody)) {
+ $this->logError(
+ 'MESSAGE_BODY_MISSING',
+ 'Message body missing',
+ 'GovTalk::packageGovTalkEnvelope',
+ );
+ return false;
+ }
- // Message details...
- $package->startElement('MessageDetails');
- $package->writeElement('Class', $this->messageClass);
- $package->writeElement('Qualifier', $this->messageQualifier);
- if ($this->messageFunction !== null) {
- $package->writeElement('Function', $this->messageFunction);
- }
- $package->writeElement('TransactionID', $this->transactionId);
- $package->writeElement('CorrelationID', $this->messageCorrelationId);
- $package->writeElement('Transformation', $this->messageTransformation);
- $package->writeElement('GatewayTest', $this->govTalkTest);
-
- /**
- * @see GovTalk::setTimestamp() for usage.
- */
- if ($this->timestamp && $this->govTalkTest === '1') {
- $package->writeElement('GatewayTimestamp', $this->timestamp->format('c'));
- }
+ $this->generateTransactionId();
- $package->endElement(); # MessageDetails
-
- // Sender details...
- $package->startElement('SenderDetails');
-
- // Authentication...
- $package->startElement('IDAuthentication');
- $package->writeElement('SenderID', $this->govTalkSenderId);
- $package->startElement('Authentication');
- switch ($this->messageAuthType) {
- case 'alternative':
- if ($authenticationArray = $this->generateAlternativeAuthentication($this->transactionId)) {
- $package->writeElement('Method', $authenticationArray['method']);
- $package->writeElement('Role', 'principal');
- $package->writeElement('Value', $authenticationArray['token']);
- } else {
- return false;
- }
- break;
- case 'clear':
- $package->writeElement('Method', 'clear');
- $package->writeElement('Role', 'principal');
- $package->writeElement('Value', $this->govTalkPassword);
- break;
- case 'MD5':
- $package->writeElement('Method', 'MD5');
- $package->writeElement('Value', base64_encode(md5(strtolower($this->govTalkPassword), true)));
- break;
- }
- $package->endElement(); # Authentication
+ // Create the XML document (in memory)...
+ $package = new XMLWriter();
+ $package->openMemory();
+ $package->setIndent(true);
- $package->endElement(); # IDAuthentication
- if ($this->senderEmailAddress !== null) {
- $package->writeElement('EmailAddress', $this->senderEmailAddress);
- }
+ // Packaging...
+ $package->startElement('GovTalkMessage');
+ $xsiSchemaName = 'http://www.govtalk.gov.uk/CM/envelope';
+ $xsiSchemaLocation = $xsiSchemaName.' http://www.govtalk.gov.uk/documents/envelope-v2-0.xsd';
+ if ($this->additionalXsiSchemaLocation !== null) {
+ $xsiSchemaLocation .= ' '.$this->additionalXsiSchemaLocation;
+ }
+ $package->writeAttribute('xmlns', $xsiSchemaName);
+ $package->writeAttributeNS(
+ 'xsi',
+ 'schemaLocation',
+ 'http://www.w3.org/2001/XMLSchema-instance',
+ $xsiSchemaLocation
+ );
+ $package->writeElement('EnvelopeVersion', '2.0');
- $package->endElement(); # SenderDetails
+ // Header...
+ $package->startElement('Header');
- $package->endElement(); # Header
+ // Message details...
+ $package->startElement('MessageDetails');
+ $package->writeElement('Class', $this->messageClass);
+ $package->writeElement('Qualifier', $this->messageQualifier);
+ if ($this->messageFunction !== null) {
+ $package->writeElement('Function', $this->messageFunction);
+ }
+ $package->writeElement('TransactionID', $this->transactionId);
+ $package->writeElement('CorrelationID', $this->messageCorrelationId);
+ $package->writeElement('Transformation', $this->messageTransformation);
+ $package->writeElement('GatewayTest', $this->govTalkTest);
+
+ /**
+ * @see GovTalk::setTimestamp() for usage.
+ */
+ if ($this->timestamp && $this->govTalkTest === '1') {
+ $package->writeElement('GatewayTimestamp', $this->timestamp->format('c'));
+ }
- // GovTalk details...
- $package->startElement('GovTalkDetails');
+ $package->endElement(); # MessageDetails
- // Keys...
- if (count($this->govTalkKeys) > 0) {
- $package->startElement('Keys');
- foreach ($this->govTalkKeys as $keyPair) {
- $package->startElement('Key');
- $package->writeAttribute('Type', $keyPair['type']);
- $package->text($keyPair['value']);
- $package->endElement(); # Key
- }
- $package->endElement(); # Keys
- }
+ // Sender details...
+ $package->startElement('SenderDetails');
- // Target details...
- if (count($this->messageTargetDetails) > 0) {
- $package->startElement('TargetDetails');
- foreach ($this->messageTargetDetails as $targetOrganisation) {
- $package->writeElement('Organisation', $targetOrganisation);
- }
- $package->endElement(); # TargetDetails
+ // Authentication...
+ $package->startElement('IDAuthentication');
+ $package->writeElement('SenderID', $this->govTalkSenderId);
+ $package->startElement('Authentication');
+ switch ($this->messageAuthType) {
+ case 'alternative':
+ if ($authenticationArray = $this->generateAlternativeAuthentication($this->transactionId)) {
+ $package->writeElement('Method', $authenticationArray['method']);
+ $package->writeElement('Role', 'principal');
+ $package->writeElement('Value', $authenticationArray['token']);
+ } else {
+ return false;
}
+ break;
+ case 'clear':
+ $package->writeElement('Method', 'clear');
+ $package->writeElement('Role', 'principal');
+ $package->writeElement('Value', $this->govTalkPassword);
+ break;
+ case 'MD5':
+ $package->writeElement('Method', 'MD5');
+ $package->writeElement('Value', base64_encode(md5(strtolower($this->govTalkPassword), true)));
+ break;
+ }
+ $package->endElement(); # Authentication
- // Channel routing...
- $channelRouteArray = $this->messageChannelRouting;
- $channelRouteArray[] = array(
- 'uri' => 'https://github.com/thebiggive/php-govtalk/',
- 'product' => 'php-govtalk',
- 'version' => self::VERSION,
- 'timestamp' => date('c')
- );
- foreach ($channelRouteArray as $channelRoute) {
- $package->startElement('ChannelRouting');
- $package->startElement('Channel');
- $package->writeElement('URI', $channelRoute['uri']);
- if (array_key_exists('product', $channelRoute)) {
- $package->writeElement('Product', $channelRoute['product']);
- }
- if (array_key_exists('version', $channelRoute)) {
- $package->writeElement('Version', $channelRoute['version']);
- }
- $package->endElement(); # Channel
-
- if (array_key_exists('id', $channelRoute) && is_array($channelRoute['id'])) {
- foreach ($channelRoute['id'] as $channelRouteId) {
- $package->startElement('ID');
- $package->writeAttribute('type', $channelRouteId['type']);
- $package->text($channelRouteId['value']);
- $package->endElement(); # ID
- }
- }
+ $package->endElement(); # IDAuthentication
+ if ($this->senderEmailAddress !== null) {
+ $package->writeElement('EmailAddress', $this->senderEmailAddress);
+ }
- $package->writeElement('Timestamp', $channelRoute['timestamp']);
- $package->endElement(); # ChannelRouting
- }
- $package->endElement(); # GovTalkDetails
-
- // Body...
- $package->startElement('Body');
- if (is_string($this->messageBody)) {
- $package->writeRaw("\n".trim($this->messageBody)."\n");
- } elseif (is_a($this->messageBody, 'XMLWriter')) {
- $package->writeRaw("\n".trim($this->messageBody->outputMemory())."\n");
- }
- $package->endElement(); # Body
+ $package->endElement(); # SenderDetails
- $package->endElement(); # GovTalkMessage
+ $package->endElement(); # Header
- // Flush the buffer, run any extension-specific digests, validate the schema
- // and return the XML...
- $xmlPackage = $this->packageDigest($package->flush());
- $validXMLRequest = true;
- if (isset($this->additionalXsiSchemaLocation) && ($this->schemaValidation == true)) {
- $validation = new DOMDocument();
- $validation->loadXML($xmlPackage);
- if (!$validation->schemaValidate($this->additionalXsiSchemaLocation)) {
- $validXMLRequest = false;
- }
- }
- if ($validXMLRequest === true) {
- return $xmlPackage;
- } else {
- return false;
+ // GovTalk details...
+ $package->startElement('GovTalkDetails');
+
+ // Keys...
+ if (count($this->govTalkKeys) > 0) {
+ $package->startElement('Keys');
+ foreach ($this->govTalkKeys as $keyPair) {
+ $package->startElement('Key');
+ $package->writeAttribute('Type', $keyPair['type']);
+ $package->text($keyPair['value']);
+ $package->endElement(); # Key
+ }
+ $package->endElement(); # Keys
+ }
+
+ // Target details...
+ if (count($this->messageTargetDetails) > 0) {
+ $package->startElement('TargetDetails');
+ foreach ($this->messageTargetDetails as $targetOrganisation) {
+ $package->writeElement('Organisation', $targetOrganisation);
+ }
+ $package->endElement(); # TargetDetails
+ }
+
+ // Channel routing...
+ $channelRouteArray = $this->messageChannelRouting;
+ if ($this->autoAppendOwnChannelRouting) {
+ $channelRouteArray[] = [
+ // This URI format is not valid for HMRC submissions, but `autoAppendOwnChannelRouting`
+ // should be switched off for those anyway – so sticking with it for now in case it's helpful
+ // for other govt departments.
+ 'uri' => 'https://github.com/thebiggive/php-govtalk/',
+ 'product' => 'php-govtalk',
+ 'version' => self::VERSION,
+ 'timestamp' => date('c')
+ ];
+ }
+ foreach ($channelRouteArray as $channelRoute) {
+ $package->startElement('ChannelRouting');
+ $package->startElement('Channel');
+ $package->writeElement('URI', $channelRoute['uri']);
+ if (array_key_exists('product', $channelRoute)) {
+ $package->writeElement('Product', $channelRoute['product']);
+ }
+ if (array_key_exists('version', $channelRoute)) {
+ $package->writeElement('Version', $channelRoute['version']);
+ }
+ $package->endElement(); # Channel
+
+ if (array_key_exists('id', $channelRoute) && is_array($channelRoute['id'])) {
+ foreach ($channelRoute['id'] as $channelRouteId) {
+ $package->startElement('ID');
+ $package->writeAttribute('type', $channelRouteId['type']);
+ $package->text($channelRouteId['value']);
+ $package->endElement(); # ID
}
- } else {
- return false;
}
- } else {
- return false;
+
+ $package->writeElement('Timestamp', $channelRoute['timestamp']);
+ $package->endElement(); # ChannelRouting
}
+ $package->endElement(); # GovTalkDetails
+
+ // Body...
+ $package->startElement('Body');
+ if (is_string($this->messageBody)) {
+ $package->writeRaw("\n".trim($this->messageBody)."\n");
+ } elseif ($this->messageBody instanceof \XMLWriter) {
+ $package->writeRaw("\n".trim($this->messageBody->outputMemory())."\n");
+ }
+ $package->endElement(); # Body
+
+ $package->endElement(); # GovTalkMessage
+
+ // Flush the buffer, run any extension-specific digests, validate the schema
+ // and return the XML...
+ $xmlPackage = $this->packageDigest($package->flush());
+ $validXMLRequest = true;
+ if (isset($this->additionalXsiSchemaLocation) && ($this->schemaValidation == true)) {
+ $validation = new DOMDocument();
+ $validation->loadXML($xmlPackage);
+ if (!$validation->schemaValidate($this->additionalXsiSchemaLocation)) {
+ $validXMLRequest = false;
+ }
+ }
+ if ($validXMLRequest === true) {
+ return $xmlPackage;
+ }
+
+ return false;
}
/**
diff --git a/tests/GovTalk/GovTalkTest.php b/tests/GovTalk/GovTalkTest.php
index 6149c96..edba690 100755
--- a/tests/GovTalk/GovTalkTest.php
+++ b/tests/GovTalk/GovTalkTest.php
@@ -151,6 +151,44 @@ public function testAddingChannelRoute(): void
$this->assertTrue($this->gtService->addChannelRoute('d', 'e', 'f', null, '', true));
}
+ public function testSettingChannelRoute(): void
+ {
+ $this->setMockHttpResponse('GiftAidResponseAck.txt');
+ // Re-call this to actually replace the service with the one that has the non-empty
+ // mock response queue.
+ $this->gtService = $this->setUpService();
+ $this->gtService->setMessageBody('');
+
+ $this->gtService->setTestFlag(true);
+ $this->gtService->setMessageClass('HMRC-CHAR-CLM');
+ $this->gtService->setMessageAuthentication('clear');
+ $this->gtService->setMessageQualifier('request');
+ $this->gtService->setMessageFunction('submit');
+ $this->gtService->setMessageCorrelationId('');
+ $this->gtService->setMessageTransformation('XML');
+ $this->gtService->addTargetOrganisation('IR');
+ $this->gtService->addMessageKey('CHARID', 'CD67890');
+
+ // Note that the `$id` array is actually invalid, which for now serves as a crude, implicit
+ // check that this route is *not* used since providing [['1', '2', '3']] causes a crash!
+ $this->assertTrue($this->gtService->addChannelRoute('a', 'b', 'c', [['1','2','3']], '2014-04-04T12:28.123'));
+ $this->assertTrue(
+ $this->gtService->setChannelRoute(
+ '9999',
+ 'DownstreamApp',
+ '0.0',
+ ['type' => 'some type', 'value' => 'some value'],
+ '2021-12-07T00:00.000',
+ )
+ );
+
+ // TODO This test should ideally use the HTTP client mock to verify that the sent payload
+ // includes the second ChannelRouting's info ("URI" 9999 etc.) and not the first. But for
+ // now, as noted above, the slightly brittle message construction approach means that
+ // we are implicitly verifying this in a more crude way.
+ $this->assertTrue($this->gtService->sendMessage());
+ }
+
public function testSettingMessageBody(): void
{
$this->assertFalse($this->gtService->setMessageBody(array('')));
@@ -204,6 +242,9 @@ public function testGivenSubmission_getResponseQualifier_ReturnsAcknowledgement(
}
/**
+ * Create *and send* a mock Gift Aid claim submission, returning the request
+ * body for now.
+ *
* @return string
*/
protected function makeGiftAidSubmission(): string