Read and publish training programs, catalogs, webhooks and accreditations in the EDU-DEX Data API from PHP: an endpoint class per API area, typed models with multi-language strings, validation before you submit, and exceptions per kind of failure. Framework-agnostic, with optional Silverstripe 5 integration that adds an EDU-DEX settings tab to the CMS.
Maintained by Restruct. If this module saves you time, you can support ongoing maintenance.
EDU-DEX is the open data standard for exchanging and publishing training course information. Its Data API is described by a large OpenAPI specification: bearer-token authentication, JSON bodies with localized strings, catalogs that are static or filter-based, and validation responses with error and warning messages per field. Talking to it directly means writing the HTTP calls, the JSON mapping and the error handling yourself.
This library wraps it: $client->suppliers()->getProgram(...) returns a Program model,
$client->validations()->validateProgram(...) returns a ValidationResult you can loop over, and
an HTTP error arrives as an AuthenticationException, ValidationException or ApiException.
- API coverage: organizations, suppliers, accreditors, programs and validations, following the bundled EDU-DEX OpenAPI specification
- Framework agnostic: works with any PHP 8.1+ project
- Optional Silverstripe integration: Config API, a SiteConfig extension and an Injector mapping
- Clean architecture: PSR-4 autoloading, typed models, endpoint classes
- Type safe: PHP 8.1+ with strict type hints and return types
- Smart models: automatic hydration, serialization and type casting
- Localization: built-in
LocalizedStringhelper for multi-language content - Validation: pre-submission validation for programs, institute metadata and discounts
- Secure: bearer token authentication with environment variable support
- Documented: PHPDoc blocks throughout and usage guides in
docs/
<?php
require 'vendor/autoload.php';
use Restruct\EduDex\Client;
// Initialize with bearer token
$client = new Client('your-jwt-bearer-token');
// Or use environment variable EDUDEX_API_TOKEN
$client = new Client();
// Fetch organizations
$organizations = $client->organizations()->list();
foreach ($organizations as $org) {
echo $org->getLocalizedName('nl') . "\n";
}The constructor throws an AuthenticationException when no token is passed, configured or set in
EDUDEX_API_TOKEN.
use Restruct\EduDex\Client;
// From array
$config = [
'bearer_token' => 'your-token',
'api_base_url' => 'https://api.edudex.nl/data/v1/',
'timeout' => 30,
];
$client = Client::fromConfig($config);
// From environment variable
// Set EDUDEX_API_TOKEN in your .env file
$client = new Client();The token is taken from the argument, then bearer_token in the config array, then
EDUDEX_API_TOKEN; the base URL from the argument, then api_base_url, then the default
https://api.edudex.nl/data/v1/. The timeout defaults to 30 seconds. The config array is also
passed to Guzzle, so other Guzzle client options (verify, proxy, connect_timeout, handler, ...)
work there too, except the package's own keys (bearer_token, api_base_url, debug, cache_ttl),
which never reach Guzzle. A headers key adds to the default headers; it cannot remove the
Authorization header or replace the bearer token. Pass a PSR-3 logger as the last
argument of new Client() or Client::fromConfig() to log every request and response (debug
level) and every failed request (error level).
use Restruct\EduDex\Client;
$client = new Client();
// List all organizations
$organizations = $client->organizations()->list();
// Get specific organization
$org = $client->organizations()->get('organization-id');
echo "Name: " . $org->getLocalizedName('nl') . "\n";
echo "Roles: " . implode(', ', $org->roles) . "\n";
echo "Supplier: " . ($org->isSupplier() ? 'Yes' : 'No') . "\n";
// Working with catalogs
$catalogs = $client->organizations()->listStaticCatalogs('org-id');
foreach ($catalogs as $catalog) {
echo "{$catalog->title}: {$catalog->countActive}/{$catalog->countTotal} programs\n";
}// List programs for a supplier
$programs = $client->suppliers()->listPrograms('supplier-id');
// Get specific program
$program = $client->suppliers()->getProgram('supplier-id', 'program-id', 'client-id');
echo "Title: " . $program->getTitle('nl') . "\n";
echo "Description: " . $program->getDescription('en') . "\n";
// Create or update program
$programData = [
'editor' => 'Admin User',
'format' => 'application/vnd.edudex.program+json',
'generator' => 'My CMS v1.0',
'lastEdited' => date('c'),
'programDescriptions' => [
'title' => ['nl' => 'Cursus Titel', 'en' => 'Course Title'],
],
// ... additional program data
];
$client->suppliers()->upsertProgram('supplier-id', 'program-id', 'client-id', $programData);upsertProgram() and deleteProgram() address suppliers/{orgUnitId}/programs/{programId}/{clientId};
pass 'public' as the client id for the public catalog. (Before 1.1.0 they dropped the client id and
called a path the spec defines for GET only, #1.)
The API answers a program PUT with a status object, not the program, so upsertProgram() returns
the program as submitted (ids plus your data). To get the status (ok or unchanged), its
validationMessages and lastChange, call upsertProgramWithStatus() with the same arguments. A
rejected program (HTTP 400 with validationMessages) throws a ValidationException.
getProgram() fetches the program through POST /programs/bulk, the only place the spec returns a
program's data, and throws an ApiException with the API's error if that program cannot be fetched.
listProgramClients() lists the clients a program is published for; listPrograms() filters on its
optional client id after fetching. (Before 1.3.0, getProgram() read the client list into an empty
Program, and listPrograms()/listDiscounts() always returned empty arrays,
#3.)
Checked against the spec only: these response shapes follow the bundled
openapi-edudex-spec.jsonand are covered by tests against it, not against the live API.
// Validate program before submission
$result = $client->validations()->validateProgram($programData);
if ($result->isValid()) {
echo "Validation passed\n";
// Submit the program
$client->suppliers()->upsertProgram(...);
} else {
echo "Validation failed:\n";
foreach ($result->getErrors() as $error) {
echo " - {$error->message}";
if ($error->contextPath) {
echo " (at {$error->contextPath})";
}
echo "\n";
}
}isValid() follows the API's validates verdict, and is otherwise true when there are no errors.
Messages come from the response's validationMessages: level becomes the severity, ruleName
the code, and forClient sits in context. Before 1.3.0 these were not read, so isValid() was
always true (#3).
Checked against the spec only: the response shape follows the bundled
openapi-edudex-spec.json, not a recorded live response.
validateInstitute() and validateDiscounts() work the same way. A ValidationResult also has
getWarnings(), getInfo(), getErrorCount()/getWarningCount() and
getErrorMessages()/getWarningMessages() (plain strings).
// Fetch multiple programs at once
$programsToFetch = [
['orgUnitId' => 'supplier-1', 'programId' => 'prog-1', 'clientId' => 'public'],
['orgUnitId' => 'supplier-2', 'programId' => 'prog-2', 'clientId' => 'client-1'],
];
$response = $client->programs()->bulk($programsToFetch);
// Process results
$successful = $client->programs()->getSuccessful($response);
$failed = $client->programs()->getFailed($response);
echo "Retrieved " . count($successful) . " programs\n";
echo "Failed: " . count($failed) . " programs\n";bulk() also takes $viewerOrgUnitId (sent as viewer; the API needs a special token for it) and
$viewDiscountsForOrgUnitId (sent as viewDiscountsFor, to apply that client's discounts). Before
1.1.0 neither was sent (#1).
The response lists every requested program under programs, each with data or an error.
getSuccessful() builds a Program from each item with data (the ids, the program fields, and the
whole data as programData); getFailed() returns the items with an error. Before 1.3.0 both
read keys the spec does not have and returned empty arrays
(#3).
Checked against the spec only: the bulk response shape follows the bundled
openapi-edudex-spec.json, not a recorded live response.
use Restruct\EduDex\Exceptions\AuthenticationException;
use Restruct\EduDex\Exceptions\ValidationException;
use Restruct\EduDex\Exceptions\ApiException;
use Restruct\EduDex\Exceptions\EduDexException;
try {
$org = $client->organizations()->get('org-id');
} catch (AuthenticationException $e) {
// Handle 401/403 errors
echo "Authentication error: {$e->getMessage()}\n";
} catch (ValidationException $e) {
// Handle validation errors with detailed messages
foreach ($e->getErrors() as $error) {
echo "Error: {$error['message']}\n";
}
} catch (ApiException $e) {
// Handle other API errors
echo "API error: {$e->getMessage()}\n";
} catch (EduDexException $e) {
// Catch-all
echo "Error: {$e->getMessage()}\n";
}A ValidationException is thrown for a 400 response that carries validation messages; its
getErrors() and getWarnings() return the API's message arrays. Other 4xx responses, 404, 408,
5xx and network errors (no response at all) become an ApiException. All of them extend EduDexException.
For Silverstripe projects, use the optional integration layer:
composer require restruct/edudex-api-client
composer require silverstripe/framework:^5.0The package ships its own _config/edudex.yml, so in a Silverstripe project two things are applied
automatically, without any project config:
SiteConfigExtensionis added toSiteConfig(an "EduDex" tab under Settings in the CMS; rundev/buildafter installing).- The Injector maps
Restruct\EduDex\ClienttoSilverStripeClient.
The EduDex tab is shown to administrators only. It holds an API base URL and a bearer token, and
shows a connection status. Opening the tab does not call the API; its "Test connection" link reloads
Settings with ?edudexcheck=1, which calls organizations()->list() once with the configured token,
or EDUDEX_API_TOKEN, and reports how many organizations it found or why it failed. The token and
base URL stored there are used by the extension's own getEduDexClient(), getEduDexToken() and
getEduDexBaseUrl() on SiteConfig. getEduDexClient() builds the client through the Injector, so
it is a SilverStripeClient and its timeout and debug config apply. SilverStripeClient itself
does not read the stored values: it takes its token from the constructor argument or
EDUDEX_API_TOKEN, and its base URL from the constructor argument or config.
Client settings are optional; override them in your own config, e.g. app/_config/edudex.yml:
---
Name: app-edudex-client
---
Restruct\EduDex\Integration\SilverStripe\SilverStripeClient:
api_base_url: 'https://api.edudex.nl/data/v1/'
timeout: 30SilverStripeClient also has a debug setting (default false). When it is on and no logger is
passed to the constructor, every request and response is logged at debug level to the Silverstripe
logger service (Psr\Log\LoggerInterface). It is never passed to Guzzle: before 1.2.0 it was, and
Guzzle's own debug option prints the whole transfer, bearer token included, into the output.
cache_ttl and getCacheTTL() are deprecated since 1.2.0 and will be removed in 2.0: the client has
never cached anything, so the setting had no effect.
To switch the CMS integration off, remove the extension in your project's app/_config.php. The
package adds it as an unnamed list entry, which YAML config cannot unset, so this has to be PHP:
use Restruct\EduDex\Integration\SilverStripe\Extensions\SiteConfigExtension;
use SilverStripe\SiteConfig\SiteConfig;
SiteConfig::remove_extension(SiteConfigExtension::class);use Restruct\EduDex\Integration\SilverStripe\SilverStripeClient;
// Using singleton
$client = SilverStripeClient::singleton();
// Or via Injector
$client = Injector::inst()->get(SilverStripeClient::class);
// Same API as standalone
$organizations = $client->organizations()->list();
// Or with the token and base URL stored in the CMS (null when no token is set)
$client = SiteConfig::current_site_config()->getEduDexClient();See docs/SILVERSTRIPE_INTEGRATION.md for complete documentation.
| Branch | Package version | PHP | Guzzle | Silverstripe integration |
|---|---|---|---|---|
main |
1.x |
8.1+ | 7 | Silverstripe 5 |
php74-ss37-backport |
0.0.x |
7.4 | 6 | Silverstripe 3.7 |
composer.json on each branch is the source of truth for exact constraints.
- PHP 8.1 or higher (PHP 7.4 version in the
php74-ss37-backportbranch) - GuzzleHTTP ^7.0
- PSR-3 Logger interface (
psr/log)
Optional:
- Silverstripe Framework ^5.0 (for Silverstripe integration;
SiteConfigExtensionextendsDataExtension, which Silverstripe 6 removed, so the integration layer is Silverstripe 5 only)
composer require restruct/edudex-api-client- Organizations (
$client->organizations()): organizations, static and dynamic catalogs, their programs and changes (listStaticCatalogPrograms(),getStaticCatalogChanges()and the dynamic counterparts), bulk add/remove of static catalog programs, dynamic catalog suppliers (addSupplierToDynamicCatalog()with aselectionofalloronlyincompanyordiscount,listDynamicCatalogSuppliers(),getDynamicCatalogSupplier()), webhooks.listCatalogs()is deprecated: the spec has no such path, so it now combines the static and dynamic lists. - Suppliers (
$client->suppliers()): suppliers, metadata, programs, discounts - Accreditors (
$client->accreditors()): accreditors and their accreditations - Programs (
$client->programs()): bulk program retrieval - Validations (
$client->validations()): validate programs, institute metadata and discounts
Organization: organization with roles and accreditationsSupplier: training providerAccreditor: accreditation organizationAccreditation: supplier accreditation with validityStaticCatalog: manual program catalogDynamicCatalog: automatic catalog with filtersWebhook: event notification endpointProgram: training program/courseValidationResult: validation response
LocalizedString: multi-language content handlerValidationMessage: validation message with severity
EduDexException: base exceptionAuthenticationException: 401/403 errors, and a missing tokenValidationException: 400 validation errorsApiException: other HTTP errors
- Standalone Usage Guide: complete guide for non-Silverstripe projects
- Silverstripe Integration: Silverstripe-specific setup and usage
- Implementation Details: technical implementation overview
- Working Examples: ready-to-run code examples
The PHPUnit suite in tests/ drives the real Client and GuzzleClient against a Guzzle
MockHandler, so it makes no network calls and needs no API token. Every request a test sends is
also checked against the bundled openapi-edudex-spec.json: the operation (method and path), its
query parameters and the top-level shape of the JSON body.
The suite has never been run against the live API. Request and response shapes are therefore confirmed against the bundled spec only; if the live API turns out to differ, please open an issue.
# Install dev dependencies
composer install
# Run the standalone suite
composer testtests/SilverStripe/ needs a booted Silverstripe 5 app and is excluded from the standalone run. CI
(.github/workflows/ci.yml) runs it in a throwaway Silverstripe host project, next to the standalone
suite on every PHP version composer.json allows.
The static analysis and code style scripts:
# Run static analysis
composer phpstan
# Check code style
composer cs-check
# Fix code style
composer cs-fix- Issues: https://github.com/restruct/edudex-api-client/issues
- EDU-DEX Data API Documentation: https://api.edudex.nl/data/v1/
- EDU-DEX Data API in Swagger: https://api.edudex.nl/data/v1/openapi/swagger-ui
- EDUDOX: https://www.edudex.nl/edudox/
- API Getting started: https://mijn.edudex.nl/api-getting-started/
This library is open-sourced software licensed under the MIT license.
The MIT license covers this library's own code. The bundled openapi-edudex-spec.json is the EDU-DEX Data API specification, published by EDU-DEX; it is included for reference and code generation only, is not covered by the MIT license, and no separate license for it is stated by EDU-DEX. EDU-DEX is an open data standard for exchanging and publishing training course information; see https://www.edudex.nl for its terms of use.
- Michael van Schaik (Restruct.nl): development
- Bart van Irsel (Webium): AI consultant/guru
- EDU-DEX API by WebHare BV