Completed
Push — master ( 760352...c3f335 )
by Christian
03:16
created

StatementsApiClient::doGetStatements()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.8667

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 11
cts 19
cp 0.5789
rs 8.439
c 0
b 0
f 0
cc 5
eloc 21
nc 4
nop 2
crap 6.8667
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\Api;
13
14
use Xabbuh\XApi\Client\Http\MultipartStatementBody;
15
use Xabbuh\XApi\Client\Request\HandlerInterface;
16
use Xabbuh\XApi\Model\StatementId;
17
use Xabbuh\XApi\Serializer\ActorSerializerInterface;
18
use Xabbuh\XApi\Serializer\StatementResultSerializerInterface;
19
use Xabbuh\XApi\Serializer\StatementSerializerInterface;
20
use Xabbuh\XApi\Model\Actor;
21
use Xabbuh\XApi\Model\Statement;
22
use Xabbuh\XApi\Model\StatementResult;
23
use Xabbuh\XApi\Model\StatementsFilter;
24
25
/**
26
 * Client to access the statements API of an xAPI based learning record store.
27
 *
28
 * @author Christian Flothmann <[email protected]>
29
 */
30
final class StatementsApiClient implements StatementsApiClientInterface
31
{
32
    private $requestHandler;
33
    private $version;
34
    private $statementSerializer;
35
    private $statementResultSerializer;
36
    private $actorSerializer;
37
38
    /**
39
     * @param HandlerInterface                   $requestHandler            The HTTP request handler
40
     * @param string                             $version                   The xAPI version
41
     * @param StatementSerializerInterface       $statementSerializer       The statement serializer
42
     * @param StatementResultSerializerInterface $statementResultSerializer The statement result serializer
43
     * @param ActorSerializerInterface           $actorSerializer           The actor serializer
44
     */
45 17
    public function __construct(
46
        HandlerInterface $requestHandler,
47
        $version,
48
        StatementSerializerInterface $statementSerializer,
49
        StatementResultSerializerInterface $statementResultSerializer,
50
        ActorSerializerInterface $actorSerializer
51
    ) {
52 17
        $this->requestHandler = $requestHandler;
53 17
        $this->version = $version;
54 17
        $this->statementSerializer = $statementSerializer;
55 17
        $this->statementResultSerializer = $statementResultSerializer;
56 17
        $this->actorSerializer = $actorSerializer;
57 17
    }
58
59
    /**
60
     * {@inheritDoc}
61
     */
62 4
    public function storeStatement(Statement $statement)
63
    {
64 4
        if (null !== $statement->getId()) {
65 2
            return $this->doStoreStatements(
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doStoreStatements...d()->getValue()), 204); of type array|Xabbuh\XApi\Model\Statement adds the type array to the return on line 65 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...terface::storeStatement of type Xabbuh\XApi\Model\Statement.
Loading history...
66
                $statement,
67 2
                'put',
68 2
                array('statementId' => $statement->getId()->getValue()),
69 2
                204
70
            );
71
        } else {
72 2
            return $this->doStoreStatements($statement);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doStoreStatements($statement); of type array|Xabbuh\XApi\Model\Statement adds the type array to the return on line 72 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...terface::storeStatement of type Xabbuh\XApi\Model\Statement.
Loading history...
73
        }
74
    }
75
76
    /**
77
     * {@inheritDoc}
78
     */
79 4
    public function storeStatements(array $statements)
