Skip to main content

MIH API (Developer Guide)

The MIH API allows other Moodle plugins to dispatch events or make requests to registered services without worrying about the underlying transport, authentication, or resilience patterns.

Concept

Instead of using curl or guzzle directly, you use the mih facade. This ensures your request is logged, monitored, and retried automatically.

Usage

1. Static Request

The simplest way to make a request.

use local_integrationhub\mih;

try {
$response = mih::request('my-service-slug', '/api/users', ['id' => 123], 'POST');

if ($response->is_ok()) {
$data = $response->json();
// ...
}
} catch (\moodle_exception $e) {
// Handle service not found or circuit breaker open.
}

A more readable way to construct requests.

use local_integrationhub\mih;

$response = mih::send('my-service-slug')
->to('/api/users')
->with(['id' => 123])
->dispatch();

Overview

The mih facade is the single entry point for all outbound integrations. It is a facade — you always access it via mih::.

use local_integrationhub\mih;

$response = mih::request('service-name', '/endpoint', $payload, 'POST');

The MIH API handles everything internally:

  • Resolving the service configuration from the database
  • Checking the circuit breaker
  • Selecting the correct transport driver
  • Applying retry logic
  • Logging the request
  • Returning a consistent response object

The request() Method

public function request(
string $servicename,
string $endpoint = '/',
array $payload = [],
string $method = ''
): mih_response

Parameters

ParameterTypeRequiredDescription
$servicenamestringYesThe service slug as registered in the dashboard. Case-sensitive.
$endpointstringNoPath appended to the service's base URL. For AMQP: acts as the routing key. For SOAP: the method name. Default: /
$payloadarrayNoData to send. Serialized to JSON for HTTP/AMQP, passed as parameters for SOAP. Default: []
$methodstringNoHTTP method: GET, POST, PUT, PATCH, DELETE. Ignored for AMQP and SOAP. Default: POST

Return Value

Returns a \local_integrationhub\mih_response object. See the Response Object section below.

Exceptions

request() throws \moodle_exception in these cases:

Exception keyWhen
service_not_foundNo service with the given name exists in the database
service_disabledThe service exists but is marked as disabled
circuit_openThe circuit breaker is OPEN and the cooldown has not expired

Important: Network errors, timeouts, and HTTP error responses (4xx, 5xx) do not throw exceptions — they are returned as a failed mih_response with is_ok() === false. Only the three cases above throw.


The mih_response Object

class mih_response {
public bool $success;
public ?int $httpstatus;
public ?string $body;
public ?string $error;
public int $latencyms;
public int $attempts;

public function is_ok(): bool { ... }
public function json(bool $assoc = true): mixed { ... }
}

Properties

PropertyTypeDescription
$successbooltrue if the request completed successfully (HTTP 2xx, or AMQP publish succeeded)
$httpstatusint|nullHTTP response code. null for AMQP (no HTTP response).
$bodystring|nullRaw response body as a string. null if no body was returned.
$errorstring|nullHuman-readable error message. null on success.
$latencymsintTotal time from request start to response, in milliseconds. Includes all retry delays.
$attemptsintTotal number of attempts made (1 = no retries needed).

Methods

is_ok(): bool

Returns true if $success === true. Shorthand for checking the response status.

if ($response->is_ok()) {
// Handle success
}

json(bool $assoc = true): mixed

Decodes $body as JSON.

  • $assoc = true (default): returns an associative array
  • $assoc = false: returns a stdClass object

Returns null if $body is null or not valid JSON.

$data = $response->json();           // array
$obj = $response->json(false); // stdClass

Usage Examples

Basic POST Request

$response = \local_integrationhub\mih::request(
'my-api',
'/api/v1/events',
[
'type' => 'user.login',
'user_id' => $USER->id,
'time' => time(),
],
'POST'
);

if ($response->is_ok()) {
$result = $response->json();
// $result['status'] === 'accepted', etc.
} else {
debugging("Integration failed: {$response->error} (HTTP {$response->httpstatus})");
}

GET Request with No Payload

$response = \local_integrationhub\mih::request('my-api', '/api/v1/health', [], 'GET');

