Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Am Driver Laravel Package

application-manager-tools/am-driver

Symfony bundle and framework-agnostic PHP library for integrating managed applications with Application Manager. Supports orchestration commands, consumption webhooks, and operational state push, plus an OpenAPI 3.1.1 spec and integration docs for PHP 7.4+ and Symfony 5.4–8.0.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle with a framework-agnostic Core, not a Laravel package. There is no AmDriverServiceProvider, no config/am_driver.php to publish, and the shipped routes.yaml is Symfony routing. In Laravel you consume it through ApplicationManagerTools\AmDriver\Core\, which is exactly what the README calls the "Core-only (no bundle)" mode. Plan for roughly half a day of wiring, not ten minutes.

Read in this order (all docs are in French, budget for that):

  1. docs/INTEGRATION.md — the technical contract: endpoints, payloads, tokens.
  2. docs/openapi/ — the OpenAPI 3.1.1 spec is the authoritative source when the README and the code disagree.
  3. docs/ECARTS-AM.md — known divergences between bundle behaviour and the Application Manager side.
  4. changelog/0.0.*.md — mandatory, because this package renames core fields in patch releases.

Install and pin:

composer require application-manager-tools/am-driver:0.0.23

Pin the exact version. The project is pre-1.0, every release bumps the patch digit, and releases 0.0.9 / 0.0.14 / 0.0.16 / 0.0.17 / 0.0.19 / 0.0.22 all carry Breaking headings. ^0.0.23 happens to be safe (Composer's caret locks the patch when the major and minor are zero), but write the exact version so a future 0.1.0 cannot slide in.

Wire the container. You will bind around ten interfaces. Do it explicitly in a dedicated provider rather than relying on autoconfigure:

// app/Providers/AmDriverServiceProvider.php
public function register(): void
{
    $this->app->bind(CreateInstanceHandlerInterface::class, CreateInstanceHandler::class);
    $this->app->bind(StartInstanceHandlerInterface::class, StartInstanceHandler::class);
    $this->app->bind(StopInstanceHandlerInterface::class, StopInstanceHandler::class);
    $this->app->bind(QuotaExceededInstanceHandlerInterface::class, QuotaExceededInstanceHandler::class);
    $this->app->bind(GetInfoInstanceHandlerInterface::class, GetInfoInstanceHandler::class);
    $this->app->bind(SetStateViewInstanceHandlerInterface::class, SetStateViewInstanceHandler::class);
    $this->app->bind(UpgradeInstanceHandlerInterface::class, UpgradeInstanceHandler::class);
    $this->app->bind(DeferredCreateInstanceDispatcherInterface::class, QueueCreateDispatcher::class);
    $this->app->bind(DeferredUpgradeInstanceDispatcherInterface::class, QueueUpgradeDispatcher::class);
}

Declare the routes yourself — do not try to import the Symfony ones. AM calls fixed paths, so the URI segments must match the spec even though the routing layer is yours:

// routes/api.php
Route::middleware('am.token')->prefix('am')->group(function () {
    Route::post('orchestration/commands', [OrchestrationController::class, 'commands']);
    Route::post('instance-operational-state', [OperationalStateController::class, 'store']);
    Route::post('consumption-webhook', [ConsumptionController::class, 'store']);
});

First use case worth building: implement only CreateInstanceHandlerInterface, expose /am/orchestration/commands, and prove the loop locally with the bundled console tool before touching anything else:

vendor/bin/am-driver serve --port=8099 --token-command=dev-command-token --token-state=dev-state-token
vendor/bin/am-driver orchestration:simulate create --token=dev-command-token

The receptacle persists to /tmp/am-driver-receptacle and gives you a fake AM that POSTs a real OrchestrationCommand at your app. That single command is the fastest way to validate your DTO parsing, your idempotency guard and your callback payload without a reachable AM instance.

Implementation Patterns

Thin controllers, fat handlers. The bundle's whole point is that controllers are boilerplate and the host app owns product logic. Keep one invokable handler per orchestration operation:

final class CreateInstanceHandler implements CreateInstanceHandlerInterface
{
    public function __construct(private ProvisionTenant $provision) {}

    public function handle(OrchestrationCommand $command): CreateInstanceHandlerResult
    {
        $tenant = $this->provision->run(
            instanceId: $command->instanceId(),
            name: $command->name(),
            login: $command->credentialsLogin(),
            metadata: $command->metadata(),
        );

        return CreateInstanceHandlerResult::fromArray([
            'startedAt' => now()->toIso8601String(),   // required, non-empty
            'integrationInstanceId' => $tenant->id,   // required, non-empty
            'location' => $tenant->loginUrl,          // free-form, relayed verbatim
            'region' => $tenant->region,              // also relayed verbatim
        ]);
    }
}

startedAt and integrationInstanceId are validated by the bundle; everything else in that array is opaque and forwarded as-is in the callback body. That extra channel is your extension point for domain data AM should see.

Idempotency is yours to enforce. OrchestrationCommand carries an idempotencyKey, and AM will retry. Use a unique index, not a cache check:

DB::transaction(function () use ($command) {
    $receipt = AmReceipt::firstOrCreate(
        ['idempotency_key' => $command->idempotencyKey()],
        ['operation' => $command->operation(), 'instance_id' => $command->instanceId()],
    );
    if (! $receipt->wasRecentlyCreated) {
        return $receipt->response; // replay the stored callback body
    }
    // ... run the handler, persist $result->toArray() into $receipt->response
});

This is the single most important pattern. Provisioning a tenant twice because of an AM retry is the failure mode that hurts.

Deferred execution → Laravel queues. CREATE_INSTANCE and UPGRADE_INSTANCE can run asynchronously. Rather than reimplementing the dispatcher, implement DeferredCreateInstanceDispatcherInterface / DeferredUpgradeInstanceDispatcherInterface as thin wrappers that push a job:

final class QueueCreateDispatcher implements DeferredCreateInstanceDispatcherInterface
{
    public function dispatch(OrchestrationCommand $command): void
    {
        ProvisionInstanceJob::dispatch($command)->onQueue('am-provisioning');
    }
}

The mirror-image config key in Symfony is upgrade_instance_execution (sync/deferred, default deferred). In Core mode there is no config file — the choice is expressed by which dispatcher you bind. Give provisioning its own queue so a slow tenant build cannot starve your default worker.

Callbacks out. After the handler finishes, the callback to AM is an OrchestrationCallbackRequest. Since 0.0.16 its constructor takes an array $extra = [] fourth argument, and extra() replaces the old location()/startedAt() accessors. Do not pass location as a named parameter — the shape changed and your code will silently lose the field.

Pushing consumption from domain events. When a tenant's seat count or storage changes, push it rather than waiting for AM to poll:

Event::listen(SeatAssigned::class, function (SeatAssigned $e) use ($publisher) {
    $publisher->pushResourceConsumption($e->instanceId, 'seats', $e->count);
});

Note the argument is instanceId, not tenantId — see the gotchas.

Auth middleware. Since 0.0.14 there is a single token, sent as X-AM-Application-Token, both inbound and outbound. A one-file middleware covers all three routes:

public function handle(Request $request, Closure $next)
{
    $expected = (string) config('services.am.application_token');
    $given = (string) $request->header('X-AM-Application-Token');
    abort_unless($expected !== '' && hash_equals($expected, $given), 401);
    return $next($request);
}

Testing. Nothing here is Laravel-native, so test at the HTTP boundary: post a recorded OrchestrationCommand JSON at /api/am/orchestration/commands with the token header, assert 2xx, then assert your callback was attempted with the right body (Http::fake() on your client, or Http::assertSent). Keep a folder of captured AM payloads — they are your regression suite against the frequent breaking releases.

Gotchas and Tips

tenantId was renamed to instanceId in 0.0.9, everywhere. DTOs, snapshots, operational state, consumption, even local filenames (snapshots/{tenantId}.json → snapshots/{instanceId}.json). If you follow any pre-0.0.9 tutorial or blog snippet, every call will be wrong. findByTenantId() → findByInstanceId(), --tenant-id → --instance-id. The validator's guard is now expected_instance_id; expected_tenant_id is gone.

CreateInstanceHandlerResult is not a plain DTO anymore. Before 0.0.16 it was new CreateInstanceHandlerResult($location, $startedAt). Now it must be built with fromArray(), and both startedAt and integrationInstanceId must be non-empty strings or the bundle throws. ->toArray() is how you read it back; only startedAt() and integrationInstanceId() survive as getters.

Field validation is conditional on the operation. name, credentials and metadata are only legal on CREATE_INSTANCE; supplying them on START_INSTANCE is rejected. The legacy shortname key is rejected outright. If a test payload fails inexplicably, check for a stray shortname.

Do not construct OrchestrationCommandProcessor by hand. Its constructor argument list grew to at least seven handler parameters, with UpgradeInstanceHandlerInterface in the 7th position and QuotaExceededInstanceHandlerInterface in the 4th. If you new it up, every new operation shifts your arguments and you get a silent type error at runtime instead of boot. Let the container autowire it and bind only the interfaces.

Route paths are part of the contract. route_prefix: am in Symfony yields /am/orchestration/commands, /am/instance-operational-state, /am/consumption-webhook. AM has these configured on its side. If you change the prefix for "Laravel tidiness", inbound calls 404 and you will spend an hour reading the driver's logs for a problem in your routes/api.php.

The file-based helpers write to /tmp by default. FileStateViewWriter, FileTenantWorkspace, FileActualResourcesConsumptionReader and the receptacle stores are filesystem-backed. In a multi-node deployment or a container that restarts, state vanishes between requests and idempotency breaks. Either point them at storage_path('app/am') on shared storage, or — better — implement the store interfaces against your database and treat the file helpers as local-dev-only.

You will ship two HTTP clients. The Core classes pull symfony/http-client; Laravel ships Guzzle via the Http facade. That is dead weight but harmless — unless you also mock HTTP in tests. Http::fake() does not intercept Symfony's client. Either fake at the Guzzle handler level, or implement AmApiClientInterface yourself with Http:: and bind it, which is the cleaner path if you care about observability and retries anyway.

Auth configuration migration trap. 0.0.14 collapsed four token keys into application_token / AM_DRIVER_APPLICATION_TOKEN, but the four legacy keys are still accepted as aliases. If your env has both old and new set, you can end up validating against the wrong one and get 401s that look like the header name is wrong. Delete the legacy keys outright.

Token auth is the only security. There is no HMAC signature, no timestamp window, no replay protection on inbound webhooks beyond the idempotency key you implement. Terminate TLS in front of the route, and never expose these endpoints on a public hostname without the middleware above.

No retry or backoff on callbacks. If AM is down when you POST the callback, the bundle does not queue a retry. Wrap the callback in your own job with exponential backoff, and make the callback body idempotent on AM's side by including your idempotencyKey.

Consumption webhook field changed. ConsumptionWebhookEvent uses instanceId in JSON; ManagedInstanceResourceSnapshot likewise. Any DTO of yours hydrating these payloads needs the same rename.

GET_INFO_INSTANCE and SET_STATEVIEW_INSTANCE are no longer handled internally (0.0.17). If you are porting from an older integration, you must supply GetInfoInstanceHandlerInterface and SetStateViewInstanceHandlerInterface explicitly or the processor fails to build.

Upgrade sessions take three arguments. createSubscriptionUpgradeSession($instanceId, $returnUrl, $targetFormulaId) since 0.0.23 — the third parameter was added and is not optional. The target formula is an AM-side identifier (am_sfm_…) that you must obtain from AM, not invent.

Vendor risk is real. Zero stars, zero dependents, two contributors, and breaking changes in the majority of releases. Abstract it: define your own ProvisioningGateway interface in app/Contracts, implement it with the AM driver, and let application code depend on your interface. That gives you a one-file escape hatch if the package stalls, and it makes your handlers testable without booting the vendor code.

Local E2E is genuinely good — use it. vendor/bin/am-driver serve boots a fake AM on a port; orchestration:simulate create, state:push-sample and consumption:push --instance-id=… --resource-key=seats --value=12 --am-url=… --token=… cover all three flows. It runs on Symfony Console inside your Laravel vendor/bin, which is odd-looking but works fine and is far faster than standing up a real AM.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log