ixudra/toggl
ixudra/toggl is a Laravel package offering a fluent client for the Toggl API, so you can manage time entries, projects, clients, workspaces, and reports directly from your PHP application. Includes a service provider, facade, and config for quick integration.
Minimal path from zero to a logged time entry:
Install.
composer require ixudra/toggl
On Laravel 5.5+ the Ixudra\Toggl\TogglServiceProvider is auto-discovered. On older versions register it in config/app.php and add the alias:
'Toggl' => Ixudra\Toggl\Facades\Toggl::class,
Publish the config so you can point the client at the right API version and workspace:
php artisan vendor:publish --provider="Ixudra\Toggl\TogglServiceProvider"
Then set TOGGL_API_KEY (from the bottom of your Toggl profile page) and usually TOGGL_WORKSPACE_ID in .env. Never commit the token — use config('toggl.api_key') only on the server.
Look first at config/toggl.php (api key, workspace id, api url) and the facade's method list. The facade proxies 1:1 onto Toggl REST endpoints, so the Toggl API docs are your real reference — the package method names track them closely.
First useful call — sanity-check auth and discover your workspace:
$me = Toggl::currentUser(); // confirms the token works
$workspaces = Toggl::workspaces(); // grab the id you'll pass everywhere else
First real use case — log a completed entry:
Toggl::createTimeEntry([
'wid' => config('toggl.workspace_id'),
'start' => now()->toIso8601String(),
'duration' => 3600,
'description' => 'Code review',
'pid' => $projectId,
]);
// Or start/stop a running timer:
Toggl::startTimeEntry(['wid' => $wid, 'description' => 'Working']);
Toggl::stopTimeEntry($entryId);
Wrap the facade in your own service. The facade is a thin transport. Give it a home so you can mock it in tests and centralise your wid/pid defaults:
class TogglService
{
public function __construct(private int $workspaceId) {}
public function log(string $description, int $seconds, ?int $projectId = null): array
{
return Toggl::createTimeEntry([
'wid' => $this->workspaceId,
'start' => now()->subSeconds($seconds)->toIso8601String(),
'duration' => $seconds,
'description' => $description,
'pid' => $projectId,
]);
}
}
Build a config-driven defaults layer. Nearly every endpoint needs a workspace id, and many need a project id. Keep a small resolver (ProjectResolver, WorkspaceResolver) that maps your app's own entities to Toggl ids, cached in Redis or a toggl_projects table. This turns "hardcoded magic ids" into one lookup point.
Sync rather than query-on-read. For anything dashboard- or reporting-oriented, run an artisan command (scheduled) that pulls Toggl::clients($wid), Toggl::projects($wid), and Toggl::timeEntries([...]) into local tables keyed by Toggl id. Your UI then reads from your own DB — far faster than hitting a rate-limited remote on every page load, and robust when Toggl is slow.
Model the start/stop lifecycle explicitly. Running entries have a negative duration (-1 * unix_start). If you mirror that state locally, store an is_running flag rather than trying to interpret the negative number in your views. Always call Toggl::stopTimeEntry($id) before starting a new one — Toggl allows only one running entry per user, and a silent conflict returns a bare 4xx rather than a helpful exception.
Payload arrays over builders. The package favours plain associative arrays for create/update payloads, which makes it trivial to build them from Eloquent models with $model->only([...]) and to test with array comparisons. Treat the expected shape as your contract and validate it in a FormRequest before it reaches the facade.
Reports are a different world. The Toggl Reports API lives on a different host, uses a different token-in-query auth scheme, and requires a user agent. The package's reporting coverage (if present) is limited — for summaries, either build your own Guzzle client against toggl.com/reports/api/v2, or aggregate from synced timeEntries locally. Don't assume Toggl::reports() mirrors the tracking endpoints.
Testing. Bind a fake to the container in tests rather than hitting the network:
$this->swap(\Ixudra\Toggl\TogglFactory::class, $mock);
or mock your own TogglService wrapper — the wrapper exists precisely so you never need to mock the facade.
config/toggl.php and re-check after any composer update. If calls mysteriously change shape, this is the first place to look.start values you send are interpreted in your account's timezone setting, not necessarily UTC. Be explicit in your payloads and consistent in your sync logic or your daily totals will drift by an hour at DST boundaries.timeEntries() accepts a since/date range and returns what the API returns. For long histories, page manually with successive start_date/end_date windows — don't assume one call returns everything.usleep(1_000_000) (or a queued job with a rate limiter) in any loop that iterates remote calls.config('toggl.api_key') singleton is only suitable for a single-account setup. For multi-tenant, resolve the client per-user rather than relying on the facade default.Ixudra\Toggl\TogglFactory) for multi-tenant work instead of mutating config mid-request.Toggl is a common name — if you have your own class with that name, alias the facade explicitly ('TogglApi' => ...) to avoid silent resolution to your own class in some Laravel versions.How can I help you explore Laravel packages today?