Search by

blindaue / phpade

blindaue

PHP 8 ADE Web API Client

Package info

git.unistra.fr/e.blindauer/phpade

pkg:composer/blindaue/phpade

Statistics

Installs: 756

Dependents: 0

Suggesters: 0

1.5.2 2026-09-19 11:55 UTC

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: use detail=4 for base event data.
  • getEvents: use detail=7 for lifecycle fields such as creation and lastUpdate.
  • getEvents: use detail=8 for full payload with nested resources / additional.
  • getActivities: use detail=15 to include nested resources in activities.

Notes:

  • getEvents(detail>4) now auto-falls back to detail=4 when 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] Compare getEvents detail 4, 7 and 8, 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] Walk getActivities from detail 1 to 17 and 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 optional codeX.
  • php sample/events_by_email_and_codex_monthly.php <email> [projectId] [--codex=ILL] [--sessions] [--json] Monthly aggregation/reporting sample (--sessions also prints creation / 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.
  • getEvents fallback also handles degenerate high-detail payloads ([[], [], ...]) by retrying with detail=4, then overwriting the high-detail cache key with recovered data.
  • queryActivities now 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=2 is usually enough for path/category traversal.
  • detail=10 is useful when matching on fields like email.

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 4 or 10 if coverage is already good.
  • Escalation path: move to 15 only 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|network
  • fallbackReason=detail_ladder_to_10|detail_fallback_to_4|http_500|...
  • requestedDetail / detailUsed (for queryActivities)

5. Typical Efficient Sequence

  1. Resolve a resource (findResourceByField or findResourceByPathInCategory)
  2. Fetch activities with adaptive ladder (queryActivities)
  3. Fetch events for selected activity IDs (queryEvents)
  4. 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

MethodDescription
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.