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
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 <christian.flothmann@xabbuh.de>
*
* 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 <christian.flothmann@xabbuh.de>
*/
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