blindaue / phpade
PHP 8 ADE Web API Client
Requires
- php: >=8.1
- ext-simplexml: *
- guzzlehttp/guzzle: ^7.8
- psr/simple-cache: ^3.0
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- phpunit/phpunit: ^9.6
- squizlabs/php_codesniffer: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP 8 client for ADE Web API from Adesoft.
This is an unofficial development. I am in no way related to this company. Use it at your own risk.
Installation
composer require blindaue/phpade
Configuration
Environment Variables
Create a .env.local file in your project root:
ADE_WEB_API_URL="https://ade.domain.tld/jsp/webapi"
ADE_WEB_API_LOGIN="your_login"
ADE_WEB_API_PASSWORD="your_password"
Or use environment variables directly:
export ADE_WEB_API_URL="https://ade.domain.tld/jsp/webapi"
export ADE_WEB_API_LOGIN="your_login"
export ADE_WEB_API_PASSWORD="your_password"
Standalone Usage
<?php
require_once 'vendor/autoload.php';
use Phpade\AdeWebAPI;
$api = new AdeWebAPI();
$api->connect();
$api->setProject(4);
$projects = $api->getProjects(['detail' => 4]);
$instructors = $api->getResources(['category' => 'instructor', 'detail' => 6]);
foreach ($instructors as $instructor) {
echo $instructor['name'] . ' - ' . ($instructor['email'] ?? 'no email') . "\n";
}
$api->disconnect();
Detail Levels (ADE Reader 10/07/2025)
Recommended levels for current ADE Reader docs:
getEvents: usedetail=4for base event data.getEvents: usedetail=7for lifecycle fields such ascreationandlastUpdate.getEvents: usedetail=8for full payload with nestedresources/additional.getActivities: usedetail=15to include nestedresourcesin activities.
Notes:
getEvents(detail>4)now auto-falls back todetail=4when ADE returns only empty<event/>nodes.- Flattened rows now keep nested children (
resources,additional,rights, etc.), including single-item responses.
Samples
php sample/events_detail_probe.php [activityId] [projectId]ComparegetEventsdetail4,7and8, print diagnostics and lifecycle/resource fields.php sample/events_cache_fallback_probe.php [activityId] [projectId]Simulate a degenerate high-detail cache payload and show automatic fallback/recovery.php sample/activities_detail_ladder_probe.php [codeX] [projectId]Demonstrate adaptive activity detail ladder (4 -> 10 -> 15) with required-field coverage.php sample/activities_incremental_detail.php [codeX] [projectId] [activityId]WalkgetActivitiesfrom detail1to17and print the fields added by each level.php sample/object_search_playbook.php [projectId] [codeX] [--email=...] [--path=...] [--category=trainee]End-to-end object-search playbook with diagnostics for resources and activities.php sample/events_by_email_and_codex.php <email> [codeX] [projectId]Fetch instructor events with optionalcodeX.php sample/events_by_email_and_codex_monthly.php <email> [projectId] [--codex=ILL] [--sessions] [--json]Monthly aggregation/reporting sample (--sessionsalso printscreation/lastUpdate).
Caching
The API supports file-based caching:
$api->setCacheDir('/tmp/ade_cache', 172800, 21600);
// 172800s = 48h for resources/activities
// 21600s = 6h for events
Or use any PSR-16 compatible cache:
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
$psr6Pool = new FilesystemAdapter('ade_api', 0, '/tmp/ade_cache');
$psr16Cache = new Psr16Cache($psr6Pool);
$api->setCache($psr16Cache);
Cache Policy, Fallback and Diagnostics
Lazy ADE sessions and data-cache renewal
useProject() selects the logical project used for cache keys before calling
your callback. It opens an ADE session and selects the project on the server
only when an operation needs the network. Fresh cached results, including empty
days, therefore require no ADE requests. listProjectsNormalizedSafe() also
connects only on a cache miss.
Sessions are not persisted in the data cache. A session opened by the outermost
useProject() call is closed when the callback finishes, even on failure.
An already connected caller keeps ownership of its session; nested project
contexts restore the previous project. Code that needs to retain a connection
must call connect() explicitly and later call disconnect().
When ADE explicitly reports an invalid/expired session, phpade reconnects,
selects the project again and retries the failed request once, before considering
stale-cache fallback. The callback itself is never replayed. A generic 401, 403
or 429 without a session-error indication does not trigger this reconnect.
If renewal fails, the existing stale policy applies, without extending the old
entry's expiration. Use FileCache for stale fallback: generic PSR-16 caches do
not expose expired entries.
With a persistent cache configured, its expiration and invalidation are checked on every read, even if the same API instance already loaded the entry in memory. Data keys depend on the project and query parameters, not on the automatically injected session ID.
You can configure stale-cache fallback globally:
$api->setCachePolicy([
'stale_on_error' => true,
'stale_max_age_seconds' => 7 * 24 * 3600, // optional
]);
And override policy per call:
$resources = $api->getResources(
['category' => 'instructor', 'detail' => 6],
true,
['stale_on_error' => false]
);
This also applies to tree=true lookups and hierarchy helpers:
$nodes = $api->useProject(4, fn (AdeWebAPI $api) => $api->getHierarchyFromPath(
'Iut Illkirch.CHIMIE',
[
'resourceCategory' => 'trainee',
'leafCategory' => 'trainee',
'cachePolicy' => [
'stale_on_error' => true,
'stale_max_age_seconds' => 604800,
],
]
));
Fallback behavior details:
- HTTP client and server errors (4xx and 5xx), XML parsing failures, timeouts, and network/transport errors are treated as fallbackable when
stale_on_error=true. findResourceByPath*first tries stale cache with the exact segment query key (name,fatherIds,detail,category), then falls back to broader cached category snapshots.getEventsfallback also handles degenerate high-detail payloads ([[], [], ...]) by retrying withdetail=4, then overwriting the high-detail cache key with recovered data.queryActivitiesnow supports adaptive detail ladders to limit cost and escalate only when needed:
$activities = $api->queryActivities(
['codeX' => 'ILL'],
15,
[
'prefer_lower_detail' => true, // try 4 then 10 before 15
'required_fields' => ['code', 'CODE'], // escalate if missing
'min_required_fields_coverage' => 0.85, // expected row coverage
// optional: 'detail_ladder' => [4, 10, 15], // explicit order
]
);
After a call, diagnostics are available:
$diagnostic = $api->getLastCallDiagnostic();
// [
// 'source' => 'network' | 'cache_fresh' | 'cache_stale',
// 'fallbackReason' => 'http_4xx' | 'http_500' | 'http_5xx' | 'timeout' | 'parse_error' | ...
// 'cache' => ['key' => ..., 'expiresAt' => ..., 'isExpired' => ...]
// ]
Cache metadata can include expired entries:
$meta = $api->getCacheEntryMetadata(
'getResources',
['category' => 'instructor', 'detail' => 6],
true // includeExpired
);
Efficient Object Search
When ADE datasets are large, performance mainly depends on query order and detail levels. Use this playbook to keep requests fast and only pay for high-detail payloads when required.
1. Start With Narrow Resource Lookups
Prefer precise lookups before broad lists:
$instructor = $api->findResourceByField('instructor', 'email', $email, 10);
$group = $api->findResourceByPathInCategory('Iut Illkirch.CHIMIE', 'trainee', 2);
detail=2is usually enough for path/category traversal.detail=10is useful when matching on fields likeemail.
2. Use Adaptive Detail For Activities
Activities can be expensive at high detail. Use the ladder:
$activities = $api->queryActivities(
['codeX' => 'ILL'],
15,
[
'prefer_lower_detail' => true,
'detail_ladder' => [4, 10, 15],
'required_fields' => ['code', 'CODE'],
'min_required_fields_coverage' => 0.85,
]
);
- Fast path: keep detail
4or10if coverage is already good. - Escalation path: move to
15only if required fields are missing.
3. Query Events Only For Selected Activities
Build event queries from resolved activity IDs (not broad time windows when avoidable):
$eventRows = $api->queryEvents([
'activities' => implode('|', $activityIds),
], 7);
Use detail=8 only if you also need nested children (resources, additional).
getEvents already contains native fallback for degenerate high-detail payloads.
4. Keep Cache And Diagnostics On
$api->setCacheDir('/tmp/ade_cache', 172800, 21600);
$api->setCachePolicy([
'stale_on_error' => true,
'stale_max_age_seconds' => 7 * 24 * 3600,
]);
After each step, inspect diagnostics:
$diagnostic = $api->getLastCallDiagnostic();
Useful indicators:
source=cache_fresh|cache_stale|networkfallbackReason=detail_ladder_to_10|detail_fallback_to_4|http_500|...requestedDetail/detailUsed(forqueryActivities)
5. Typical Efficient Sequence
- Resolve a resource (
findResourceByFieldorfindResourceByPathInCategory) - Fetch activities with adaptive ladder (
queryActivities) - Fetch events for selected activity IDs (
queryEvents) - Check diagnostics and tune ladder thresholds
Symfony Integration
1. Install
composer require blindaue/phpade
2. Configure (.env)
ADE_WEB_API_URL=https://ade.domain.tld/jsp/webapi
ADE_WEB_API_LOGIN=your_login
ADE_WEB_API_PASSWORD=your_password
3. Register Services
config/services.yaml:
services:
Phpade\Config:
public: true
ade.cache.pool:
class: Symfony\Component\Cache\Adapter\FilesystemAdapter
arguments:
- 'ade_api'
- 0
- '%kernel.cache_dir%/ade_api'
ade.cache.psr16:
class: Symfony\Component\Cache\Psr16Cache
arguments:
- '@ade.cache.pool'
Phpade\AdeWebAPI:
arguments:
$config: '@Phpade\Config'
calls:
- method: setCache
arguments:
- '@ade.cache.psr16'
Or with a framework cache pool:
framework:
cache:
pools:
cache.app.ade_api:
adapter: cache.adapter.filesystem
default_lifetime: 86400
services:
Phpade\Config:
public: true
ade.cache.psr16:
class: Symfony\Component\Cache\Psr16Cache
arguments:
- '@cache.app.ade_api'
Phpade\AdeWebAPI:
arguments:
$config: '@Phpade\Config'
calls:
- method: setCache
arguments:
- '@ade.cache.psr16'
4. Usage in a Controller
<?php
namespace App\Controller;
use Phpade\AdeWebAPI;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ScheduleController extends AbstractController
{
#[Route('/instructors', name: 'instructors')]
public function listInstructors(AdeWebAPI $api): Response
{
$api->connect();
$api->setProject(4);
$instructors = $api->getResources([
'category' => 'instructor',
'detail' => 6
]);
$api->disconnect();
return $this->render('instructors.html.twig', [
'instructors' => $instructors
]);
}
}
API Reference
See adesoft_webapi_doc.txt for complete API documentation.
Main Methods
| Method | Description |
|---|---|
connect() | Connect to the API |
disconnect() | Disconnect from the API |
setCachePolicy([...]) | Configure global cache fallback policy |
getLastCallDiagnostic() | Get last call source/fallback/cache diagnostic |
getCacheEntryMetadata($function, $params, $includeExpired = false) | Get cache metadata (optionally including expired entry) |
getProjects(['detail' => 4], $useCache = true, $cachePolicy = []) | Get available projects |
setProject($projectId) | Set the current project |
getResources([...], $useCache = true, $cachePolicy = []) | Get resources (instructors, rooms, etc.) |
findResourceByPath(..., $cachePolicy = []) | Resolve a path with native stale-cache fallback |
getActivities([...], $useCache = true, $cachePolicy = []) | Get activities |
getEvents([...], $useCache = true, $cachePolicy = []) | Get events (includes nested child nodes such as resources) |
getEventsByInstructorEmail($email, $codeX = null, $eventDetail = 4) | High-level lookup by instructor email with configurable event detail |
getHierarchyFromPath($path, ['cachePolicy' => ...]) | High-level hierarchy lookup with fallback policy support |
queryActivities($filters, $detail, $options) | Adaptive activity detail ladder (prefer_lower_detail, required_fields, detail_ladder) |
queryEvents($filters, $detail = 4) | Events query helper (use detail=7 for creation/lastUpdate) |
Error Handling
Transport exceptions are enriched with:
getHttpStatus()getAdeFunction()getFailureReason()(for example:http_4xx,http_500,http_5xx,timeout,parse_error,transport_error)
Testing
vendor/bin/phpunit tests/php/
vendor/bin/phpcs --standard=PSR12 src/
Integration test is opt-in:
ADE_RUN_INTEGRATION_TESTS=1 ADE_TEST_PROJECT_ID=4 vendor/bin/phpunit tests/php/AdeWebAPITest.php
Gestion des erreurs Java ADE
Les lectures getEvents, getActivities, getResources et getProjects retentent
les réponses XML contenant java.lang.NullPointerException trois fois au maximum
(délais de 250 puis 500 ms). Cette règle couvre aussi les réponses HTTP 5xx avec
un corps XML ADE. Les erreurs HTTP 4xx restent des erreurs client ; une erreur
de session reconnue conserve son mécanisme de reconnexion distinct.
Après épuisement des tentatives, la politique stale_on_error permet un repli sur
le cache expiré, sous réserve de stale_max_age_seconds. Ce repli ne prolonge pas
la validité du cache. getLastCallDiagnostic() indique alors source=cache_stale
et fallbackReason=ade_server_error. Sans cache admissible, l'exception remonte :
une NullPointerException ne déclenche pas de découpage ni d'omission d'activités.
AdeWebAPIException::getSummary() fournit la première ligne pour l'affichage ;
getMessage() conserve la trace technique complète et son comportement existant.
getHttpStatus() n'interprète plus les numéros de ligne Java comme des codes HTTP.
Journal d'appels et cadence des requêtes
AdeWebAPI::setApiCallLogFile() active un journal JSON Lines. Chaque ligne décrit
une réponse réseau (fonction ADE, projet, succès ou erreur, statut HTTP, durée),
et les diagnostics ajoutent une ligne pour chaque succès du cache ou repli sur
cache périmé. Les paramètres de requête, identifiants de session et identifiants
utilisateur ne sont pas écrits dans ce journal.
Les messages HTTP bruts ne sont pas journalisés : ils peuvent contenir l’URL
complète et ses paramètres sensibles. Le journal conserve la fonction, le statut
HTTP et la catégorie d’erreur (failureReason).
Si le découpage d’un lot queryEvents() laisse des activités en échec,
IncompleteEventsException est levée au lieu de retourner des événements partiels
ou une liste vide trompeuse. getActivityIds() fournit les activités concernées.
Le journal signale outcome=incomplete_result avec le nombre d’activités en échec.
AdeWebAPI::setRequestDelayMs() règle la pause minimale entre les départs de
requêtes HTTP successives. Sa valeur par défaut est 10 ms ; les applications
peuvent choisir leur cadence. Les lectures servies depuis le cache ne sont pas
retardées.