80
    {
81
        // check that only Statements without ids will be sent to the LRS
82 4
        foreach ($statements as $statement) {
83
            /** @var Statement $statement */
84
85 4
            $isStatement = is_object($statement) && $statement instanceof Statement;
86
87 4
            if (!$isStatement || null !== $statement->getId()) {
88 4
                throw new \InvalidArgumentException('API can only handle statements without ids');
89
            }
90
        }
91
92 1
        return $this->doStoreStatements($statements);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doStoreStatements($statements); of type array|Xabbuh\XApi\Model\Statement adds the type Xabbuh\XApi\Model\Statement to the return on line 92 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...erface::storeStatements of type Xabbuh\XApi\Model\Statement[].
Loading history...
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     */
98 1
    public function voidStatement(Statement $statement, Actor $actor)
99
    {
100 1
        return $this->storeStatement($statement->getVoidStatement($actor));
101
    }
102
103
    /**
104
     * {@inheritDoc}
105
     */
106 2
    public function getStatement(StatementId $statementId, $attachments = true)
107
    {
108 2
        return $this->doGetStatements('statements', array(
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doGetStatements('...s ? 'true' : 'false')); of type Xabbuh\XApi\Model\Statem...i\Model\StatementResult adds the type Xabbuh\XApi\Model\StatementResult to the return on line 108 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...Interface::getStatement of type Xabbuh\XApi\Model\Statement.
Loading history...
109 2
            'statementId' => $statementId->getValue(),
110 2
            'attachments' => $attachments ? 'true' : 'false',
111
        ));
112
    }
113
114
    /**
115
     * {@inheritDoc}
116
     */
117 2
    public function getVoidedStatement(StatementId $statementId, $attachments = true)
118
    {
119 2
        return $this->doGetStatements('statements', array(
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doGetStatements('...s ? 'true' : 'false')); of type Xabbuh\XApi\Model\Statem...i\Model\StatementResult adds the type Xabbuh\XApi\Model\StatementResult to the return on line 119 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...ace::getVoidedStatement of type Xabbuh\XApi\Model\Statement.
Loading history...
120 2
            'voidedStatementId' => $statementId->getValue(),
121 2
            'attachments' => $attachments ? 'true' : 'false',
122
        ));
123
    }
124
125
    /**
126
     * {@inheritDoc}
127
     */
128 4
    public function getStatements(StatementsFilter $filter = null, $attachments = true)
129
    {
130 4
        $urlParameters = array();
131
132 4
        if (null !== $filter) {
133 3
            $urlParameters = $filter->getFilter();
134
        }
135
136
        // the Agent must be JSON encoded
137 4
        if (isset($urlParameters['agent'])) {
138 1
            $urlParameters['agent'] = $this->actorSerializer->serializeActor($urlParameters['agent']);
139
        }
140
141 4
        return $this->doGetStatements('statements', $urlParameters);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doGetStatements('...ents', $urlParameters); of type Xabbuh\XApi\Model\Statem...i\Model\StatementResult adds the type Xabbuh\XApi\Model\Statement to the return on line 141 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...nterface::getStatements of type Xabbuh\XApi\Model\StatementResult.
Loading history...
142
    }
143
144
    /**
145
     * {@inheritDoc}
146
     */
147 1
    public function getNextStatements(StatementResult $statementResult)
148
    {
149 1
        return $this->doGetStatements($statementResult->getMoreUrlPath()->getValue());
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->doGetStatements($...UrlPath()->getValue()); of type Xabbuh\XApi\Model\Statem...i\Model\StatementResult adds the type Xabbuh\XApi\Model\Statement to the return on line 149 which is incompatible with the return type declared by the interface Xabbuh\XApi\Client\Api\S...face::getNextStatements of type Xabbuh\XApi\Model\StatementResult.
Loading history...
150
    }
151
152
    /**
153
     * @param Statement|Statement[] $statements
154
     * @param string                $method
155
     * @param string[]              $parameters
156
     * @param int                   $validStatusCode
157
     *
158
     * @return Statement|Statement[] The created statement(s)
159
     */
160 5
    private function doStoreStatements($statements, $method = 'post', $parameters = array(), $validStatusCode = 200)
161
    {
162 5
        $attachments = array();
163
164 5
        if (is_array($statements)) {
165 1
            foreach ($statements as $statement) {
166 1
                if (null !== $statement->getAttachments()) {
167
                    foreach ($statement->getAttachments() as $attachment) {
168
                        if ($attachment->getContent()) {
169 1
                            $attachments[] = $attachment;
170
                        }
171
                    }
172
                }
173
            }
174
175 1
            $serializedStatements = $this->statementSerializer->serializeStatements($statements);
176
        } else {
177 4
            if (null !== $statements->getAttachments()) {
178
                foreach ($statements->getAttachments() as $attachment) {
179
                    if ($attachment->getContent()) {
180
                        $attachments[] = $attachment;
181
                    }
182
                }
183
            }
184
185 4
            $serializedStatements = $this->statementSerializer->serializeStatement($statements);
186
        }
187
188 5
        $headers = array();
189
190 5
        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 5
            $body = $serializedStatements;
198
        }
199
200 5
        $request = $this->requestHandler->createRequest(
201
            $method,
202 5
            'statements',
203
            $parameters,
204
            $body,
205
            $headers
206
        );
207 5
        $response = $this->requestHandler->executeRequest($request, array($validStatusCode));
208 5
        $statementIds = json_decode((string) $response->getBody());
209
210 5
        if (is_array($statements)) {
211
            /** @var Statement[] $statements */
212 1
            $createdStatements = array();
213
214 1
            foreach ($statements as $index => $statement) {
215 1
                $createdStatements[] = $statement->withId(StatementId::fromString($statementIds[$index]));
216
            }
217
218 1
            return $createdStatements;
219
        } else {
220
            /** @var Statement $statements */
221
222 4
            if (200 === $validStatusCode) {
223 2
                return $statements->withId(StatementId::fromString($statementIds[0]));
224
            } else {
225 2
                return $statements;
226
            }
227
        }
228
    }
229
230
    /**
231
     * Fetch one or more Statements.
232
     *
233
     * @param string $url           URL to request
234
     * @param array  $urlParameters URL parameters
235
     *
236
     * @return Statement|StatementResult
237
     */
238 9
    private function doGetStatements($url, array $urlParameters = array())
239
    {
240 9
        $request = $this->requestHandler->createRequest('get', $url, $urlParameters);
241 9
        $response = $this->requestHandler->executeRequest($request, array(200));
242
243 7
        $contentType = $response->getHeader('Content-Type')[0];
244 7
        $body = (string) $response->getBody();
245 7
        $attachments = array();
246
247 7
        if (false !== strpos($contentType, 'application/json')) {
248 7
            $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
264 7
        if (isset($urlParameters['statementId']) || isset($urlParameters['voidedStatementId'])) {
265 2
            return $this->statementSerializer->deserializeStatement($serializedStatement, $attachments);
266
        } else {
267 5
            return $this->statementResultSerializer->deserializeStatementResult($serializedStatement, $attachments);
268
        }
269
    }
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
    }
309
}
310