flyo/nitro-php
A PHP SDK for the Flyo Nitro content API, giving Laravel and plain PHP apps a simple way to fetch and deliver structured content from Flyo's headless CMS, with a lightweight client and straightforward integration.
Install and configure
composer require flyo/nitro-php
Start by reading the README and then src/ — this is a small, low-traffic SDK, so the source is short enough to read end-to-end in one sitting. Identify three things before writing any application code:
Configure credentials in .env, never in code:
NITRO_API_URL=https://nitro.example.com
NITRO_TOKEN=...
NITRO_PREVIEW_TOKEN=...
If the package ships a Laravel service provider, publish and review its config. If it doesn't, register the client as a singleton yourself in AppServiceProvider:
$this->app->singleton(NitroClient::class, fn () => new NitroClient(
baseUrl: config('services.nitro.url'),
token: config('services.nitro.token'),
));
First use case
Pick the narrowest real endpoint: fetch a single page by slug/path and render it in a Blade view. Get that working before touching blocks, navigation, or media — those are where the data shape gets nested and where you'll want to understand the DTOs properly.
Route::get('/{slug?}', function (NitroClient $nitro, string $slug = 'home') {
$page = $nitro->page($slug); // verify exact method name in src/
abort_if($page === null, 404);
return view('page', ['page' => $page]);
});
Treat the exact method names above as placeholders — the public API surface is small and undocumented enough that you should confirm against src/ rather than trust any blog post or example.
Wrap the SDK in an adapter. This is the single highest-value decision with a package this small. Define your own interface, implement it with the SDK, and bind it in the container. When the SDK changes shape (or you outgrow it), you rewrite one class instead of grepping the whole app.
interface ContentRepository {
public function page(string $slug, ?string $locale = null): ?Page;
public function navigation(string $handle): array;
}
final class NitroContentRepository implements ContentRepository { /* SDK calls here */ }
Repository + cache layer. Nitro is a content delivery API, so most reads are cacheable and reads dominate writes. Cache the DTO/serialized result, not the HTTP response, so you can transform it:
public function page(string $slug, ?string $locale = null): ?Page
{
return Cache::remember(
"nitro:page:{$locale}:{$slug}",
now()->addMinutes(10),
fn () => $this->client->page($slug, $locale),
);
}
Key the cache on everything that changes the response: slug, locale, and preview/live mode. Mixing draft content into a live cache is the classic failure mode.
Preview mode via middleware. Keep two tokens — a public/live one and a preview one. Toggle with a session flag set by a signed route, and have the repository choose the token accordingly. Then make sure the cache key includes the mode:
$key = sprintf('nitro:%s:%s:%s', $this->mode(), $locale, $slug);
Also send Cache-Control: no-store on preview responses so nothing downstream caches them.
Blocks → Blade components. Headless CMS content is almost always an array of typed blocks. Map content type names to components explicitly rather than using dynamic component resolution on untrusted API data:
@foreach ($page->blocks as $block)
@switch($block->type)
@case('text') <x-block.text :block="$block" /> @break
@case('teaser') <x-block.teaser :block="$block" /> @break
@default {{-- log unknown type, render nothing --}}
@endswitch
@endforeach
The @default branch matters: a designer adding a new block type in the CMS should degrade gracefully, not throw a 500 in production.
Testing. If the SDK uses Guzzle, inject a MockHandler; if it uses Laravel's HTTP client, Http::fake() works transparently. Build a small fixture factory that returns representative payloads, and test your repository and Blade rendering against those fixtures — you are not testing the vendor SDK, you're testing your mapping. Keep one integration test against a real (or recorded) Nitro instance to catch upstream shape changes.
Resilience. Content APIs go down. Wrap reads so a failure degrades to a stale cache entry rather than a blank page where you can afford it — Cache::remember plus a try/catch that falls back to Cache::get() past its TTL is a cheap win for a marketing site.
Audit before you depend on it. Zero stars, zero dependents, and a very small contributor base. That doesn't make it bad — vendor-maintained SDKs for commercial products are often exactly like this — but it does mean: pin an exact version in composer.json, commit the lock file, and assume no community answers on Stack Overflow. Your fallback is reading src/, which is feasible here because the package is small.
Version and PHP constraints. Check composer.json for the PHP requirement and the SDK's release cadence before upgrading Laravel. A single-maintainer package can lag behind new PHP majors, and the release dates here are irregular enough that you shouldn't assume timely support for a new PHP version.
Tokens are not interchangeable. Live and preview tokens have different scopes. Never ship the preview token to the browser, and make sure the preview path is behind auth or a signed URL — otherwise you've exposed unpublished content and, depending on the API, potentially more.
Cache invalidation is your job. The SDK gives you reads; it will not tell you when content changed. You need either a short TTL plus a webhook from Flyo hitting an endpoint that flushes nitro:* keys, or a versioned cache namespace you bump on publish. Decide this on day one — retrofitting invalidation is painful.
Avoid per-item API calls. It's tempting to loop over a list and fetch each related entity. That turns one page render into twenty HTTP round-trips and will dominate your TTFB. Check whether the endpoint supports includes/relationship expansion; if not, fetch the collection once and resolve relations in memory.
Timeout and retry. Set an explicit connect/read timeout (2–5s is typical for a content API) and at most one retry on connection errors. Default Guzzle timeouts are effectively infinite and will hang your workers under upstream degradation.
Locale fallback. If you serve multiple languages, verify what Nitro returns for an untranslated entity — null, the fallback locale's content, or a 404. Your cache key must reflect the resolved locale, not just the requested one, or you'll serve English content to German visitors from cache.
Media URLs may be expiring or transform-parameterized. If images come back with signed URLs or query-based transformations, don't cache the URL string beyond its signature lifetime. Cache the media identifier and build the URL at render time.
Debugging. First move when something looks wrong: log the raw response body before DTO hydration. Most "the SDK is broken" reports turn out to be an unexpected payload shape (empty blocks, a renamed field, a 200 with an error envelope) that the hydrator swallows silently. If you have a dd()-able response object, use it once and keep the output as a fixture.
Extension points are thin. With a package this size, expect to subclass or decorate the client rather than find official hooks for custom headers, retries, or logging. Guzzle middleware (or a global Http::beforeSending hook) is usually the cleanest place to add request IDs and observability without forking.
Bottom line: viable if you're already on Flyo Nitro, and the SDK is small enough to audit and wrap. Budget half a day for the adapter, cache, and invalidation work — the SDK handles transport, but the production concerns (preview isolation, cache keys, failure modes) are all yours.
How can I help you explore Laravel packages today?