Skip to content

Instantly share code, notes, and snippets.

@sebastiaanluca
Last active March 24, 2024 14:39
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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
<?php
declare(strict_types=1);
namespace App\DataTransferObjects;
use Carbon\CarbonImmutable;
use Spatie\DataTransferObject\Caster;
class CarbonImmutableCaster implements Caster
{
public function cast(mixed $value): CarbonImmutable
{
return new CarbonImmutable($value);
}
}
<?php
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)
->withHeaders($this->headers())
->withToken($this->accessToken)
->asJson()
->get('time_entries', [
'from' => $from?->format('Y-m-d'),
'to' => $to?->format('Y-m-d'),
])
->throw();
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(
$source,
'time_entries',
static fn (Config $config): Config => $config
->items('total_entries')
->pages('total_pages')
->lastPage('links.last')
->perPage(100, 'per_page')
// Optimize for low memory usage
->chunk(5)
// Fetch 5 pages at a time
->concurrency(5)
->timeout(5)
->attempts(2)
// 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
->mapInto(TimeEntryDto::class)
// Doesn't execute yet either
->map(static function (TimeEntryDto $timeEntryDto): array {
return TimeEntryFactory::dtoToArray($timeEntryDto);
})
// Still doesn't loop the contents
->chunk(100)
// 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 {
TimeEntry::insert($chunk->toArray());
});
});
<?php
declare(strict_types=1);
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;
#[Strict]
#[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 [
'user',
'client',
'project',
'user_assignment',
'task_assignment',
'task',
'invoice',
];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment