├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── doc ├── activity_profile.md ├── agent_profile.md ├── client.md ├── index.md ├── state.md └── statements.md ├── phpspec.yml.dist ├── phpunit.xml.dist ├── spec ├── Request │ └── HandlerSpec.php ├── XApiClientBuilderSpec.php └── XApiClientSpec.php ├── src ├── Api │ ├── ActivityProfileApiClient.php │ ├── ActivityProfileApiClientInterface.php │ ├── AgentProfileApiClient.php │ ├── AgentProfileApiClientInterface.php │ ├── DocumentApiClient.php │ ├── StateApiClient.php │ ├── StateApiClientInterface.php │ ├── StatementsApiClient.php │ └── StatementsApiClientInterface.php ├── Http │ └── MultipartStatementBody.php ├── Request │ ├── Handler.php │ └── HandlerInterface.php ├── XApiClient.php ├── XApiClientBuilder.php ├── XApiClientBuilderInterface.php └── XApiClientInterface.php └── tests └── Api ├── ActivityProfileApiClientTest.php ├── AgentProfileApiClientTest.php ├── ApiClientTest.php ├── StateApiClientTest.php └── StatementsApiClientTest.php /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | 3 | on: 4 | - 'push' 5 | - 'pull_request' 6 | 7 | jobs: 8 | tests: 9 | name: 'Tests' 10 | 11 | runs-on: 'ubuntu-latest' 12 | 13 | strategy: 14 | matrix: 15 | include: 16 | - php-version: '7.1' 17 | composer-options: '--prefer-stable' 18 | - php-version: '7.1' 19 | composer-options: '--prefer-lowest --prefer-stable' 20 | - php-version: '7.2' 21 | composer-options: '--prefer-stable' 22 | - php-version: '7.3' 23 | composer-options: '--prefer-stable' 24 | 25 | steps: 26 | - name: 'Check out' 27 | uses: 'actions/checkout@v2' 28 | 29 | - name: 'Set up PHP' 30 | uses: 'shivammathur/setup-php@v2' 31 | with: 32 | php-version: '${{ matrix.php-version }}' 33 | coverage: 'none' 34 | 35 | - name: 'Get Composer cache directory' 36 | id: 'composer-cache' 37 | run: 'echo "::set-output name=cache-dir::$(composer config cache-files-dir)"' 38 | 39 | - name: 'Cache dependencies' 40 | uses: 'actions/cache@v2' 41 | with: 42 | path: '${{ steps.composer-cache.outputs.cache-dir }}' 43 | key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" 44 | restore-keys: 'php-${{ matrix.php-version }}-composer-locked-' 45 | 46 | - name: 'Install dependencies' 47 | run: 'composer update --no-progress $COMPOSER_OPTIONS' 48 | 49 | - name: 'Install PHPUnit' 50 | run: 'vendor/bin/simple-phpunit install' 51 | 52 | - name: 'Run PhpSpec' 53 | run: 'vendor/bin/phpspec run' 54 | 55 | - name: 'Run PHPUnit' 56 | run: 'SYMFONY_DEPRECATIONS_HELPER=weak vendor/bin/simple-phpunit' 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.result.cache 2 | /composer.lock 3 | /phpunit.xml 4 | /vendor 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 0.7.0 5 | ----- 6 | 7 | * dropped support for PHP < 7.1 8 | 9 | 0.6.0 10 | ----- 11 | 12 | * added compatibility with HTTPlug 2 13 | 14 | * dropped the support for HHVM and for PHP < 5.6 15 | 16 | * updated the `X-Experience-API-Version` header to default to the latest patch 17 | version (`1.0.3`) 18 | 19 | * allow `2.x` releases of the `php-xapi/model` package too 20 | 21 | * allow `3.x` releases of the `php-xapi/model` package for PHP 7.2 compatibility 22 | 23 | 0.5.0 24 | ----- 25 | 26 | * **CAUTION**: This release drops support for PHP 5.3 due to the introduced 27 | dependency on `php-http/httplug` (see below). 28 | 29 | * The client now depends on the [HTTPlug library](http://httplug.io/) to 30 | perform HTTP requests. This means that the package now depends the virtual 31 | `php-http/client-implementation`. To satisfy this dependency you have to 32 | pick [an implementation](https://packagist.org/providers/php-http/client-implementation) 33 | and install it together with `php-xapi/client`. 34 | 35 | For example, if you prefer to use [Guzzle 6](http://docs.guzzlephp.org/en/latest/) 36 | you would do the following: 37 | 38 | ```bash 39 | $ composer require --no-update php-http/guzzle6-adapter 40 | $ composer require php-xapi/client 41 | ``` 42 | 43 | * The `setHttpClient()` and `setRequestFactory()` method have been added 44 | to the `XApiClientBuilderInterface` and must be used to configure the 45 | `HttpClient` and `RequestFactory` instances you intend to use. 46 | 47 | To use [Guzzle 6](http://docs.guzzlephp.org/en/latest/), for example, 48 | this will look like this: 49 | 50 | ```php 51 | use Http\Adapter\Guzzle6\Client; 52 | use Http\Message\MessageFactory\GuzzleMessageFactory; 53 | use Xabbuh\XApi\Client\XApiClientBuilder; 54 | 55 | $builder = new XApiClientBuilder(); 56 | $client = $builder->setHttpClient(new Client()) 57 | ->setRequestFactory(new GuzzleMessageFactory()) 58 | ->setBaseUrl('http://example.com/xapi/') 59 | ->build(); 60 | ``` 61 | 62 | You can avoid calling `setHttpClient()` and `setRequestFactory` by installing 63 | the [HTTP discovery](http://php-http.org/en/latest/discovery.html) package. 64 | 65 | * The `xabbuh/oauth1-authentication` package now must be installed if you want 66 | to use OAuth1 authentication. 67 | 68 | * Bumped the required versions of all `php-xapi` packages to the `1.x` release 69 | series. 70 | 71 | * Include the raw attachment content wrapped in a `multipart/mixed` encoded 72 | request when raw content is part of a statement's attachment. 73 | 74 | * Added the possibility to decide whether or not to include attachments when 75 | requesting statements from an LRS. A second optional `$attachments` argument 76 | (defaulting to `true`) has been added for this purpose to the `getStatement()`, 77 | `getVoidedStatement()`, and `getStatements()` methods of the `StatementsApiClient` 78 | class and the `StatementsApiClientInterface`. 79 | 80 | * An optional fifth `$headers` parameter has been added to the `createRequest()` 81 | method of the `HandlerInterface` and the `Handler` class which allows to pass 82 | custom headers when performing HTTP requests. 83 | 84 | 0.4.0 85 | ----- 86 | 87 | * The `XApiClientBuilder` class now makes use of the `SerializerFactoryInterface` 88 | introduced in release `0.4.0` of the `php-xapi/serializer` package. By 89 | default, it will fall back to the `SerializerFactory` implemented provided 90 | by the `php-xapi/symfony-serializer` to maintain backwards-compatibility 91 | with the previous release. However, you are now able to inject arbitrary 92 | implementations of the `SerializerFactoryInterface` into the constructor 93 | of the `XApiClientBuilder` to use whatever alternative implementation 94 | (packages providing such an implementation should provide the virtual 95 | `php-xapi/serializer-implementation` package). 96 | 97 | 0.3.0 98 | ----- 99 | 100 | * Do not send authentication headers when no credentials have been configured. 101 | 102 | * Fixed treating HTTP methods case insensitive. Rejecting uppercased HTTP 103 | method names contradicts the HTTP specification. Lowercased method names 104 | will still be supported to keep backwards compatibility though. 105 | 106 | * Fixed creating `XApiClient` instances in an invalid state. The `XApiClientBuilder` 107 | now throws a `\LogicException` when the `build()` method is called before 108 | a base URI was configured. 109 | 110 | * Removed the `ApiClient` class. The `$requestHandler` and `$version` attributes 111 | have been moved to the former child classes of the `ApiClient` class and 112 | their visibility has been changed to `private`. 113 | 114 | * The visibility of the `$documentDataSerializer` property of the `ActivityProfileApiClient`, 115 | `AgentProfileApiClient`, `DocumentApiClient`, and `StateApiClient` classes 116 | has been changed to `private`. 117 | 118 | * Removed the `getRequestHandler()` method from the API classes: 119 | 120 | * `ActivityProfileApiClient::getRequestHandler()` 121 | * `AgentProfileApiClient::getRequestHandler()` 122 | * `ApiClient::getRequestHandler()` 123 | * `DocumentApiClient::getRequestHandler()` 124 | * `StateApiClient::getRequestHandler()` 125 | * `StatementsApiClient::getRequestHandler()` 126 | 127 | * Removed the `getVersion()` method from the API interfaces: 128 | 129 | * `ActivityProfileApiClientInterface::getVersion()` 130 | * `AgentProfileApiClientInterface::getVersion()` 131 | * `StateApiClientInterface::getVersion()` 132 | * `StatementsApiClientInterface::getVersion()` 133 | 134 | * Removed the `getVersion()` method from the API classes: 135 | 136 | * `ActivityProfileApiClient::getVersion()` 137 | * `AgentProfileApiClient::getVersion()` 138 | * `ApiClient::getVersion()` 139 | * `DocumentApiClient::getVersion()` 140 | * `StateApiClient::getVersion()` 141 | * `StatementsApiClient::getVersion()` 142 | * `XApiClient::getVersion()` 143 | 144 | * Removed the `getUsername()` and `getPassword()` methods from the `HandlerInterface` 145 | and the `Handler` class. 146 | 147 | * Removed the `getHttpClient()` method from the `Handler` class. 148 | 149 | * Removed the `getSerializerRegistry()` method from the `XApiClient` class. 150 | 151 | * Made all classes final. 152 | 153 | 0.2.0 154 | ----- 155 | 156 | * made the client compatible with version 0.5 of the `php-xapi/model` package 157 | 158 | * made the client compatible with version 0.3 of the `php-xapi/serializer` package 159 | 160 | 0.1.0 161 | ----- 162 | 163 | First release of an Experience API client based on the Guzzle HTTP library. 164 | 165 | This package replaces the `xabbuh/xapi-client` package which is now deprecated 166 | and should no longer be used. 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2019 Christian Flothmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP xApi (Experience API) Client 2 | ================================ 3 | 4 | [![Build Status](https://travis-ci.org/php-xapi/client.svg?branch=master)](https://travis-ci.org/php-xapi/client) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/php-xapi/client/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/php-xapi/client/?branch=master) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/php-xapi/client/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/php-xapi/client/?branch=master) 7 | 8 | Client side PHP implementation of the 9 | [Experience API](https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md). 10 | 11 | Installation 12 | ------------ 13 | 14 | The recommended way to install the xAPI client is using 15 | [Composer](http://getcomposer.org/): 16 | 17 | 1. Add ``php-xapi/client`` as a dependency to your project: 18 | 19 | ```bash 20 | $ composer require php-xapi/client 21 | ``` 22 | 23 | 1. Require Composer's autoloader: 24 | 25 | ``` php 26 | require __DIR__.'/vendor/autoload.php'; 27 | ``` 28 | 29 | Usage 30 | ----- 31 | 32 | Read the [documentation](doc/index.md) to find out how to use the library. 33 | 34 | Issues 35 | ------ 36 | 37 | Report issues in the [issue tracker of this package](https://github.com/php-xapi/client/issues). 38 | 39 | License 40 | ------- 41 | 42 | This package is under the MIT license. See the complete license in the 43 | [LICENSE](LICENSE) file. 44 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | UPGRADE 2 | ======= 3 | 4 | Upgrading from 0.4 to 0.5 5 | ------------------------- 6 | 7 | * **CAUTION**: This release drops support for PHP 5.3 due to the introduced 8 | dependency on `php-http/httplug` (see below). 9 | 10 | * The client now depends on the [HTTPlug library](http://httplug.io/) to 11 | perform HTTP requests. This means that the package now depends the virtual 12 | `php-http/client-implementation`. To satisfy this dependency you have to 13 | pick [an implementation](https://packagist.org/providers/php-http/client-implementation) 14 | and install it together with `php-xapi/client`. 15 | 16 | For example, if you prefer to use [Guzzle 6](http://docs.guzzlephp.org/en/latest/) 17 | you would do the following: 18 | 19 | ```bash 20 | $ composer require --no-update php-http/guzzle6-adapter 21 | $ composer require php-xapi/client 22 | ``` 23 | 24 | * The `setHttpClient()` and `setRequestFactory()` method have been added 25 | to the `XApiClientBuilderInterface` and must be used to configure the 26 | `HttpClient` and `RequestFactory` instances you intend to use. 27 | 28 | To use [Guzzle 6](http://docs.guzzlephp.org/en/latest/), for example, 29 | this will look like this: 30 | 31 | ```php 32 | use Http\Adapter\Guzzle6\Client; 33 | use Http\Message\MessageFactory\GuzzleMessageFactory; 34 | use Xabbuh\XApi\Client\XApiClientBuilder; 35 | 36 | $builder = new XApiClientBuilder(); 37 | $client = $builder->setHttpClient(new Client()) 38 | ->setRequestFactory(new GuzzleMessageFactory()) 39 | ->setBaseUrl('http://example.com/xapi/') 40 | ->build(); 41 | ``` 42 | 43 | You can avoid calling `setHttpClient()` and `setRequestFactory` by installing 44 | the [HTTP discovery](http://php-http.org/en/latest/discovery.html) package. 45 | 46 | * The `xabbuh/oauth1-authentication` package now must be installed if you want 47 | to use OAuth1 authentication. 48 | 49 | * A second optional `$attachments` argument (defaulting to `true`) has been added 50 | to the `getStatement()`, `getVoidedStatement()`, and `getStatements()` methods 51 | of the `StatementsApiClient` class and the `StatementsApiClientInterface`. 52 | 53 | * An optional fifth `$headers` parameter has been added to the `createRequest()` 54 | method of the `HandlerInterface` and the `Handler` class which allows to pass 55 | custom headers when performing HTTP requests. 56 | 57 | Upgrading from 0.2 to 0.3 58 | ------------------------- 59 | 60 | * Removed the `ApiClient` class. The `$requestHandler` and `$version` attributes 61 | have been moved to the former child classes of the `ApiClient` class and 62 | their visibility has been changed to `private`. 63 | 64 | * The visibility of the `$documentDataSerializer` property of the `ActivityProfileApiClient`, 65 | `AgentProfileApiClient`, `DocumentApiClient`, and `StateApiClient` classes 66 | has been changed to `private`. 67 | 68 | * Removed the `getRequestHandler()` method from the API classes: 69 | 70 | * `ActivityProfileApiClient::getRequestHandler()` 71 | * `AgentProfileApiClient::getRequestHandler()` 72 | * `ApiClient::getRequestHandler()` 73 | * `DocumentApiClient::getRequestHandler()` 74 | * `StateApiClient::getRequestHandler()` 75 | * `StatementsApiClient::getRequestHandler()` 76 | * `XApiClient::getRequestHandler()` 77 | 78 | * Removed the `getVersion()` method from the API interfaces: 79 | 80 | * `ActivityProfileApiClientInterface::getVersion()` 81 | * `AgentProfileApiClientInterface::getVersion()` 82 | * `StateApiClientInterface::getVersion()` 83 | * `StatementsApiClientInterface::getVersion()` 84 | 85 | * Removed the `getVersion()` method from the API classes: 86 | 87 | * `ActivityProfileApiClient::getVersion()` 88 | * `AgentProfileApiClient::getVersion()` 89 | * `ApiClient::getVersion()` 90 | * `DocumentApiClient::getVersion()` 91 | * `StateApiClient::getVersion()` 92 | * `StatementsApiClient::getVersion()` 93 | * `XApiClient::getVersion()` 94 | 95 | * Removed the `getUsername()` and `getPassword()` methods from the `HandlerInterface` 96 | and the `Handler` class. 97 | 98 | * Removed the `getHttpClient()` method from the `Handler` class. 99 | 100 | * Removed the `getSerializerRegistry()` method from the `XApiClient` class. 101 | 102 | * All classes are final now which means that you can now longer extend them. 103 | Consider using composition/decoration instead if you need to build functionality 104 | on top of the built-in classes. 105 | 106 | Upgrading from 0.1 to 0.2 107 | ------------------------- 108 | 109 | * Statement identifiers must be passed as `StatementId` objects instead of 110 | strings. 111 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-xapi/client", 3 | "type": "library", 4 | "description": "client library for the Experience API (xAPI)", 5 | "keywords": ["xAPI", "Experience API", "Tin Can API", "client"], 6 | "homepage": "https://github.com/php-xapi/client", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Christian Flothmann", 11 | "homepage": "https://github.com/xabbuh" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.1", 16 | "php-http/client-common": "^1.0 || ^2.0", 17 | "php-http/client-implementation": "^1.0", 18 | "php-http/httplug": "^1.0 || ^2.0", 19 | "php-http/message": "^1.0", 20 | "php-http/message-factory": "^1.0", 21 | "php-xapi/exception": "^0.1 || ^0.2", 22 | "php-xapi/model": "^1.0 || ^2.0 || ^3.0", 23 | "php-xapi/serializer": "^2.0", 24 | "php-xapi/serializer-implementation": "^2.0", 25 | "php-xapi/symfony-serializer": "^2.0", 26 | "psr/http-message": "^1.0" 27 | }, 28 | "require-dev": { 29 | "phpspec/phpspec": "^2.4", 30 | "php-http/mock-client": "^1.2", 31 | "php-xapi/test-fixtures": "^1.0", 32 | "symfony/phpunit-bridge": "^5.2" 33 | }, 34 | "minimum-stability": "dev", 35 | "suggest": { 36 | "php-http/discovery": "For automatic discovery of HTTP clients and request factories", 37 | "xabbuh/oauth1-authentication": "For OAuth1 authentication support" 38 | }, 39 | "conflict": { 40 | "xabbuh/xapi-client": "*" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Xabbuh\\XApi\\Client\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "spec\\Xabbuh\\XApi\\Client\\": "spec/", 50 | "Xabbuh\\XApi\\Client\\Tests\\": "tests/" 51 | } 52 | }, 53 | "extra": { 54 | "branch-alias": { 55 | "dev-master": "0.7.x-dev" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /doc/activity_profile.md: -------------------------------------------------------------------------------- 1 | The Activity Profile API 2 | ======================== 3 | 4 | Activity Profiles 5 | ----------------- 6 | 7 | A LMS can use the xAPI to store documents associated to a certain activity using 8 | activity profiles. An activity profile is dedicated to an activity and a profile 9 | id: 10 | 11 | ```php 12 | use Xabbuh\XApi\Model\ActivityProfile; 13 | 14 | // ... 15 | $profile = new ActivityProfile(); 16 | $profile->setActivity($activity); 17 | $profile->setProfileId($profileId); 18 | ``` 19 | 20 | Documents 21 | --------- 22 | 23 | Documents are simple collections of key-value pairs and can be accessed like arrays: 24 | 25 | ```php 26 | use Xabbuh\XApi\Model\ActivityProfileDocument; 27 | 28 | // ... 29 | $document = new ActivityProfileDocument(); 30 | $document->setActivityProfile($profile); 31 | $document['x'] = 'foo'; 32 | $document['y'] = 'bar'; 33 | ``` 34 | 35 | Obtaining the Activity Profile API Client 36 | ----------------------------------------- 37 | 38 | After you have [built the global xAPI client](client.md), you can obtain an activity 39 | profile API client by calling its ``getActivityProfileApiClient()`` method: 40 | 41 | ```php 42 | $activityProfileApiClient = $xApiClient->getActivityProfileApiClient(); 43 | ``` 44 | 45 | Storing Activity Profile Documents 46 | ---------------------------------- 47 | 48 | You can simply store an ``ActivityProfileDocument`` passing it to the 49 | ``createOrUpdateActivityProfileDocument()`` method of the xAPI client: 50 | 51 | ```php 52 | $document = ...; // the activity profile document 53 | $activityProfileApiClient->createOrUpdateActivityProfileDocument($document); 54 | ``` 55 | 56 | If a document already exists for this activity profile, the existing document will 57 | be updated. This means that new fields will be updated, existing fields that are 58 | included in the new document will be overwritten and existing fields that are 59 | not included in the new document will be kept as they are. 60 | 61 | If you want to replace a document, use the ``createOrReplaceActivityProfileDocument()`` 62 | method instead: 63 | 64 | ```php 65 | $document = ...; // the activity profile document 66 | $activityProfileApiClient->createOrReplaceActivityProfileDocument($document); 67 | ``` 68 | 69 | Deleting Activity Profile Documents 70 | ----------------------------------- 71 | 72 | An ``ActivityProfileDocument`` is deleted by passing the particular ``ActivityProfile`` 73 | to the ``deleteActivityProfileDocument()`` method: 74 | 75 | ```php 76 | $profile = ...; // the activity profile the document should be deleted from 77 | $activityProfileApiClient->deleteActivityProfileDocument($profile); 78 | ``` 79 | 80 | Retrieving Activity Profile Documents 81 | ------------------------------------- 82 | 83 | Similarly, you receive a document for a particular activity profile by passing 84 | the profile to the ``getActivityProfileDocument()`` method: 85 | 86 | ```php 87 | $profile = ...; // the activity profile the document should be retrieved from 88 | $document = $activityProfileApiClient->getActivityProfileDocument($profile); 89 | ``` 90 | -------------------------------------------------------------------------------- /doc/agent_profile.md: -------------------------------------------------------------------------------- 1 | The Agent Profile API 2 | ===================== 3 | 4 | Agent Profiles 5 | -------------- 6 | 7 | A LMS can use the xAPI to store documents associated to a certain agent using 8 | agent profiles. An agent profile is dedicated to an agent and a profile id: 9 | 10 | ```php 11 | use Xabbuh\XApi\Model\AgentProfile; 12 | 13 | // ... 14 | $profile = new AgentProfile(); 15 | $profile->setAgent($agent); 16 | $profile->setProfileId($profileId); 17 | ``` 18 | 19 | Documents 20 | --------- 21 | 22 | Documents are simple collections of key-value pairs and can be accessed like arrays: 23 | 24 | ```php 25 | use Xabbuh\XApi\Model\AgentProfileDocument; 26 | 27 | // ... 28 | $document = new AgentProfileDocument(); 29 | $document->setAgentProfile($profile); 30 | $document['x'] = 'foo'; 31 | $document['y'] = 'bar'; 32 | ``` 33 | 34 | Obtaining the Agent Profile API Client 35 | -------------------------------------- 36 | 37 | After you have [built the global xAPI client](client.md), you can obtain an agent 38 | profile API client by calling its ``getAgentProfileApiClient()`` method: 39 | 40 | ```php 41 | $agentProfileApiClient = $xApiClient->getAgentProfileApiClient(); 42 | ``` 43 | 44 | Storing Agent Profile Documents 45 | ------------------------------- 46 | 47 | You can simply store an ``AgentProfileDocument`` passing it to the 48 | ``createOrUpdateAgentProfileDocument()`` method of the xAPI client: 49 | 50 | ```php 51 | $document = ...; // the agent profile document 52 | $agentProfileApiClient->createOrUpdateAgentProfileDocument($document); 53 | ``` 54 | 55 | If a document already exists for this agent profile, the existing document will 56 | be updated. This means that new fields will be updated, existing fields that are 57 | included in the new document will be overwritten and existing fields that are 58 | not included in the new document will be kept as they are. 59 | 60 | If you want to replace a document, use the ``createOrReplaceAgentProfileDocument()`` 61 | method instead: 62 | 63 | ```php 64 | $document = ...; // the agent profile document 65 | $agentProfileApiClient->createOrReplaceAgentProfileDocument($document); 66 | ``` 67 | 68 | Deleting Agent Profile Documents 69 | -------------------------------- 70 | 71 | An ``AgentProfileDocument`` is deleted by passing the particular ``AgentProfile`` 72 | to the ``deleteAgentProfileDocument()`` method: 73 | 74 | ```php 75 | $profile = ...; // the agent profile the document should be deleted from 76 | $agentProfileApiClient->deleteAgentProfileDocument($profile); 77 | ``` 78 | 79 | Retrieving Agent Profile Documents 80 | ---------------------------------- 81 | 82 | Similarly, you receive a document for a particular agent profile by passing the 83 | profile to the ``getAgentProfileDocument()`` method: 84 | 85 | ```php 86 | $profile = ...; // the agent profile the document should be retrieved from 87 | $document = $agentProfileApiClient->getAgentProfileDocument($profile); 88 | ``` 89 | -------------------------------------------------------------------------------- /doc/client.md: -------------------------------------------------------------------------------- 1 | Building an xAPI Client 2 | ======================= 3 | 4 | The xAPI client library ships with a builder class which eases the process of 5 | creating an instance of an ``XApiClient`` class: 6 | 7 | ```php 8 | use Xabbuh\XApi\Client\XApiClientBuilder; 9 | 10 | $builder = new XApiClientBuilder(); 11 | $xApiClient = $builder->setBaseUrl('http://example.com/lrs/api') 12 | ->setVersion('1.0.0') 13 | ->build(); 14 | ``` 15 | 16 | The builder creates a client for the 1.0.1 API version if you don't set a version. 17 | 18 | HTTP Basic Authentication 19 | ------------------------- 20 | 21 | Use the ``setAuth()`` method if access to the LRS resources is protected with 22 | HTTP Basic authentication: 23 | 24 | ```php 25 | use Xabbuh\XApi\Client\XApiClientBuilder; 26 | 27 | $builder = new XApiClientBuilder(); 28 | $xApiClient = $builder->setBaseUrl('http://example.com/lrs/api') 29 | ->setAuth('username', 'password') 30 | ->build(); 31 | ``` 32 | 33 | OAuth1 Authentication 34 | --------------------- 35 | 36 | Using the ``setOAuthCredentials()`` method, you can configure the client to 37 | access OAuth1 protected resources: 38 | 39 | ```php 40 | use Xabbuh\XApi\Client\XApiClientBuilder; 41 | 42 | $builder = new XApiClientBuilder(); 43 | $xApiClient = $builder->setBaseUrl('http://example.com/lrs/api') 44 | ->setOAuthCredentials('consumer-key', 'consumer-secret', 'token', 'token-secret') 45 | ->build(); 46 | ``` 47 | 48 | Using the APIs 49 | -------------- 50 | 51 | The Experience API consists of four sub APIs: the statements API, the state API, 52 | the activity profile API and the agent profile API. A client for each of these 53 | APIs can be obtained from the global ``XApiClient`` instance: 54 | 55 | ```php 56 | $statementsApiClient = $xApiClient->getStatementsApiClient(); 57 | $stateApiClient = $xApiClient->getStateApiClient(); 58 | $activityProfileApiClient = $xApiClient->getActivityProfileApiClient(); 59 | $agentProfileApiClient = $xApiClient->getAgentProfileApiClient(); 60 | ``` 61 | 62 | Read the dedicated chapters of the sub APIs to learn how to make use of them: 63 | 64 | 1. [The Statements API](statements.md) 65 | 66 | 1. [The State API](state.md) 67 | 68 | 1. [The Activity Profile API](activity_profile.md) 69 | 70 | 1. [The Agent profile API](agent_profile.md) 71 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | Documentation 2 | ============= 3 | 4 | 1. [Obtaining an xAPI client](client.md) 5 | 6 | 1. [The Statements API](statements.md) 7 | 8 | 1. [The State API](state.md) 9 | 10 | 1. [The Activity Profile API](activity_profile.md) 11 | 12 | 1. [The Agent profile API](agent_profile.md) 13 | -------------------------------------------------------------------------------- /doc/state.md: -------------------------------------------------------------------------------- 1 | The State API 2 | ============= 3 | 4 | States 5 | ------ 6 | 7 | A LMS can use the xAPI to store documents associated to a certain state. A state 8 | is dedicated to an activity, an actor, a state id and an optional registration 9 | id (for example a user id): 10 | 11 | ```php 12 | use Xabbuh\XApi\Model\State; 13 | 14 | // ... 15 | $state = new State(); 16 | $state->setActivity($activity); 17 | $state->setActor($actor); 18 | $state->setStateId($stateId); 19 | ``` 20 | 21 | Documents 22 | --------- 23 | 24 | Documents are simple collections of key-value pairs and can be accessed like arrays: 25 | 26 | ```php 27 | use Xabbuh\XApi\Model\StateDocument; 28 | 29 | // ... 30 | $document = new StateDocument(); 31 | $document->setState($state); 32 | $document['x'] = 'foo'; 33 | $document['y'] = 'bar'; 34 | ``` 35 | 36 | Obtaining the State API Client 37 | ------------------------------ 38 | 39 | After you have [built the global xAPI client](client.md), you can obtain a state 40 | API client by calling its ``getStateApiClient()`` method: 41 | 42 | ```php 43 | $stateApiClient = $xApiClient->getStateApiClient(); 44 | ``` 45 | 46 | Storing State Documents 47 | ----------------------- 48 | 49 | You can simply store a ``StateDocument`` passing it to the ``createOrUpdateStateDocument()`` 50 | method of the xAPI client: 51 | 52 | ```php 53 | $document = ...; // the state document 54 | $stateApiClient->createOrUpdateStateDocument($document); 55 | ``` 56 | 57 | If a document already exists for this state, the existing document will be updated. 58 | This means that new fields will be updated, existing fields that are included in 59 | the new document will be overwritten and existing fields that are not included in 60 | the new document will be kept as they are. 61 | 62 | If you want to replace a document, use the ``createOrReplaceStateDocument()`` method 63 | instead: 64 | 65 | ```php 66 | $document = ...; // the state document 67 | $stateApiClient->createOrReplaceStateDocument($document); 68 | ``` 69 | 70 | Deleting State Documents 71 | ------------------------ 72 | 73 | A ``StateDocument`` is deleted by passing the particular ``State`` to the ``deleteStateDocument()`` 74 | method: 75 | 76 | ```php 77 | $state = ...; // the state the document should be deleted from 78 | $stateApiClient->deleteStateDocument($state); 79 | ``` 80 | 81 | Retrieving State Documents 82 | -------------------------- 83 | 84 | Similarly, you receive a document for a particular state by passing the state to 85 | the ``getStateDocument()`` method: 86 | 87 | ```php 88 | $state = ...; // the state the document should be retrieved from 89 | $document = $stateApiClient->getStateDocument($state); 90 | ``` 91 | -------------------------------------------------------------------------------- /doc/statements.md: -------------------------------------------------------------------------------- 1 | The Statements API 2 | ================== 3 | 4 | Obtaining the Agent Profile API Client 5 | -------------------------------------- 6 | 7 | After you have [built the global xAPI client](client.md), you can obtain a statements 8 | API client by calling its ``getStatementsApiClient()`` method: 9 | 10 | ```php 11 | $statementsApiClient = $xApiClient->getStatementsApiClient(); 12 | ``` 13 | 14 | Storing Statements 15 | ------------------ 16 | 17 | The ``storeStatement()`` and ``storeStatements()`` methods can be used to store 18 | a single Statement or a collection of Statements. Both method return the stored 19 | Statement(s) each having a unique id created by the remote LRS. 20 | 21 | ```php 22 | 23 | use Xabbuh\XApi\Model\Statement; 24 | 25 | $statement = new Statement(); 26 | // ... 27 | 28 | // store a single Statement 29 | $statementsApiClient->storeStatement($statement); 30 | 31 | $statement2 = new Statement(); 32 | // ... 33 | 34 | // store a collection of clients 35 | $statementsApiClient->storeStatements(array($statement, $statement2)); 36 | ``` 37 | 38 | Retrieving Statements 39 | --------------------- 40 | 41 | Use the ``getStatement()`` method to obtain a certain Statement given its id: 42 | 43 | ```php 44 | // ... 45 | 46 | // get a single Statement 47 | $statement = $statementsApiClient->getStatement($statementId); 48 | ``` 49 | 50 | ``getStatements()`` returns a collection of Statements encapsulated in a 51 | StatementResult instance: 52 | 53 | ```php 54 | // ... 55 | 56 | // returns all accessible Statements 57 | $result = $statementsApiClient->getStatements(); 58 | ``` 59 | 60 | You can even filter Statements using a StatementFilter: 61 | 62 | ```php 63 | use Xabbuh\XApi\Model\StatementsFilter; 64 | 65 | // ... 66 | $filter = new StatementsFilter(); 67 | $filter 68 | ->byActor($actor) // filter by Actor 69 | ->byVerb($verb) // filter by Verb 70 | ->byActivity($activity) // filter by Activity 71 | ->byRegistration(...) // filter for Statements matching the given 72 | // registration id 73 | ->enableRelatedActivityFilter() // apply the Activity filter to Sub-Statements 74 | ->disableRelatedActivityFilter() // apply the Activity filter to Sub-Statements 75 | ->enableRelatedAgentFilter() // apply the Agent filter to Sub-Statements 76 | ->disableRelatedAgentFilter() // apply the Agent filter to Sub-Statements 77 | ->since(new \DateTime(...)) // filter for Statements stored since 78 | // the given timestamp 79 | ->until(new \DateTime(...)) // filter for Statements stored before 80 | // the given timestamp 81 | ->limit(5) // limit the number of Statements returned 82 | ->format(...) // the result format (one of "ids", "exact", 83 | // "canonical") 84 | ->includeAttachments() // return Statements with attachments included 85 | ->excludeAttachments() // return Statements without attachments 86 | ->ascending() // ascending order of stored time 87 | ->descending(); // ascending order of stored time 88 | 89 | $result = $statementsApiClient->getStatements($filter->getFilter()); 90 | ``` 91 | 92 | If you limited the number of returned results, you can get the next Statements 93 | by calling the ``getNextStatements()`` method passing the ``StatementResult`` 94 | of the previous request to it: 95 | 96 | ```php 97 | // .... 98 | $filter = new StatementsFilter(); 99 | $filter->limit(3); 100 | $firstStatementResult = $statementsApiClient->getStatements($filter); 101 | 102 | // get the next Statements 103 | $nextStatementResult = $statementsApiClient->getNextStatements($firstStatementResult); 104 | ``` 105 | 106 | The Experience API doesn't allow to delete Statements. You have to mark them as 107 | voided instead: 108 | 109 | ```php 110 | // ... 111 | $statement = ...; // The Statement being voided 112 | $actor = ...; // The Actor voiding the Statement 113 | $statementsApiClient->voidStatement($statement, $actor); 114 | ``` 115 | 116 | Voided Statements won't be returned when requesting either a single Statement or 117 | a collection of Statements. Though, you can retrieve a single voided Statement 118 | using the ``getVoidedStatement()`` method: 119 | 120 | ```php 121 | // ... 122 | $voidedStatement = $statementsApiClient->getVoidedStatement($statementId); 123 | ``` 124 | -------------------------------------------------------------------------------- /phpspec.yml.dist: -------------------------------------------------------------------------------- 1 | suites: 2 | default: 3 | namespace: Xabbuh\XApi\Client 4 | psr4_prefix: Xabbuh\XApi\Client 5 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/Request/HandlerSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($client, $requestFactory, 'http://example.com/xapi/', '1.0.1'); 20 | } 21 | 22 | function it_throws_an_exception_if_a_request_is_created_with_an_invalid_method() 23 | { 24 | $this->shouldThrow('\InvalidArgumentException')->during('createRequest', array('options', '/xapi/statements')); 25 | } 26 | 27 | function it_returns_get_request_created_by_the_http_client(RequestFactory $requestFactory, RequestInterface $request) 28 | { 29 | $requestFactory->createRequest('GET', 'http://example.com/xapi/statements', array( 30 | 'X-Experience-API-Version' => '1.0.1', 31 | 'Content-Type' => 'application/json', 32 | ), null)->willReturn($request); 33 | 34 | $this->createRequest('get', '/statements')->shouldReturn($request); 35 | $this->createRequest('GET', '/statements')->shouldReturn($request); 36 | } 37 | 38 | function it_returns_post_request_created_by_the_http_client(RequestFactory $requestFactory, RequestInterface $request) 39 | { 40 | $requestFactory->createRequest('POST', 'http://example.com/xapi/statements', array( 41 | 'X-Experience-API-Version' => '1.0.1', 42 | 'Content-Type' => 'application/json', 43 | ), 'body')->willReturn($request); 44 | 45 | $this->createRequest('post', '/statements', array(), 'body')->shouldReturn($request); 46 | $this->createRequest('POST', '/statements', array(), 'body')->shouldReturn($request); 47 | } 48 | 49 | function it_returns_put_request_created_by_the_http_client(RequestFactory $requestFactory, RequestInterface $request) 50 | { 51 | $requestFactory->createRequest('PUT', 'http://example.com/xapi/statements', array( 52 | 'X-Experience-API-Version' => '1.0.1', 53 | 'Content-Type' => 'application/json', 54 | ), 'body')->willReturn($request); 55 | 56 | $this->createRequest('put', '/statements', array(), 'body')->shouldReturn($request); 57 | $this->createRequest('PUT', '/statements', array(), 'body')->shouldReturn($request); 58 | } 59 | 60 | function it_returns_delete_request_created_by_the_http_client(RequestFactory $requestFactory, RequestInterface $request) 61 | { 62 | $requestFactory->createRequest('DELETE', 'http://example.com/xapi/statements', array( 63 | 'X-Experience-API-Version' => '1.0.1', 64 | 'Content-Type' => 'application/json', 65 | ), null)->willReturn($request); 66 | 67 | $this->createRequest('delete', '/statements')->shouldReturn($request); 68 | $this->createRequest('DELETE', '/statements')->shouldReturn($request); 69 | } 70 | 71 | function it_throws_an_access_denied_exception_when_a_401_status_code_is_returned(HttpClient $client, RequestInterface $request, ResponseInterface $response) 72 | { 73 | $client->sendRequest($request)->willReturn($response); 74 | $response->getStatusCode()->willReturn(401); 75 | $response->getBody()->willReturn('body'); 76 | 77 | $this->shouldThrow(AccessDeniedException::class)->during('executeRequest', array($request, array(200))); 78 | } 79 | 80 | function it_throws_an_access_denied_exception_when_a_403_status_code_is_returned(HttpClient $client, RequestInterface $request, ResponseInterface $response) 81 | { 82 | $client->sendRequest($request)->willReturn($response); 83 | $response->getStatusCode()->willReturn(403); 84 | $response->getBody()->willReturn('body'); 85 | 86 | $this->shouldThrow(AccessDeniedException::class)->during('executeRequest', array($request, array(200))); 87 | } 88 | 89 | function it_throws_a_not_found_exception_when_a_404_status_code_is_returned(HttpClient $client, RequestInterface $request, ResponseInterface $response) 90 | { 91 | $client->sendRequest($request)->willReturn($response); 92 | $response->getStatusCode()->willReturn(404); 93 | $response->getBody()->willReturn('body'); 94 | 95 | $this->shouldThrow(NotFoundException::class)->during('executeRequest', array($request, array(200))); 96 | } 97 | 98 | function it_throws_a_conflict_exception_when_a_409_status_code_is_returned(HttpClient $client, RequestInterface $request, ResponseInterface $response) 99 | { 100 | $client->sendRequest($request)->willReturn($response); 101 | $response->getStatusCode()->willReturn(409); 102 | $response->getBody()->willReturn('body'); 103 | 104 | $this->shouldThrow(ConflictException::class)->during('executeRequest', array($request, array(200))); 105 | } 106 | 107 | function it_throws_an_xapi_exception_when_an_unexpected_status_code_is_returned(HttpClient $client, RequestInterface $request, ResponseInterface $response) 108 | { 109 | $client->sendRequest($request)->willReturn($response); 110 | $response->getStatusCode()->willReturn(204); 111 | $response->getBody()->willReturn('body'); 112 | 113 | $this->shouldThrow(XApiException::class)->during('executeRequest', array($request, array(200))); 114 | } 115 | 116 | function it_returns_the_response_on_success(HttpClient $client, RequestInterface $request, ResponseInterface $response) 117 | { 118 | $client->sendRequest($request)->willReturn($response); 119 | $response->getStatusCode()->willReturn(200); 120 | $response->getBody()->willReturn('body'); 121 | 122 | $this->executeRequest($request, array(200))->shouldReturn($response); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /spec/XApiClientBuilderSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(XApiClientBuilderInterface::class); 20 | } 21 | 22 | function it_creates_an_xapi_client(HttpClient $httpClient, RequestFactory $requestFactory) 23 | { 24 | $this->setHttpClient($httpClient); 25 | $this->setRequestFactory($requestFactory); 26 | $this->setBaseUrl('http://example.com/xapi/'); 27 | $this->build()->shouldHaveType(XApiClientInterface::class); 28 | } 29 | 30 | function its_methods_can_be_chained(HttpClient $httpClient, RequestFactory $requestFactory) 31 | { 32 | $this->setHttpClient($httpClient)->shouldReturn($this); 33 | $this->setRequestFactory($requestFactory)->shouldReturn($this); 34 | $this->setBaseUrl('http://example.com/xapi/')->shouldReturn($this); 35 | $this->setVersion('1.0.0')->shouldReturn($this); 36 | $this->setAuth('foo', 'bar')->shouldReturn($this); 37 | $this->setOAuthCredentials('consumer key', 'consumer secret', 'token', 'token secret')->shouldReturn($this); 38 | } 39 | 40 | function it_throws_an_exception_if_the_http_client_is_not_configured(RequestFactory $requestFactory) 41 | { 42 | if ($this->isAbleToDiscoverHttpClient()) { 43 | throw new SkippingException('The builder does not throw an exception if it can automatically discover an HTTP client.'); 44 | } 45 | 46 | $this->setRequestFactory($requestFactory); 47 | $this->setBaseUrl('http://example.com/xapi/'); 48 | 49 | $this->shouldThrow('\LogicException')->during('build'); 50 | } 51 | 52 | function it_throws_an_exception_if_the_request_factory_is_not_configured(HttpClient $httpClient) 53 | { 54 | if ($this->isAbleToDiscoverRequestFactory()) { 55 | throw new SkippingException('The builder does not throw an exception if it can automatically discover a request factory.'); 56 | } 57 | 58 | $this->setHttpClient($httpClient); 59 | $this->setBaseUrl('http://example.com/xapi/'); 60 | 61 | $this->shouldThrow('\LogicException')->during('build'); 62 | } 63 | 64 | function it_can_build_the_client_when_it_is_able_to_discover_the_http_client_and_the_request_factory_without_configuring_them_explicitly() 65 | { 66 | if (!class_exists(HttpClientDiscovery::class)) { 67 | throw new SkippingException(sprintf('The "%s" class is required to let the builder auto discover the HTTP client and request factory.', HttpClientDiscovery::class)); 68 | } 69 | 70 | if (!$this->isAbleToDiscoverHttpClient()) { 71 | throw new SkippingException('Unable to discover an HTTP client.'); 72 | } 73 | 74 | if (!$this->isAbleToDiscoverRequestFactory()) { 75 | throw new SkippingException('Unable to discover a request factory.'); 76 | } 77 | 78 | $this->setBaseUrl('http://example.com/xapi/'); 79 | 80 | $this->build()->shouldReturnAnInstanceOf(XApiClientInterface::class); 81 | } 82 | 83 | function it_throws_an_exception_if_the_base_uri_is_not_configured(HttpClient $httpClient, RequestFactory $requestFactory) 84 | { 85 | $this->setHttpClient($httpClient); 86 | $this->setRequestFactory($requestFactory); 87 | 88 | $this->shouldThrow('\LogicException')->during('build'); 89 | } 90 | 91 | function it_throws_an_exception_when_oauth_credentials_are_configured_but_the_auth_package_is_missing(HttpClient $httpClient, RequestFactory $requestFactory) 92 | { 93 | if (class_exists(OAuth1::class)) { 94 | throw new SkippingException('OAuth1 credentials can be used when the "xabbuh/oauth1-authentication" package is present.'); 95 | } 96 | 97 | $this->setHttpClient($httpClient); 98 | $this->setRequestFactory($requestFactory); 99 | $this->setBaseUrl('http://example.com/xapi/'); 100 | $this->setOAuthCredentials('consumer_key', 'consumer_secret', 'access_token', 'token_secret'); 101 | 102 | $this->shouldThrow(new \LogicException('The "xabbuh/oauth1-authentication package is needed to use OAuth1 authorization.'))->during('build'); 103 | } 104 | 105 | function it_accepts_oauth_credentials_when_the_auth_package_is_present(HttpClient $httpClient, RequestFactory $requestFactory) 106 | { 107 | if (!class_exists(OAuth1::class)) { 108 | throw new SkippingException('OAuth1 credentials cannot be used when the "xabbuh/oauth1-authentication" package is missing.'); 109 | } 110 | 111 | $this->setHttpClient($httpClient); 112 | $this->setRequestFactory($requestFactory); 113 | $this->setBaseUrl('http://example.com/xapi/'); 114 | $this->setOAuthCredentials('consumer_key', 'consumer_secret', 'access_token', 'token_secret'); 115 | $this->build(); 116 | } 117 | 118 | private function isAbleToDiscoverHttpClient() 119 | { 120 | try { 121 | HttpClientDiscovery::find(); 122 | 123 | return true; 124 | } catch (\Exception $e) { 125 | return false; 126 | } 127 | } 128 | 129 | private function isAbleToDiscoverRequestFactory() 130 | { 131 | try { 132 | MessageFactoryDiscovery::find(); 133 | 134 | return true; 135 | } catch (\Exception $e) { 136 | return false; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /spec/XApiClientSpec.php: -------------------------------------------------------------------------------- 1 | getActorSerializer()->willReturn($actorSerializer); 28 | $serializerRegistry->getDocumentDataSerializer()->willReturn($documentDataSerializer); 29 | $serializerRegistry->getStatementSerializer()->willReturn($statementSerializer); 30 | $serializerRegistry->getStatementResultSerializer()->willReturn($statementResultSerializer); 31 | 32 | $this->beConstructedWith($requestHandler, $serializerRegistry, '1.0.1'); 33 | } 34 | 35 | function it_returns_a_statements_api_client_instance() 36 | { 37 | $this->getStatementsApiClient()->shouldBeAnInstanceOf(StatementsApiClientInterface::class); 38 | } 39 | 40 | function it_returns_an_activity_profile_api_client_instance() 41 | { 42 | $this->getActivityProfileApiClient()->shouldBeAnInstanceOf(ActivityProfileApiClientInterface::class); 43 | } 44 | 45 | function it_returns_an_agent_profile_api_client_instance() 46 | { 47 | $this->getAgentProfileApiClient()->shouldBeAnInstanceOf(AgentProfileApiClientInterface::class); 48 | } 49 | 50 | function it_returns_a_state_api_client_instance() 51 | { 52 | $this->getStateApiClient()->shouldBeAnInstanceOf(StateApiClientInterface::class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/ActivityProfileApiClient.php: -------------------------------------------------------------------------------- 1 | 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\Model\ActivityProfile; 15 | use Xabbuh\XApi\Model\ActivityProfileDocument; 16 | 17 | /** 18 | * Client to access the activity profile API of an xAPI based learning record 19 | * store. 20 | * 21 | * @author Christian Flothmann 22 | */ 23 | final class ActivityProfileApiClient extends DocumentApiClient implements ActivityProfileApiClientInterface 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function createOrUpdateDocument(ActivityProfileDocument $document) 29 | { 30 | $this->doStoreActivityProfileDocument('post', $document); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function createOrReplaceDocument(ActivityProfileDocument $document) 37 | { 38 | $this->doStoreActivityProfileDocument('put', $document); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function deleteDocument(ActivityProfile $profile) 45 | { 46 | $this->doDeleteDocument('activities/profile', array( 47 | 'activityId' => $profile->getActivity()->getId()->getValue(), 48 | 'profileId' => $profile->getProfileId(), 49 | )); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public function getDocument(ActivityProfile $profile) 56 | { 57 | /** @var \Xabbuh\XApi\Model\DocumentData $documentData */ 58 | $documentData = $this->doGetDocument('activities/profile', array( 59 | 'activityId' => $profile->getActivity()->getId()->getValue(), 60 | 'profileId' => $profile->getProfileId(), 61 | )); 62 | 63 | return new ActivityProfileDocument($profile, $documentData); 64 | } 65 | 66 | /** 67 | * Stores a state document. 68 | * 69 | * @param string $method HTTP method to use 70 | * @param ActivityProfileDocument $document The document to store 71 | */ 72 | private function doStoreActivityProfileDocument($method, ActivityProfileDocument $document) 73 | { 74 | $profile = $document->getActivityProfile(); 75 | $this->doStoreDocument( 76 | $method, 77 | 'activities/profile', 78 | array( 79 | 'activityId' => $profile->getActivity()->getId()->getValue(), 80 | 'profileId' => $profile->getProfileId(), 81 | ), 82 | $document 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Api/ActivityProfileApiClientInterface.php: -------------------------------------------------------------------------------- 1 | 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\Model\ActivityProfile; 15 | use Xabbuh\XApi\Model\ActivityProfileDocument; 16 | 17 | /** 18 | * Client to access the activity profile API of an xAPI based learning record 19 | * store. 20 | * 21 | * @author Christian Flothmann 22 | */ 23 | interface ActivityProfileApiClientInterface 24 | { 25 | /** 26 | * Stores a document for an activity profile. Updates an existing document 27 | * for this activity profile if one exists. 28 | * 29 | * @param ActivityProfileDocument $document The document to store 30 | */ 31 | public function createOrUpdateDocument(ActivityProfileDocument $document); 32 | 33 | /** 34 | * Stores a document for an activity profile. Replaces any existing document 35 | * for this activity profile. 36 | * 37 | * @param ActivityProfileDocument $document The document to store 38 | */ 39 | public function createOrReplaceDocument(ActivityProfileDocument $document); 40 | 41 | /** 42 | * Deletes a document stored for the given activity profile. 43 | * 44 | * @param ActivityProfile $profile The activity profile 45 | */ 46 | public function deleteDocument(ActivityProfile $profile); 47 | 48 | /** 49 | * Returns the document for an activity profile. 50 | * 51 | * @param ActivityProfile $profile The activity profile to request the 52 | * document for 53 | * 54 | * @return ActivityProfileDocument The document 55 | */ 56 | public function getDocument(ActivityProfile $profile); 57 | } 58 | -------------------------------------------------------------------------------- /src/Api/AgentProfileApiClient.php: -------------------------------------------------------------------------------- 1 | 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\Request\HandlerInterface; 15 | use Xabbuh\XApi\Serializer\ActorSerializerInterface; 16 | use Xabbuh\XApi\Serializer\DocumentDataSerializerInterface; 17 | use Xabbuh\XApi\Model\AgentProfile; 18 | use Xabbuh\XApi\Model\AgentProfileDocument; 19 | 20 | /** 21 | * Client to access the agent profile API of an xAPI based learning record 22 | * store. 23 | * 24 | * @author Christian Flothmann 25 | */ 26 | final class AgentProfileApiClient extends DocumentApiClient implements AgentProfileApiClientInterface 27 | { 28 | /** 29 | * @var ActorSerializerInterface 30 | */ 31 | private $actorSerializer; 32 | 33 | /** 34 | * @param HandlerInterface $requestHandler The HTTP request handler 35 | * @param string $version The xAPI version 36 | * @param DocumentDataSerializerInterface $documentDataSerializer The document data serializer 37 | * @param ActorSerializerInterface $actorSerializer The actor serializer 38 | */ 39 | public function __construct( 40 | HandlerInterface $requestHandler, 41 | $version, 42 | DocumentDataSerializerInterface $documentDataSerializer, 43 | ActorSerializerInterface $actorSerializer 44 | ) { 45 | parent::__construct($requestHandler, $version, $documentDataSerializer); 46 | 47 | $this->actorSerializer = $actorSerializer; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function createOrUpdateDocument(AgentProfileDocument $document) 54 | { 55 | $profile = $document->getAgentProfile(); 56 | $this->doStoreDocument('post', 'agents/profile', array( 57 | 'agent' => $this->actorSerializer->serializeActor($profile->getAgent()), 58 | 'profileId' => $profile->getProfileId(), 59 | ), $document); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function createOrReplaceDocument(AgentProfileDocument $document) 66 | { 67 | $profile = $document->getAgentProfile(); 68 | $this->doStoreDocument('put', 'agents/profile', array( 69 | 'agent' => $this->actorSerializer->serializeActor($profile->getAgent()), 70 | 'profileId' => $profile->getProfileId(), 71 | ), $document); 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function deleteDocument(AgentProfile $profile) 78 | { 79 | $this->doDeleteDocument('agents/profile', array( 80 | 'agent' => $this->actorSerializer->serializeActor($profile->getAgent()), 81 | 'profileId' => $profile->getProfileId(), 82 | )); 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public function getDocument(AgentProfile $profile) 89 | { 90 | /** @var \Xabbuh\XApi\Model\DocumentData $documentData */ 91 | $documentData = $this->doGetDocument('agents/profile', array( 92 | 'agent' => $this->actorSerializer->serializeActor($profile->getAgent()), 93 | 'profileId' => $profile->getProfileId(), 94 | )); 95 | 96 | return new AgentProfileDocument($profile, $documentData); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Api/AgentProfileApiClientInterface.php: -------------------------------------------------------------------------------- 1 | 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\Model\AgentProfile; 15 | use Xabbuh\XApi\Model\AgentProfileDocument; 16 | 17 | /** 18 | * Client to access the agent profile API of an xAPI based learning record 19 | * store. 20 | * 21 | * @author Christian Flothmann 22 | */ 23 | interface AgentProfileApiClientInterface 24 | { 25 | /** 26 | * Stores a document for an agent profile. Updates an existing document for 27 | * this agent profile if one exists. 28 | * 29 | * @param AgentProfileDocument $document The document to store 30 | */ 31 | public function createOrUpdateDocument(AgentProfileDocument $document); 32 | 33 | /** 34 | * Stores a document for an agent profile. Replaces any existing document 35 | * for this agent profile. 36 | * 37 | * @param AgentProfileDocument $document The document to store 38 | */ 39 | public function createOrReplaceDocument(AgentProfileDocument $document); 40 | 41 | /** 42 | * Deletes a document stored for the given agent profile. 43 | * 44 | * @param AgentProfile $profile The agent profile 45 | */ 46 | public function deleteDocument(AgentProfile $profile); 47 | 48 | /** 49 | * Returns the document for an agent profile. 50 | * 51 | * @param AgentProfile $profile The agent profile to request the document for 52 | * 53 | * @return AgentProfileDocument The document 54 | */ 55 | public function getDocument(AgentProfile $profile); 56 | } 57 | -------------------------------------------------------------------------------- /src/Api/DocumentApiClient.php: -------------------------------------------------------------------------------- 1 | 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\Request\HandlerInterface; 15 | use Xabbuh\XApi\Model\Document; 16 | use Xabbuh\XApi\Model\DocumentData; 17 | use Xabbuh\XApi\Serializer\DocumentDataSerializerInterface; 18 | 19 | /** 20 | * Base class for the document API classes. 21 | * 22 | * @author Christian Flothmann 23 | */ 24 | abstract class DocumentApiClient 25 | { 26 | private $requestHandler; 27 | private $version; 28 | private $documentDataSerializer; 29 | 30 | /** 31 | * @param HandlerInterface $requestHandler The HTTP request handler 32 | * @param string $version The xAPI version 33 | * @param DocumentDataSerializerInterface $documentDataSerializer The document data serializer 34 | */ 35 | public function __construct(HandlerInterface $requestHandler, $version, DocumentDataSerializerInterface $documentDataSerializer) 36 | { 37 | $this->requestHandler = $requestHandler; 38 | $this->version = $version; 39 | $this->documentDataSerializer = $documentDataSerializer; 40 | } 41 | 42 | /** 43 | * Stores a document. 44 | * 45 | * @param string $method HTTP method to use 46 | * @param string $uri Endpoint URI 47 | * @param array $urlParameters URL parameters 48 | * @param Document $document The document to store 49 | */ 50 | protected function doStoreDocument($method, $uri, $urlParameters, Document $document) 51 | { 52 | $request = $this->requestHandler->createRequest( 53 | $method, 54 | $uri, 55 | $urlParameters, 56 | $this->documentDataSerializer->serializeDocumentData($document->getData()) 57 | ); 58 | $this->requestHandler->executeRequest($request, array(204)); 59 | } 60 | 61 | /** 62 | * Deletes a document. 63 | * 64 | * @param string $uri The endpoint URI 65 | * @param array $urlParameters The URL parameters 66 | */ 67 | protected function doDeleteDocument($uri, array $urlParameters) 68 | { 69 | $request = $this->requestHandler->createRequest('delete', $uri, $urlParameters); 70 | $this->requestHandler->executeRequest($request, array(204)); 71 | } 72 | 73 | /** 74 | * Returns a document. 75 | * 76 | * @param string $uri The endpoint URI 77 | * @param array $urlParameters The URL parameters 78 | * 79 | * @return Document The document 80 | */ 81 | protected function doGetDocument($uri, array $urlParameters) 82 | { 83 | $request = $this->requestHandler->createRequest('get', $uri, $urlParameters); 84 | $response = $this->requestHandler->executeRequest($request, array(200)); 85 | $document = $this->deserializeDocument((string) $response->getBody()); 86 | 87 | return $document; 88 | } 89 | 90 | /** 91 | * Deserializes the data of a document. 92 | * 93 | * @param string $data The serialized document data 94 | * 95 | * @return DocumentData The parsed document data 96 | */ 97 | protected function deserializeDocument($data) 98 | { 99 | return $this->documentDataSerializer->deserializeDocumentData($data); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Api/StateApiClient.php: -------------------------------------------------------------------------------- 1 | 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\Request\HandlerInterface; 15 | use Xabbuh\XApi\Serializer\ActorSerializerInterface; 16 | use Xabbuh\XApi\Serializer\DocumentDataSerializerInterface; 17 | use Xabbuh\XApi\Model\StateDocument; 18 | use Xabbuh\XApi\Model\State; 19 | 20 | /** 21 | * Client to access the state API of an xAPI based learning record store. 22 | * 23 | * @author Christian Flothmann 24 | */ 25 | final class StateApiClient extends DocumentApiClient implements StateApiClientInterface 26 | { 27 | /** 28 | * @var ActorSerializerInterface 29 | */ 30 | private $actorSerializer; 31 | 32 | /** 33 | * @param HandlerInterface $requestHandler The HTTP request handler 34 | * @param string $version The xAPI version 35 | * @param DocumentDataSerializerInterface $documentDataSerializer The document data serializer 36 | * @param ActorSerializerInterface $actorSerializer The actor serializer 37 | */ 38 | public function __construct( 39 | HandlerInterface $requestHandler, 40 | $version, 41 | DocumentDataSerializerInterface $documentDataSerializer, 42 | ActorSerializerInterface $actorSerializer 43 | ) { 44 | parent::__construct($requestHandler, $version, $documentDataSerializer); 45 | 46 | $this->actorSerializer = $actorSerializer; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function createOrUpdateDocument(StateDocument $document) 53 | { 54 | $this->doStoreStateDocument('post', $document); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function createOrReplaceDocument(StateDocument $document) 61 | { 62 | $this->doStoreStateDocument('put', $document); 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function deleteDocument(State $state) 69 | { 70 | $this->doDeleteDocument('activities/state', array( 71 | 'activityId' => $state->getActivity()->getId()->getValue(), 72 | 'agent' => $this->actorSerializer->serializeActor($state->getActor()), 73 | 'stateId' => $state->getStateId(), 74 | )); 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function getDocument(State $state) 81 | { 82 | /** @var \Xabbuh\XApi\Model\DocumentData $documentData */ 83 | $documentData = $this->doGetDocument('activities/state', array( 84 | 'activityId' => $state->getActivity()->getId()->getValue(), 85 | 'agent' => $this->actorSerializer->serializeActor($state->getActor()), 86 | 'stateId' => $state->getStateId(), 87 | )); 88 | 89 | return new StateDocument($state, $documentData); 90 | } 91 | 92 | /** 93 | * Stores a state document. 94 | * 95 | * @param string $method HTTP method to use 96 | * @param StateDocument $document The document to store 97 | */ 98 | private function doStoreStateDocument($method, StateDocument $document) 99 | { 100 | $state = $document->getState(); 101 | $this->doStoreDocument( 102 | $method, 103 | 'activities/state', 104 | array( 105 | 'activityId' => $state->getActivity()->getId()->getValue(), 106 | 'agent' => $this->actorSerializer->serializeActor($state->getActor()), 107 | 'stateId' => $state->getStateId(), 108 | ), 109 | $document 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Api/StateApiClientInterface.php: -------------------------------------------------------------------------------- 1 | 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\Model\StateDocument; 15 | use Xabbuh\XApi\Model\State; 16 | 17 | /** 18 | * Client to access the state API of an xAPI based learning record store. 19 | * 20 | * @author Christian Flothmann 21 | */ 22 | interface StateApiClientInterface 23 | { 24 | /** 25 | * Stores a document for a state. Updates an existing document for this 26 | * state if one exists. 27 | * 28 | * @param StateDocument $document The document to store 29 | */ 30 | public function createOrUpdateDocument(StateDocument $document); 31 | 32 | /** 33 | * Stores a document for a state. Replaces any existing document for this 34 | * state. 35 | * 36 | * @param StateDocument $document The document to store 37 | */ 38 | public function createOrReplaceDocument(StateDocument $document); 39 | 40 | /** 41 | * Deletes a document stored for the given state. 42 | * 43 | * @param State $state The state 44 | */ 45 | public function deleteDocument(State $state); 46 | 47 | /** 48 | * Returns the document for a state. 49 | * 50 | * @param State $state The state to request the document for 51 | * 52 | * @return StateDocument The document 53 | */ 54 | public function getDocument(State $state); 55 | } 56 | -------------------------------------------------------------------------------- /src/Api/StatementsApiClient.php: -------------------------------------------------------------------------------- 1 | 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 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 | public function __construct( 46 | HandlerInterface $requestHandler, 47 | $version, 48 | StatementSerializerInterface $statementSerializer, 49 | StatementResultSerializerInterface $statementResultSerializer, 50 | ActorSerializerInterface $actorSerializer 51 | ) { 52 | $this->requestHandler = $requestHandler; 53 | $this->version = $version; 54 | $this->statementSerializer = $statementSerializer; 55 | $this->statementResultSerializer = $statementResultSerializer; 56 | $this->actorSerializer = $actorSerializer; 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | public function storeStatement(Statement $statement) 63 | { 64 | if (null !== $statement->getId()) { 65 | return $this->doStoreStatements( 66 | $statement, 67 | 'put', 68 | array('statementId' => $statement->getId()->getValue()), 69 | 204 70 | ); 71 | } else { 72 | return $this->doStoreStatements($statement); 73 | } 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public function storeStatements(array $statements) 80 | { 81 | // check that only Statements without ids will be sent to the LRS 82 | foreach ($statements as $statement) { 83 | /** @var Statement $statement */ 84 | 85 | $isStatement = is_object($statement) && $statement instanceof Statement; 86 | 87 | if (!$isStatement || null !== $statement->getId()) { 88 | throw new \InvalidArgumentException('API can only handle statements without ids'); 89 | } 90 | } 91 | 92 | return $this->doStoreStatements($statements); 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | public function voidStatement(Statement $statement, Actor $actor) 99 | { 100 | return $this->storeStatement($statement->getVoidStatement($actor)); 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | public function getStatement(StatementId $statementId, $attachments = true) 107 | { 108 | return $this->doGetStatements('statements', array( 109 | 'statementId' => $statementId->getValue(), 110 | 'attachments' => $attachments ? 'true' : 'false', 111 | )); 112 | } 113 | 114 | /** 115 | * {@inheritDoc} 116 | */ 117 | public function getVoidedStatement(StatementId $statementId, $attachments = true) 118 | { 119 | return $this->doGetStatements('statements', array( 120 | 'voidedStatementId' => $statementId->getValue(), 121 | 'attachments' => $attachments ? 'true' : 'false', 122 | )); 123 | } 124 | 125 | /** 126 | * {@inheritDoc} 127 | */ 128 | public function getStatements(StatementsFilter $filter = null, $attachments = true) 129 | { 130 | $urlParameters = array(); 131 | 132 | if (null !== $filter) { 133 | $urlParameters = $filter->getFilter(); 134 | } 135 | 136 | // the Agent must be JSON encoded 137 | if (isset($urlParameters['agent'])) { 138 | $urlParameters['agent'] = $this->actorSerializer->serializeActor($urlParameters['agent']); 139 | } 140 | 141 | return $this->doGetStatements('statements', $urlParameters); 142 | } 143 | 144 | /** 145 | * {@inheritDoc} 146 | */ 147 | public function getNextStatements(StatementResult $statementResult) 148 | { 149 | return $this->doGetStatements($statementResult->getMoreUrlPath()->getValue()); 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 | private function doStoreStatements($statements, $method = 'post', $parameters = array(), $validStatusCode = 200) 161 | { 162 | $attachments = array(); 163 | 164 | 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 | 175 | $serializedStatements = $this->statementSerializer->serializeStatements($statements); 176 | } else { 177 | if (null !== $statements->getAttachments()) { 178 | foreach ($statements->getAttachments() as $attachment) { 179 | if ($attachment->getContent()) { 180 | $attachments[] = $attachment; 181 | } 182 | } 183 | } 184 | 185 | $serializedStatements = $this->statementSerializer->serializeStatement($statements); 186 | } 187 | 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 | 200 | $request = $this->requestHandler->createRequest( 201 | $method, 202 | 'statements', 203 | $parameters, 204 | $body, 205 | $headers 206 | ); 207 | $response = $this->requestHandler->executeRequest($request, array($validStatusCode)); 208 | $statementIds = json_decode((string) $response->getBody()); 209 | 210 | if (is_array($statements)) { 211 | /** @var Statement[] $statements */ 212 | $createdStatements = array(); 213 | 214 | foreach ($statements as $index => $statement) { 215 | $createdStatements[] = $statement->withId(StatementId::fromString($statementIds[$index])); 216 | } 217 | 218 | return $createdStatements; 219 | } else { 220 | /** @var Statement $statements */ 221 | 222 | if (200 === $validStatusCode) { 223 | return $statements->withId(StatementId::fromString($statementIds[0])); 224 | } else { 225 | 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 | private function doGetStatements($url, array $urlParameters = array()) 239 | { 240 | $request = $this->requestHandler->createRequest('get', $url, $urlParameters); 241 | $response = $this->requestHandler->executeRequest($request, array(200)); 242 | 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 | 264 | if (isset($urlParameters['statementId']) || isset($urlParameters['voidedStatementId'])) { 265 | return $this->statementSerializer->deserializeStatement($serializedStatement, $attachments); 266 | } else { 267 | 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 | -------------------------------------------------------------------------------- /src/Api/StatementsApiClientInterface.php: -------------------------------------------------------------------------------- 1 | 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\Common\Exception\ConflictException; 15 | use Xabbuh\XApi\Common\Exception\NotFoundException; 16 | use Xabbuh\XApi\Common\Exception\XApiException; 17 | use Xabbuh\XApi\Model\Actor; 18 | use Xabbuh\XApi\Model\Statement; 19 | use Xabbuh\XApi\Model\StatementId; 20 | use Xabbuh\XApi\Model\StatementResult; 21 | use Xabbuh\XApi\Model\StatementsFilter; 22 | 23 | /** 24 | * Client to access the statements API of an xAPI based learning record store. 25 | * 26 | * @author Christian Flothmann 27 | */ 28 | interface StatementsApiClientInterface 29 | { 30 | /** 31 | * Stores a single {@link Statement}. 32 | * 33 | * @param Statement $statement The Statement to store 34 | * 35 | * @return Statement The Statement as it has been stored in the remote LRS, 36 | * this is not necessarily the same object that was 37 | * passed to storeStatement() 38 | * 39 | * @throws ConflictException if a Statement with the given id already exists 40 | * and the given Statement does not match the 41 | * stored Statement 42 | * @throws XApiException for all other xAPI related problems 43 | */ 44 | public function storeStatement(Statement $statement); 45 | 46 | /** 47 | * Stores a collection of {@link Statement Statements}. 48 | * 49 | * @param Statement[] $statements The statements to store 50 | * 51 | * @return Statement[] The stored Statements 52 | * 53 | * @throws \InvalidArgumentException if a given object is no Statement or 54 | * if one of the Statements has an id 55 | * @throws XApiException for all other xAPI related problems 56 | */ 57 | public function storeStatements(array $statements); 58 | 59 | /** 60 | * Marks a {@link Statement} as voided. 61 | * 62 | * @param Statement $statement The Statement to void 63 | * @param Actor $actor The Actor voiding the given Statement 64 | * 65 | * @return Statement The Statement sent to the remote LRS to void the 66 | * given Statement 67 | * 68 | * @throws XApiException for all other xAPI related problems 69 | */ 70 | public function voidStatement(Statement $statement, Actor $actor); 71 | 72 | /** 73 | * Retrieves a single {@link Statement Statement}. 74 | * 75 | * @param StatementId $statementId The Statement id 76 | * @param bool $attachments Whether or not to request raw attachment data 77 | * 78 | * @return Statement The Statement 79 | * 80 | * @throws NotFoundException if no statement with the given id could be found 81 | * @throws XApiException for all other xAPI related problems 82 | */ 83 | public function getStatement(StatementId $statementId, $attachments = true); 84 | 85 | /** 86 | * Retrieves a voided {@link Statement Statement}. 87 | * 88 | * @param StatementId $statementId The id of the voided Statement 89 | * @param bool $attachments Whether or not to request raw attachment data 90 | * 91 | * @return Statement The voided Statement 92 | * 93 | * @throws NotFoundException if no statement with the given id could be found 94 | * @throws XApiException for all other xAPI related problems 95 | */ 96 | public function getVoidedStatement(StatementId $statementId, $attachments = true); 97 | 98 | /** 99 | * Retrieves a collection of {@link Statement Statements}. 100 | * 101 | * @param StatementsFilter $filter Optional Statements filter 102 | * @param bool $attachments Whether or not to request raw attachment data 103 | * 104 | * @return StatementResult The {@link StatementResult} 105 | * 106 | * @throws XApiException in case of any problems related to the xAPI 107 | */ 108 | public function getStatements(StatementsFilter $filter = null, $attachments = true); 109 | 110 | /** 111 | * Returns the next {@link Statement Statements} for a limited Statement 112 | * result. 113 | * 114 | * @param StatementResult $statementResult The former StatementResult 115 | * 116 | * @return StatementResult The {@link StatementResult} 117 | * 118 | * @throws XApiException in case of any problems related to the xAPI 119 | */ 120 | public function getNextStatements(StatementResult $statementResult); 121 | } 122 | -------------------------------------------------------------------------------- /src/Http/MultipartStatementBody.php: -------------------------------------------------------------------------------- 1 | 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 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 | } 66 | -------------------------------------------------------------------------------- /src/Request/Handler.php: -------------------------------------------------------------------------------- 1 | 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\Request; 13 | 14 | use Http\Client\Exception; 15 | use Http\Client\HttpClient; 16 | use Http\Message\RequestFactory; 17 | use Psr\Http\Message\RequestInterface; 18 | use Xabbuh\XApi\Common\Exception\AccessDeniedException; 19 | use Xabbuh\XApi\Common\Exception\ConflictException; 20 | use Xabbuh\XApi\Common\Exception\NotFoundException; 21 | use Xabbuh\XApi\Common\Exception\XApiException; 22 | 23 | /** 24 | * Prepares and executes xAPI HTTP requests. 25 | * 26 | * @author Christian Flothmann 27 | */ 28 | final class Handler implements HandlerInterface 29 | { 30 | private $httpClient; 31 | private $requestFactory; 32 | private $baseUri; 33 | private $version; 34 | 35 | /** 36 | * @param HttpClient $httpClient The HTTP client sending requests to the remote LRS 37 | * @param RequestFactory $requestFactory The factory used to create PSR-7 HTTP requests 38 | * @param string $baseUri The APIs base URI (all end points will be created relatively to this URI) 39 | * @param string $version The xAPI version 40 | */ 41 | public function __construct(HttpClient $httpClient, RequestFactory $requestFactory, $baseUri, $version) 42 | { 43 | $this->httpClient = $httpClient; 44 | $this->requestFactory = $requestFactory; 45 | $this->baseUri = $baseUri; 46 | $this->version = $version; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function createRequest($method, $uri, array $urlParameters = array(), $body = null, array $headers = array()) 53 | { 54 | if (!in_array(strtoupper($method), array('GET', 'POST', 'PUT', 'DELETE'))) { 55 | throw new \InvalidArgumentException(sprintf('"%s" is no valid HTTP method (expected one of [GET, POST, PUT, DELETE]) in an xAPI context.', $method)); 56 | } 57 | 58 | $uri = rtrim($this->baseUri, '/').'/'.ltrim($uri, '/'); 59 | 60 | if (count($urlParameters) > 0) { 61 | $uri .= '?'.http_build_query($urlParameters); 62 | } 63 | 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 | } 71 | 72 | return $this->requestFactory->createRequest(strtoupper($method), $uri, $headers, $body); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function executeRequest(RequestInterface $request, array $validStatusCodes) 79 | { 80 | try { 81 | $response = $this->httpClient->sendRequest($request); 82 | } catch (Exception $e) { 83 | throw new XApiException($e->getMessage(), $e->getCode(), $e); 84 | } 85 | 86 | // catch some common errors 87 | if (in_array($response->getStatusCode(), array(401, 403))) { 88 | throw new AccessDeniedException( 89 | (string) $response->getBody(), 90 | $response->getStatusCode() 91 | ); 92 | } elseif (404 === $response->getStatusCode()) { 93 | throw new NotFoundException((string) $response->getBody()); 94 | } elseif (409 === $response->getStatusCode()) { 95 | throw new ConflictException((string) $response->getBody()); 96 | } 97 | 98 | if (!in_array($response->getStatusCode(), $validStatusCodes)) { 99 | throw new XApiException((string) $response->getBody(), $response->getStatusCode()); 100 | } 101 | 102 | return $response; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Request/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | 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\Request; 13 | 14 | use Psr\Http\Message\RequestInterface; 15 | use Psr\Http\Message\ResponseInterface; 16 | use Psr\Http\Message\StreamInterface; 17 | use Xabbuh\XApi\Common\Exception\XApiException; 18 | 19 | /** 20 | * Prepare and execute xAPI HTTP requests. 21 | * 22 | * @author Christian Flothmann 23 | */ 24 | interface HandlerInterface 25 | { 26 | /** 27 | * @param string $method The HTTP method 28 | * @param string $uri The URI to send the request to 29 | * @param array $urlParameters Optional url parameters 30 | * @param string $body An optional request body 31 | * @param array $headers Optional additional HTTP headers 32 | * 33 | * @return RequestInterface The request 34 | * 35 | * @throws \InvalidArgumentException when no valid HTTP method is given 36 | */ 37 | public function createRequest($method, $uri, array $urlParameters = array(), $body = null, array $headers = array()); 38 | 39 | /** 40 | * Performs the given HTTP request. 41 | * 42 | * @param RequestInterface $request The HTTP request to perform 43 | * @param int[] $validStatusCodes A list of HTTP status codes 44 | * the calling method is able to 45 | * handle 46 | * 47 | * @return ResponseInterface The remote server's response 48 | * 49 | * @throws XApiException when the request fails 50 | */ 51 | public function executeRequest(RequestInterface $request, array $validStatusCodes); 52 | } 53 | -------------------------------------------------------------------------------- /src/XApiClient.php: -------------------------------------------------------------------------------- 1 | 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; 13 | 14 | use Xabbuh\XApi\Client\Api\ActivityProfileApiClient; 15 | use Xabbuh\XApi\Client\Api\AgentProfileApiClient; 16 | use Xabbuh\XApi\Client\Api\ApiClient; 17 | use Xabbuh\XApi\Client\Api\StateApiClient; 18 | use Xabbuh\XApi\Client\Api\StatementsApiClient; 19 | use Xabbuh\XApi\Client\Request\HandlerInterface; 20 | use Xabbuh\XApi\Serializer\SerializerRegistryInterface; 21 | 22 | /** 23 | * An Experience API client. 24 | * 25 | * @author Christian Flothmann 26 | */ 27 | final class XApiClient implements XApiClientInterface 28 | { 29 | /** 30 | * @var SerializerRegistryInterface 31 | */ 32 | private $serializerRegistry; 33 | 34 | /** 35 | * @param HandlerInterface $requestHandler The HTTP request handler 36 | * @param SerializerRegistryInterface $serializerRegistry The serializer registry 37 | * @param string $version The xAPI version 38 | */ 39 | public function __construct(HandlerInterface $requestHandler, SerializerRegistryInterface $serializerRegistry, $version) 40 | { 41 | $this->requestHandler = $requestHandler; 42 | $this->serializerRegistry = $serializerRegistry; 43 | $this->version = $version; 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function getStatementsApiClient() 50 | { 51 | return new StatementsApiClient( 52 | $this->requestHandler, 53 | $this->version, 54 | $this->serializerRegistry->getStatementSerializer(), 55 | $this->serializerRegistry->getStatementResultSerializer(), 56 | $this->serializerRegistry->getActorSerializer() 57 | ); 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | public function getStateApiClient() 64 | { 65 | return new StateApiClient( 66 | $this->requestHandler, 67 | $this->version, 68 | $this->serializerRegistry->getDocumentDataSerializer(), 69 | $this->serializerRegistry->getActorSerializer() 70 | ); 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function getActivityProfileApiClient() 77 | { 78 | return new ActivityProfileApiClient( 79 | $this->requestHandler, 80 | $this->version, 81 | $this->serializerRegistry->getDocumentDataSerializer() 82 | ); 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public function getAgentProfileApiClient() 89 | { 90 | return new AgentProfileApiClient( 91 | $this->requestHandler, 92 | $this->version, 93 | $this->serializerRegistry->getDocumentDataSerializer(), 94 | $this->serializerRegistry->getActorSerializer() 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/XApiClientBuilder.php: -------------------------------------------------------------------------------- 1 | 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; 13 | 14 | use ApiClients\Tools\Psr7\Oauth1\Definition\AccessToken; 15 | use ApiClients\Tools\Psr7\Oauth1\Definition\ConsumerKey; 16 | use ApiClients\Tools\Psr7\Oauth1\Definition\ConsumerSecret; 17 | use ApiClients\Tools\Psr7\Oauth1\Definition\TokenSecret; 18 | use ApiClients\Tools\Psr7\Oauth1\RequestSigning\RequestSigner; 19 | use Http\Client\Common\Plugin\AuthenticationPlugin; 20 | use Http\Client\Common\PluginClient; 21 | use Http\Client\HttpClient; 22 | use Http\Discovery\HttpClientDiscovery; 23 | use Http\Discovery\MessageFactoryDiscovery; 24 | use Http\Message\Authentication\BasicAuth; 25 | use Http\Message\RequestFactory; 26 | use Xabbuh\Http\Authentication\OAuth1; 27 | use Xabbuh\XApi\Client\Request\Handler; 28 | use Xabbuh\XApi\Serializer\SerializerFactoryInterface; 29 | use Xabbuh\XApi\Serializer\SerializerRegistry; 30 | use Xabbuh\XApi\Serializer\Symfony\SerializerFactory; 31 | 32 | /** 33 | * xAPI client builder. 34 | * 35 | * @author Christian Flothmann 36 | */ 37 | final class XApiClientBuilder implements XApiClientBuilderInterface 38 | { 39 | private $serializerFactory; 40 | 41 | /** 42 | * @var HttpClient|null 43 | */ 44 | private $httpClient; 45 | 46 | /** 47 | * @var RequestFactory|null 48 | */ 49 | private $requestFactory; 50 | 51 | private $baseUrl; 52 | private $version; 53 | private $username; 54 | private $password; 55 | private $consumerKey; 56 | private $consumerSecret; 57 | private $accessToken; 58 | private $tokenSecret; 59 | 60 | public function __construct(SerializerFactoryInterface $serializerFactory = null) 61 | { 62 | $this->serializerFactory = $serializerFactory ?: new SerializerFactory(); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function setHttpClient(HttpClient $httpClient) 69 | { 70 | $this->httpClient = $httpClient; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function setRequestFactory(RequestFactory $requestFactory) 79 | { 80 | $this->requestFactory = $requestFactory; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public function setBaseUrl($baseUrl) 89 | { 90 | $this->baseUrl = $baseUrl; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | public function setVersion($version) 99 | { 100 | $this->version = $version; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | public function setAuth($username, $password) 109 | { 110 | $this->username = $username; 111 | $this->password = $password; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public function setOAuthCredentials($consumerKey, $consumerSecret, $token, $tokenSecret) 120 | { 121 | $this->consumerKey = $consumerKey; 122 | $this->consumerSecret = $consumerSecret; 123 | $this->accessToken = $token; 124 | $this->tokenSecret = $tokenSecret; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * {@inheritDoc} 131 | */ 132 | public function build() 133 | { 134 | if (null === $this->httpClient && class_exists(HttpClientDiscovery::class)) { 135 | try { 136 | $this->httpClient = HttpClientDiscovery::find(); 137 | } catch (\Exception $e) { 138 | } 139 | } 140 | 141 | if (null === $httpClient = $this->httpClient) { 142 | throw new \LogicException('No HTTP client was configured.'); 143 | } 144 | 145 | if (null === $this->requestFactory && class_exists(MessageFactoryDiscovery::class)) { 146 | try { 147 | $this->requestFactory = MessageFactoryDiscovery::find(); 148 | } catch (\Exception $e) { 149 | } 150 | } 151 | 152 | if (null === $this->requestFactory) { 153 | throw new \LogicException('No request factory was configured.'); 154 | } 155 | 156 | if (null === $this->baseUrl) { 157 | throw new \LogicException('Base URI value was not configured.'); 158 | } 159 | 160 | $serializerRegistry = new SerializerRegistry(); 161 | $serializerRegistry->setStatementSerializer($this->serializerFactory->createStatementSerializer()); 162 | $serializerRegistry->setStatementResultSerializer($this->serializerFactory->createStatementResultSerializer()); 163 | $serializerRegistry->setActorSerializer($this->serializerFactory->createActorSerializer()); 164 | $serializerRegistry->setDocumentDataSerializer($this->serializerFactory->createDocumentDataSerializer()); 165 | 166 | $plugins = array(); 167 | 168 | if (null !== $this->username && null !== $this->password) { 169 | $plugins[] = new AuthenticationPlugin(new BasicAuth($this->username, $this->password)); 170 | } 171 | 172 | if (null !== $this->consumerKey && null !== $this->consumerSecret && null !== $this->accessToken && null !== $this->tokenSecret) { 173 | if (!class_exists(OAuth1::class)) { 174 | throw new \LogicException('The "xabbuh/oauth1-authentication package is needed to use OAuth1 authorization.'); 175 | } 176 | 177 | $requestSigner = new RequestSigner(new ConsumerKey($this->consumerKey), new ConsumerSecret($this->consumerSecret)); 178 | $oauth = new OAuth1($requestSigner, new AccessToken($this->accessToken), new TokenSecret($this->tokenSecret)); 179 | $plugins[] = new AuthenticationPlugin($oauth); 180 | } 181 | 182 | if (!empty($plugins)) { 183 | $httpClient = new PluginClient($httpClient, $plugins); 184 | } 185 | 186 | $version = null === $this->version ? '1.0.3' : $this->version; 187 | $requestHandler = new Handler($httpClient, $this->requestFactory, $this->baseUrl, $version); 188 | 189 | return new XApiClient($requestHandler, $serializerRegistry, $this->version); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/XApiClientBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 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; 13 | 14 | use Http\Client\HttpClient; 15 | use Http\Message\RequestFactory; 16 | 17 | /** 18 | * xAPI client builder. 19 | * 20 | * @author Christian Flothmann 21 | */ 22 | interface XApiClientBuilderInterface 23 | { 24 | /** 25 | * Sets the HTTP client implementation that will be used to issue HTTP requests. 26 | * 27 | * @param HttpClient $httpClient The HTTP client implementation 28 | * 29 | * @return XApiClientBuilderInterface The builder 30 | */ 31 | public function setHttpClient(HttpClient $httpClient); 32 | 33 | /** 34 | * Sets the requests factory which creates requests that are then handled by the HTTP client. 35 | * 36 | * @param RequestFactory $requestFactory The request factory 37 | * 38 | * @return XApiClientBuilderInterface The builder 39 | */ 40 | public function setRequestFactory(RequestFactory $requestFactory); 41 | 42 | /** 43 | * Sets the LRS base URL. 44 | * 45 | * @param string $baseUrl The base url 46 | * 47 | * @return XApiClientBuilderInterface The builder 48 | */ 49 | public function setBaseUrl($baseUrl); 50 | 51 | /** 52 | * Sets the xAPI version. 53 | * 54 | * @param string $version The version to use 55 | * 56 | * @return XApiClientBuilderInterface The builder 57 | */ 58 | public function setVersion($version); 59 | 60 | /** 61 | * Sets HTTP authentication credentials. 62 | * 63 | * @param string $username The username 64 | * @param string $password The password 65 | * 66 | * @return XApiClientBuilderInterface The builder 67 | */ 68 | public function setAuth($username, $password); 69 | 70 | /** 71 | * Sets OAuth credentials. 72 | * 73 | * @param string $consumerKey The consumer key 74 | * @param string $consumerSecret The consumer secret 75 | * @param string $token The token 76 | * @param string $tokenSecret The secret token 77 | * 78 | * @return XApiClientBuilderInterface The builder 79 | */ 80 | public function setOAuthCredentials($consumerKey, $consumerSecret, $token, $tokenSecret); 81 | 82 | /** 83 | * Builds the xAPI client. 84 | * 85 | * @return XApiClientInterface The xAPI client 86 | * 87 | * @throws \LogicException if no base URI was configured 88 | */ 89 | public function build(); 90 | } 91 | -------------------------------------------------------------------------------- /src/XApiClientInterface.php: -------------------------------------------------------------------------------- 1 | 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; 13 | 14 | /** 15 | * An Experience API client. 16 | * 17 | * @author Christian Flothmann 18 | */ 19 | interface XApiClientInterface 20 | { 21 | /** 22 | * Returns an API client to access the statements API of an xAPI based LRS. 23 | * 24 | * @return \Xabbuh\XApi\Client\Api\StatementsApiClientInterface The API client 25 | */ 26 | public function getStatementsApiClient(); 27 | 28 | /** 29 | * Returns an API client to access the state API of an xAPI based LRS. 30 | * 31 | * @return \Xabbuh\XApi\Client\Api\StateApiClientInterface The API client 32 | */ 33 | public function getStateApiClient(); 34 | 35 | /** 36 | * Returns an API client to access the activity profile API of an xAPI based 37 | * LRS. 38 | * 39 | * @return \Xabbuh\XApi\Client\Api\ActivityProfileApiClientInterface The API client 40 | */ 41 | public function getActivityProfileApiClient(); 42 | 43 | /** 44 | * Returns an API client to access the agent profile API of an xAPI based 45 | * LRS. 46 | * 47 | * @return \Xabbuh\XApi\Client\Api\AgentProfileApiClientInterface The API client 48 | */ 49 | public function getAgentProfileApiClient(); 50 | } 51 | -------------------------------------------------------------------------------- /tests/Api/ActivityProfileApiClientTest.php: -------------------------------------------------------------------------------- 1 | 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\Tests\Api; 13 | 14 | use Xabbuh\XApi\Client\Api\ActivityProfileApiClient; 15 | use Xabbuh\XApi\DataFixtures\DocumentFixtures; 16 | use Xabbuh\XApi\Model\Activity; 17 | use Xabbuh\XApi\Model\ActivityProfile; 18 | use Xabbuh\XApi\Model\ActivityProfileDocument; 19 | use Xabbuh\XApi\Model\IRI; 20 | use Xabbuh\XApi\Serializer\Symfony\DocumentDataSerializer; 21 | 22 | /** 23 | * @author Christian Flothmann 24 | */ 25 | class ActivityProfileApiClientTest extends ApiClientTest 26 | { 27 | /** 28 | * @var ActivityProfileApiClient 29 | */ 30 | private $client; 31 | 32 | protected function setUp(): void 33 | { 34 | parent::setUp(); 35 | $this->client = new ActivityProfileApiClient( 36 | $this->requestHandler, 37 | '1.0.1', 38 | new DocumentDataSerializer($this->serializer) 39 | ); 40 | } 41 | 42 | public function testCreateOrUpdateDocument() 43 | { 44 | $document = DocumentFixtures::getActivityProfileDocument(); 45 | 46 | $this->validateStoreApiCall( 47 | 'post', 48 | 'activities/profile', 49 | array( 50 | 'activityId' => 'activity-id', 51 | 'profileId' => 'profile-id', 52 | ), 53 | 204, 54 | '', 55 | $document->getData() 56 | ); 57 | 58 | $this->client->createOrUpdateDocument($document); 59 | } 60 | 61 | public function testCreateOrReplaceDocument() 62 | { 63 | $document = DocumentFixtures::getActivityProfileDocument(); 64 | 65 | $this->validateStoreApiCall( 66 | 'put', 67 | 'activities/profile', 68 | array( 69 | 'activityId' => 'activity-id', 70 | 'profileId' => 'profile-id', 71 | ), 72 | 204, 73 | '', 74 | $document->getData() 75 | ); 76 | 77 | $this->client->createOrReplaceDocument($document); 78 | } 79 | 80 | public function testDeleteDocument() 81 | { 82 | $activityProfile = $this->createActivityProfile(); 83 | 84 | $this->validateRequest( 85 | 'delete', 86 | 'activities/profile', 87 | array( 88 | 'activityId' => 'activity-id', 89 | 'profileId' => 'profile-id', 90 | ), 91 | '' 92 | ); 93 | $this->validateSerializer(array()); 94 | 95 | $this->client->deleteDocument($activityProfile); 96 | } 97 | 98 | public function testGetDocument() 99 | { 100 | $document = DocumentFixtures::getActivityProfileDocument(); 101 | $activityProfile = $document->getActivityProfile(); 102 | 103 | $this->validateRetrieveApiCall( 104 | 'get', 105 | 'activities/profile', 106 | array( 107 | 'activityId' => 'activity-id', 108 | 'profileId' => 'profile-id', 109 | ), 110 | 200, 111 | 'DocumentData', 112 | $document->getData() 113 | ); 114 | 115 | $document = $this->client->getDocument($activityProfile); 116 | 117 | $this->assertInstanceOf(ActivityProfileDocument::class, $document); 118 | $this->assertEquals($activityProfile, $document->getActivityProfile()); 119 | } 120 | 121 | private function createActivityProfile() 122 | { 123 | $activity = new Activity(IRI::fromString('activity-id')); 124 | $activityProfile = new ActivityProfile('profile-id', $activity); 125 | 126 | return $activityProfile; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Api/AgentProfileApiClientTest.php: -------------------------------------------------------------------------------- 1 | 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\Tests\Api; 13 | 14 | use Xabbuh\XApi\Client\Api\AgentProfileApiClient; 15 | use Xabbuh\XApi\DataFixtures\DocumentFixtures; 16 | use Xabbuh\XApi\Model\Agent; 17 | use Xabbuh\XApi\Model\AgentProfile; 18 | use Xabbuh\XApi\Model\AgentProfileDocument; 19 | use Xabbuh\XApi\Model\InverseFunctionalIdentifier; 20 | use Xabbuh\XApi\Model\IRI; 21 | use Xabbuh\XApi\Serializer\Symfony\ActorSerializer; 22 | use Xabbuh\XApi\Serializer\Symfony\DocumentDataSerializer; 23 | 24 | /** 25 | * @author Christian Flothmann 26 | */ 27 | class AgentProfileApiClientTest extends ApiClientTest 28 | { 29 | /** 30 | * @var AgentProfileApiClient 31 | */ 32 | private $client; 33 | 34 | protected function setUp(): void 35 | { 36 | parent::setUp(); 37 | $this->client = new AgentProfileApiClient( 38 | $this->requestHandler, 39 | '1.0.1', 40 | new DocumentDataSerializer($this->serializer), 41 | new ActorSerializer($this->serializer) 42 | ); 43 | } 44 | 45 | public function testCreateOrUpdateDocument() 46 | { 47 | $document = DocumentFixtures::getAgentProfileDocument(); 48 | $profile = $document->getAgentProfile(); 49 | 50 | $this->validateStoreApiCall( 51 | 'post', 52 | 'agents/profile', 53 | array( 54 | 'agent' => 'agent-as-json', 55 | 'profileId' => 'profile-id', 56 | ), 57 | 204, 58 | '', 59 | $document->getData(), 60 | array(array('data' => $profile->getAgent(), 'result' => 'agent-as-json')) 61 | ); 62 | 63 | $this->client->createOrUpdateDocument($document); 64 | } 65 | 66 | public function testCreateOrReplaceDocument() 67 | { 68 | $document = DocumentFixtures::getAgentProfileDocument(); 69 | $profile = $document->getAgentProfile(); 70 | 71 | $this->validateStoreApiCall( 72 | 'put', 73 | 'agents/profile', 74 | array( 75 | 'agent' => 'agent-as-json', 76 | 'profileId' => 'profile-id', 77 | ), 78 | 204, 79 | '', 80 | $document->getData(), 81 | array(array('data' => $profile->getAgent(), 'result' => 'agent-as-json')) 82 | ); 83 | 84 | $this->client->createOrReplaceDocument($document); 85 | } 86 | 87 | public function testDeleteDocument() 88 | { 89 | $profile = $this->createAgentProfile(); 90 | 91 | $this->validateRequest( 92 | 'delete', 93 | 'agents/profile', 94 | array( 95 | 'agent' => 'agent-as-json', 96 | 'profileId' => 'profile-id', 97 | ), 98 | '' 99 | ); 100 | $this->validateSerializer(array(array('data' => $profile->getAgent(), 'result' => 'agent-as-json'))); 101 | 102 | $this->client->deleteDocument( 103 | $profile 104 | ); 105 | } 106 | 107 | public function testGetDocument() 108 | { 109 | $document = DocumentFixtures::getAgentProfileDocument(); 110 | $profile = $document->getAgentProfile(); 111 | 112 | $this->validateRetrieveApiCall( 113 | 'get', 114 | 'agents/profile', 115 | array( 116 | 'agent' => 'agent-as-json', 117 | 'profileId' => 'profile-id', 118 | ), 119 | 200, 120 | 'DocumentData', 121 | $document->getData(), 122 | array(array('data' => $profile->getAgent(), 'result' => 'agent-as-json')) 123 | ); 124 | 125 | $document = $this->client->getDocument($profile); 126 | 127 | $this->assertInstanceOf(AgentProfileDocument::class, $document); 128 | $this->assertEquals($profile, $document->getAgentProfile()); 129 | } 130 | 131 | private function createAgentProfile() 132 | { 133 | $agent = new Agent(InverseFunctionalIdentifier::withMbox(IRI::fromString('mailto:christian@example.com'))); 134 | $profile = new AgentProfile('profile-id', $agent); 135 | 136 | return $profile; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/Api/ApiClientTest.php: -------------------------------------------------------------------------------- 1 | 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\Tests\Api; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use Psr\Http\Message\RequestInterface; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Symfony\Component\Serializer\SerializerInterface; 18 | use Xabbuh\XApi\Client\Request\HandlerInterface; 19 | use Xabbuh\XApi\Common\Exception\NotFoundException; 20 | use Xabbuh\XApi\Serializer\SerializerRegistry; 21 | use Xabbuh\XApi\Serializer\Symfony\ActorSerializer; 22 | use Xabbuh\XApi\Serializer\Symfony\DocumentDataSerializer; 23 | use Xabbuh\XApi\Serializer\Symfony\StatementResultSerializer; 24 | use Xabbuh\XApi\Serializer\Symfony\StatementSerializer; 25 | 26 | /** 27 | * @author Christian Flothmann 28 | */ 29 | abstract class ApiClientTest extends TestCase 30 | { 31 | /** 32 | * @var HandlerInterface|\PHPUnit_Framework_MockObject_MockObject 33 | */ 34 | protected $requestHandler; 35 | 36 | /** 37 | * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject 38 | */ 39 | protected $serializer; 40 | 41 | /** 42 | * @var SerializerRegistry 43 | */ 44 | protected $serializerRegistry; 45 | 46 | protected function setUp(): void 47 | { 48 | $this->requestHandler = $this->getMockBuilder(HandlerInterface::class)->getMock(); 49 | $this->serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); 50 | $this->serializerRegistry = $this->createSerializerRegistry(); 51 | } 52 | 53 | protected function createSerializerRegistry() 54 | { 55 | $registry = new SerializerRegistry(); 56 | $registry->setStatementSerializer(new StatementSerializer($this->serializer)); 57 | $registry->setStatementResultSerializer(new StatementResultSerializer($this->serializer)); 58 | $registry->setActorSerializer(new ActorSerializer($this->serializer)); 59 | $registry->setDocumentDataSerializer(new DocumentDataSerializer($this->serializer)); 60 | 61 | return $registry; 62 | } 63 | 64 | protected function validateSerializer(array $serializerMap) 65 | { 66 | $this 67 | ->serializer 68 | ->expects($this->any()) 69 | ->method('serialize') 70 | ->willReturnCallback(function ($data) use ($serializerMap) { 71 | foreach ($serializerMap as $entry) { 72 | if ($data == $entry['data']) { 73 | return $entry['result']; 74 | } 75 | } 76 | 77 | return ''; 78 | }); 79 | } 80 | 81 | protected function validateRequest($method, $uri, array $urlParameters, $body = null) 82 | { 83 | $request = $this->getMockBuilder(RequestInterface::class)->getMock(); 84 | $this 85 | ->requestHandler 86 | ->expects($this->once()) 87 | ->method('createRequest') 88 | ->with($method, $uri, $urlParameters, $body) 89 | ->willReturn($request); 90 | 91 | return $request; 92 | } 93 | 94 | protected function validateRetrieveApiCall($method, $uri, array $urlParameters, $statusCode, $type, $transformedResult, array $serializerMap = array()) 95 | { 96 | $rawResponse = 'the-server-response'; 97 | $response = $this->getMockBuilder(ResponseInterface::class)->getMock(); 98 | $response->expects($this->any())->method('getStatusCode')->willReturn($statusCode); 99 | $response->expects($this->any())->method('getHeader')->with('Content-Type')->willReturn(array('application/json')); 100 | $response->expects($this->any())->method('getBody')->willReturn($rawResponse); 101 | $request = $this->validateRequest($method, $uri, $urlParameters); 102 | 103 | if (404 === $statusCode) { 104 | $this 105 | ->requestHandler 106 | ->expects($this->once()) 107 | ->method('executeRequest') 108 | ->with($request) 109 | ->willThrowException(new NotFoundException('Not found')); 110 | } else { 111 | $this 112 | ->requestHandler 113 | ->expects($this->once()) 114 | ->method('executeRequest') 115 | ->with($request) 116 | ->willReturn($response); 117 | } 118 | 119 | $this->validateSerializer($serializerMap); 120 | 121 | if ($statusCode < 400) { 122 | $this->serializer 123 | ->expects($this->once()) 124 | ->method('deserialize') 125 | ->with($rawResponse, 'Xabbuh\XApi\Model\\'.$type, 'json') 126 | ->willReturn($transformedResult); 127 | } 128 | } 129 | 130 | protected function validateStoreApiCall($method, $uri, array $urlParameters, $statusCode, $rawResponse, $object, array $serializerMap = array()) 131 | { 132 | $rawRequest = 'the-request-body'; 133 | $response = $this->getMockBuilder(ResponseInterface::class)->getMock(); 134 | $response->expects($this->any())->method('getStatusCode')->willReturn($statusCode); 135 | $response->expects($this->any())->method('getBody')->willReturn($rawResponse); 136 | $request = $this->validateRequest($method, $uri, $urlParameters, $rawRequest); 137 | $this 138 | ->requestHandler 139 | ->expects($this->once()) 140 | ->method('executeRequest') 141 | ->with($request, array($statusCode)) 142 | ->willReturn($response); 143 | $serializerMap[] = array('data' => $object, 'result' => $rawRequest); 144 | $this->validateSerializer($serializerMap); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/Api/StateApiClientTest.php: -------------------------------------------------------------------------------- 1 | 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\Tests\Api; 13 | 14 | use Xabbuh\XApi\Client\Api\StateApiClient; 15 | use Xabbuh\XApi\DataFixtures\DocumentFixtures; 16 | use Xabbuh\XApi\Model\Activity; 17 | use Xabbuh\XApi\Model\Agent; 18 | use Xabbuh\XApi\Model\InverseFunctionalIdentifier; 19 | use Xabbuh\XApi\Model\IRI; 20 | use Xabbuh\XApi\Model\State; 21 | use Xabbuh\XApi\Model\StateDocument; 22 | use Xabbuh\XApi\Serializer\Symfony\ActorSerializer; 23 | use Xabbuh\XApi\Serializer\Symfony\DocumentDataSerializer; 24 | 25 | /** 26 | * @author Christian Flothmann 27 | */ 28 | class StateApiClientTest extends ApiClientTest 29 | { 30 | /** 31 | * @var StateApiClient 32 | */ 33 | private $client; 34 | 35 | protected function setUp(): void 36 | { 37 | parent::setUp(); 38 | $this->client = new StateApiClient( 39 | $this->requestHandler, 40 | '1.0.1', 41 | new DocumentDataSerializer($this->serializer), 42 | new ActorSerializer($this->serializer) 43 | ); 44 | } 45 | 46 | public function testCreateOrUpdateDocument() 47 | { 48 | $document = DocumentFixtures::getStateDocument(); 49 | 50 | $this->validateStoreApiCall( 51 | 'post', 52 | 'activities/state', 53 | array( 54 | 'activityId' => 'activity-id', 55 | 'agent' => 'agent-as-json', 56 | 'stateId' => 'state-id', 57 | ), 58 | 204, 59 | '', 60 | $document->getData(), 61 | array(array('data' => $document->getState()->getActor(), 'result' => 'agent-as-json')) 62 | ); 63 | 64 | $this->client->createOrUpdateDocument($document); 65 | } 66 | 67 | public function testCreateOrReplaceDocument() 68 | { 69 | $document = DocumentFixtures::getStateDocument(); 70 | 71 | $this->validateStoreApiCall( 72 | 'put', 73 | 'activities/state', 74 | array( 75 | 'activityId' => 'activity-id', 76 | 'agent' => 'agent-as-json', 77 | 'stateId' => 'state-id', 78 | ), 79 | 204, 80 | '', 81 | $document->getData(), 82 | array(array('data' => $document->getState()->getActor(), 'result' => 'agent-as-json')) 83 | ); 84 | 85 | $this->client->createOrReplaceDocument($document); 86 | } 87 | 88 | public function testDeleteDocument() 89 | { 90 | $state = $this->createState(); 91 | 92 | $this->validateRequest( 93 | 'delete', 94 | 'activities/state', 95 | array( 96 | 'activityId' => 'activity-id', 97 | 'agent' => 'agent-as-json', 98 | 'stateId' => 'state-id', 99 | ), 100 | '' 101 | ); 102 | $this->validateSerializer(array(array('data' => $state->getActor(), 'result' => 'agent-as-json'))); 103 | 104 | $this->client->deleteDocument($state); 105 | } 106 | 107 | public function testGetDocument() 108 | { 109 | $document = DocumentFixtures::getStateDocument(); 110 | $state = $document->getState(); 111 | 112 | $this->validateRetrieveApiCall( 113 | 'get', 114 | 'activities/state', 115 | array( 116 | 'activityId' => 'activity-id', 117 | 'agent' => 'agent-as-json', 118 | 'stateId' => 'state-id', 119 | ), 120 | 200, 121 | 'DocumentData', 122 | $document->getData(), 123 | array(array('data' => $state->getActor(), 'result' => 'agent-as-json')) 124 | ); 125 | 126 | $document = $this->client->getDocument($state); 127 | 128 | $this->assertInstanceOf(StateDocument::class, $document); 129 | $this->assertEquals($state, $document->getState()); 130 | } 131 | 132 | private function createState() 133 | { 134 | $agent = new Agent(InverseFunctionalIdentifier::withMbox(IRI::fromString('mailto:alice@example.com'))); 135 | $activity = new Activity(IRI::fromString('activity-id')); 136 | $state = new State($activity, $agent, 'state-id'); 137 | 138 | return $state; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/Api/StatementsApiClientTest.php: -------------------------------------------------------------------------------- 1 | 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\Tests\Api; 13 | 14 | use Xabbuh\XApi\Client\Api\StatementsApiClient; 15 | use Xabbuh\XApi\Common\Exception\NotFoundException; 16 | use Xabbuh\XApi\DataFixtures\StatementFixtures; 17 | use Xabbuh\XApi\Model\Agent; 18 | use Xabbuh\XApi\Model\InverseFunctionalIdentifier; 19 | use Xabbuh\XApi\Model\IRI; 20 | use Xabbuh\XApi\Model\IRL; 21 | use Xabbuh\XApi\Model\Statement; 22 | use Xabbuh\XApi\Model\StatementId; 23 | use Xabbuh\XApi\Model\StatementReference; 24 | use Xabbuh\XApi\Model\StatementResult; 25 | use Xabbuh\XApi\Model\StatementsFilter; 26 | use Xabbuh\XApi\Model\Verb; 27 | use Xabbuh\XApi\Serializer\Symfony\ActorSerializer; 28 | use Xabbuh\XApi\Serializer\Symfony\StatementResultSerializer; 29 | use Xabbuh\XApi\Serializer\Symfony\StatementSerializer; 30 | 31 | /** 32 | * @author Christian Flothmann 33 | */ 34 | class StatementsApiClientTest extends ApiClientTest 35 | { 36 | /** 37 | * @var StatementsApiClient 38 | */ 39 | private $client; 40 | 41 | protected function setUp(): void 42 | { 43 | parent::setUp(); 44 | $this->client = new StatementsApiClient( 45 | $this->requestHandler, 46 | '1.0.1', 47 | new StatementSerializer($this->serializer), 48 | new StatementResultSerializer($this->serializer), 49 | new ActorSerializer($this->serializer) 50 | ); 51 | } 52 | 53 | public function testStoreStatement() 54 | { 55 | $statementId = '12345678-1234-5678-1234-567812345678'; 56 | $statement = $this->createStatement(); 57 | $this->validateStoreApiCall( 58 | 'post', 59 | 'statements', 60 | array(), 61 | 200, 62 | '["'.$statementId.'"]', 63 | $this->createStatement() 64 | ); 65 | $returnedStatement = $this->client->storeStatement($statement); 66 | $expectedStatement = $this->createStatement($statementId); 67 | 68 | $this->assertEquals($expectedStatement, $returnedStatement); 69 | } 70 | 71 | public function testStoreStatementWithId() 72 | { 73 | $statementId = '12345678-1234-5678-1234-567812345678'; 74 | $statement = $this->createStatement($statementId); 75 | $this->validateStoreApiCall( 76 | 'put', 77 | 'statements', 78 | array('statementId' => $statementId), 79 | 204, 80 | '["'.$statementId.'"]', 81 | $statement 82 | ); 83 | 84 | $this->assertEquals($statement, $this->client->storeStatement($statement)); 85 | } 86 | 87 | public function testStoreStatementWithIdEnsureThatTheIdIsNotOverwritten() 88 | { 89 | $statementId = '12345678-1234-5678-1234-567812345678'; 90 | $statement = $this->createStatement($statementId); 91 | $this->validateStoreApiCall( 92 | 'put', 93 | 'statements', 94 | array('statementId' => $statementId), 95 | 204, 96 | '', 97 | $statement 98 | ); 99 | $storedStatement = $this->client->storeStatement($statement); 100 | 101 | $this->assertEquals($statementId, $storedStatement->getId()->getValue()); 102 | } 103 | 104 | public function testStoreStatements() 105 | { 106 | $statementId1 = '12345678-1234-5678-1234-567812345678'; 107 | $statementId2 = '12345678-1234-5678-1234-567812345679'; 108 | $statement1 = $this->createStatement(); 109 | $statement2 = $this->createStatement(); 110 | $this->validateStoreApiCall( 111 | 'post', 112 | 'statements', 113 | array(), 114 | '200', 115 | '["'.$statementId1.'","'.$statementId2.'"]', 116 | array($this->createStatement(), $this->createStatement()) 117 | ); 118 | $statements = $this->client->storeStatements(array($statement1, $statement2)); 119 | $expectedStatement1 = $this->createStatement($statementId1); 120 | $expectedStatement2 = $this->createStatement($statementId2); 121 | $expectedStatements = array($expectedStatement1, $expectedStatement2); 122 | 123 | $this->assertNotContains($statements[0], array($statement1, $statement2)); 124 | $this->assertNotContains($statements[1], array($statement1, $statement2)); 125 | $this->assertEquals($expectedStatements, $statements); 126 | $this->assertEquals($statementId1, $statements[0]->getId()->getValue()); 127 | $this->assertEquals($statementId2, $statements[1]->getId()->getValue()); 128 | } 129 | 130 | public function testStoreStatementsWithNonStatementObject() 131 | { 132 | $this->expectException(\InvalidArgumentException::class); 133 | 134 | $statement1 = $this->createStatement(); 135 | $statement2 = $this->createStatement(); 136 | 137 | $this->client->storeStatements(array($statement1, new \stdClass(), $statement2)); 138 | } 139 | 140 | public function testStoreStatementsWithNonObject() 141 | { 142 | $this->expectException(\InvalidArgumentException::class); 143 | 144 | $statement1 = $this->createStatement(); 145 | $statement2 = $this->createStatement(); 146 | 147 | $this->client->storeStatements(array($statement1, 'foo', $statement2)); 148 | } 149 | 150 | public function testStoreStatementsWithId() 151 | { 152 | $this->expectException(\InvalidArgumentException::class); 153 | 154 | $statement1 = $this->createStatement(); 155 | $statement2 = $this->createStatement('12345678-1234-5678-1234-567812345679'); 156 | 157 | $this->client->storeStatements(array($statement1, $statement2)); 158 | } 159 | 160 | public function testVoidStatement() 161 | { 162 | $voidedStatementId = '12345678-1234-5678-1234-567812345679'; 163 | $voidingStatementId = '12345678-1234-5678-1234-567812345678'; 164 | $agent = new Agent(InverseFunctionalIdentifier::withMbox(IRI::fromString('mailto:john.doe@example.com'))); 165 | $statementReference = new StatementReference(StatementId::fromString($voidedStatementId)); 166 | $voidingStatement = new Statement(null, $agent, Verb::createVoidVerb(), $statementReference); 167 | $voidedStatement = $this->createStatement($voidedStatementId); 168 | $this->validateStoreApiCall( 169 | 'post', 170 | 'statements', 171 | array(), 172 | 200, 173 | '["'.$voidingStatementId.'"]', 174 | $voidingStatement 175 | ); 176 | $returnedVoidingStatement = $this->client->voidStatement($voidedStatement, $agent); 177 | $expectedVoidingStatement = new Statement( 178 | StatementId::fromString($voidingStatementId), 179 | $agent, 180 | Verb::createVoidVerb(), 181 | $statementReference 182 | ); 183 | 184 | $this->assertEquals($expectedVoidingStatement, $returnedVoidingStatement); 185 | } 186 | 187 | public function testGetStatement() 188 | { 189 | $statementId = '12345678-1234-5678-1234-567812345678'; 190 | $statement = $this->createStatement(); 191 | $this->validateRetrieveApiCall( 192 | 'get', 193 | 'statements', 194 | array('statementId' => $statementId, 'attachments' => 'true'), 195 | 200, 196 | 'Statement', 197 | $statement 198 | ); 199 | 200 | $this->client->getStatement(StatementId::fromString($statementId)); 201 | } 202 | 203 | public function testGetStatementWithNotExistingStatement() 204 | { 205 | $this->expectException(NotFoundException::class); 206 | 207 | $statementId = '12345678-1234-5678-1234-567812345678'; 208 | $this->validateRetrieveApiCall( 209 | 'get', 210 | 'statements', 211 | array('statementId' => $statementId, 'attachments' => 'true'), 212 | 404, 213 | 'Statement', 214 | 'There is no statement associated with this id' 215 | ); 216 | 217 | $this->client->getStatement(StatementId::fromString($statementId)); 218 | } 219 | 220 | public function testGetVoidedStatement() 221 | { 222 | $statementId = '12345678-1234-5678-1234-567812345678'; 223 | $statement = $this->createStatement(); 224 | $this->validateRetrieveApiCall( 225 | 'get', 226 | 'statements', 227 | array('voidedStatementId' => $statementId, 'attachments' => 'true'), 228 | 200, 229 | 'Statement', 230 | $statement 231 | ); 232 | 233 | $this->client->getVoidedStatement(StatementId::fromString($statementId)); 234 | } 235 | 236 | public function testGetVoidedStatementWithNotExistingStatement() 237 | { 238 | $this->expectException(NotFoundException::class); 239 | 240 | $statementId = '12345678-1234-5678-1234-567812345678'; 241 | $this->validateRetrieveApiCall( 242 | 'get', 243 | 'statements', 244 | array('voidedStatementId' => $statementId, 'attachments' => 'true'), 245 | 404, 246 | 'Statement', 247 | 'There is no statement associated with this id' 248 | ); 249 | 250 | $this->client->getVoidedStatement(StatementId::fromString($statementId)); 251 | } 252 | 253 | public function testGetStatements() 254 | { 255 | $statementResult = $this->createStatementResult(); 256 | $this->validateRetrieveApiCall( 257 | 'get', 258 | 'statements', 259 | array(), 260 | 200, 261 | 'StatementResult', 262 | $statementResult 263 | ); 264 | 265 | $this->assertEquals($statementResult, $this->client->getStatements()); 266 | } 267 | 268 | public function testGetStatementsWithStatementsFilter() 269 | { 270 | $filter = new StatementsFilter(); 271 | $filter->limit(10)->ascending(); 272 | $statementResult = $this->createStatementResult(); 273 | $this->validateRetrieveApiCall( 274 | 'get', 275 | 'statements', 276 | array('limit' => 10, 'ascending' => 'true'), 277 | 200, 278 | 'StatementResult', 279 | $statementResult 280 | ); 281 | 282 | $this->assertEquals($statementResult, $this->client->getStatements($filter)); 283 | } 284 | 285 | public function testGetStatementsWithAgentInStatementsFilter() 286 | { 287 | // {"mbox":"mailto:alice@example.com","objectType":"Agent"} 288 | $filter = new StatementsFilter(); 289 | $agent = new Agent(InverseFunctionalIdentifier::withMbox(IRI::fromString('mailto:alice@example.com'))); 290 | $filter->byActor($agent); 291 | $statementResult = $this->createStatementResult(); 292 | $agentJson = '{"mbox":"mailto:alice@example.com","objectType":"Agent"}'; 293 | $this->serializer 294 | ->expects($this->once()) 295 | ->method('serialize') 296 | ->with($agent, 'json') 297 | ->willReturn($agentJson); 298 | $this->validateRetrieveApiCall( 299 | 'get', 300 | 'statements', 301 | array('agent' => $agentJson), 302 | 200, 303 | 'StatementResult', 304 | $statementResult 305 | ); 306 | 307 | $this->assertEquals($statementResult, $this->client->getStatements($filter)); 308 | } 309 | 310 | public function testGetStatementsWithVerbInStatementsFilter() 311 | { 312 | $filter = new StatementsFilter(); 313 | $verb = new Verb(IRI::fromString('http://adlnet.gov/expapi/verbs/attended')); 314 | $filter->byVerb($verb); 315 | $statementResult = $this->createStatementResult(); 316 | $this->validateRetrieveApiCall( 317 | 'get', 318 | 'statements', 319 | array('verb' => 'http://adlnet.gov/expapi/verbs/attended'), 320 | 200, 321 | 'StatementResult', 322 | $statementResult 323 | ); 324 | 325 | $this->assertEquals($statementResult, $this->client->getStatements($filter)); 326 | } 327 | 328 | public function testGetNextStatements() 329 | { 330 | $moreUrl = '/xapi/statements/more/b381d8eca64a61a42c7b9b4ecc2fabb6'; 331 | $previousStatementResult = new StatementResult(array(), IRL::fromString($moreUrl)); 332 | $this->validateRetrieveApiCall( 333 | 'get', 334 | $moreUrl, 335 | array(), 336 | 200, 337 | 'StatementResult', 338 | $previousStatementResult 339 | ); 340 | 341 | $statementResult = $this->client->getNextStatements($previousStatementResult); 342 | 343 | $this->assertInstanceOf(StatementResult::class, $statementResult); 344 | } 345 | 346 | /** 347 | * @param int $id 348 | * 349 | * @return Statement 350 | */ 351 | private function createStatement($id = null) 352 | { 353 | $statement = StatementFixtures::getMinimalStatement($id); 354 | 355 | if (null === $id) { 356 | $statement = $statement->withId(null); 357 | } 358 | 359 | return $statement; 360 | } 361 | 362 | /** 363 | * @return StatementResult 364 | */ 365 | private function createStatementResult() 366 | { 367 | return new StatementResult(array()); 368 | } 369 | } 370 | --------------------------------------------------------------------------------