Skip to content

Commit

Permalink
adding support to send and fetch attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
xabbuh committed Oct 30, 2016
1 parent 7603529 commit c3f3356
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 29 deletions.
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ matrix:
fast_finish: true
include:
- php: 5.4
env: deps="low"
env: $COMPOSER_OPTIONS="--prefer-lowest --ignore-platform-reqs"
- php: 5.6
env: PACKAGES="php-http/discovery:^1.0 php-http/guzzle6-adapter:^1.0 php-http/message:^1.0"
- php: 7.0
Expand All @@ -31,8 +31,7 @@ before_install:

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

script:
- vendor/bin/phpspec run
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ CHANGELOG
* Bumped the required versions of all `php-xapi` packages to the `1.x` release
series.

* Include the raw attachment content wrapped in a `multipart/mixed` encoded
request when raw content is part of a statement's attachment.

* Added the possibility to decide whether or not to include attachments when
requesting statements from an LRS. A second optional `$attachments` argument
(defaulting to `true`) has been added for this purpose to the `getStatement()`,
`getVoidedStatement()`, and `getStatements()` methods of the `StatementsApiClient`
class and the `StatementsApiClientInterface`.

* An optional fifth `$headers` parameter has been added to the `createRequest()`
method of the `HandlerInterface` and the `Handler` class which allows to pass
custom headers when performing HTTP requests.

0.4.0
-----

Expand Down
8 changes: 8 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ Upgrading from 0.4 to 0.5
You can avoid calling `setHttpClient()` and `setRequestFactory` by installing
the [HTTP discovery](http://php-http.org/en/latest/discovery.html) package.

* A second optional `$attachments` argument (defaulting to `true`) has been added
to the `getStatement()`, `getVoidedStatement()`, and `getStatements()` methods
of the `StatementsApiClient` class and the `StatementsApiClientInterface`.

* An optional fifth `$headers` parameter has been added to the `createRequest()`
method of the `HandlerInterface` and the `Handler` class which allows to pass
custom headers when performing HTTP requests.

Upgrading from 0.2 to 0.3
-------------------------

Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@
"php-http/message-factory": "^1.0",
"php-xapi/exception": "^0.1.0",
"php-xapi/model": "^1.0",
"php-xapi/serializer": "^1.0",
"php-xapi/serializer-implementation": "^1.0",
"php-xapi/symfony-serializer": "^1.0",
"php-xapi/serializer": "^2.0",
"php-xapi/serializer-implementation": "^2.0",
"php-xapi/symfony-serializer": "^2.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"phpspec/phpspec": "^2.3",
"php-http/mock-client": "^0.3",
"php-xapi/test-fixtures": "^1.0"
},
"minimum-stability": "dev",
"suggest": {
"php-http/discovery": "For automatic discovery of HTTP clients and request factories"
},
Expand Down
116 changes: 108 additions & 8 deletions src/Api/StatementsApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Xabbuh\XApi\Client\Api;

use Xabbuh\XApi\Client\Http\MultipartStatementBody;
use Xabbuh\XApi\Client\Request\HandlerInterface;
use Xabbuh\XApi\Model\StatementId;
use Xabbuh\XApi\Serializer\ActorSerializerInterface;
Expand Down Expand Up @@ -102,23 +103,29 @@ public function voidStatement(Statement $statement, Actor $actor)
/**
* {@inheritDoc}
*/
public function getStatement(StatementId $statementId)
public function getStatement(StatementId $statementId, $attachments = true)
{
return $this->doGetStatements('statements', array('statementId' => $statementId->getValue()));
return $this->doGetStatements('statements', array(
'statementId' => $statementId->getValue(),
'attachments' => $attachments ? 'true' : 'false',
));
}

/**
* {@inheritDoc}
*/
public function getVoidedStatement(StatementId $statementId)
public function getVoidedStatement(StatementId $statementId, $attachments = true)
{
return $this->doGetStatements('statements', array('voidedStatementId' => $statementId->getValue()));
return $this->doGetStatements('statements', array(
'voidedStatementId' => $statementId->getValue(),
'attachments' => $attachments ? 'true' : 'false',
));
}

