freeloapp / php-sdk
Modern PHP SDK for Freelo API - Project and task management
Requires
- php: ^8.1
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0|^2.0
- psr/simple-cache: ^2.0|^3.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8
- nyholm/psr7: ^1.8
- php-http/guzzle7-adapter: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.8
- symfony/cache: ^6.4|^7.0
- symfony/yaml: ^6.4|^7.0
Suggests
- guzzlehttp/guzzle: For HTTP client implementation
- nyholm/psr7: For PSR-7 message implementation
- symfony/cache: For caching support
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-09 14:29:44 UTC
README
Modern, lightweight PHP SDK for Freelo API.
Features
- PHP 8.1+ with strict types
- PSR-18/17/7/16 compliant (bring your own HTTP client)
- Full API coverage (projects, tasks, files, comments, time tracking, etc.)
- Dynamic credentials with server-safe per-request switching
- Low-level
call()method for arbitrary API endpoints - Automatic pagination support
- Rate limiting detection
- Typed exceptions for error handling
Installation
composer require freeloapp/php-sdk
Install an HTTP client (e.g., Guzzle):
composer require guzzlehttp/guzzle php-http/guzzle7-adapter nyholm/psr7
Quick Start
use Freelo\Sdk\Freelo; use Freelo\Sdk\Auth\ApiKeyCredentials; $freelo = new Freelo( new ApiKeyCredentials('your-api-key', 'your-email@example.com'), userAgent: 'MyApp/1.0', ); // List projects foreach ($freelo->projects()->list() as $project) { echo $project->name . "\n"; } // Create a task $task = $freelo->tasks()->create( projectId: 123, tasklistId: 456, data: ['name' => 'New task', 'priority_enum' => 'h'] );
Authentication
Get your API key from Freelo Settings (scroll to API section).
use Freelo\Sdk\Auth\ApiKeyCredentials; // Using environment variables (recommended) $credentials = new ApiKeyCredentials( apiKey: getenv('FREELO_API_KEY'), email: getenv('FREELO_EMAIL') ); $freelo = new Freelo($credentials, userAgent: 'MyApp/1.0');
Usage Examples
Projects
$projects = $freelo->projects()->list(); // List active projects $project = $freelo->projects()->get(123); // Get project by ID $freelo->projects()->archive(123); // Archive project $freelo->projects()->activate(123); // Activate archived project $freelo->projects()->update(123, ['name' => 'New name', 'due_date' => '2026-08-15']); $budget = $freelo->projects()->getBudget(123); // Budget settings + consumption // Paginated results $result = $freelo->projects()->getAll(['p' => 0]); echo "Total: " . $result->getTotal();
Tasks
$tasks = $freelo->tasks()->listInTasklist(123, 456); // List tasks in tasklist $task = $freelo->tasks()->get(789); // Get task by ID $freelo->tasks()->finish(789); // Mark as complete $freelo->tasks()->activate(789); // Reopen task $freelo->tasks()->move(789, 999); // Move to another tasklist $freelo->tasks()->addComment(789, 'Comment text'); // Add comment $freelo->tasks()->createRelation(789, 'blocks', 790); // Relate two tasks $relations = $freelo->tasks()->getRelations(789); // Read task relations
Time Tracking
$freelo->timeTracking()->start(taskId: 123, note: 'Working'); $report = $freelo->timeTracking()->stop(); // Manual work report $freelo->workReports()->create(123, [ 'minutes' => 120, 'date_reported' => '2024-01-15', 'note' => 'Development work', ]);
Files
$freelo->files()->uploadToTask(taskId: 123, filePath: '/path/to/file.pdf'); $freelo->files()->download(fileId: 456, destination: '/path/to/save.pdf');
Dynamic Credentials
Per-request credentials (server-safe)
Use withCredentials() to create an isolated instance with different credentials. Each derived instance has its own client and state — safe for concurrent requests in multi-tenant server applications.
// Shared base instance (e.g. in a service provider) $freelo = new Freelo(new ApiKeyCredentials( apiKey: 'default-key', email: 'default@example.com' )); // Per-request — fully isolated, concurrency-safe $userFreelo = $freelo->withCredentials(new ApiKeyCredentials( apiKey: $user->apiKey, email: $user->email )); // Optionally override userAgent per tenant $userFreelo = $freelo->withCredentials( new ApiKeyCredentials($user->apiKey, $user->email), userAgent: 'TenantApp/1.0', ); $projects = $userFreelo->projects()->list();
For simple single-user scripts, you can swap credentials in place with setCredentials():
$freelo->setCredentials(new ApiKeyCredentials( apiKey: 'new-key', email: 'new@example.com' )); // With userAgent override $freelo->setCredentials( new ApiKeyCredentials('new-key', 'new@example.com'), userAgent: 'MyApp/2.0', );
Note:
setCredentials()mutates the shared instance and is not safe for concurrent requests.
Lazy initialization
The SDK supports creating a client without credentials upfront. Credentials can be provided later via setCredentials() — validation is deferred to the first API request.
$freelo = new Freelo(); // ... later, when credentials are available $freelo->setCredentials(new ApiKeyCredentials( apiKey: $apiKey, email: $email )); $freelo->projects()->list(); // works
Low-level API Calls
Use call() to hit arbitrary API endpoints not yet covered by typed resource classes:
// GET request with query parameters $response = $freelo->call('/projects', 'GET', params: ['p' => 0]); $data = $response->json(); // POST request with body $response = $freelo->call('/projects/123/tasklists/456/tasks', 'POST', data: [ 'name' => 'New task', 'priority_enum' => 'h', ]); // Also available on derived instances $userFreelo = $freelo->withCredentials($credentials); $response = $userFreelo->call('/users/me', 'GET');
Available Resources
| Resource | Method | Description |
|---|---|---|
| Projects | projects() |
Project management |
| Tasks | tasks() |
Task management |
| Tasklists | tasklists() |
Tasklist management |
| Comments | comments() |
Comment management |
| Files | files() |
Attachment upload/download, Files section listing |
| Directories | directories() |
Files section directories |
| Links | links() |
Links to external documents in the Files section |
| Project Files | projectFiles() |
Files stored in the Files section |
| Task Labels | taskLabels() |
Task label management |
| Project Labels | projectLabels() |
Project label management |
| Subtasks | subtasks() |
Subtask management |
| Time Tracking | timeTracking() |
Time tracking |
| Work Reports | workReports() |
Work report management |
| Users | users() |
User management |
| Notifications | notifications() |
Notifications |
| Events | events() |
Activity events |
| Notes | notes() |
Note management |
| Search | search() |
Full-text search |
| Custom Fields | customFields() |
Custom fields |
| Invoices | invoices() |
Invoice management |
Pagination
use Freelo\Sdk\Http\Paginator; // Auto-paginate through all results foreach (Paginator::fetchAll(fn($p) => $freelo->projects()->getAll(['p' => $p])) as $project) { echo $project->name . "\n"; } // Manual pagination $page = $freelo->tasks()->getAll(['p' => 0]); echo "Page {$page->getPage()} of " . ceil($page->getTotal() / 20); if ($page->hasNextPage()) { $nextPage = $freelo->tasks()->getAll(['p' => $page->getNextPage()]); }
Error Handling
use Freelo\Sdk\Exception\{ ApiException, AuthenticationException, NotFoundException, RateLimitException, ValidationException }; try { $project = $freelo->projects()->get(999); } catch (NotFoundException $e) { // Resource not found (404) } catch (AuthenticationException $e) { // Invalid credentials (401) } catch (RateLimitException $e) { // Rate limited (429) - retry after $e->getRetryAfter() seconds } catch (ValidationException $e) { // Invalid input (422) } catch (ApiException $e) { // Other API errors }
Working with Dates
Datetime fields on model objects (Task::dateAdd, WorkReport::dateReported, …) are exposed as \DateTimeImmutable in UTC. The Freelo Public API V1 returns naive datetime strings without a timezone (e.g. 2026-04-24T11:12:38); these represent Europe/Prague local time, as documented in the OpenAPI spec's "Timestamp Format" section. The SDK parses them as Prague-local and converts to UTC, so you can compare and format them safely without worrying about silent timezone drift.
$task = $freelo->tasks()->get(123); // $task->dateAdd is \DateTimeImmutable in UTC echo $task->dateAdd->format('c'); // 2026-04-24T09:12:38+00:00 (UTC) echo $task->dateAdd ->setTimezone(new \DateTimeZone('Europe/Prague')) ->format('c'); // 2026-04-24T11:12:38+02:00 (Prague) // Raw API payload (still strings) is available via toArray(): $raw = $task->toArray()['date_add']; // '2026-04-24T11:12:38'
FilterBuilder accepts either \DateTimeImmutable or Y-m-d strings for date-range filters; objects are formatted in Europe/Prague to match what the API expects:
$filters = FilterBuilder::create() ->dueDateRange(new \DateTimeImmutable('2026-04-01'), new \DateTimeImmutable('2026-04-30')) ->build();
Rate Limiting
The API limits requests per Freelo user (token bucket). The limit is shared by every application and API key of the same user, and its size is configurable on the server and may change, so don't hardcode it. Each response carries the current state in IETF structured headers, for example:
RateLimit-Policy: "default";q=60;w=20 # q requests per w seconds
RateLimit: "default";r=42;t=5 # r requests left, full again in t seconds
The SDK reads them after every request (legacy X-RateLimit-* headers are still understood):
$limiter = $freelo->getClient()->getRateLimiter(); $limiter->getLimit(); // q from RateLimit-Policy $limiter->getRemaining(); // r from RateLimit $limiter->getSecondsUntilReset(); // t from RateLimit (getResetAt() = now + t) if ($limiter->isLimitExceeded()) { sleep($limiter->getSecondsUntilReset()); }
When the limit is exceeded the API answers 429 with Retry-After: <seconds>, which RateLimitException::getRetryAfter() returns. Retry-After is authoritative: wait at least that long before retrying, and back off exponentially if the 429s repeat. A RetryHandler passed to FreeloClient retries 429s and honours Retry-After (see examples/rate-limiting.php).
Custom HTTP Client
use GuzzleHttp\Client; use Http\Adapter\Guzzle7\Client as GuzzleAdapter; $httpClient = new GuzzleAdapter(new Client(['timeout' => 30])); $freelo = new Freelo($credentials, httpClient: $httpClient);
Caching (Optional)
use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\Cache\Adapter\FilesystemAdapter; $cache = new Psr16Cache(new FilesystemAdapter('freelo', 3600)); $freelo = new Freelo($credentials, cache: $cache);
Development
composer install # Install dependencies composer test # Run tests composer phpstan # Static analysis composer check # Run all checks
Requirements
- PHP 8.1+
- PSR-18 HTTP Client (e.g., Guzzle)
- PSR-17 HTTP Factories (e.g., nyholm/psr7)
License
MIT License. See LICENSE.