A Laravel package for the Visma API, built with Saloon and automatically generated from OpenAPI specifications.
- Requirements
- Installation
- Configuration
- Usage
- HTTP Methods
- Available Resources
- JSON:API Support
- Regenerating the SDK
- Development
- License
- Credits
- PHP 8.2 or higher
- Laravel 10.x or higher
composer require pionect/visma-sdkThe package automatically registers itself via Laravel auto-discovery.
Publish the config file:
php artisan vendor:publish --tag=visma-configAdd your API credentials to .env:
VISMA_APPLICATION_ID=your-application-id
VISMA_APPLICATION_SECRET=your-application-secret
VISMA_TENANT_ID=your-tenant-id
VISMA_BASE_URL=https://integration.visma.net/APIThe SDK connector is automatically registered in Laravel's service container, making it easy to inject into your controllers, commands, and other classes:
use Pionect\VismaSdk\Requests\Customer\CustomerGetAllCollectionRequest;
use Pionect\VismaSdk\Requests\Customer\CustomerGetBycustomerCdRequest;
use Pionect\VismaSdk\Requests\Customer\CustomerPostRequest;
use Pionect\VismaSdk\Dto\CustomerDto;
use Pionect\VismaSdk\VismaConnector;
class CustomerController extends Controller
{
public function __construct(
protected VismaConnector $visma
) {}
public function index()
{
// Fetch a single customer by customer number
$defaultCustomer = $this->visma->send(
new CustomerGetBycustomerCdRequest(customerCd: 'CUST001')
)->dtoOrFail();
// Fetch all customers using pagination
$customers = $this->visma->paginate(new CustomerGetAllCollectionRequest())
->dtoCollection();
return view('customers.index', compact('customers', 'defaultCustomer'));
}
public function store(Request $request)
{
$customer = new CustomerDto([
'number' => $request->input('number'),
'name' => $request->input('name'),
'status' => 'Active',
'creditLimit' => $request->input('credit_limit'),
'currencyId' => 'NOK',
]);
$created = $this->visma
->send(new CustomerPostRequest($customer))
->dtoOrFail();
return redirect()->route('customers.show', $created->number);
}
}In Console Commands:
use Pionect\VismaSdk\Requests\Customer\CustomerGetAllCollectionRequest;
use Pionect\VismaSdk\VismaConnector;
class SyncCustomersCommand extends Command
{
public function handle(VismaConnector $visma): int
{
$customers = $visma->paginate(
new CustomerGetAllCollectionRequest()
)->dtoCollection();
foreach ($customers as $customer) {
// Process customers
$this->info("Syncing customer: {$customer->name}");
}
return Command::SUCCESS;
}
}When testing code that uses the Visma SDK, you can mock the connector and its responses using factories. The SDK includes factory classes for all DTOs that make it easy to generate test data.
Here's an example of testing the CustomerController from the example above:
use Pionect\VismaSdk\VismaConnector;
use Pionect\VismaSdk\Dto\CustomerDto;
use Pionect\VismaSdk\Dto\CustomerInvoiceDto;
use Pionect\VismaSdk\Requests\Customer\CustomerGetAllCollectionRequest;
use Pionect\VismaSdk\Requests\Customer\CustomerGetBycustomerCdRequest;
use Pionect\VismaSdk\Requests\Customer\CustomerPostRequest;
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;
test('it displays customers', function () {
// Generate test data using factories
$customers = CustomerDto::factory()->count(2)->make();
// Create mock responses using factory-generated data
$mockClient = MockClient::global([
CustomerGetAllCollectionRequest::class => MockResponse::make($customers->toArray(), 200),
]);
// Make request
$response = $this->get(route('customers.index'));
// Assert
$response->assertOk();
$response->assertViewHas('customers');
$response->assertViewHas('defaultCustomer');
});
test('it creates a new customer', function () {
// Generate test data with specific attributes
$customer = CustomerDto::factory()->state([
'number' => 'CUST002',
'name' => 'New Customer Ltd',
'status' => 'Active',
'creditLimit' => 50000.00,
'currencyId' => 'NOK',
])->make();
$mockClient = MockClient::global([
CustomerPostRequest::class => MockResponse::make($customer->toArray(), 201),
]);
$response = $this->post(route('customers.store'), [
'number' => 'CUST002',
'name' => 'New Customer Ltd',
'credit_limit' => 50000.00,
]);
$response->assertRedirect(route('customers.show', 'CUST002'));
});
test('it sends a POST request to create a customer using the SDK', function () {
$customerToCreate = CustomerDto::factory()->state([
'number' => 'CUST003',
'name' => 'Test Customer AS',
'status' => 'Active',
'creditLimit' => 100000.00,
'currencyId' => 'NOK',
])->make();
$createdCustomer = CustomerDto::factory()->state([
'number' => 'CUST003',
'name' => 'Test Customer AS',
'status' => 'Active',
'creditLimit' => 100000.00,
'currencyId' => 'NOK',
])->make();
$mockClient = MockClient::global([
CustomerPostRequest::class => MockResponse::make($createdCustomer->toArray(), 201),
]);
artisan('sync:customers')->assertOk();
// Assert the request body was sent correctly
$mockClient->assertSent(function (CustomerPostRequest $request) {
$body = $request->body()->all();
return $body['number'] === 'CUST003'
&& $body['name'] === 'Test Customer AS'
&& $body['creditLimit'] === 100000.00;
});
});Every DTO in the SDK has a corresponding factory class with the following methods:
// Create a single model with random data, without an ID
$customer = CustomerDto::factory()->make();
// Create multiple models with unique UUID IDs
$customers = CustomerDto::factory()->withId()->count(3)->make(); // Returns Collection
// Override specific attributes
$customer = CustomerDto::factory()->state([
'number' => 'CUST100',
'name' => 'Q1 Customer',
'status' => 'Active',
'creditLimit' => 10000.00,
])->make();
// Chain state calls for complex scenarios
$customer = CustomerDto::factory()
->state(['number' => $customerNumber])
->state(['currencyId' => 'NOK'])
->make();For more information on mocking Saloon requests, see the Saloon Mocking Documentation.
The SDK supports automatic pagination for all collection endpoints using Saloon's pagination plugin:
use Pionect\VismaSdk\VismaConnector;
use Pionect\VismaSdk\Requests\Customer\CustomerGetAllCollectionRequest;
class CustomerController extends Controller
{
public function index(VismaConnector $visma)
{
// Create a paginator
$paginator = $visma->paginate(new CustomerGetAllCollectionRequest());
// Optionally set items per page (default is API's default)
$paginator->setPerPageLimit(50);
// Iterate through all pages automatically
foreach ($paginator->items() as $customer) {
// Process each customer across all pages
// The paginator handles pagination automatically
}
// Or collect all items at once
$allCustomers = $paginator->dtoCollection();
}
}The paginator:
- Automatically handles JSON:API pagination (
page[number]andpage[size]) - Detects the last page via
links.next - Works with all GET collection requests (CustomerGetAllCollectionRequest, CustomerInvoiceGetAllCollectionRequest, etc.)
All responses are instances of VismaResponse which extends Saloon's Response with JSON:API convenience methods:
use Pionect\VismaSdk\Requests\CustomerInvoice\CustomerInvoiceGetAllCollectionRequest;
$response = $visma->send(new CustomerInvoiceGetAllCollectionRequest());
// Get the first item from a collection
$firstInvoice = $response->firstItem();
// Check for errors
if ($response->hasErrors()) {
$errors = $response->errors();
// Handle errors...
}
// Access JSON:API meta information
$meta = $response->meta();
$total = $meta['total'] ?? 0;
// Access pagination links
$links = $response->links();
$nextPage = $links['next'] ?? null;
// Access included resources
$included = $response->included();
foreach ($included as $resource) {
// Process related resources
}- Extends
Modelbase class - Property attributes via
#[Property]for serialization - DateTime handling with Carbon instances
- Type safety with PHP 8.1+ type hints
- HasAttributes trait for easy attribute manipulation
- Relationship support with automatic hydration and serialization
This SDK is generated from the Visma API OpenAPI specification. Download the OpenApi specification from https://api.finance.visma.net/API-index/index.html To regenerate:
# Regenerate all DTOs and Requests
./bin/generate-sdk generate path/to/openapi.json - Built with Saloon
- Generated using Saloon SDK Generator