if ($response->is_ok()) {
$health = $response->json();
echo $health['status']; // 'ok'
}

PUT Request (Update)

$response = \local_integrationhub\mih::request(
'crm-service',
'/contacts/' . $userid,
['email' => $newemail, 'updated_at' => date('c')],
'PUT'
);

AMQP Publish

// The endpoint acts as the routing key
$response = \local_integrationhub\mih::request(
'rabbitmq-prod',
'events.course.completed',
[
'user_id' => $userid,
'course_id' => $courseid,
'grade' => $grade,
'timestamp' => time(),
]
// Method is ignored for AMQP
);

if ($response->is_ok()) {
// Message published to exchange with routing key 'events.course.completed'
}

SOAP Call

// The endpoint is the SOAP method name
$response = \local_integrationhub\mih::request(
'legacy-erp',
'SyncUser',
[
'UserId' => $userid,
'FullName' => fullname($user),
'Email' => $user->email,
]
);

Full Error Handling Pattern

function my_plugin_sync_user(int $userid): bool {
global $DB;

$user = $DB->get_record('user', ['id' => $userid], '*', MUST_EXIST);

try {
$response = \local_integrationhub\mih::request(
'user-sync-api',
'/users',
[
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'firstname' => $user->firstname,
'lastname' => $user->lastname,
'created' => $user->timecreated,
],
'POST'
);

if ($response->is_ok()) {
debugging(
"User {$userid} synced. Latency: {$response->latencyms}ms, Attempts: {$response->attempts}",
DEBUG_DEVELOPER
);
return true;
}

// HTTP error (4xx, 5xx) — log but don't crash
debugging(
"Sync failed for user {$userid}: HTTP {$response->httpstatus} — {$response->error}",
DEBUG_DEVELOPER
);
return false;

} catch (\moodle_exception $e) {
// Service not found, disabled, or circuit open
// Log but don't interrupt the user flow
debugging("Integration Hub error: " . $e->getMessage(), DEBUG_DEVELOPER);
return false;
}
}

Checking Latency and Attempts

$response = \local_integrationhub\mih::request('my-api', '/slow-endpoint', $data);

echo "Completed in {$response->latencyms}ms";
echo "Took {$response->attempts} attempt(s)";

if ($response->attempts > 1) {
debugging("Service required retries — check its health", DEBUG_DEVELOPER);
}

Accessing the Raw Body

$response = \local_integrationhub\mih::request('my-api', '/raw-data');

if ($response->is_ok()) {
$raw = $response->body; // string: '{"status":"ok","data":[...]}'
$decoded = $response->json(); // array: ['status' => 'ok', 'data' => [...]]
$obj = $response->json(false); // stdClass: $obj->status === 'ok'
}

Using MIH in Scheduled Tasks

The MIH API works inside Moodle scheduled and adhoc tasks:

class my_plugin_sync_task extends \core\task\scheduled_task {

public function get_name(): string {
return 'Sync users to external CRM';
}

public function execute(): void {
global $DB;

try {
// Get users to sync
$users = $DB->get_records('user', ['dirty' => 1]);

foreach ($users as $user) {
try {
// Call external service using MIH
$response = \local_integrationhub\mih::request(
'crm-sync',
'/sync/user',
['id' => $user->id, 'email' => $user->email]
);

if ($response->is_ok()) {
// Success logic
} else {
mtrace("Failed to sync user {$user->id}: {$response->error}");
}
} catch (\moodle_exception $e) {
mtrace("MIH error for user {$user->id}: " . $e->getMessage());
// Continue with next user
}
}
} catch (\Exception $e) {
// Handle any other unexpected exceptions during task execution
mtrace("An unexpected error occurred during user sync: " . $e->getMessage());
}
}
}

Service Name Conventions

By convention, service names should:

  • Be lowercase with hyphens: my-service-name
  • Be descriptive of the external system: salesforce-crm, slack-notifications, rabbitmq-prod
  • Not contain spaces or special characters

The name is used as the lookup key in mih::request(), so it must match exactly what is registered in the dashboard.


Thread Safety

The MIH singleton is safe to use within a single PHP request. Moodle's PHP execution model is single-threaded per request, so there are no concurrency concerns within a single request lifecycle.