mdbitz/hapi
Hapi is a Laravel package that streamlines API development with conventions and helpers for defining, organizing, and exposing HTTP endpoints, helping you build cleaner APIs with less boilerplate and tighter integration with Laravel’s routing and request lifecycle.
Install and inspect before you integrate. Run composer require mdbitz/hapi, then immediately read the repository's README and src/ directory. With only ~19 stars, documentation is likely thin — the resource classes themselves are the real API reference. Identify the main entry point (typically a HaPi / Client class) and the per-resource classes (time entries, projects, clients, users, tasks). Pin the version explicitly in composer.json; an unmaintained package can shift between minor releases.
Configure credentials in config/, never inline. Harvest needs an account subdomain (and, on API v2, an account ID) plus either a personal access token or Basic auth credentials. Put these in .env and expose them through a config/harvest.php file so php artisan config:cache doesn't null them out:
// config/harvest.php
return [
'account_id' => env('HARVEST_ACCOUNT_ID'),
'token' => env('HARVEST_TOKEN'),
'subdomain' => env('HARVEST_SUBDOMAIN'),
'user_agent' => env('HARVEST_USER_AGENT', 'MyApp ([email protected])'),
];
Bind it once in a service provider. Register the client as a singleton in AppServiceProvider::register() (or a dedicated HarvestServiceProvider), injecting config values. Resolve it via the container everywhere else — never new HaPi(...) in a controller.
First use case: read projects, write a time entry. The fastest way to validate credentials end-to-end is to (a) list active projects, (b) create one time entry against the first project, (c) fetch it back. That exercises auth, serialization, and the create/read paths you'll use daily.
App\Contracts\HarvestGateway with typed methods like projects(): Collection, createTimeEntry(TimeEntryData $dto): TimeEntry. Implement it with HaPi inside. This isolates the package so a v2 migration (or replacing it with raw Http:: calls) is a one-file change.harvest:sync-reference) that iterates the package's list methods and upserts into local tables. Run it hourly or nightly; cache the results with Cache::remember for the API-call-heavy reads.CreateHarvestTimeEntry job with $tries = 3, exponential backoff, and a unique key. Persist the Harvest ID back onto your local record so retries don't create duplicates.TimeEntryData DTO with spent_date, hours, project_id, task_id, notes keeps validation in one place and makes testing trivial.Http::fake() won't intercept it — you'll need to swap the gateway or inject a Guzzle MockHandler.projects(), clients(), and tasks() in Cache::remember('harvest.projects', now()->addMinutes(30), ...) and bust the cache inside your sync command.Link-header pagination. Verify which the package targets before integrating — if it's v1-only, plan to wrap Illuminate\Support\Facades\Http directly for v2 and use the package only as a model/normalizer reference.->retry(3, 1000, fn ($e) => $e->getCode() === 429) or a job middleware, and respect Retry-After. Bulk syncs should throttle deliberately, not hammer.->all() call returns everything. Loop on page / per_page and watch for repeated pages when records are mutated mid-sync.spent_date as Y-m-d and hours as a decimal — never a Carbon object or a float with drift. Don't trust a Carbon::parse() round-trip; format on the way out and parse on the way in.is_active. Filter in the sync or you'll recreate deleted local records.false or a generic exception on 4xx/5xx. Log the raw status code and response body before throwing; Harvest's error JSON includes a human-readable message you'll want in your report.env() call inside the package source will return null after php artisan config:cache. If credentials inexplicably break in production, that's the first thing to check.How can I help you explore Laravel packages today?