/**
* {@inheritDoc}
*/
public function getStatements(StatementsFilter $filter = null)
public function getStatements(StatementsFilter $filter = null, $attachments = true)
{
$urlParameters = array();

Expand Down Expand Up @@ -152,17 +159,50 @@ public function getNextStatements(StatementResult $statementResult)
*/
private function doStoreStatements($statements, $method = 'post', $parameters = array(), $validStatusCode = 200)
{
$attachments = array();

if (is_array($statements)) {
foreach ($statements as $statement) {
if (null !== $statement->getAttachments()) {
foreach ($statement->getAttachments() as $attachment) {
if ($attachment->getContent()) {
$attachments[] = $attachment;
}
}
}
}

$serializedStatements = $this->statementSerializer->serializeStatements($statements);
} else {
if (null !== $statements->getAttachments()) {
foreach ($statements->getAttachments() as $attachment) {
if ($attachment->getContent()) {
$attachments[] = $attachment;
}
}
}

$serializedStatements = $this->statementSerializer->serializeStatement($statements);
}

$headers = array();

if (!empty($attachments)) {
$builder = new MultipartStatementBody($serializedStatements, $attachments);
$headers = array(
'Content-Type' => 'multipart/mixed; boundary='.$builder->getBoundary(),
);
$body = $builder->build();
} else {
$body = $serializedStatements;
}

$request = $this->requestHandler->createRequest(
$method,
'statements',
$parameters,
$serializedStatements
$body,
$headers
);
$response = $this->requestHandler->executeRequest($request, array($validStatusCode));
$statementIds = json_decode((string) $response->getBody());
Expand Down Expand Up @@ -200,10 +240,70 @@ private function doGetStatements($url, array $urlParameters = array())
$request = $this->requestHandler->createRequest('get', $url, $urlParameters);
$response = $this->requestHandler->executeRequest($request, array(200));

$contentType = $response->getHeader('Content-Type')[0];
$body = (string) $response->getBody();
$attachments = array();

if (false !== strpos($contentType, 'application/json')) {
$serializedStatement = $body;
} else {
$boundary = substr($contentType, strpos($contentType, '=') + 1);
$parts = $this->parseMultipartResponseBody($body, $boundary);
$serializedStatement = $parts[0]['content'];

unset($parts[0]);

foreach ($parts as $part) {
$attachments[$part['headers']['X-Experience-API-Hash'][0]] = array(
'type' => $part['headers']['Content-Type'][0],
'content' => $part['content'],
);
}
}

if (isset($urlParameters['statementId']) || isset($urlParameters['voidedStatementId'])) {
return $this->statementSerializer->deserializeStatement((string) $response->getBody());
return $this->statementSerializer->deserializeStatement($serializedStatement, $attachments);
} else {
return $this->statementResultSerializer->deserializeStatementResult((string) $response->getBody());
return $this->statementResultSerializer->deserializeStatementResult($serializedStatement, $attachments);
}
}

private function parseMultipartResponseBody($body, $boundary)
{
$parts = array();
$lines = explode("\r\n", $body);
$currentPart = null;
$isHeaderLine = true;

foreach ($lines as $line) {
if (false !== strpos($line, '--'.$boundary)) {
if (null !== $currentPart) {
$parts[] = $currentPart;
}

$currentPart = array(
'headers' => array(),
'content' => '',
);
$isBoundaryLine = true;
$isHeaderLine = true;
} else {
$isBoundaryLine = false;
}

if ('' === $line) {
$isHeaderLine = false;
continue;
}

if (!$isBoundaryLine && !$isHeaderLine) {
$currentPart['content'] .= $line;
} elseif (!$isBoundaryLine && $isHeaderLine) {
list($name, $value) = explode(':', $line, 2);
$currentPart['headers'][$name][] = $value;
}
}

return $parts;
}
}
11 changes: 7 additions & 4 deletions src/Api/StatementsApiClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,36 +73,39 @@ public function voidStatement(Statement $statement, Actor $actor);
* Retrieves a single {@link Statement Statement}.
*
* @param StatementId $statementId The Statement id
* @param bool $attachments Whether or not to request raw attachment data
*
* @return Statement The Statement
*
* @throws NotFoundException if no statement with the given id could be found
* @throws XApiException for all other xAPI related problems
*/
public function getStatement(StatementId $statementId);
public function getStatement(StatementId $statementId, $attachments = true);

