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

Dynamics Api Bundle Laravel Package

explicatis/dynamics-api-bundle

Symfony bundle for integrating the Dynamics 365 API into PHP applications. It provides the groundwork for connecting a Symfony app to Microsoft Dynamics 365 services, keeping API configuration and service wiring inside the Symfony ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. That single fact drives everything below: there is no service provider, no facade, no publishable config, and no config/dynamics.php. Composer will install it fine, but Laravel won't auto-discover or boot anything useful.

Minimal steps:

  1. composer require explicatis/dynamics-api-bundle
  2. Do not add it to config/app.php providers — there's nothing to register.
  3. Read src/DependencyInjection/Configuration.php and src/DependencyInjection/*Extension.php first. With a README that is one line long, the bundle's config tree is your only source of truth for required keys — expect something like tenant ID, client ID, client secret, and a resource/base URL for https://{org}.crm{region}.dynamics.com/api/data/v9.2.
  4. Identify the concrete client/service class the bundle exposes (usually a thin wrapper over Symfony HttpClient or a PSR-18 client). That class is what you actually want; the bundle wrapper around it is not.
  5. In Laravel, bind that client as a singleton in AppServiceProvider using values from config/services.php + .env:
$this->app->singleton(DynamicsClient::class, fn () => new DynamicsClient(
    tenantId: config('services.dynamics.tenant'),
    clientId: config('services.dynamics.client_id'),
    clientSecret: config('services.dynamics.client_secret'),
    baseUrl: config('services.dynamics.url'),
));

First use case: client-credentials auth against your tenant, then a read-only smoke test — GET /accounts?$select=name&$top=5. If that returns five account names, auth, base URL, and OData serialization are all working. Do this before writing a single line of domain code.

Implementation Patterns

Wrap it, don't leak it. The package is a transport detail. Put it behind your own repository so a future swap (or a vendor fork) doesn't ripple through the codebase:

interface AccountRepository {
    public function find(string $id): ?Account;
    public function modifiedSince(CarbonImmutable $since): Collection;
}

final class DynamicsAccountRepository implements AccountRepository
{
    public function __construct(private DynamicsClient $client) {}
}

Token caching is mandatory. Client-credentials tokens last ~60 minutes. If the bundle doesn't cache them (check — many thin wrappers don't), add a Cache::remember around the token fetch keyed on tenant+client, expiring at expires_in - 300. Re-authenticating per HTTP call will get you throttled fast.

Query building. Dynamics 365 exposes OData v4. Keep query construction in one place so encoding is consistent:

$query = [
    '$select' => 'accountid,name,modifiedon',
    '$filter' => "modifiedon gt 2024-01-01T00:00:00Z",
    '$orderby' => 'modifiedon asc',
    '$top' => 5000,
];

Use the HTTP client's query option to encode — never urlencode() a filter string yourself, or you'll double-encode $ and quotes.

Pagination. Responses cap at 5,000 rows and return @odata.nextLink. Loop until it's absent; treat it as an opaque absolute URL and pass it through unchanged.

Incremental sync. Keep a modifiedon watermark per entity in your own table. $filter=modifiedon gt {watermark} plus $orderby=modifiedon asc gives you a resumable pull. Overlap the window by a minute or two to survive clock skew, and de-duplicate on the entity ID.

Writes belong in jobs. Creates and updates are slow and rate-limited. Dispatch SyncAccountToDynamics jobs, and use the $batch endpoint for bulk — one HTTP round trip for up to ~100 operations instead of 100 throttled calls.

Hydrate into DTOs at the boundary. OData JSON uses logical names (accountid, _primarycontactid_value, new_customfield) and annotates lookups with @odata.bind. Map to your own value objects inside the repository so the rest of your app never sees _-prefixed keys.

Gotchas and Tips

The Symfony/Laravel impedance mismatch is the main risk. The bundle class itself implements Symfony\Component\HttpKernel\Bundle\Bundle and its extension consumes Symfony's DI config arrays. You can install it in Laravel and simply never boot the bundle — the non-bundle classes autoload normally. But if the client is constructed only via DI with array config, you'll be reverse-engineering the config tree to instantiate it by hand. Check this before committing to the package.

Transitive dependency conflicts. Symfony bundles commonly require symfony/http-client, symfony/options-resolver, or specific PSR-18 implementations, which can collide with Laravel's Guzzle version. Run composer why-not if install fails, and be prepared to pin or add symfony/http-client explicitly if the bundle expects it but doesn't require it.

Undocumented config keys. With a one-line README, Configuration.php is the contract. Read it, and write your own config mapping rather than guessing env var names.

OData sharp edges:

  • Logical names are case-sensitive and differ from display names — pull the $metadata document once and keep it handy.
  • Option sets are integers, not strings.
  • Setting a lookup on write requires "[email protected]": "/contacts(guid)", not a plain GUID.
  • Datetimes must be ISO 8601 with Z; naive Y-m-d H:i:s silently fails or shifts.

Throttling is real. Dynamics 365 returns 429 with Retry-After under service protection limits. Add retry middleware (3 attempts, honour Retry-After) on the underlying HTTP client — if the bundle doesn't expose its client, this alone may justify bypassing it.

Errors hide in the body. A failed call often returns a structured {"error": {"code": ..., "message": ...}}; don't rely on status codes alone. Log the full body and the x-ms-service-request-id response header — Microsoft support will ask for it.

Debugging. Install a logging middleware on the HTTP client and dump method, URL, and body for non-2xx responses in local/staging. Redact the Authorization header.

Extension points. Prefer decorating the client service over subclassing. If you must extend, the bundle's client class is the seam — but with zero stars and zero dependents, assume no upstream fixes are coming: pin the exact version in composer.json and be ready to fork into your own namespace if a Dynamics API version bump breaks it.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib