PHPackages                             weare-awesome/frisbee-php-api - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. weare-awesome/frisbee-php-api

ActiveLibrary[API Development](/categories/api)

weare-awesome/frisbee-php-api
=============================

Read-only PHP client for the Frisbee headless CMS Read API.

v2.2.0(2w ago)0732↓91.9%Apache-2.0PHPPHP ^8.0.2

Since Jul 5Pushed 7mo ago1 watchersCompare

[ Source](https://github.com/weare-awesome/frisbee-php-api)[ Packagist](https://packagist.org/packages/weare-awesome/frisbee-php-api)[ RSS](/packages/weare-awesome-frisbee-php-api/feed)WikiDiscussions master Synced 2w ago

READMEChangelog (1)Dependencies (6)Versions (8)Used By (0)

frisbee-php-api
===============

[](#frisbee-php-api)

A thin, **read-only** PHP client for the [Frisbee](https://frisbeecms.com) headless CMS Read API.

Content is authored in Frisbee and served over HTTP. This SDK fetches pages, lists, menus and the site map for a single **distribution** (one published site/channel). It does not write, publish or mutate anything.

> Building a site with an AI coding agent? Point it at [`AGENTS.md`](AGENTS.md) — a self-contained guide covering the full public surface plus how to design content-type schemas that match your templates field-for-field.

---

Requirements
------------

[](#requirements)

- PHP `^8.0.2`
- `guzzlehttp/guzzle`, `illuminate/collections`, `nesbot/carbon` (pulled in automatically)

Return types use `Illuminate\Support\Collection`, so it feels native in Laravel but works in any PHP app.

### Version compatibility

[](#version-compatibility)

Laravel 13 dropped Carbon 2. Version `2.x` widens the Carbon and Collections constraints to span both eras, so it installs cleanly on everything from Laravel 9 to 13:

SDK versionLaravel`nesbot/carbon`Status`^2.0`9 – 13`^2.62|^3.0`Current`^1.0`9 – 12`^2.62`Maintenance (Carbon 2 only)There are no code changes between the lines beyond dependency constraints and `ListCall`'s publish-date filtering — upgrading from `1.x` to `2.x` requires no changes to your integration.

Install
-------

[](#install)

```
composer require weare-awesome/frisbee-php-api
```

Configuration
-------------

[](#configuration)

Three values, all from the environment — never hard-code the token:

```
FRISBEE_READ_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
FRISBEE_DISTRIBUTION_ID=2
FRISBEE_READ_API_URL=https://read-v2.frisbeecms.com/api
```

Every request is a `GET` authenticated with `Authorization: Bearer `.

Getting started
---------------

[](#getting-started)

```
use GuzzleHttp\Client;
use WeAreAwesome\FrisbeePHPAPI\Frisbee;
use WeAreAwesome\FrisbeePHPAPI\Requests\Content\PageRequest;

$frisbee = Frisbee::make(
    new Client(),
    env('FRISBEE_READ_API_TOKEN'),
    (int) env('FRISBEE_DISTRIBUTION_ID'),
    env('FRISBEE_READ_API_URL'),
);

$page = $frisbee->read()->page(PageRequest::make('home'));

echo $page->title;
echo $page->section('Hero')->content('title')->raw();
```

### In Laravel

[](#in-laravel)

Bind it once in a service provider, then type-hint `Frisbee` anywhere:

```
// app/Providers/AppServiceProvider.php → boot()
$this->app->bind(Frisbee::class, fn () => Frisbee::make(
    new Client(),
    env('FRISBEE_READ_API_TOKEN'),
    (int) env('FRISBEE_DISTRIBUTION_ID'),
    env('FRISBEE_READ_API_URL'),
));
```

### Optional modifiers

[](#optional-modifiers)

```
$frisbee->read()->inLang('en');                        // ?lang=en
$frisbee->distributionTagOverride('preview')->read();  // ?distribution_tag=preview
```

Both are chainable and optional — omit them for the default live content.

---

The read API
------------

[](#the-read-api)

`$frisbee->read()` returns a `ReadAPI` with exactly five operations:

MethodReturnsEndpointUse for`page(PageRequest|PageCall)``Page``GET /page`One page by path/slug`pages(PagesRequest)`content resource(batch of `/page`)Several pages by path in one round`list(ListCall)``ContentList``GET /content-list`A paginated list of pages by type`map()``SiteMap``GET /distribution/map`The whole site map (for sitemap.xml)`distributionCall(DistributionCall)``DistributionPage``GET /distribution`Distribution-wide data### A single page

[](#a-single-page)

```
$page = $frisbee->read()->page(PageRequest::make('home'));
$page = $frisbee->read()->page(PageRequest::make('our-work/some-case-study'));
```

### A page plus related lists, concurrently

[](#a-page-plus-related-lists-concurrently)

Additional calls attached to a `PageRequest` run **in parallel** with the page fetch (Guzzle async), so a landing page and its posts cost one round-trip's latency, not two:

```
use WeAreAwesome\FrisbeePHPAPI\Requests\Content\ListCall;

$page = $frisbee->read()->page(
    PageRequest::make('blog')->addCall(new ListCall(
        'items',                    // key used to retrieve the result
        [76],                       // content_type_ids
        $request->input('page', 1), // page number
        10,                         // per page
        'published',                // order by
        'desc',                     // order direction
        [],                         // tag_ids to include
        [29, 30],                   // tag_ids to exclude
    ))
);

$items = $page->getAdditionalContent('items');  // ContentList
```

### A standalone list

[](#a-standalone-list)

```
$list = $frisbee->read()->list(new ListCall('work', [66], 1, 20, 'published', 'asc'));

$pages      = $list->content();     // Collection
$pagination = $list->pagination();  // ['total','per_page','current_page','cdn_url', …]
```

`ListCall` defaults:

```
new ListCall(
    string $key            = 'content-list',
    array  $contentTypeIds = [],
    int    $page           = 1,
    int    $perPage        = 100,
    string $orderBy        = 'publish_date',
    string $orderDirection = 'asc',
    array  $tags           = [],
    array  $excludeTags    = [],
);
```

Content-type and tag ids are numeric ids defined in Frisbee — get them from the CMS, don't guess.

### The site map

[](#the-site-map)

```
$items = $frisbee->read()->map()->getData();  // Collection of ['path','content_type_id', …]
```

---

Reading a page
--------------

[](#reading-a-page)

Pages expose properties via a magic getter — `cached_at`, `cdn_url`, `slug`, `title`, `description`, `published`, `content_type`, `content_version`, `menus`, `distribution`, `distribution_settings`, `meta`, `tags`. Anything else returns `null`.

Helpers:

MethodReturnsNotes`section(string $name)``SectionInterface`Missing → `NullSection` (safe)`menu(string $name)``MenuInterface`Missing → `NullMenu` (safe)`getMeta(string $key, string $default = '')`stringe.g. `seo_title`, `seo_description``metaWithCDN(string $key, $default = '')`stringMeta resolved to a full CDN image URL`getSetting(string $key, $default = '')` / `hasSetting()` / `joinSettings()`Distribution settings`publishedFormatted(string $format = 'd/m/Y')`string`published` via Carbon`availableLanguages()`arrayLanguage codes for the distribution`contentTypeName()`?stringContent type name, lower-cased`sectionDisplayable(string $name)`bool`getAdditionalContent(string $key)``ContentResource|null`Result of an additional call### Sections and content items

[](#sections-and-content-items)

The CMS defines section names and content-item titles; your templates address them by string.

```
$section = $page->section('Hero');
$section->isDisplayed();            // author's on/off toggle — always guard on this
$section->content('title')->raw();  // one item by title
$section->all();                    // Collection of every item, sorted by order
```

Content items resolve to a class based on their `type`: `text-box` → `Text`, `image` → `Image`, `gallery` → `Gallery`, `video-file` → `VideoFile`, `text-input-select` → `Select`, `link` → `Link`, `component` → `Component`, everything else → generic `ContentItem`.

MemberApplies toNotes`raw()`allThe raw body/value — the primary getter for text`notEmpty()` / `empty()`allPresence check`type`, `title`, `order`allProperties`meta(string $key, $default = '')`allItem-level metadata`render(array $attrs = [])`allBody wrapped in a `` with Frisbee edit tags`url()``Image`, `VideoFile`Full CDN-resolved URL`variant(string $size)``Image``Image::SMALL|MEDIUM|LARGE`; falls back to `url()``items()``Gallery``Image[]``variant(string $format)``VideoFile`URL for `VideoFile::WEBM|MP4|OGG`; falls back to `url()``sources()``VideoFile`Ordered `` list (preferred first): `['format','mime','file','url']``poster()` / `fallback()` / `preferredFormat()``VideoFile`Poster image URL, fallback text, preferred format key`value()``Select`The chosen option's value (alias of `raw()`)`is(string $value)` / `in(array $values)``Select`Test the selection — branch content on it`url()``Link`The link target (href); also `Image`/`VideoFile``displayText()` / `hasDisplayText()``Link`Display text (falls back to the URL); whether one was set`rows()``Component``Collection`; each row's sub-fields hydrate to their real types`content(string $title)` / `all()``ComponentRow`Read one sub-field by title / all sub-fields (sorted)Image URLs resolve against the page's `cdn_url`: a bare filename becomes `{cdn_url}/images/{filename}`; an absolute `http…` value is returned as-is.

### Menus

[](#menus)

```
foreach ($page->menu('Footer')->all() as $item) {
    $item->title(); $item->url(); $item->hasChildren(); $item->children();
}
```

### Blade example

[](#blade-example)

```
@if($page->section('Hero')->isDisplayed())

@endif
```

**Self-hosted video (`video-file`):** iterate `sources()` (preferred format first) to emit a standard multi-format `` element with a poster and fallback text:

```
@php($video = $page->section('Hero')->content('background_video'))
@if($video->notEmpty())

        @foreach($video->sources() as $source)

        @endforeach
        {{ $video->fallback() }}

@endif
```

**Conditional content from a select (`text-input-select`):** branch on the chosen value with `is()` / `in()` (they compare the option's machine value, not its label):

```
@php($layout = $page->section('Features')->content('layout'))
@if($layout->is('grid'))

@elseif($layout->in(['list', 'compact']))

@endif
```

**Link (`link`):** `url()` is the href, `displayText()` the label (falling back to the URL):

```
@php($cta = $page->section('Hero')->content('cta'))
@if($cta->notEmpty())
    {{ $cta->displayText() }}
@endif
```

**Repeater (`component`):** iterate `rows()`; each row's sub-fields hydrate to their real types, so nested images/videos/selects behave exactly like top-level content — no raw-array plumbing. Address sub-fields by title (which for a component sub-field is the schema **label**, falling back to `name`):

```
@foreach($page->section('Cards')->content('cards')->rows() as $row)

@endforeach
```

---

Errors and null-safety
----------------------

[](#errors-and-null-safety)

**Fetch time** — `page()`, `list()` and `map()` throw when the HTTP call fails:

ExceptionWhen`Requests\Content\Exceptions\FrisbeeContentNotFound`404 / 422 — page not found or not distributed`Exceptions\FrisbeeAuthorizationException`401 — bad or expired token`Exceptions\FrisbeeException`any other API error`Content\Exceptions\FrisbeeMalformedContentException`unexpected response shape```
try {
    $page = $frisbee->read()->page(PageRequest::make($slug));
} catch (FrisbeeContentNotFound) {
    abort(404);
}
```

**Read time** — content lookups **never throw**. Missing sections, items and menus return `NullSection` / `NullContent` / `NullMenu`, whose methods return safe empties (`''`, `false`, empty collection). So `$page->section('Maybe')->content('maybe')->raw()` degrades to `''` rather than erroring. Use `isDisplayed()` and `notEmpty()` to decide whether to render a block at all.

Caching
-------

[](#caching)

The SDK does **not** cache — every call hits the API. In production, cache fetched pages and lists at the application layer (keyed by slug) and bust on publish.

Namespace cheat sheet
---------------------

[](#namespace-cheat-sheet)

```
WeAreAwesome\FrisbeePHPAPI\Frisbee                         // entry point
WeAreAwesome\FrisbeePHPAPI\Api\ReadAPI                     // read operations
WeAreAwesome\FrisbeePHPAPI\Requests\Content\PageRequest    // build a page fetch
WeAreAwesome\FrisbeePHPAPI\Requests\Content\PagesRequest   // build a multi-page fetch
WeAreAwesome\FrisbeePHPAPI\Requests\Content\ListCall       // build a list fetch / additional call
WeAreAwesome\FrisbeePHPAPI\Requests\Content\DistributionCall
WeAreAwesome\FrisbeePHPAPI\Content\Page
WeAreAwesome\FrisbeePHPAPI\Content\ContentList             // content() + pagination()
WeAreAwesome\FrisbeePHPAPI\Content\SiteMap                 // getData()
WeAreAwesome\FrisbeePHPAPI\Content\Sections\Section
WeAreAwesome\FrisbeePHPAPI\Content\Types\{Text,Image,Gallery,VideoFile,Select,Link,Component,ComponentRow,ContentItem}
WeAreAwesome\FrisbeePHPAPI\Content\Menus\{Menu,MenuItem}
WeAreAwesome\FrisbeePHPAPI\Exceptions\{FrisbeeException,FrisbeeAuthorizationException}
WeAreAwesome\FrisbeePHPAPI\Requests\Content\Exceptions\FrisbeeContentNotFound

```

Licence
-------

[](#licence)

Apache-2.0

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance78

Regular maintenance activity

Popularity17

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~65 days

Recently: every ~2 days

Total

7

Last Release

19d ago

Major Versions

1.0.0 → 2.x-dev2026-07-21

1.x-dev → v2.1.02026-07-29

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/8135514?v=4)[Chris Endcliffe](/maintainers/chrisendcliffe)[@chrisendcliffe](https://github.com/chrisendcliffe)

---

Top Contributors

[![chrisendcliffe](https://avatars.githubusercontent.com/u/8135514?v=4)](https://github.com/chrisendcliffe "chrisendcliffe (21 commits)")

### Embed Badge

![Health badge](/badges/weare-awesome-frisbee-php-api/health.svg)

```
[![Health](https://phpackages.com/badges/weare-awesome-frisbee-php-api/health.svg)](https://phpackages.com/packages/weare-awesome-frisbee-php-api)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.9k556.2M21.5k](/packages/laravel-framework)[statamic/cms

The Statamic CMS Core Package

4.9k3.8M1.2k](/packages/statamic-cms)[craftcms/cms

Craft CMS

3.6k3.7M3.4k](/packages/craftcms-cms)[illuminate/support

The Illuminate Support package.

583115.4M45.2k](/packages/illuminate-support)[illuminate/http

The Illuminate Http package.

11938.5M8.2k](/packages/illuminate-http)[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3661.3M49](/packages/tencentcloud-tencentcloud-sdk-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
