justbetter / laravel-dynamics-client
A client to connect with Microsoft Dynamics
Package info
github.com/justbetter/laravel-dynamics-client
Type:package
pkg:composer/justbetter/laravel-dynamics-client
Requires
- php: ^8.4
- guzzlehttp/guzzle: ^7.5
- laravel/framework: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.29
- orchestra/testbench: ^10.0|^11.0
- phpstan/phpstan-mockery: ^2.0
- phpunit/phpunit: ^12.0|^13.0
- rector/rector: ^2.4
README
Laravel Dynamics Client
This package connects your Laravel application to the Microsoft Dynamics 365 Business Central API. It authenticates with OAuth client credentials and uses the HTTP client of Laravel, which means responses, retries and fakes work exactly as you already know them.
use JustBetter\DynamicsClient\Client\Dynamics; use JustBetter\DynamicsClient\Data\Entity; use JustBetter\DynamicsClient\Query\QueryBuilder; $dynamics = app(Dynamics::class); $customers = $dynamics->entities('customers', QueryBuilder::make() ->where('city', 'Alkmaar') ->orderBy('displayName') ->get()); $customer = $customers->first(); $customer->displayName = 'John Doe'; $dynamics->entity($customer)->update();
Important
Upgrading from 1.x? See UPGRADING.
Requirements
- PHP 8.4 or higher
- Laravel 12.0 or 13.0
Installation
Install the composer package.
composer require justbetter/laravel-dynamics-client
Publish the configuration of the package.
php artisan vendor:publish --provider="JustBetter\DynamicsClient\ServiceProvider" --tag=config
Configuration
Add your Dynamics credentials to your .env:
DYNAMICS_TENANT_ID= DYNAMICS_ENVIRONMENT= DYNAMICS_COMPANY_ID= DYNAMICS_OAUTH_CLIENT_ID= DYNAMICS_OAUTH_CLIENT_SECRET=
OAuth
When using D365 cloud with Microsoft identity platform your redirect uri will be: https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token
and your base url should be https://api.businesscentral.dynamics.com/v2.0/<tenant>/<environment>.
URL templates
Both the API URL and the token URL are templates. Every {key} is replaced with the parameter of the same name:
'base_url' => 'https://api.businesscentral.dynamics.com/v2.0/{tenant_id}/{environment}/api/{api}/companies({company_id})', 'token_url' => 'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token', 'parameters' => [ 'tenant_id' => env('DYNAMICS_TENANT_ID'), 'environment' => env('DYNAMICS_ENVIRONMENT'), 'api' => env('DYNAMICS_API', 'v2.0'), 'company_id' => env('DYNAMICS_COMPANY_ID'), ],
Parameter overrides
Every parameter can be overridden.
$dynamics = app(Dynamics::class); $dynamics ->tenantId('::tenant-id::') ->environment('Production') ->api('justbetter/general/v1.0') ->companyId('::company-id::'); // Any other placeholder, or several at once. $dynamics->set('environment', 'Sandbox'); $dynamics->parameters(['environment' => 'Sandbox', 'api' => 'v2.0']); $dynamics->reset();
Companies
Rather than passing company IDs, map a friendly name to an ID in your configuration:
'companies' => [ 'acme' => env('DYNAMICS_COMPANY_ACME_ID'), 'other' => env('DYNAMICS_COMPANY_OTHER_ID'), ],
Select one with company(), which resolves the name and sets the company_id parameter:
$dynamics->company('acme')->entities('customers');
Multiple connections
Multiple connections are supported. Add as many as you wish to the connections array of your configuration and
select one with connection():
$dynamics->connection('other')->entities('customers');
Requests
The client exposes the HTTP verbs directly. Every method returns the Illuminate\Http\Client\Response,.
$response = $dynamics->get('customers', ['$top' => 10]); $response = $dynamics->post('customers', ['displayName' => 'John Doe']); $response = $dynamics->patch('customers(::id::)', ['displayName' => 'Jane Doe']); $response = $dynamics->put('customers(::id::)', ['displayName' => 'Jane Doe']); $response = $dynamics->delete('customers(::id::)'); $response->throw();
Add headers for a single request with header() or headers(). Headers are cleared after the request is sent.
$dynamics->header('Prefer', 'return=representation')->post('customers', ['displayName' => 'John Doe']);
Entities
entities() maps the value array of a response onto Entity objects that remember the endpoint they came from:
$customers = $dynamics->entities('customers'); $customer = $customers->first(); $customer->displayName; // Attributes are accessed as properties $customer->id(); // The "id" attribute $customer->etag(); // The "@odata.etag" attribute $customer->endpoint(); // "customers" $customer->url(); // "customers(::id::)"
Use Entity::from() to build one from a single-record response, for example after a create:
use JustBetter\DynamicsClient\Data\Entity; $response = $dynamics->post('customers', ['displayName' => 'John Doe'])->throw(); $customer = Entity::from($response, ['endpoint' => 'customers']);
Updating and deleting
Scope the client to an entity to derive the URL and the If-Match header from it. The scope lasts for one request.
$customer->displayName = 'Jane Doe'; $dynamics->entity($customer)->update();
update() without arguments sends only the changed attributes. You may also pass an array:
$dynamics->entity($customer)->update(['displayName' => 'Jane Doe']);
Delete the scoped entity by calling delete() without a path:
$dynamics->entity($customer)->delete();
If the entity was read from another endpoint than the one you want to write to, pass the endpoint as the second argument:
$dynamics->entity($customer, 'customers')->update();
Concurrency and If-Match
Business Central requires an If-Match header on every write. The client adds one for you:
- The header you set yourself with
header('If-Match', $etag)wins. - Otherwise the ETag of the scoped entity is used, so the write fails when the record changed in the meantime.
- Otherwise
If-Match: *is sent, which overwrites the record regardless of its version.
That means an unscoped patch(), put() or delete() is an unconditional write by default. Scope the call to an
entity when you care about lost updates, or narrow it yourself:
$dynamics->header('If-Match', $etag)->patch('customers(::id::)', ['displayName' => 'Jane Doe']);
Lazy pagination
Use lazy() to walk every record of an endpoint without holding them all in memory. It pages with $top and $skip
until a page comes back with fewer records than the page size, and yields each entry of the response's value array
as an array.
$dynamics ->lazy('customers', ['$orderby' => 'id']) ->each(function (array $customer): void { // });
The page size defaults to the connection's page_size; pass a third argument to override it per call.
$dynamics->lazy('customers', ['$orderby' => 'id'], 100);
Use lazyEntities() when you want Entity objects back:
$dynamics ->lazyEntities('customers', ['$orderby' => 'id']) ->each(function (Entity $customer): void { // });
Query builder
QueryBuilder builds the OData query parameters.
use JustBetter\DynamicsClient\Query\QueryBuilder; $query = QueryBuilder::make() ->select(['id', 'number', 'displayName']) ->where('city', 'Alkmaar') ->where('balance', '>', 100) ->whereIn('number', ['1000', '2000']) ->whereNotNull('phoneNumber') ->orderByDesc('lastModifiedDateTime') ->take(50) ->get(); $customers = $dynamics->entities('customers', $query);
QueryBuilder::make() ->where('city', 'Alkmaar') ->where('balance', '>', 100) ->orWhere('blocked', 'All') ->get(); // $filter=city eq 'Alkmaar' and (balance gt 100 or blocked eq 'All')
Availability
This client can prevent requests from going to Dynamics when it is giving HTTP status codes 503, 504 or timeouts. This can be configured per connection in the availability settings. Enable the throw option to prevent any requests from going to Dynamics.
Testing
Call Dynamics::fake() to configure a connection with placeholder credentials and stub the OAuth token request, then
fake the HTTP client for the endpoints you call. No real credentials are needed and the URLs are stable, so you can
key your fakes on them.
use Illuminate\Support\Facades\Http; use JustBetter\DynamicsClient\Client\Dynamics; Dynamics::fake(); Http::fake([ 'dynamics/customers*' => Http::response([ 'value' => [ [ '@odata.etag' => '::etag::', 'id' => '::id::', 'displayName' => 'John Doe', ], ], ]), ]); $customers = app(Dynamics::class)->entities('customers');
Commands
Run the following command to check whether you can successfully connect to Dynamics. It reads the company of the connection and prints its name.
php artisan dynamics:connect {connection?}
Quality
To ensure the quality of this package, run the following command:
composer quality
This will execute the following tasks:
- Runs the test suite
- Checks for any issues using static code analysis
- Checks if the code is correctly formatted
- Checks if code coverage is at 100%
- Checks for possible improvements using Rector
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.