Skip to content

Instantly share code, notes, and snippets.

Last active March 24, 2024 14:39
Show Gist options
  • Save sebastiaanluca/83fbf3bb5db3e463d92b3f59fe130be7 to your computer and use it in GitHub Desktop.
Save sebastiaanluca/83fbf3bb5db3e463d92b3f59fe130be7 to your computer and use it in GitHub Desktop.
Lazy Laravel Harvest API service
namespace App\DataTransferObjects;
use Carbon\CarbonImmutable;
use Spatie\DataTransferObject\Caster;
class CarbonImmutableCaster implements Caster
public function cast(mixed $value): CarbonImmutable
return new CarbonImmutable($value);
use App\Domain\Harvest\Dtos\TimeEntryDto;
use App\Domain\Harvest\Factories\TimeEntryFactory;
use App\Domain\Harvest\Models\TimeEntry;
use Carbon\CarbonImmutable;
use Cerbero\LazyJsonPages\Config;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\LazyCollection;
class HarvestApiService
public function timeEntries(CarbonImmutable $from = null, CarbonImmutable $to = null): LazyCollection
// Calling "get()" on the client makes the first page request
$source = Http::baseUrl($this->baseUri)
->get('time_entries', [
'from' => $from?->format('Y-m-d'),
'to' => $to?->format('Y-m-d'),
return $this->load($source);
public function load(PromiseInterface|Response $source): LazyCollection
// Feeding the response to a LazyCollection enables us
// to only load the other pages when we need them
return LazyCollection::fromJsonPages(
static fn (Config $config): Config => $config
->perPage(100, 'per_page')
// Optimize for low memory usage
// Fetch 5 pages at a time
// Try with more time in-between attempts
->backoff(static fn (int $attempt): int => $attempt ** 2 * 100),
$timeEntries = app(HarvestApiService::class)
->timeEntries(from: new CarbonImmutable('2021-01-01'))
// Doesn't execute yet
// Doesn't execute yet either
->map(static function (TimeEntryDto $timeEntryDto): array {
return TimeEntryFactory::dtoToArray($timeEntryDto);
// Still doesn't loop the contents
// Only this starts getting pages from the API, maps them into a DTO, converts
// them to an array, groups them by 100 items, and loops each chunk. Note that
// it doesn't get nor processes all pages at once. It only fetches enough pages
// to provide us with 100 items in a chunk. When we iterate the next chunk,
// it'll get the next page or set of pages and do the conversions.
->each(static function (LazyCollection $chunk): void {
DB::transaction(static function () use ($chunk): void {
namespace App\Domain\Harvest\Dtos;
use App\DataTransferObjects\CarbonImmutableCaster;
use Carbon\CarbonImmutable;
use Spatie\DataTransferObject\Attributes\DefaultCast;
use Spatie\DataTransferObject\Attributes\Strict;
use Spatie\DataTransferObject\DataTransferObject;
#[DefaultCast(CarbonImmutable::class, CarbonImmutableCaster::class)]
class TimeEntryDto extends DataTransferObject
public int $id;
public ?float $billable_rate;
public ?float $cost_rate;
public float $hours;
public ?float $rounded_hours;
public ?float $hours_without_timer;
public bool $billable;
public bool $budgeted;
public bool $is_running;
public bool $is_billed;
public bool $is_locked;
public bool $is_closed;
public ?string $notes;
public ?string $locked_reason;
public ?ExternalReferenceDto $external_reference;
public ?CarbonImmutable $started_time;
public ?CarbonImmutable $ended_time;
public ?CarbonImmutable $spent_date;
public ?CarbonImmutable $timer_started_at;
public CarbonImmutable $created_at;
public CarbonImmutable $updated_at;
public UserDto $user;
public ClientDto $client;
public ProjectDto $project;
public UserAssignmentDto $user_assignment;
public TaskAssignmentDto $task_assignment;
public TaskDto $task;
public ?InvoiceDto $invoice;
public static function relations(): array
return [
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment