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.
}
2. Fluent Interface (Recommended)
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
| Parameter | Type | Required | Description |
|---|---|---|---|
$servicename | string | Yes | The service slug as registered in the dashboard. Case-sensitive. |
$endpoint | string | No | Path appended to the service's base URL. For AMQP: acts as the routing key. For SOAP: the method name. Default: / |
$payload | array | No | Data to send. Serialized to JSON for HTTP/AMQP, passed as parameters for SOAP. Default: [] |
$method | string | No | HTTP 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 key | When |
|---|---|
service_not_found | No service with the given name exists in the database |
service_disabled | The service exists but is marked as disabled |
circuit_open | The 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_responsewithis_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
| Property | Type | Description |
|---|---|---|
$success | bool | true if the request completed successfully (HTTP 2xx, or AMQP publish succeeded) |
$httpstatus | int|null | HTTP response code. null for AMQP (no HTTP response). |
$body | string|null | Raw response body as a string. null if no body was returned. |
$error | string|null | Human-readable error message. null on success. |
$latencyms | int | Total time from request start to response, in milliseconds. Includes all retry delays. |
$attempts | int | Total 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 astdClassobject
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.