restruct / edudex-api-client
PHP client for the EDU-DEX Data API: endpoint classes, typed models, validation and exceptions, framework-agnostic with optional Silverstripe 5 integration
Fund package maintenance!
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- psr/log: ^1.0|^2.0|^3.0
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5|^11.0
- squizlabs/php_codesniffer: ^3.7
Suggests
- monolog/monolog: For enhanced logging support
- silverstripe/framework: For SilverStripe integration (^5.0)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-02 13:17:20 UTC
README
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.
The EDU-DEX Data API is an OpenAPI spec, not a PHP library
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.
Programs, catalogs and validation from PHP
- 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/
Quick start (any PHP project)
<?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.
Configuration
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).
Working with organizations
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"; }
Managing programs
// 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.
Validating data
// 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).
Bulk operations
// 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.
Error handling
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.
Silverstripe integration
For Silverstripe projects, use the optional integration layer:
composer require restruct/edudex-api-client composer require silverstripe/framework:^5.0
Configuration
The 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: 30
SilverStripeClient 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);
Usage in Silverstripe
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.
Technical
Version compatibility
| 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.
Requirements
- 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)
Installation
composer require restruct/edudex-api-client
API reference
Endpoints
- 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
Models
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
Type helpers
LocalizedString: multi-language content handlerValidationMessage: validation message with severity
Exceptions
EduDexException: base exceptionAuthenticationException: 401/403 errors, and a missing tokenValidationException: 400 validation errorsApiException: other HTTP errors
Documentation
- 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
Testing
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 test
tests/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
Support
- 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/
License
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.
Credits
- Michael van Schaik (Restruct.nl): development
- Bart van Irsel (Webium): AI consultant/guru
- EDU-DEX API by WebHare BV