A Laravel package for the Timatic API, built with Saloon and automatically generated from OpenAPI specifications.
- PHP 8.2 or higher
- Laravel 10.x or higher
composer require timatic/php-sdkThe package automatically registers itself via Laravel auto-discovery.
Publish the config file:
php artisan vendor:publish --tag=timatic-configAdd your API credentials to .env:
TIMATIC_BASE_URL=https://api.app.timatic.test
TIMATIC_API_TOKEN=your-api-token-hereThe SDK connector is automatically registered in Laravel's service container, making it easy to inject into your controllers, commands, and other classes:
use Timatic\Requests\Budget\GetBudgetsCollectionRequest;
use Timatic\Requests\Budget\GetBudgetRequest;
use Timatic\Requests\Budget\PostBudgetsRequest;
use Timatic\Requests\BudgetType\GetBudgetTypesCollectionRequest;
use Timatic\TimaticConnector;
class BudgetController extends Controller
{
public function __construct(
protected TimaticConnector $timatic
) {}
public function index()
{
// fetch one or more items, limited by the default page size from the api
$budgetTypes = $this->timatic
->send(new GetBudgetTypesCollectionRequest())
->dto();
$defaultBudget = $this->timatic->send(
new \Timatic\Requests\Budget\GetBudgetRequest(id: '1337')
)->dtoOrFail();
// fetch all DTO's
$budgets = $this->timatic->paginate(new GetBudgetsCollectionRequest())
->dtoCollection();
return view('budgets.index', compact('budgets', 'budgetTypes', 'defaultBudget'));
}
public function store(Request $request)
{
$budget = new \Timatic\Dto\Budget([
'title' => $request->input('title'),
'totalPrice' => $request->input('total_price'),
]);
$created = $this->timatic
->send(new PostBudgetsRequest($budget))
->dtoOrFail();
return redirect()->route('budgets.show', $created->id);
}
}In Console Commands:
use \Timatic\Requests\Budget\GetBudgetsCollectionRequest;
use Timatic\TimaticConnector;
class SyncBudgetsCommand extends Command
{
public function handle(TimaticConnector $timatic): int
{
$budgets = $timatic->paginate(
new GetBudgetsCollectionRequest()
)->dtoCollection()
foreach ($budgets as $budget) {
// Process budgets
}
return Command::SUCCESS;
}
}When testing code that uses the Timatic 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 BudgetController from the example above:
use Timatic\TimaticConnector;
use Timatic\Dto\Budget;
use Timatic\Dto\BudgetType;
use Timatic\Requests\Budget\GetBudgetsRequest;
use Timatic\Requests\Budget\PostBudgetsRequest;
use Timatic\Requests\BudgetType\GetBudgetTypesRequest;
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;
test('it displays budgets and budget types', function () {
// Generate test data using factories
$budget = Budget::factory()->state(['id' => '1'])->make();
$budgetType = BudgetType::factory()->state(['id' => '1'])->make();
// Create mock responses using factory-generated data
$mockClient = MockClient::global([
GetBudgetsRequest::class => MockResponse::make([
'data' => [$budget->toJsonApi()],
], 200),
GetBudgetTypesRequest::class => MockResponse::make([
'data' => [$budgetType->toJsonApi()],
], 200),
]);
// Make request
$response = $this->get(route('budgets.index'));
// Assert
$response->assertOk();
$response->assertViewHas('budgets');
$response->assertViewHas('budgetTypes');
});
test('it creates a new budget', function () {
// Generate test data with specific attributes
$budget = Budget::factory()->state([
'id' => '2',
'title' => 'New Budget',
'totalPrice' => '5000.00',
])->make();
$mockClient = MockClient::global([
PostBudgetsRequest::class => MockResponse::make([
'data' => $budget->toJsonApi(),
], 201),
]);
$response = $this->post(route('budgets.store'), [
'title' => 'New Budget',
'total_price' => 5000.00,
]);
$response->assertRedirect(route('budgets.show', '2'));
});
test('it sends a POST request to create a budget using the SDK', function () {
$budgetToCreate = Budget::factory()->state([
'title' => 'New Budget',
'totalPrice' => '5000.00',
'customerId' => 'customer-123',
])->make();
$createdBudget = Budget::factory()->state([
'id' => 'created-456',
'title' => 'New Budget',
'totalPrice' => '5000.00',
'customerId' => 'customer-123',
])->make();
$mockClient = MockClient::global([
PostBudgetsRequest::class => MockResponse::make([
'data' => $createdBudget->toJsonApi(),
], 201),
]);
artisan('sync:budgets')->assertOk();
// Assert the request body was sent correctly
$mockClient->assertSent(function (PostBudgetsRequest $request) {
$body = $request->body()->all();
return $body['data']['attributes']['title'] === 'New Budget'
&& $body['data']['attributes']['totalPrice'] === '5000.00'
&& $body['data']['attributes']['customerId'] === 'customer-123';
});
});Every DTO in the SDK has a corresponding factory class with the following methods:
// Create a single model with random data, without an ID
$budget = Budget::factory()->make();
// Create multiple models with unique UUID IDs
$budgets = Budget::factory()->withId()->count(3)->make(); // Returns Collection
// Override specific attributes
$budget = Budget::factory()->state([
'title' => 'Q1 Budget',
'totalPrice' => '10000.00',
])->make();
// Chain state calls for complex scenarios
$budget = Budget::factory()
->state(['customerId' => $customerId])
->state(['budgetTypeId' => $budgetTypeId])
->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 Timatic\TimaticConnector;
use Timatic\Requests\Budget\GetBudgets;
class BudgetController extends Controller
{
public function index(TimaticConnector $timatic)
{
// Create a paginator
$paginator = $timatic->paginate(new GetBudgets());
// Optionally set items per page (default is API's default)
$paginator->setPerPageLimit(50);
// Iterate through all pages automatically
foreach ($paginator->items() as $budget) {
// Process each budget across all pages
// The paginator handles pagination automatically
}
// Or collect all items at once
$allBudgets = $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 (GetBudgets, GetCustomers, GetUsers, etc.)
All responses are instances of TimaticResponse which extends Saloon's Response with JSON:API convenience methods:
$response = $timatic->send(new GetBudgetsCollectionRequest());
// Get the first item from a collection
$firstBudget = $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
}This SDK follows REST best practices and does not support PUT requests. Instead:
- POST - Create new resources
- PATCH - Partially update existing resources
- GET - Retrieve resources
- DELETE - Remove resources
PUT is intentionally excluded because resources are never completely replaced by Timatic.
The SDK provides access to the following resources:
- Budgets - Manage budgets and budget entries
- Customers - Customer management
- Users - User management
- Teams - Team management
- Entries - Time entry management
- Incidents - Incident tracking
- Changes - Change tracking
- Overtimes - Overtime management
- Events - Event logging
- And more...
This SDK uses a custom JSON:API DTO Generator that automatically flattens JSON:API attributes into proper Model properties. Instead of having generic $attributes, $type, and $relationships objects, each model has specific typed properties.
Instead of:
$budget->attributes->title; // ❌ Generic structureYou get:
$budget->title; // ✅ Proper typed property
$budget->budgetTypeId;
$budget->startedAt; // Carbon instance for datetime fields- Extends
Modelbase class with JSON:API support - 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
This SDK is automatically generated from the Timatic API OpenAPI specification using a custom JSON:API generator. To regenerate the SDK with the latest API changes:
composer regenerateThis will:
- Download the latest OpenAPI specification from the API
- Generate Models with flattened JSON:API attributes
- Update the autoloader
- Format the code with Laravel Pint
The SDK uses a custom JsonApiDtoGenerator that:
- Detects JSON:API schemas in the OpenAPI specification
- Extracts properties from the
attributesobject - Generates proper Model classes with specific properties
- Adds
#[Property]and#[DateTime]attributes - Uses Carbon for datetime fields
# Run tests
composer test
# Run tests with code coverage
composer coverageThis package is licensed under the Elastic License 2.0 (ELv2).
- Built with Saloon
- Generated using Saloon SDK Generator