mpp/apicil-client-bundle
MPP Apicil Client Bundle integrates the Apicil API client into Symfony applications, offering service configuration, dependency injection, and a convenient setup for connecting PHP projects to Apicil services.
First, a reality check on stack fit. mpp/apicil-client-bundle is a Symfony bundle, not a Laravel package. It ships a *Bundle class, a DependencyInjection/Configuration.php tree, and service definitions wired through Symfony's container. Laravel has no equivalent of bundle registration, so nothing auto-discovers, no config tree is merged, and none of the service wiring happens for you. You are effectively consuming the underlying PHP classes directly, and re-doing the glue yourself.
Update since the last review: the package is on v3.3.2, not 1.0.0. The previous assessment's "single release, unpublished hobby project" framing is stale — there is a real release history behind it, and v3.3.2 is a patch-level bugfix to a write operation ("client to create an arbitrage", PR #51). That tells you two useful things up front: the library covers mutating calls (fund arbitrage / allocation switches on a contract), not just reads, and the maintainers are shipping targeted fixes to them. It also tells you the wire behaviour of write endpoints has changed inside a patch bump, which is worth pinning for.
That reframing should still drive your decision:
src/Client, src/Api, or a composer.json require on a sibling *-client package), depend on that, not the bundle.composer require it and instantiate classes manually — you just lose the config/DI niceties and inherit a Symfony-flavored dependency graph.Minimal steps (Laravel side):
composer require mpp/apicil-client-bundle:3.3.2
Pin exactly. Do not use ^3.3 while you are still learning the client — v3.3.2 itself demonstrates that a patch release can change how a write request is constructed.
Then immediately read, in this order:
README.md — the only place the intended entry point is likely documented.src/DependencyInjection/Configuration.php — ground truth for every config key the package expects (base URL, credentials, timeouts). Mirror these names into config/services.php so your mental model matches upstream docs.src/DependencyInjection/*Extension.php and Resources/config/services.* — shows which class is the public entry service and what it depends on. That dependency list is your construction recipe.src/ root — identify whether the client takes a PSR-18 ClientInterface, a Symfony HttpClientInterface, or builds its own transport. This determines how much you can fake in tests.tests/ — your best usage documentation.First use case: don't build a feature. Build a throwaway php artisan tinker session or an artisan command that authenticates and performs one read call (fetch a single contract/subscriber/reference record). Confirm you get a real response before designing anything. Getting the auth handshake and base URL correct is 80% of the integration risk here.
Second use case, before you ship anything: exercise an arbitrage creation against the sandbox, using a throwaway contract, and diff the outbound payload against the v3.3.2 code path. Write calls are where this package has had recent defects, and they are the calls you cannot safely retry.
Sketch of the Laravel glue:
// config/services.php
'apicil' => [
'base_uri' => env('APICIL_BASE_URI'),
'client_id' => env('APICIL_CLIENT_ID'),
'client_secret' => env('APICIL_CLIENT_SECRET'),
'timeout' => env('APICIL_TIMEOUT', 10),
],
// app/Providers/ApicilServiceProvider.php
$this->app->singleton(ApicilClient::class, function ($app) {
$c = $app['config']['services.apicil'];
// construct exactly as the bundle's services.yaml does
return new ApicilClient(/* ... */);
});
Note: class names and method signatures still can't be confirmed from release metadata alone — treat the above as the shape of the wiring and confirm names against services.yaml and the PR #51 diff before writing it.
Wrap it, always. Never let the vendor client leak into controllers, jobs, or domain services. Define a narrow interface representing the business operations you actually need — now explicitly including the mutating ones — and adapt:
interface ApicilGateway
{
public function findContract(string $reference): ?ContractData;
public function createArbitrage(ArbitrageRequest $request): ArbitrageResult;
}
final class BundleApicilAdapter implements ApicilGateway { /* delegates */ }
The adapter is what makes a future patch, fork, or swap a one-file change. Given that v3.3.2 changed write-request construction, that insurance is now demonstrably worth having.
Bind as a singleton, transport included. HTTP clients should be long-lived so connection reuse works. Register the PSR-18/Symfony client as a singleton and inject it rather than constructing one per call. If the bundle's client creates its own transport internally and you can't inject one, that's a real limitation — note it early, because it blocks Http::fake()-style testing.
Push all calls behind queued jobs — but split reads from writes. Reads can be retried freely and batched. Arbitrage creation cannot: treat it as a non-idempotent side effect.
$tries, backoff(), aggressive caching.arbitrage_requests row with your own correlation key), and reconcile on failure rather than blind-retrying. If the API supports a client-supplied idempotency key, use it; if not, assume a timeout means "unknown state" and verify with a read before re-submitting.Cache reference data aggressively. Country codes, product/plan lists, contract typologies, eligible support/fund lists for arbitrage — anything read-mostly. Cache::remember() with a long TTL is the single highest-leverage pattern for this kind of API. It cuts latency, dodges rate limits, and reduces blast radius when the API is down.
Log at the transport layer, not the call site. If the client accepts a middleware/handler stack (Symfony HttpClient supports on_progress and middleware; Guzzle-style clients accept a handler stack), attach request/response logging there once. Essential for arbitrage, where the payload is the thing that breaks.
Test with fakes, not the network. If the client accepts an injected HTTP client, use Laravel's Http::fake() (works when the transport is Guzzle/PSR-18) or the package's own mock. If it doesn't accept injection, isolate the adapter behind your interface and fake that. Add a contract test — a single opt-in test, tagged and excluded from CI default, that hits the sandbox and creates one arbitrage — so you find out about the next PR #51-shaped regression before production does.
Normalize errors at the boundary. Translate to your own exception hierarchy (ApicilUnavailable, ApicilRejected, ApicilNotFound) so calling code branches on semantics rather than scattered HTTP codes. For arbitrage specifically, distinguish rejected (safe to fix and resubmit) from unknown (must reconcile first).
The bundle/container mismatch will bite you repeatedly. No bundle auto-registration means no config validation, no default values, no tagged services, no compiler passes. Any behavior documented as "configure mpp_apicil_client:" must be translated by hand into config/services.php. If the docs only speak in Symfony terms, allocate real time for translation rather than assuming a quick port.
Maturity: better than it looked, still not community-vetted. Correct the previous read — this is a maintained project with a multi-year release line at 3.3.x, published under the mon-petit-placement org, i.e. by the consuming platform itself rather than an anonymous third party. That's the good news: upstream has an incentive to fix things. The caution that remains:
"mpp/apicil-client-bundle": "3.3.2", no caret). Patch releases have changed write-request construction; a composer update should not be able to alter an arbitrage payload without you noticing.Credentials and token lifecycle. Expect OAuth-style client credentials with expiring tokens. If the bundle handles refresh internally, verify it under concurrency (two queue workers refreshing simultaneously is a classic thundering-herd bug). If it doesn't, wrap refresh in Cache::lock().
Sandbox vs production URLs. Two different base URIs, certainly. Enforce the difference through environment variables only — never a hardcoded fallback base URI, which is how staging calls end up switching allocations on real contracts.
Timeouts and retries are not optional. Set a short connect timeout and a sensible total timeout explicitly; library defaults are frequently "infinite," turning one slow endpoint into a wedged queue worker. This matters more for arbitrage than for reads — a hung write leaves you in exactly the unknown-state limbo described above.
GDPR and PII. Insurance and wealth-management data: names, contract numbers, possibly IBANs, and now allocation/arbitrage instructions that constitute financial decisions. Do not let debug-level HTTP logging dump full request bodies into your log stack. Scrub at the middleware level, and set short retention on any payload capture you enable for incident debugging.
Extension points to prefer if you must stay on the bundle: decorate the client service (same interface, extra behavior) rather than editing vendor code; inject a custom HTTP client if the constructor allows; hook Symfony's event dispatcher if the bundle dispatches events. In Laravel, that means binding your wrapper over the singleton — same idea, done by hand.
Debugging checklist when a call fails: confirm the base URI has no trailing-slash double-up; dump the actual outbound request path and headers once (PII scrubbed); check whether the client swallows non-2xx responses into null rather than throwing; verify content-type negotiation (Accept/Content-Type mismatches produce confusing 400s); confirm the token isn't expired-but-cached; and for arbitrage, verify the payload against the v3.3.2 code path before assuming the remote API is at fault — recent history says the client is the likelier culprit.
How can I help you explore Laravel packages today?