Skip to content

Commit c3f3356

Browse files
committed
adding support to send and fetch attachments
1 parent 7603529 commit c3f3356

11 files changed

+224
-29
lines changed

.travis.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ matrix:
1919
fast_finish: true
2020
include:
2121
- php: 5.4
22-
env: deps="low"
22+
env: $COMPOSER_OPTIONS="--prefer-lowest --ignore-platform-reqs"
2323
- php: 5.6
2424
env: PACKAGES="php-http/discovery:^1.0 php-http/guzzle6-adapter:^1.0 php-http/message:^1.0"
2525
- php: 7.0
@@ -31,8 +31,7 @@ before_install:
3131

3232
install:
3333
- if [ "$PACKAGES" != "" ]; then composer require --no-update $PACKAGES; fi
34-
- if [ "$deps" = "low" ]; then composer update --prefer-lowest --prefer-stable --ignore-platform-reqs; fi
35-
- if [ "$deps" = "" ]; then composer install; fi
34+
- composer update --prefer-stable $COMPOSER_OPTIONS
3635

3736
script:
3837
- vendor/bin/phpspec run

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ CHANGELOG
4646
* Bumped the required versions of all `php-xapi` packages to the `1.x` release
4747
series.
4848

49+
* Include the raw attachment content wrapped in a `multipart/mixed` encoded
50+
request when raw content is part of a statement's attachment.
51+
52+
* Added the possibility to decide whether or not to include attachments when
53+
requesting statements from an LRS. A second optional `$attachments` argument
54+
(defaulting to `true`) has been added for this purpose to the `getStatement()`,
55+
`getVoidedStatement()`, and `getStatements()` methods of the `StatementsApiClient`
56+
class and the `StatementsApiClientInterface`.
57+
58+
* An optional fifth `$headers` parameter has been added to the `createRequest()`
59+
method of the `HandlerInterface` and the `Handler` class which allows to pass
60+
custom headers when performing HTTP requests.
61+
4962
0.4.0
5063
-----
5164

UPGRADE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ Upgrading from 0.4 to 0.5
4343
You can avoid calling `setHttpClient()` and `setRequestFactory` by installing
4444
the [HTTP discovery](http://php-http.org/en/latest/discovery.html) package.
4545

46+
* A second optional `$attachments` argument (defaulting to `true`) has been added
47+
to the `getStatement()`, `getVoidedStatement()`, and `getStatements()` methods
48+
of the `StatementsApiClient` class and the `StatementsApiClientInterface`.
49+
50+
* An optional fifth `$headers` parameter has been added to the `createRequest()`
51+
method of the `HandlerInterface` and the `Handler` class which allows to pass
52+
custom headers when performing HTTP requests.
53+
4654
Upgrading from 0.2 to 0.3
4755
-------------------------
4856

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@
2020
"php-http/message-factory": "^1.0",
2121
"php-xapi/exception": "^0.1.0",
2222
"php-xapi/model": "^1.0",
23-
"php-xapi/serializer": "^1.0",
24-
"php-xapi/serializer-implementation": "^1.0",
25-
"php-xapi/symfony-serializer": "^1.0",
23+
"php-xapi/serializer": "^2.0",
24+
"php-xapi/serializer-implementation": "^2.0",
25+
"php-xapi/symfony-serializer": "^2.0",
2626
"psr/http-message": "^1.0"
2727
},
2828
"require-dev": {
2929
"phpspec/phpspec": "^2.3",
3030
"php-http/mock-client": "^0.3",
3131
"php-xapi/test-fixtures": "^1.0"
3232
},
33+
"minimum-stability": "dev",
3334
"suggest": {
3435
"php-http/discovery": "For automatic discovery of HTTP clients and request factories"
3536
},

src/Api/StatementsApiClient.php

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Xabbuh\XApi\Client\Api;
1313

14+
use Xabbuh\XApi\Client\Http\MultipartStatementBody;
1415
use Xabbuh\XApi\Client\Request\HandlerInterface;
1516
use Xabbuh\XApi\Model\StatementId;
1617
use Xabbuh\XApi\Serializer\ActorSerializerInterface;
@@ -102,23 +103,29 @@ public function voidStatement(Statement $statement, Actor $actor)
102103
/**
103104
* {@inheritDoc}
104105
*/
105-
public function getStatement(StatementId $statementId)
106+
public function getStatement(StatementId $statementId, $attachments = true)
106107
{
107-
return $this->doGetStatements('statements', array('statementId' => $statementId->getValue()));
108+
return $this->doGetStatements('statements', array(
109+
'statementId' => $statementId->getValue(),
110+
'attachments' => $attachments ? 'true' : 'false',
111+
));
108112
}
109113

110114
/**
111115
* {@inheritDoc}
112116
*/
113-
public function getVoidedStatement(StatementId $statementId)
117+
public function getVoidedStatement(StatementId $statementId, $attachments = true)
114118
{
115-
return $this->doGetStatements('statements', array('voidedStatementId' => $statementId->getValue()));
119+
return $this->doGetStatements('statements', array(
120+
'voidedStatementId' => $statementId->getValue(),
121+
'attachments' => $attachments ? 'true' : 'false',
122+
));
116123
}
117124

