composer demo`.
-All service methods throw `RuntimeException` if the API request fails:
+## Development
-```php
-try {
- $response = $sessionService->createSession($request);
-} catch (RuntimeException $e) {
- echo "Error: " . $e->getMessage() . "\n";
-}
+```bash
+composer install
+composer test # unit tests, no API key needed
```
+To release: update [CHANGELOG.md](CHANGELOG.md), then push a tag such as `v1.1.0`. Packagist picks up the tag automatically, and GitHub Actions creates a GitHub release from the changelog.
+
## License
-MIT License
\ No newline at end of file
+[MIT](LICENSE)
diff --git a/composer.json b/composer.json
index ec7362c..b0c3cd8 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,17 @@
{
"name": "algebrakit/sdk-php",
- "description": "Official PHP SDK for the Algebrakit Web Service API — create exercise sessions, retrieve scores, lock/unlock sessions, validate exercises, and fetch session info.",
+ "description": "Official PHP SDK for the Algebrakit API: create exercise sessions, get scores and session info, lock sessions and validate exercises.",
"type": "library",
- "keywords": ["algebrakit", "sdk", "math", "education", "exercises", "api-client"],
- "homepage": "https://github.com/algebrakit-org/sdk-php",
+ "keywords": [
+ "algebrakit",
+ "sdk",
+ "api-client",
+ "math",
+ "education",
+ "edtech",
+ "exercises"
+ ],
+ "homepage": "https://github.com/algebrakit/sdk-php",
"license": "MIT",
"authors": [
{
@@ -12,15 +20,16 @@
}
],
"support": {
- "issues": "https://github.com/algebrakit-org/sdk-php/issues",
- "source": "https://github.com/algebrakit-org/sdk-php"
+ "issues": "https://github.com/algebrakit/sdk-php/issues",
+ "source": "https://github.com/algebrakit/sdk-php",
+ "docs": "https://docs.algebrakit.com/pages/320-SDK/"
},
"require": {
- "php": "^8.0",
- "guzzlehttp/guzzle": "^7.0"
+ "php": "^8.1",
+ "guzzlehttp/guzzle": "^7.9"
},
"require-dev": {
- "phpunit/phpunit": "^9.0"
+ "phpunit/phpunit": "^10.5"
},
"autoload": {
"psr-4": {
@@ -34,5 +43,9 @@
"psr-4": {
"Algebrakit\\SDK\\Tests\\": "tests/"
}
+ },
+ "scripts": {
+ "test": "phpunit",
+ "demo": "php demo/tour.php"
}
-}
\ No newline at end of file
+}
diff --git a/demo/play-exercise.php b/demo/play-exercise.php
deleted file mode 100644
index 68dcbcd..0000000
--- a/demo/play-exercise.php
+++ /dev/null
@@ -1,241 +0,0 @@
-createSession(
- new CreateSessionRequest(
- exercises: [new ExerciseById($exerciseId, 'latest')]
- )
- );
-
- if (!empty($response) && $response[0]->success && !empty($response[0]->sessions)) {
- $session = $response[0]->sessions[0];
- $exerciseHtml = $session->html;
- } else {
- $error = $response[0]->msg ?? 'Unknown error creating session';
- }
-} catch (Exception $e) {
- $error = $e->getMessage();
-}
-
-?>
-
-
-
-
-
- Algebrakit PHP SDK - Web Demo
-
-
-
-
-
-
-
-
-
-
- Algebrakit PHP SDK - Web Demo
-
-
- = htmlspecialchars($error) ?>
-
-
- = $exerciseHtml ?>
-
-
-
-
-
Learning Events
-
-
-
-
-
-
diff --git a/demo/tour.php b/demo/tour.php
new file mode 100644
index 0000000..e732cf1
--- /dev/null
+++ b/demo/tour.php
@@ -0,0 +1,104 @@
+ composer demo
+ *
+ * To show an exercise to a student in the browser, see the runnable web examples:
+ * https://github.com/algebrakit/algebrakit-examples
+ */
+
+require __DIR__ . '/../vendor/autoload.php';
+
+use Algebrakit\SDK\Exceptions\AlgebrakitApiException;
+use Algebrakit\SDK\Models\AkExercise\AK_ContentBlock;
+use Algebrakit\SDK\Models\AkExercise\AK_Element;
+use Algebrakit\SDK\Models\AkExercise\AK_Exercise;
+use Algebrakit\SDK\Models\AkExercise\AK_InteractionBlock;
+use Algebrakit\SDK\Models\AkExercise\AK_InteractionMultistep;
+use Algebrakit\SDK\Models\AkExercise\AK_MultistepPart;
+use Algebrakit\SDK\Models\AkExercise\AK_QuestionMode;
+use Algebrakit\SDK\Models\AkExercise\AK_Symbol;
+use Algebrakit\SDK\Models\AkExercise\AK_SymbolType;
+use Algebrakit\SDK\Models\AkExercise\AK_TaskSimplify;
+use Algebrakit\SDK\Models\Requests\CreateSessionRequest;
+use Algebrakit\SDK\Models\Requests\ExerciseValidateRequest;
+use Algebrakit\SDK\Models\Requests\SessionInfoRequest;
+use Algebrakit\SDK\Models\Requests\SessionLockRequest;
+use Algebrakit\SDK\Models\Requests\SessionScoreRequest;
+use Algebrakit\SDK\Models\Shared\ExerciseById;
+use Algebrakit\SDK\Models\Shared\ExerciseBySpec;
+use Algebrakit\SDK\Services\SessionService;
+
+const EXERCISE_ID = 'fa42e943-8213-41a6-8a91-8c22a929ffe9'; // "Solve the equation 6(p - 1) = 4p + 10"
+
+$apiKey = getenv('ALGEBRAKIT_API_KEY');
+if (!$apiKey) {
+ fwrite(STDERR, "Set ALGEBRAKIT_API_KEY first, for example: ALGEBRAKIT_API_KEY= composer demo\n");
+ exit(1);
+}
+$apiUrl = getenv('ALGEBRAKIT_API_URL') ?: SessionService::DEFAULT_BASE_URL;
+$service = new SessionService($apiKey, baseUrl: $apiUrl);
+
+// An exercise defined in code instead of in the Algebrakit CMS (AK_Exercise format)
+$exerciseSpec = new AK_Exercise(
+ studentProfile: 'uk_KS5',
+ questionMode: AK_QuestionMode::ALL_AT_ONCE,
+ symbols: [new AK_Symbol(name: 'x', type: AK_SymbolType::VARIABLE)],
+ elements: [
+ new AK_Element(blocks: [
+ new AK_ContentBlock(content: 'Simplify the following expression.'),
+ new AK_InteractionBlock(interaction: new AK_InteractionMultistep(
+ solutionPart: new AK_MultistepPart(task: new AK_TaskSimplify(expression: '2x + 3x'))
+ )),
+ ]),
+ ]
+);
+
+function step(string $title): void
+{
+ echo "\n$title\n";
+}
+
+try {
+ echo "Algebrakit SDK tour\nAPI: $apiUrl\n";
+
+ step('1. Validate an exercise (validateExercise)');
+ $validation = $service->validateExercise(new ExerciseValidateRequest(exerciseId: EXERCISE_ID));
+ echo ' valid: ' . var_export($validation->valid, true) . ", marks: {$validation->marks}\n";
+
+ step('2. Create a session (createSession)');
+ $result = $service->createSession(new CreateSessionRequest(exercises: [new ExerciseById(EXERCISE_ID)]))[0];
+ if (!$result->success) {
+ throw new RuntimeException($result->msg ?? 'Session creation failed');
+ }
+ $sessionId = $result->sessions[0]->sessionId;
+ echo " session ID: $sessionId\n";
+ echo " show it in a page with: \n";
+
+ step('3. Get session info (getSessionInfo)');
+ $info = $service->getSessionInfo(new SessionInfoRequest($sessionId));
+ echo ' elements: ' . count($info->elements) . "\n";
+
+ step('4. Get the score (getSessionScore)');
+ $scoring = $service->getSessionScore(new SessionScoreRequest($sessionId))->scoring;
+ echo " marks: {$scoring->marksEarned} of {$scoring->marksTotal}, finished: " . var_export($scoring->finished, true) . "\n";
+
+ step('5. Lock and unlock the session (lockOrUnlockSession)');
+ $service->lockOrUnlockSession(new SessionLockRequest('LOCK', [$sessionId]));
+ echo " locked: the student can no longer change their answers\n";
+ $service->lockOrUnlockSession(new SessionLockRequest('UNLOCK', [$sessionId]));
+ echo " unlocked\n";
+
+ step('6. Create a session from an exercise defined in code (ExerciseBySpec)');
+ $specValidation = $service->validateExercise(new ExerciseValidateRequest(exerciseSpec: $exerciseSpec));
+ echo ' valid: ' . var_export($specValidation->valid, true) . ", marks: {$specValidation->marks}\n";
+ $specResult = $service->createSession(new CreateSessionRequest(exercises: [new ExerciseBySpec($exerciseSpec)]))[0];
+ echo " session ID: {$specResult->sessions[0]->sessionId}\n";
+
+ echo "\nDone.\n";
+} catch (AlgebrakitApiException $e) {
+ fwrite(STDERR, "\nDemo failed: {$e->getMessage()}\n");
+ exit(1);
+}
diff --git a/demo/various-functions.php b/demo/various-functions.php
deleted file mode 100644
index 91fb873..0000000
--- a/demo/various-functions.php
+++ /dev/null
@@ -1,133 +0,0 @@
-getExerciseInfo($exerciseInfoRequest);
-
- if ($exerciseInfoResponse) {
- echo "Exercise Info for " . EXERCISE_ID . ": Course Name: {$exerciseInfoResponse->courseName}, Type: {$exerciseInfoResponse->type}\n";
- } else {
- echo "Failed to retrieve exercise published info.\n";
- }
-
- // Validate the exercise
- $validateRequest = new ExerciseValidateRequest(exerciseId: EXERCISE_ID);
- $validateResponse = $sessionService->validateExercise($validateRequest);
-
- if ($validateResponse) {
- echo "Exercise Validate for " . EXERCISE_ID . ": Valid: " . ($validateResponse->valid ? 'true' : 'false') . ", Marks: {$validateResponse->marks}\n";
- } else {
- echo "Failed to validate exercise.\n";
- }
-
- // Create a session for the exercise
- $createSessionRequest = new CreateSessionRequest(
- exercises: [new ExerciseById(EXERCISE_ID, 'latest')]
- );
-
- $createSessionResponse = $sessionService->createSession($createSessionRequest);
-
- if (!empty($createSessionResponse)) {
- foreach ($createSessionResponse as $response) {
- if ($response->success && !empty($response->sessions)) {
- $session = $response->sessions[0];
- $sessionId = $session->sessionId;
- echo "Session created successfully. Session ID: {$sessionId}\n";
-
- // Get scoring results for the created session
- $sessionScoreRequest = new SessionScoreRequest($sessionId);
- $sessionScoreResponse = $sessionService->getSessionScore($sessionScoreRequest);
-
- if ($sessionScoreResponse) {
- echo "Scoring Results:\n";
- foreach ($sessionScoreResponse->questions as $question) {
- echo "Question: {$question->id}, Marks Earned: {$question->scoring->marksEarned}, Total Marks: {$question->scoring->marksTotal}\n";
- }
- } else {
- echo "Failed to retrieve scoring results.\n";
- }
-
- // Get session info
- $sessionInfoRequest = new SessionInfoRequest($sessionId);
- $sessionInfoResponse = $sessionService->getSessionInfo($sessionInfoRequest);
-
- if ($sessionInfoResponse) {
- echo "Session Info successfully retrieved.\n";
- } else {
- echo "Failed to retrieve session info.\n";
- }
- } else {
- echo "Failed to create session.\n";
- }
- }
- } else {
- echo "No sessions were created.\n";
- }
- // Create a session using an AK_Exercise specification
- echo "\nCreating session from AK_Exercise spec...\n";
- $exerciseSpec = new AK_Exercise(
- studentProfile: 'uk_KS5',
- questionMode: AK_QuestionMode::ALL_AT_ONCE,
- symbols: [
- new AK_Symbol(name: 'x', type: AK_SymbolType::VARIABLE),
- ],
- elements: [
- new AK_Element(blocks: [
- new AK_ContentBlock(content: 'Simplify the following expression.'),
- new AK_InteractionBlock(
- interaction: new AK_InteractionMultistep(
- solutionPart: new AK_MultistepPart(
- task: new AK_TaskSimplify(expression: '2x + 3x')
- )
- )
- ),
- ]),
- ]
- );
-
- // Validate the exercise spec
- $specValidateResponse = $sessionService->validateExercise(
- new ExerciseValidateRequest(exerciseSpec: $exerciseSpec)
- );
- echo "Spec validation: Valid=" . ($specValidateResponse->valid ? 'true' : 'false') . ", Marks={$specValidateResponse->marks}\n";
-
- // Create session from spec
- $specSessionResponse = $sessionService->createSession(
- new CreateSessionRequest(exercises: [new ExerciseBySpec(exerciseSpec: $exerciseSpec)])
- );
- if (!empty($specSessionResponse) && $specSessionResponse[0]->success && !empty($specSessionResponse[0]->sessions)) {
- echo "Session from spec created. Session ID: {$specSessionResponse[0]->sessions[0]->sessionId}\n";
- }
-
-} catch (Exception $e) {
- echo "An error occurred: " . $e->getMessage() . "\n";
-}
\ No newline at end of file
diff --git a/openapi-bundled.yaml b/openapi-bundled.yaml
deleted file mode 100644
index 6c19bcc..0000000
--- a/openapi-bundled.yaml
+++ /dev/null
@@ -1,3789 +0,0 @@
-openapi: 3.0.3
-info:
- title: Algebrakit Web Service API
- version: 2.0.0
- x-logo:
- url: https://public.algebrakit.nl/logo_white.png
- altText: Algebrakit Logo
- description: |
- This API allows creating and managing exercise sessions, retrieve session information and scores, and lock or unlock sessions.
-servers:
- - url: https://api.algebrakit.com
- description: Production server (Ireland)
- - url: https://prod.api.sg-1.algebrakit.com
- description: Production server (Singapore)
-paths:
- /session/create:
- post:
- summary: Create sessions
- description: |
- Create new sessions for Algebrakit exercises. Multiple sessions can be created in one call to optimize performance.
- The response contains a unique session ID for each session. It also offers optimized HTML code which you can insert into your web page.
- This HTML code includes allows for faster initialization as the web component does not need to retrieve data from the web service.
- operationId: sessionCreate
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/CreateSessionRequest'
- examples:
- example1:
- $ref: '#/components/examples/CreateSessionRequestExampleMultiple'
- responses:
- '200':
- description: Successful creation of sessions
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/CreateSessionResponse'
- examples:
- example1:
- $ref: '#/components/examples/CreateSessionResponseExample'
- security:
- - ApiKeyAuth: []
- /session/retrieve:
- post:
- summary: Retrieve sessions
- description: |
- Returns the same session information produced by /session/create, but for for existing sessions.
- Used to efficiently render the web components for existing sessions.
-
- Note that restoring an existing exercise can be done without using this endpoint, simply by the `akit-exercise` or `akit-interaction` web components:
- ```
-
- ```
- The web component will automatically retrieve all needed information (question type, learning events, etc) from the server.
-
- However, this roundtrip impacts page load time,
- especially if multiple exercises are on the page. The `/session/retrieve` endpoint lets you obtain optimized HTML code for multiple sessions in one call.
- Inserting this optimized HTML into the webpage prevents a server roundtrip per exercise and allows the exercises to show instantly.
- operationId: sessionRetrieve
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionRetrieveRequest'
- examples:
- example1:
- $ref: '#/components/examples/SessionRetrieveRequestExample'
- responses:
- '200':
- description: Successfully obtained session data
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionRetrieveResponse'
- examples:
- example1:
- $ref: '#/components/examples/SessionRetrieveResponseExample'
- security:
- - ApiKeyAuth: []
- /session/score:
- post:
- summary: Get session scores
- description: |
- Retrieve results from an exercise session. This endpoint allows you to get the scores and other details of the session.
- You can optionally lock the session to prevent any further modifications.
- operationId: sessionScore
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionScoreRequest'
- examples:
- example1:
- $ref: '#/components/examples/SessionScoreRequestExample'
- responses:
- '200':
- description: Request succeeded
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionScoreResponse'
- examples:
- example1:
- $ref: '#/components/examples/SessionScoreResponseExample'
- security:
- - ApiKeyAuth: []
- /sessions/lock:
- post:
- summary: (Un)lock sessions.
- description: |
- Lock or unlock exercise sessions. Locked sessions do not allow modifications such as new evaluation events.
- This functionality is mainly intended for assessments to guarantee nobody changes the results afterward.
-
- There are two types of lock actions. You can later undo a `LOCK` action with an `UNLOCK` action. A `FINALIZE` action locks a session forever.
-
- You can lock or unlock a batch of sessions in a single request.
- operationId: sessionLock
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionLockRequest'
- examples:
- example1:
- $ref: '#/components/examples/SessionLockRequestExample'
- responses:
- '200':
- description: Sessions successfully locked or unlocked
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionLockResponse'
- security:
- - ApiKeyAuth: []
- /session/info:
- post:
- summary: Retrieve session info
- description: |
- Retrieve detailed information about a session, including exercise layout, solutions, and scoring results.
- This endpoint provides human-readable (or AI-readable) information about the session, with enhanced annotation data
- that includes complete question views with student inputs.
-
- This endpoint will ultimately replace the `/session/score` endpoint.
- operationId: sessionInfo
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionInfoRequest'
- examples:
- example1:
- $ref: '#/components/examples/SessionInfoRequestExample'
- responses:
- '200':
- description: Request succeeded
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/SessionInfoResponse'
- examples:
- example1:
- $ref: '#/components/examples/SessionInfoResponseExample'
- security:
- - ApiKeyAuth: []
- /exercise/validate:
- post:
- summary: Validate exercise
- description: Checks whether an exercise is valid (i.e., that it runs) and obtains general information.
- operationId: exerciseValidate
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/ExerciseValidateRequest'
- examples:
- example1:
- $ref: '#/components/examples/ExerciseValidateRequestExample'
- responses:
- '200':
- description: Request was successful
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/ExerciseValidateResponse'
- examples:
- example1:
- $ref: '#/components/examples/ExerciseValidateResponseExample'
- security:
- - ApiKeyAuth: []
- /exercise/published-info:
- post:
- summary: Retrieve exercise info
- description: |
- Retrieve information about the structure, versions, and history of an exercise in Algebrakit's CMS.
- operationId: exerciseInfo
- requestBody:
- required: true
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/ExerciseInfoRequest'
- examples:
- example1:
- $ref: '#/components/examples/ExerciseInfoRequestExample'
- responses:
- '200':
- description: Information about the structure of the exercise.
- content:
- application/json; charset=utf-8:
- schema:
- $ref: '#/components/schemas/ExerciseInfoResponse'
- examples:
- example1:
- $ref: '#/components/examples/ExerciseInfoResponseExample'
-components:
- securitySchemes:
- ApiKeyAuth:
- type: apiKey
- in: header
- name: X-API-KEY
- schemas:
- IAK_Exercise:
- type: object
- properties:
- type:
- type: string
- const: AK_Exercise
- studentProfile:
- type: string
- description: ID of the student profile, which defines school level, language, etc.
- symbols:
- description: List of mathematical symbols (variables, units, functions, etc.) used in the exercise
- type: array
- items:
- type: object
- properties:
- name:
- type: string
- description: The notation of the mathematical symbol in LaTeX notation. For example, "x", "\angle A_{1}", etc.
- type:
- type: string
- enum:
- - VARIABLE
- - CONSTANT
- - FUNCTION
- - FREEVARIABLE
- description: The type of the math symbol
- synonym:
- description: An optional synonym for the math symbol. This is required if a symbol has multiple interpretations. E.g. "m" can be mass or meter. Defining a synonym allows the exercise author to specify which interpretation to use in the exercise. E.g. "mMass" and "mMeter"
- type: string
- addToFormulaEditor:
- description: Whether to add the symbol as a button to the formula editor.
- type: boolean
- notations:
- description: Alternative notations for the symbol. For example, a line segment "AB" can also be written as "BA".
- type: array
- items:
- type: string
- required:
- - name
- - type
- additionalProperties: false
- elements:
- type: array
- items:
- type: object
- properties:
- blocks:
- type: array
- items:
- oneOf:
- - type: object
- properties:
- type:
- type: string
- const: CONTENT
- content:
- type: string
- description: The content in this block in HTML format and LaTeX notation for math expressions. For example, "Calculate <latex>x</latex> if <latex>2x + 3 = 7</latex>".
- required:
- - type
- - content
- additionalProperties: false
- - type: object
- properties:
- type:
- type: string
- const: INTERACTION
- interaction:
- oneOf:
- - type: object
- properties:
- type:
- type: string
- const: CHOICE
- description: The question type of this interaction, which is "CHOICE" for multiple choice questions.
- refId:
- description: An optional reference ID for this interaction. If not provided, a pseudo ID will be generated.
- type: string
- instruction:
- description: The instruction or question prompt for this interaction
- type: string
- scored:
- description: Whether this interaction should be scored or not. Defaults to true.
- type: boolean
- hints:
- description: An optional list of general hints for this interaction. These are hints to get the student started and will be shown before any successful attempt is made.
- type: array
- items:
- type: string
- spec:
- type: object
- properties:
- options:
- type: array
- items:
- type: object
- properties:
- content:
- type: string
- description: The content of the choice option in HTML format with LaTeX embedded in <latex>...</latex> tags.
- correct:
- type: boolean
- description: If true, this option is correct
- required:
- - content
- - correct
- additionalProperties: false
- description: An array of choice options
- shuffle:
- type: boolean
- description: If true, the options are shuffled
- multipleSelect:
- type: boolean
- description: If true, multiple options can be selected
- required:
- - options
- - shuffle
- - multipleSelect
- additionalProperties: false
- required:
- - type
- - spec
- additionalProperties: false
- - type: object
- properties:
- type:
- type: string
- const: FILL_IN_THE_BLANKS
- description: The question type of this interaction, which is "FILL_IN_THE_BLANKS" for fill-in-the-blank questions.
- refId:
- description: An optional reference ID for this interaction. If not provided, a pseudo ID will be generated.
- type: string
- instruction:
- description: The instruction or question prompt for this interaction
- type: string
- scored:
- description: Whether this interaction should be scored or not. Defaults to true.
- type: boolean
- hints:
- description: An optional list of general hints for this interaction. These are hints to get the student started and will be shown before any successful attempt is made.
- type: array
- items:
- type: string
- content:
- type: string
- description: The content string in HTML with optional embedded LaTeX and placeholders in square brackets for fields. E.g. 'Calculate <latex>\sqrt{x^2}=</latex> <blank id="B1"></blank>.'
- blanks:
- type: array
- items:
- type: object
- properties:
- id:
- type: string
- description: The unique identifier for this blank, used as a placeholder in the content string
- size:
- type: string
- enum:
- - SMALL
- - MEDIUM
- - LARGE
- description: The size of the blank field
- type:
- type: string
- enum:
- - EXPRESSION
- - SELECTION
- description: The type of the blank field
- input:
- anyOf:
- - type: object
- properties:
- task:
- oneOf:
- - type: object
- properties:
- type:
- type: string
- const: SIMPLIFY
- description: Indicates the task is to simplify the given mathematical expression using mathematical and algebraic rules.
- expression:
- type: string
- description: The LaTeX string representing the mathematical expression to simplify.
- required:
- - type
- - expression
- additionalProperties: false
- - type: object
- properties:
- type:
- type: string
- const: SOLVE
- description: Indicates the task is to solve the given equation or inequality for a specific variable.
- expression:
- type: string
- description: The LaTeX string representing the equation or inequality to solve. For example, "x^2-4=0" or "2x+3>5".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '05".
- variable:
- type: string
- description: The variable to solve for, e.g. 'x'
- domain:
- description: Optional domain of the solution, written as an inequality, e.g. 'x\geq 0', '0 tags for mathematical input fields
- - tags for multiple choice options
- - \boxed{\color{blue} ...} for inline math expression inputs
- gapIndex:
- type: integer
- description: Index of the gap in a Fill in the Blanks question, starting from 1
- row:
- type: integer
- description: Row index in a Math Table question, starting from 1
- col:
- type: integer
- description: Column index in a Math Table question, starting from 1
- EventResultInfo:
- type: object
- required:
- - timestamp
- - event
- properties:
- timestamp:
- type: integer
- description: Timestamp of the event in milliseconds since the epoch.
- event:
- $ref: '#/components/schemas/EventType'
- exerciseStatus:
- description: |
- Applies to evaluation events. The status of the students work for this math problem.
- $ref: '#/components/schemas/ExerciseStatus'
- inputStatus:
- description: |
- Applies to evaluation events. The status of the students work on the current part of this math problem.
- $ref: '#/components/schemas/ExerciseStatus'
- tags:
- type: array
- description: References to skill tags for this event, which indicate skills and optional misconceptions. Descriptions of these tags are provided in the `tagDescriptions` property of the session info.
- items:
- $ref: '#/components/schemas/SkillTag'
- skillsTodo:
- type: array
- description: Estimate of skills needed to finish the derivation in this part. Descriptions of these skill IDs are provided in the `tagDescriptions` property of the session info.
- items:
- type: string
- annotations:
- type: array
- description: Meaningful information about the event. What information is provided depends on the question type.
- items:
- $ref: '#/components/schemas/InfoAnnotation'
- InteractionResultInfo:
- type: object
- required:
- - id
- - type
- - status
- - progress
- - tags
- - events
- properties:
- status:
- $ref: '#/components/schemas/ExerciseStatus'
- progress:
- type: number
- format: float
- minimum: 0
- maximum: 1
- description: Progress in range [0..1]. Note that progress is not the same as marks.
- scoring:
- $ref: '#/components/schemas/InteractionScoring'
- tags:
- type: array
- description: |
- References to skill tags for this interaction, compiled from errors, hints, completed derivations, and next step from
- incomplete derivations. See the `events`` property to see which event generated what skill tags.
- Descriptions of these tags are provided in the `tagDescriptions`` property of the session info.
- items:
- $ref: '#/components/schemas/SkillTag'
- events:
- type: array
- items:
- $ref: '#/components/schemas/EventResultInfo'
- InteractionInfo:
- type: object
- allOf:
- - $ref: '#/components/schemas/ElementItemInfoBase'
- - type: object
- required:
- - id
- - itemType
- - interactionType
- - solution
- - result
- properties:
- id:
- type: string
- description: Unique identifier ('ref-id') for this interaction.
- itemType:
- type: string
- enum:
- - INTERACTION
- interactionType:
- $ref: '#/components/schemas/InteractionType'
- content:
- type: string
- nullable: true
- solution:
- type: string
- derivation:
- type: array
- items:
- $ref: '#/components/schemas/DerivationPart'
- nullable: true
- result:
- $ref: '#/components/schemas/InteractionResultInfo'
- TextItemInfo:
- type: object
- allOf:
- - $ref: '#/components/schemas/ElementItemInfoBase'
- - type: object
- required:
- - itemType
- - content
- properties:
- itemType:
- type: string
- enum:
- - TEXT
- content:
- type: string
- description: The content, which can include HTML tags for tables, lists, and other formatting. Mathematical expressions are encoded in latex and wrapped in $ ... $.
- ElementItemInfo:
- type: object
- required:
- - itemType
- discriminator:
- propertyName: itemType
- oneOf:
- - $ref: '#/components/schemas/InteractionInfo'
- - $ref: '#/components/schemas/TextItemInfo'
- ElementInfo:
- type: object
- description: An Element is either a question block or a content block.
- properties:
- id:
- type: string
- description: Unique identifier ('ref-id') for this element block.
- type:
- type: string
- enum:
- - INSTRUCTION
- - QUESTION
- description: The type of the element. INSTRUCTION elements have no scorable interactions. QUESTION elements contain one or more scorable interactions.
- items:
- type: array
- items:
- $ref: '#/components/schemas/ElementItemInfo'
- SessionInfoResponse:
- type: object
- required:
- - elements
- - tagDescriptions
- properties:
- elements:
- type: array
- items:
- $ref: '#/components/schemas/ElementInfo'
- tagDescriptions:
- $ref: '#/components/schemas/TagDescriptions'
- scoring:
- $ref: '#/components/schemas/ScoringResult'
- ExerciseValidateRequest:
- oneOf:
- - type: object
- required:
- - exerciseId
- properties:
- exerciseId:
- type: string
- description: Unique identifier for the session to score.
- version:
- oneOf:
- - type: string
- - type: number
- description: The published version number of the exercise (number) or 'latest' (default). The latter refers to the current version of the exercise.
- - type: object
- required:
- - exerciseSpec
- properties:
- exerciseSpec:
- oneOf:
- - $ref: '#/components/schemas/IAK_Exercise'
- - type: object
- description: Legacy exercise specification format (internal format)
- description: |
- An exercise specification in the AK_Exercise format (recommended) or as obtained from the exercise authoring tool. For the full IAK_Exercise specification, see [Exercise Specification](https://docs.algebrakit.com/pages/315-exercise-specification/).
- ExerciseValidateResponse:
- type: object
- required:
- - valid
- properties:
- success:
- type: boolean
- description: Indicates if the request was successful.
- valid:
- type: boolean
- description: If false, this exercise has a problem and is not suitable to create sessions.
- msg:
- type: string
- description: A description of the problem in case the request failed or the exercise is not valid
- marks:
- type: number
- description: The number of marks for the exercise.
- random:
- type: boolean
- description: Indicates if the exercise is random.
- interactions:
- type: object
- description: Details of interactions (question type) within the exercise.
- additionalProperties:
- $ref: '#/components/schemas/InteractionDescription'
- ExerciseInfoRequest:
- type: object
- description: Request to retrieve information about an exercise.
- required:
- - id
- properties:
- id:
- type: string
- description: The exercise ID.
- ExerciseInfoCommitData:
- type: object
- description: Commit data for an exercise.
- properties:
- versionNumber:
- type: string
- commitDate:
- type: string
- format: date-time
- commitMessage:
- type: string
- authorUsername:
- type: string
- ExerciseInfoInteraction:
- type: object
- description: Interaction info for an exercise version.
- properties:
- block:
- description: Describes where the interaction is located.
- type: string
- refId:
- description: The ID of the interaction in the exercise.
- type: string
- refName:
- description: The name of the interaction in the exercise editor.
- type: string
- type:
- $ref: '#/components/schemas/InteractionType'
- ExerciseInfoResource:
- type: object
- description: Resource info for an exercise version.
- properties:
- refId:
- description: The ID of the resource in the exercise.
- type: string
- type:
- type: string
- refName:
- description: The name of the resource in the exercise editor.
- type: string
- ExerciseInfoPublishedVersion:
- type: object
- description: Published version info for an exercise.
- properties:
- name:
- type: string
- majorVersion:
- oneOf:
- - type: integer
- - type: string
- enum:
- - latest
- metadata:
- type: object
- additionalProperties:
- type: string
- contentVersion:
- type: string
- numberOfLevels:
- type: integer
- interactions:
- type: array
- items:
- $ref: '#/components/schemas/ExerciseInfoInteraction'
- resources:
- type: array
- items:
- $ref: '#/components/schemas/ExerciseInfoResource'
- ExerciseInfoResponse:
- type: object
- description: Information about the structure of the exercise.
- properties:
- commitHistory:
- type: array
- items:
- $ref: '#/components/schemas/ExerciseInfoCommitData'
- id:
- type: string
- publishedVersions:
- type: array
- items:
- $ref: '#/components/schemas/ExerciseInfoPublishedVersion'
- courseName:
- type: string
- type:
- type: string
- description: regular exercise, single reference, or arrangement exercise
- examples:
- CreateSessionRequestExampleMultiple:
- value:
- exercises:
- - exerciseId: fa42e943-8213-41a6-8a91-8c22a929ffe9
- version: latest
- - exerciseId: 0ce4c011-28e3-4423-9429-75ae28e3bc10
- version: latest
- assessmentMode: true
- scoringModel: default
- apiVersion: 2
- CreateSessionResponseExample:
- value:
- - success: true
- sessions:
- - success: true
- sessionId: 691ddb39-e830-41ae-8f97-8c94dcb6120b
- type: SINGLE
- solution: false
- marksTotal: 1
- interactions:
- lNzHy:
- type: MULTISTEP
- marks: 1
- scorable: true
- html:
- - success: true
- sessions:
- - success: true
- sessionId: 62ea6f14-bfcc-42a7-8682-7aff31339e24
- type: SINGLE
- solution: false
- marksTotal: 1
- interactions:
- mFzbB:
- type: GEOMETRY
- marks: 1
- scorable: true
- html:
- SessionRetrieveRequestExample:
- value:
- sessionIds:
- - 77aca88f-e2f4-4daf-ba2e-41008fee1f9a
- SessionRetrieveResponseExample:
- value:
- - success: true
- sessionId: 77aca88f-e2f4-4daf-ba2e-41008fee1f9a
- type: SINGLE
- solution: false
- marksTotal: 1
- interactions:
- lNzHy:
- type: MULTISTEP
- marks: 1
- scorable: true
- html: ''
- SessionScoreRequestExample:
- value:
- sessionId: 77aca88f-e2f4-4daf-ba2e-41008fee1f9a
- lockSession: true
- withTags: true
- withEvents: true
- withEventAnnotations: true
- withScoring: true
- SessionScoreResponseExample:
- value:
- questions:
- - id: Q1
- interactions:
- - id: lNzHy
- type: MULTISTEP
- progress: 0.5
- status: CORRECT
- events:
- - timestamp: 1752395844080
- event: EVALUATE
- annotations:
- - type: INPUT_EXPRESSION
- expr:
- - content: 6p-1=4p+10
- mimeType: application/x-tex
- - type: ERROR_FEEDBACK
- expr:
- - mimeType: application/x-tex
- content: $$6 p - \begingroup\color{darkred}\begingroup 1\endgroup\endgroup=4 p + 10$$
- akit: DerivationContext[Solve[6 p+Highlight[-1]=4 p+10, p, True],Exact=>True]
- main:
- - mimeType: text/plain
- content: You did not compute the multiplication correctly.
- sub:
- - mimeType: text/plain
- content: 'You did not multiply all terms of '
- - mimeType: application/x-tex
- content: $p - 1$
- akit: p+-1
- - mimeType: text/plain
- content: ' by '
- - mimeType: application/x-tex
- content: $6$
- akit: '6'
- - mimeType: text/plain
- content: .
- exerciseStatus: ERROR
- progress: 0
- inputStatus: ERROR
- - timestamp: 1752395846387
- event: EVALUATE
- annotations:
- - type: INPUT_EXPRESSION
- expr:
- - content: 6p-6=4p+10
- mimeType: application/x-tex
- exerciseStatus: CORRECT
- progress: 0
- inputStatus: CORRECT
- - timestamp: 1752395850393
- event: HINT
- annotations:
- - type: HINT
- main:
- - mimeType: text/plain
- content: 'Balance method: subtract '
- - mimeType: application/x-tex
- content: $4 p $
- akit: 4 p
- - mimeType: text/plain
- content: ' from both sides'
- expr:
- - mimeType: application/x-tex
- content: $$\begingroup\color{#336699}\begingroup 6 p \endgroup\endgroup- 6=\begingroup\color{#336699}\begingroup 4 p \endgroup\endgroup+ 10$$
- akit: DerivationContext[Highlight[6 p]+-6=Highlight[4 p]+10,Exact=>True]
- - timestamp: 1752395852101
- event: EVALUATE
- annotations:
- - type: INPUT_EXPRESSION
- expr:
- - content: 2p=16
- mimeType: application/x-tex
- exerciseStatus: CORRECT
- progress: 0
- inputStatus: CORRECT
- - timestamp: 1752395854898
- event: EVALUATE
- annotations:
- - type: INPUT_EXPRESSION
- expr:
- - content: p=\frac{16}{2}
- mimeType: application/x-tex
- exerciseStatus: CORRECT
- progress: 0.5
- inputStatus: CORRECT
- - timestamp: 1752395865482
- event: HINT
- annotations:
- - type: HINT
- main:
- - mimeType: text/plain
- content: 'Calculate: '
- - mimeType: application/x-tex
- content: $\frac{16}{2}$
- akit: 16/2
- expr:
- - mimeType: application/x-tex
- content: $$p =\begingroup\color{#336699}\begingroup \frac{16}{2}\endgroup\endgroup$$
- akit: DerivationContext[Solve[p=Highlight[16/2], p, True],Exact=>True]
- scoring:
- marksEarned: 0
- marksTotal: 1
- penalties:
- marksPenalty: 0
- hintsRequested: 2
- mathErrors: 1
- finished: false
- scoring:
- finished: false
- marksTotal: 1
- marksEarned: 0
- penalties:
- marksPenalty: 0
- hintsRequested: 2
- mathErrors: 1
- scoring:
- finished: false
- marksEarned: 0
- marksTotal: 1
- penalties:
- marksPenalty: 0
- hintsRequested: 2
- mathErrors: 1
- SessionLockRequestExample:
- value:
- action: LOCK
- sessionIds:
- - 77aca88f-e2f4-4daf-ba2e-41008fee1f9a
- SessionInfoRequestExample:
- value:
- sessionId: 77aca88f-e2f4-4daf-ba2e-41008fee1f9a
- withDerivations: true
- withTags: true
- withEvents: true
- withEventAnnotations: true
- withScoring: true
- SessionInfoResponseExample:
- value:
- success: true
- creationTimestamp: 1752395839
- elements:
- - id: Q1
- type: QUESTION
- items:
- - itemType: TEXT
- content: Solve the equation.
- - itemType: TEXT
- content: $$6\left ( p - 1\right ) = 4p + 10$$
- - id: lNzHy
- itemType: INTERACTION
- interactionType: MULTISTEP
- result:
- status: CORRECT
- progress: 0.5
- events:
- - timestamp: 1752395844080
- event: EVALUATE
- tags:
- - id: ExpandSingleBrackets
- weight: 100
- errors:
- - SkippingTerms
- source: ERROR_FEEDBACK
- annotations:
- - type: INPUT
- content: $6p-1=4p+10$
- - type: ERROR_FEEDBACK
- content: You did not expand the parentheses correctly.
You did not multiply all terms of $p - 1$ by $6$ .
- expression: $6 p - \begingroup\color{darkred}\begingroup 1\endgroup\endgroup=4 p + 10$
- exerciseStatus: ERROR
- progress: 0
- inputStatus: ERROR
- scoring:
- marksEarned: 0
- marksTotal: 1
- penalties:
- marksPenalty: 0
- hintsRequested: 2
- mathErrors: 1
- finished: false
- solution: $$p =8$$
- derivation:
- - expression: $$6 \left(p - 1\right)=4 p + 10$$
- - result: $$6 p - 6=4 p + 10$$
- skills:
- - ExpandSingleBrackets
- description: Expand the brackets
- - result: $$6 p - 4 p - 6=10$$
- skills:
- - EquationsBalanceMethodArrangeTerms
- - EquationsOperationsAdd
- description: 'Balance method: subtract $4 p $ from both sides'
- - result: $$2 p - 6=10$$
- skills:
- - AlgebraAddingLikeTerms
- description: Combine the similar terms
- - result: $$2 p =16$$
- skills:
- - EquationsBalanceMethodArrangeTerms
- - EquationsOperationsAdd
- description: 'Balance method: add $6$ to both sides.'
- - result: $$p =8$$
- skills:
- - EquationsBalanceMethodDivideCoefficient
- - EquationsOperationsMultiply
- description: Divide both sides by $2$
- tagDescriptions:
- ExpandSingleBrackets:
- descr:
- en: Expanding a single pair of parentheses
- stepType: ALGEBRA
- errors:
- - id: SkippingTerms
- type:
- - MISCONCEPTION
- descr:
- en: Not multiplying all terms by the factored constant.
- EquationsBalanceMethodArrangeTerms:
- descr:
- en: Arrange terms with variable to the left and other terms to the right.
- stepType: STRATEGY
- EquationsOperationsAdd:
- descr:
- en: Add or subtract terms using the balance method
- stepType: STRATEGY
- AlgebraAddingLikeTerms:
- descr:
- en: Adding like terms
- stepType: ALGEBRA
- EquationsBalanceMethodDivideCoefficient:
- descr:
- en: Divide by the coefficient of the variable to get the solution.
- stepType: STRATEGY
- EquationsOperationsMultiply:
- descr:
- en: Multiply or divide an equation using the balance method
- stepType: STRATEGY
- scoring:
- finished: false
- marksEarned: 0
- marksTotal: 1
- ExerciseValidateRequestExample:
- value:
- exerciseId: e6cef2b5-b114-4931-ac68-b77289d7c0a3
- ExerciseValidateResponseExample:
- value:
- success: true
- valid: true
- marks: 1
- random: true
- interactions:
- Wp0Hr:
- type: MULTISTEP
- marks: 1
- ExerciseInfoRequestExample:
- value:
- id: fa42e943-8213-41a6-8a91-8c22a929ffe9
- ExerciseInfoResponseExample:
- value:
- commitHistory: []
- id: fa42e943-8213-41a6-8a91-8c22a929ffe9
- endpoint: https://algebrakit.eu
- publishedVersions:
- - name: A linear equation
- resources: []
- metadata:
- question-type: Multistep
- creator: Algebrakit
- contentVersion: '11.0'
- numberOfLevels: 0
- interactions:
- - block: Question a
- refId: lNzHy
- type: MULTISTEP
- refName: I1
- majorVersion: latest
- courseName: AlgebraKiT-demo
- type: regular exercise
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..f8b2ca6
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,13 @@
+
+
+
+
+ tests
+
+
+
diff --git a/src/Exceptions/AlgebrakitApiException.php b/src/Exceptions/AlgebrakitApiException.php
new file mode 100644
index 0000000..ddd59e6
--- /dev/null
+++ b/src/Exceptions/AlgebrakitApiException.php
@@ -0,0 +1,27 @@
+statusCode;
+ }
+}
diff --git a/src/Services/SessionService.php b/src/Services/SessionService.php
index dc84be0..2aa8379 100644
--- a/src/Services/SessionService.php
+++ b/src/Services/SessionService.php
@@ -2,226 +2,199 @@
namespace Algebrakit\SDK\Services;
-use GuzzleHttp\Client;
-use GuzzleHttp\Exception\GuzzleException;
+use Algebrakit\SDK\Exceptions\AlgebrakitApiException;
use Algebrakit\SDK\Models\Requests\CreateSessionRequest;
-use Algebrakit\SDK\Models\Requests\SessionScoreRequest;
+use Algebrakit\SDK\Models\Requests\ExerciseInfoRequest;
+use Algebrakit\SDK\Models\Requests\ExerciseValidateRequest;
use Algebrakit\SDK\Models\Requests\SessionInfoRequest;
-use Algebrakit\SDK\Models\Requests\SessionRetrieveRequest;
use Algebrakit\SDK\Models\Requests\SessionLockRequest;
-use Algebrakit\SDK\Models\Requests\ExerciseValidateRequest;
-use Algebrakit\SDK\Models\Requests\ExerciseInfoRequest;
+use Algebrakit\SDK\Models\Requests\SessionRetrieveRequest;
+use Algebrakit\SDK\Models\Requests\SessionScoreRequest;
use Algebrakit\SDK\Models\Responses\CreateSessionResponse;
-use Algebrakit\SDK\Models\Responses\SessionScoreResponse;
+use Algebrakit\SDK\Models\Responses\ExerciseInfoResponse;
+use Algebrakit\SDK\Models\Responses\ExerciseValidateResponse;
use Algebrakit\SDK\Models\Responses\SessionInfoResponse;
-use Algebrakit\SDK\Models\Responses\SessionRetrieveResponse;
use Algebrakit\SDK\Models\Responses\SessionLockResponse;
-use Algebrakit\SDK\Models\Responses\ExerciseValidateResponse;
-use Algebrakit\SDK\Models\Responses\ExerciseInfoResponse;
-use RuntimeException;
-
+use Algebrakit\SDK\Models\Responses\SessionRetrieveResponse;
+use Algebrakit\SDK\Models\Responses\SessionScoreResponse;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use GuzzleHttp\Exception\RequestException;
+use JsonSerializable;
+
+/**
+ * Client for the Algebrakit web service API.
+ *
+ * Example:
+ * $sessionService = new SessionService(getenv('ALGEBRAKIT_API_KEY'));
+ */
class SessionService
{
+ /** The Algebrakit API in the default region (Ireland). */
+ public const DEFAULT_BASE_URL = 'https://api.algebrakit.com';
+
private Client $httpClient;
private string $apiKey;
- public function __construct(string $apiKey, ?Client $httpClient = null, string $baseUrl = 'https://api.algebrakit.com')
- {
+ /**
+ * @param string $apiKey Your Algebrakit API key. Keep it on your server; never send it to the browser.
+ * @param Client|null $httpClient Your own Guzzle client, if you need one. It must send the API key itself.
+ * @param string $baseUrl Base URL of the Algebrakit API, for example to use another region.
+ * @param float $timeout Request timeout in seconds. Ignored if you pass your own $httpClient.
+ */
+ public function __construct(
+ string $apiKey,
+ ?Client $httpClient = null,
+ string $baseUrl = self::DEFAULT_BASE_URL,
+ float $timeout = 30.0
+ ) {
$this->apiKey = $apiKey;
$this->httpClient = $httpClient ?? new Client([
'base_uri' => $baseUrl,
+ 'timeout' => $timeout,
'headers' => [
'X-API-KEY' => $this->apiKey,
'Content-Type' => 'application/json',
- 'Accept' => 'application/json'
- ]
+ 'Accept' => 'application/json',
+ ],
]);
}
/**
* Creates new exercise sessions.
- *
- * @param CreateSessionRequest $request The request containing session details
- * @return array Array of CreateSessionResponse objects
- * @throws RuntimeException If the request fails
+ *
+ * @param CreateSessionRequest $request The exercises to create sessions for
+ * @return CreateSessionResponse[] For each exercise, whether it succeeded and the created sessions
+ * @throws AlgebrakitApiException If the request fails
*/
public function createSession(CreateSessionRequest $request): array
{
- try {
- $response = $this->httpClient->post('/session/create', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
- }
-
- $results = [];
- foreach ($data as $item) {
- $results[] = CreateSessionResponse::fromArray($item);
- }
-
- return $results;
- } catch (GuzzleException $e) {
- throw new RuntimeException('Failed to create session: ' . $e->getMessage(), 0, $e);
- }
+ return array_map(
+ fn (array $item) => CreateSessionResponse::fromArray($item),
+ $this->post('/session/create', $request)
+ );
}
/**
* Retrieves scores for a specific session.
- *
- * @param SessionScoreRequest $request The request containing session ID and lock option
+ *
+ * @param SessionScoreRequest $request The session ID, and whether to lock the session
* @return SessionScoreResponse The response containing session scores
- * @throws RuntimeException If the request fails
+ * @throws AlgebrakitApiException If the request fails
*/
public function getSessionScore(SessionScoreRequest $request): SessionScoreResponse
{
- try {
- $response = $this->httpClient->post('/session/score', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
- }
-
- return SessionScoreResponse::fromArray($data);
- } catch (GuzzleException $e) {
- throw new RuntimeException('Failed to get session score: ' . $e->getMessage(), 0, $e);
- }
+ return SessionScoreResponse::fromArray($this->post('/session/score', $request));
}
/**
* Locks or unlocks exercise sessions.
- *
- * @param SessionLockRequest $request The request containing session IDs and action
+ *
+ * @param SessionLockRequest $request The action ('LOCK', 'UNLOCK' or 'FINALIZE') and the session IDs
* @return SessionLockResponse The response for the lock or unlock action
- * @throws RuntimeException If the request fails
+ * @throws AlgebrakitApiException If the request fails
*/
public function lockOrUnlockSession(SessionLockRequest $request): SessionLockResponse
{
- try {
- $response = $this->httpClient->post('/session/lock', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
- }
-
- return SessionLockResponse::fromArray($data);
- } catch (GuzzleException $e) {
- throw new RuntimeException('Failed to lock/unlock session: ' . $e->getMessage(), 0, $e);
- }
+ return SessionLockResponse::fromArray($this->post('/sessions/lock', $request));
}
/**
* Retrieves information for a specific session.
- *
+ *
* @param SessionInfoRequest $request The request containing session ID
* @return SessionInfoResponse The response containing session information
- * @throws RuntimeException If the request fails
+ * @throws AlgebrakitApiException If the request fails
*/
public function getSessionInfo(SessionInfoRequest $request): SessionInfoResponse
{
- try {
- $response = $this->httpClient->post('/session/info', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
- }
-
- return SessionInfoResponse::fromArray($data);
- } catch (GuzzleException $e) {
- throw new RuntimeException('Failed to get session info: ' . $e->getMessage(), 0, $e);
- }
+ return SessionInfoResponse::fromArray($this->post('/session/info', $request));
}
/**
* Retrieves existing sessions by their IDs.
- *
+ *
* @param SessionRetrieveRequest $request The request containing session IDs
* @return SessionRetrieveResponse The response containing session data
- * @throws RuntimeException If the request fails
+ * @throws AlgebrakitApiException If the request fails
*/
public function retrieveSessions(SessionRetrieveRequest $request): SessionRetrieveResponse
{
- try {
- $response = $this->httpClient->post('/session/retrieve', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
- }
-
- return SessionRetrieveResponse::fromArray($data);
- } catch (GuzzleException $e) {
- throw new RuntimeException('Failed to retrieve sessions: ' . $e->getMessage(), 0, $e);
- }
+ return SessionRetrieveResponse::fromArray($this->post('/session/retrieve', $request));
}
/**
* Validates an exercise.
- *
- * @param ExerciseValidateRequest $request The request containing exercise details
+ *
+ * @param ExerciseValidateRequest $request The exercise ID or specification to validate
* @return ExerciseValidateResponse The response containing validation results
- * @throws RuntimeException If the request fails
+ * @throws AlgebrakitApiException If the request fails
*/
public function validateExercise(ExerciseValidateRequest $request): ExerciseValidateResponse
{
- try {
- $response = $this->httpClient->post('/exercise/validate', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
- }
-
- return ExerciseValidateResponse::fromArray($data);
- } catch (GuzzleException $e) {
- throw new RuntimeException('Failed to validate exercise: ' . $e->getMessage(), 0, $e);
- }
+ return ExerciseValidateResponse::fromArray($this->post('/exercise/validate', $request));
}
/**
* Retrieves published information about an exercise.
- *
+ *
* @param ExerciseInfoRequest $request The request containing exercise ID
* @return ExerciseInfoResponse The response containing exercise information
- * @throws RuntimeException If the request fails
+ * @throws AlgebrakitApiException If the request fails
*/
public function getExerciseInfo(ExerciseInfoRequest $request): ExerciseInfoResponse
+ {
+ return ExerciseInfoResponse::fromArray($this->post('/exercise/published-info', $request));
+ }
+
+ /**
+ * Sends a request to the API and returns the decoded JSON response.
+ *
+ * @throws AlgebrakitApiException If the request fails or the response is not valid JSON
+ */
+ private function post(string $path, JsonSerializable $request): array
{
try {
- $response = $this->httpClient->post('/exercise/published-info', [
- 'json' => $request->jsonSerialize()
- ]);
-
- $data = json_decode($response->getBody()->getContents(), true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
+ $response = $this->httpClient->post($path, ['json' => $request->jsonSerialize()]);
+ } catch (RequestException $e) {
+ if ($e->hasResponse()) {
+ $status = $e->getResponse()->getStatusCode();
+ $message = self::errorMessage((string) $e->getResponse()->getBody());
+ throw new AlgebrakitApiException("Algebrakit API error $status: $message", $status, $e);
}
-
- return ExerciseInfoResponse::fromArray($data);
+ throw new AlgebrakitApiException('Algebrakit API request failed: ' . $e->getMessage(), null, $e);
} catch (GuzzleException $e) {
- throw new RuntimeException('Failed to get exercise info: ' . $e->getMessage(), 0, $e);
+ throw new AlgebrakitApiException('Algebrakit API request failed: ' . $e->getMessage(), null, $e);
+ }
+
+ $status = $response->getStatusCode();
+ $body = (string) $response->getBody();
+ if ($status >= 400) {
+ // Only reached with a custom client that has 'http_errors' => false
+ throw new AlgebrakitApiException("Algebrakit API error $status: " . self::errorMessage($body), $status);
+ }
+
+ $data = json_decode($body, true);
+ if (!is_array($data)) {
+ throw new AlgebrakitApiException('Could not decode the Algebrakit API response: ' . json_last_error_msg(), $status);
+ }
+ return $data;
+ }
+
+ /**
+ * Extracts the error message from an API error response body,
+ * for example {"success":false,"error":"Could not validate credentials: ..."}.
+ */
+ private static function errorMessage(string $body): string
+ {
+ $data = json_decode($body, true);
+ if (is_array($data)) {
+ foreach (['error', 'msg', 'message'] as $key) {
+ if (isset($data[$key]) && is_string($data[$key])) {
+ return $data[$key];
+ }
+ }
}
+ return $body !== '' ? $body : 'no response body';
}
-}
\ No newline at end of file
+}
diff --git a/tests/SessionServiceTest.php b/tests/SessionServiceTest.php
new file mode 100644
index 0000000..4e4f5e7
--- /dev/null
+++ b/tests/SessionServiceTest.php
@@ -0,0 +1,150 @@
+history. */
+ private function service(Response|ConnectException ...$responses): SessionService
+ {
+ $stack = HandlerStack::create(new MockHandler($responses));
+ $stack->push(Middleware::history($this->history));
+ return new SessionService('test-key', new Client(['handler' => $stack, 'base_uri' => 'https://api.test']));
+ }
+
+ private static function json(mixed $body, int $status = 200): Response
+ {
+ return new Response($status, ['Content-Type' => 'application/json'], json_encode($body));
+ }
+
+ private static function httpClientOf(SessionService $service): Client
+ {
+ return (new ReflectionProperty(SessionService::class, 'httpClient'))->getValue($service);
+ }
+
+ public function testUsesTheProductionApiAndSendsTheKeyByDefault(): void
+ {
+ $client = self::httpClientOf(new SessionService('my-key'));
+
+ $this->assertSame('https://api.algebrakit.com', (string) $client->getConfig('base_uri'));
+ $this->assertSame('my-key', $client->getConfig('headers')['X-API-KEY']);
+ $this->assertSame(30.0, $client->getConfig('timeout'));
+ }
+
+ public function testBaseUrlAndTimeoutCanBeSetWithNamedArguments(): void
+ {
+ $client = self::httpClientOf(new SessionService('key', baseUrl: 'https://prod.api.sg-1.algebrakit.com', timeout: 5));
+
+ $this->assertSame('https://prod.api.sg-1.algebrakit.com', (string) $client->getConfig('base_uri'));
+ $this->assertSame(5.0, $client->getConfig('timeout'));
+ }
+
+ public function testCallsTheRightEndpointForEachMethod(): void
+ {
+ $service = $this->service(
+ self::json([['success' => true, 'sessions' => []]]),
+ self::json([]),
+ self::json([]),
+ self::json([]),
+ self::json([]),
+ self::json(['valid' => true]),
+ self::json([]),
+ );
+
+ $service->createSession(new CreateSessionRequest(exercises: [new ExerciseById('e1')]));
+ $service->getSessionScore(new SessionScoreRequest('s1'));
+ $service->lockOrUnlockSession(new SessionLockRequest('LOCK', ['s1']));
+ $service->getSessionInfo(new SessionInfoRequest('s1'));
+ $service->retrieveSessions(new SessionRetrieveRequest(['s1']));
+ $service->validateExercise(new ExerciseValidateRequest(exerciseId: 'e1'));
+ $service->getExerciseInfo(new ExerciseInfoRequest('e1'));
+
+ $this->assertSame(
+ [
+ 'POST /session/create',
+ 'POST /session/score',
+ 'POST /sessions/lock', // 1.0.0 used /session/lock, which doesn't exist
+ 'POST /session/info',
+ 'POST /session/retrieve',
+ 'POST /exercise/validate',
+ 'POST /exercise/published-info',
+ ],
+ array_map(fn (array $t) => $t['request']->getMethod() . ' ' . $t['request']->getUri()->getPath(), $this->history)
+ );
+ }
+
+ public function testCreateSessionSendsApiVersion2(): void
+ {
+ $this->service(self::json([['success' => true, 'sessions' => []]]))
+ ->createSession(new CreateSessionRequest(exercises: [new ExerciseById('e1')]));
+
+ $body = json_decode((string) $this->history[0]['request']->getBody(), true);
+ $this->assertSame(2, $body['apiVersion']);
+ $this->assertSame([['exerciseId' => 'e1', 'version' => 'latest']], $body['exercises']);
+ }
+
+ public function testErrorsIncludeTheApiMessageAndStatus(): void
+ {
+ $service = $this->service(self::json(
+ ['success' => false, 'error' => 'Could not validate credentials: the provided api-key is not valid'],
+ 401
+ ));
+
+ try {
+ $service->getSessionInfo(new SessionInfoRequest('s1'));
+ $this->fail('Expected an AlgebrakitApiException');
+ } catch (AlgebrakitApiException $e) {
+ $this->assertSame(
+ 'Algebrakit API error 401: Could not validate credentials: the provided api-key is not valid',
+ $e->getMessage()
+ );
+ $this->assertSame(401, $e->getStatusCode());
+ $this->assertInstanceOf(RuntimeException::class, $e); // existing catch (RuntimeException) blocks keep working
+ }
+ }
+
+ public function testNetworkErrorsHaveNoStatusCode(): void
+ {
+ $service = $this->service(new ConnectException('Connection refused', new Request('POST', '/session/info')));
+
+ try {
+ $service->getSessionInfo(new SessionInfoRequest('s1'));
+ $this->fail('Expected an AlgebrakitApiException');
+ } catch (AlgebrakitApiException $e) {
+ $this->assertSame('Algebrakit API request failed: Connection refused', $e->getMessage());
+ $this->assertNull($e->getStatusCode());
+ }
+ }
+
+ public function testInvalidJsonIsReported(): void
+ {
+ $this->expectException(AlgebrakitApiException::class);
+ $this->expectExceptionMessage('Could not decode the Algebrakit API response');
+
+ $this->service(new Response(200, [], 'not json'))->getSessionInfo(new SessionInfoRequest('s1'));
+ }
+}