/**
* Retrieves a voided {@link Statement Statement}.
*
* @param StatementId $statementId The id of the voided Statement
* @param bool $attachments Whether or not to request raw attachment data
*
* @return Statement The voided Statement
*
* @throws NotFoundException if no statement with the given id could be found
* @throws XApiException for all other xAPI related problems
*/
public function getVoidedStatement(StatementId $statementId);
public function getVoidedStatement(StatementId $statementId, $attachments = true);

/**
* Retrieves a collection of {@link Statement Statements}.
*
* @param StatementsFilter $filter Optional Statements filter
* @param StatementsFilter $filter Optional Statements filter
* @param bool $attachments Whether or not to request raw attachment data
*
* @return StatementResult The {@link StatementResult}
*
* @throws XApiException in case of any problems related to the xAPI
*/
public function getStatements(StatementsFilter $filter = null);
public function getStatements(StatementsFilter $filter = null, $attachments = true);

/**
* Returns the next {@link Statement Statements} for a limited Statement
Expand Down
65 changes: 65 additions & 0 deletions src/Http/MultipartStatementBody.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Xabbuh\XApi\Client\Http;

use Xabbuh\XApi\Model\Attachment;

/**
* HTTP message body containing serialized statements and their attachments.
*
* @author Christian Flothmann <[email protected]>
*/
final class MultipartStatementBody
{
private $boundary;
private $serializedStatements;
private $attachments;

/**
* @param string $serializedStatements The JSON encoded statement(s)
* @param Attachment[] $attachments The statement attachments that include not only a file URL
*/
public function __construct($serializedStatements, array $attachments)
{
$this->boundary = uniqid();
$this->serializedStatements = $serializedStatements;
$this->attachments = $attachments;
}

public function getBoundary()
{
return $this->boundary;
}

public function build()
{
$body = '--'.$this->boundary."\r\n";
$body .= "Content-Type: application/json\r\n";
$body .= 'Content-Length: '.strlen($this->serializedStatements)."\r\n";
$body .= "\r\n";
$body .= $this->serializedStatements."\r\n";

foreach ($this->attachments as $attachment) {
$body .= '--'.$this->boundary."\r\n";
$body .= 'Content-Type: '.$attachment->getContentType()."\r\n";
$body .= "Content-Transfer-Encoding: binary\r\n";
$body .= 'Content-Length: '.$attachment->getLength()."\r\n";
$body .= 'X-Experience-API-Hash: '.$attachment->getSha2()."\r\n";
$body .= "\r\n";
$body .= $attachment->getContent()."\r\n";
}

$body .= '--'.$this->boundary.'--'."\r\n";

return $body;
}
}
15 changes: 9 additions & 6 deletions src/Request/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function __construct(HttpClient $httpClient, RequestFactory $requestFacto
/**
* {@inheritDoc}
*/
public function createRequest($method, $uri, array $urlParameters = array(), $body = null)
public function createRequest($method, $uri, array $urlParameters = array(), $body = null, array $headers = array())
{
if (!in_array(strtoupper($method), array('GET', 'POST', 'PUT', 'DELETE'))) {
throw new \InvalidArgumentException(sprintf('"%s" is no valid HTTP method (expected one of [GET, POST, PUT, DELETE]) in an xAPI context.', $method));
Expand All @@ -61,12 +61,15 @@ public function createRequest($method, $uri, array $urlParameters = array(), $bo
$uri .= '?'.http_build_query($urlParameters);
}

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

if (!isset($headers['Content-Type'])) {
$headers['Content-Type'] = 'application/json';
}

return $request;
return $this->requestFactory->createRequest(strtoupper($method), $uri, $headers, $body);
}

/**
Expand Down
Loading

0 comments on commit c3f3356

Please sign in to comment.