Last active
March 24, 2024 14:39
-
-
Save sebastiaanluca/83fbf3bb5db3e463d92b3f59fe130be7 to your computer and use it in GitHub Desktop.
Lazy Laravel Harvest API service
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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()); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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