118125
/**
119126
* {@inheritDoc}
120127
*/
121-
public function getStatements(StatementsFilter $filter = null)
128+
public function getStatements(StatementsFilter $filter = null, $attachments = true)
122129
{
123130
$urlParameters = array();
124131

@@ -152,17 +159,50 @@ public function getNextStatements(StatementResult $statementResult)
152159
*/
153160
private function doStoreStatements($statements, $method = 'post', $parameters = array(), $validStatusCode = 200)
154161
{
162+
$attachments = array();
163+
155164
if (is_array($statements)) {
165+
foreach ($statements as $statement) {
166+
if (null !== $statement->getAttachments()) {
167+
foreach ($statement->getAttachments() as $attachment) {
168+
if ($attachment->getContent()) {
169+
$attachments[] = $attachment;
170+
}
171+
}
172+
}
173+
}
174+
156175
$serializedStatements = $this->statementSerializer->serializeStatements($statements);
157176
} else {
177+
if (null !== $statements->getAttachments()) {
178+
foreach ($statements->getAttachments() as $attachment) {
179+
if ($attachment->getContent()) {
180+
$attachments[] = $attachment;
181+
}
182+
}
183+
}
184+
158185
$serializedStatements = $this->statementSerializer->serializeStatement($statements);
159186
}
160187

188+
$headers = array();
189+
190+
if (!empty($attachments)) {
191+
$builder = new MultipartStatementBody($serializedStatements, $attachments);
192+
$headers = array(
193+
'Content-Type' => 'multipart/mixed; boundary='.$builder->getBoundary(),
194+
);
195+
$body = $builder->build();
196+
} else {
197+
$body = $serializedStatements;
198+
}
199+
161200
$request = $this->requestHandler->createRequest(
162201
$method,
163202
'statements',
164203
$parameters,
165-
$serializedStatements
204+
$body,
205+
$headers
166206
);
167207
$response = $this->requestHandler->executeRequest($request, array($validStatusCode));
168208
$statementIds = json_decode((string) $response->getBody());
@@ -200,10 +240,70 @@ private function doGetStatements($url, array $urlParameters = array())
200240
$request = $this->requestHandler->createRequest('get', $url, $urlParameters);
201241
$response = $this->requestHandler->executeRequest($request, array(200));
202242

243+
$contentType = $response->getHeader('Content-Type')[0];
244+
$body = (string) $response->getBody();
245+
$attachments = array();
246+
247+
if (false !== strpos($contentType, 'application/json')) {
248+
$serializedStatement = $body;
249+
} else {
250+
$boundary = substr($contentType, strpos($contentType, '=') + 1);
251+
$parts = $this->parseMultipartResponseBody($body, $boundary);
252+
$serializedStatement = $parts[0]['content'];
253+
254+
unset($parts[0]);
255+
256+
foreach ($parts as $part) {
257+
$attachments[$part['headers']['X-Experience-API-Hash'][0]] = array(
258+
'type' => $part['headers']['Content-Type'][0],
259+
'content' => $part['content'],
260+
);
261+
}
262+
}
263+
203264
if (isset($urlParameters['statementId']) || isset($urlParameters['voidedStatementId'])) {
204-
return $this->statementSerializer->deserializeStatement((string) $response->getBody());
265+
return $this->statementSerializer->deserializeStatement($serializedStatement, $attachments);
205266
} else {
206-
return $this->statementResultSerializer->deserializeStatementResult((string) $response->getBody());
267+
return $this->statementResultSerializer->deserializeStatementResult($serializedStatement, $attachments);
207268
}
208269
}
270+
271+
private function parseMultipartResponseBody($body, $boundary)
272+
{
273+
$parts = array();
274+
$lines = explode("\r\n", $body);
275+
$currentPart = null;
276+
$isHeaderLine = true;
277+
278+
foreach ($lines as $line) {
279+
if (false !== strpos($line, '--'.$boundary)) {
280+
if (null !== $currentPart) {
281+
$parts[] = $currentPart;
282+
}
283+
284+
$currentPart = array(
285+
'headers' => array(),
286+
'content' => '',
287+
);
288+
$isBoundaryLine = true;
289+
$isHeaderLine = true;
290+
} else {
291+
$isBoundaryLine = false;
292+
}
293+
294+
if ('' === $line) {
295+
$isHeaderLine = false;
296+
continue;
297+
}
298+
299+
if (!$isBoundaryLine && !$isHeaderLine) {
300+
$currentPart['content'] .= $line;
301+
} elseif (!$isBoundaryLine && $isHeaderLine) {
302+
list($name, $value) = explode(':', $line, 2);
303+
$currentPart['headers'][$name][] = $value;
304+
}
305+
}
306+
307+
return $parts;
308+
}
209309
}

src/Api/StatementsApiClientInterface.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,36 +73,39 @@ public function voidStatement(Statement $statement, Actor $actor);
7373
* Retrieves a single {@link Statement Statement}.
7474
*
7575
* @param StatementId $statementId The Statement id
76+
* @param bool $attachments Whether or not to request raw attachment data
7677
*
7778
* @return Statement The Statement
7879
*
7980
* @throws NotFoundException if no statement with the given id could be found
8081
* @throws XApiException for all other xAPI related problems
8182
*/
82-
public function getStatement(StatementId $statementId);
83+
public function getStatement(StatementId $statementId, $attachments = true);
8384

8485
/**
8586
* Retrieves a voided {@link Statement Statement}.
8687
*
8788
* @param StatementId $statementId The id of the voided Statement
89+
* @param bool $attachments Whether or not to request raw attachment data
8890
*
8991
* @return Statement The voided Statement
9092
*
9193
* @throws NotFoundException if no statement with the given id could be found
9294
* @throws XApiException for all other xAPI related problems
9395
*/
94-
public function getVoidedStatement(StatementId $statementId);
96+
public function getVoidedStatement(StatementId $statementId, $attachments = true);
9597

9698
/**
9799
* Retrieves a collection of {@link Statement Statements}.
98100
*
99-
* @param StatementsFilter $filter Optional Statements filter
101+
* @param StatementsFilter $filter Optional Statements filter
102+
* @param bool $attachments Whether or not to request raw attachment data
100103
*
101104
* @return StatementResult The {@link StatementResult}
102105
*
103106
* @throws XApiException in case of any problems related to the xAPI
104107
*/
105-
public function getStatements(StatementsFilter $filter = null);
108+
public function getStatements(StatementsFilter $filter = null, $attachments = true);
106109

107110
/**
108111
* Returns the next {@link Statement Statements} for a limited Statement

src/Http/MultipartStatementBody.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the xAPI package.
5+
*
6+
* (c) Christian Flothmann <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Xabbuh\XApi\Client\Http;
13+
14+
use Xabbuh\XApi\Model\Attachment;
15+
16+
/**
17+
* HTTP message body containing serialized statements and their attachments.
18+
*
19+
* @author Christian Flothmann <[email protected]>
20+
*/
21+
final class MultipartStatementBody
22+
{
23+
private $boundary;
24+
private $serializedStatements;
25+
private $attachments;
26+
27+
/**
28+
* @param string $serializedStatements The JSON encoded statement(s)
29+
* @param Attachment[] $attachments The statement attachments that include not only a file URL
30+
*/
31+
public function __construct($serializedStatements, array $attachments)
32+
{
33+
$this->boundary = uniqid();
34+
$this->serializedStatements = $serializedStatements;
35+
$this->attachments = $attachments;
36+
}
37+
38+
public function getBoundary()
39+
{
40+
return $this->boundary;
41+
}
42+
43+
public function build()
44+
{
45+
$body = '--'.$this->boundary."\r\n";
46+
$body .= "Content-Type: application/json\r\n";
47+
$body .= 'Content-Length: '.strlen($this->serializedStatements)."\r\n";
48+
$body .= "\r\n";
49+
$body .= $this->serializedStatements."\r\n";
50+
51+
foreach ($this->attachments as $attachment) {
52+
$body .= '--'.$this->boundary."\r\n";
53+
$body .= 'Content-Type: '.$attachment->getContentType()."\r\n";
54+
$body .= "Content-Transfer-Encoding: binary\r\n";
55+
$body .= 'Content-Length: '.$attachment->getLength()."\r\n";
56+
$body .= 'X-Experience-API-Hash: '.$attachment->getSha2()."\r\n";
57+
$body .= "\r\n";
58+
$body .= $attachment->getContent()."\r\n";
59+
}
60+
61+
$body .= '--'.$this->boundary.'--'."\r\n";
62+
63+
return $body;
64+
}
65+
}

src/Request/Handler.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function __construct(HttpClient $httpClient, RequestFactory $requestFacto
4949
/**
5050
* {@inheritDoc}
5151
*/
52-
public function createRequest($method, $uri, array $urlParameters = array(), $body = null)
52+
public function createRequest($method, $uri, array $urlParameters = array(), $body = null, array $headers = array())
5353
{
5454
if (!in_array(strtoupper($method), array('GET', 'POST', 'PUT', 'DELETE'))) {
5555
throw new \InvalidArgumentException(sprintf('"%s" is no valid HTTP method (expected one of [GET, POST, PUT, DELETE]) in an xAPI context.', $method));
@@ -61,12 +61,15 @@ public function createRequest($method, $uri, array $urlParameters = array(), $bo
6161
$uri .= '?'.http_build_query($urlParameters);
6262
}
6363

64-
$request = $this->requestFactory->createRequest(strtoupper($method), $uri, array(
65-
'X-Experience-API-Version' => $this->version,
66-
'Content-Type' => 'application/json',
67-
), $body);
64+
if (!isset($headers['X-Experience-API-Version'])) {
65+
$headers['X-Experience-API-Version'] = $this->version;
66+
}
67+
68+
if (!isset($headers['Content-Type'])) {
69+
$headers['Content-Type'] = 'application/json';
70+
}
6871

69-
return $request;
72+
return $this->requestFactory->createRequest(strtoupper($method), $uri, $headers, $body);
7073
}
7174

7275
/**

0 commit comments

Comments
 (0)