# Streams > Streams is an agentic-first Laravel package system (Tailwind, Alpine, Laravel, Livewire) for data modeling, admin UI, REST APIs, and developer tooling. Streams are defined as JSON in streams/*.json and queried through a criteria and repository abstraction, so storage can change without changing application code. --- # Get started: Introduction > Streams overview for Laravel developers and teams. Source: https://streams.dev/docs/introduction ## What is Streams? Streams is a system of unified Laravel packages that provide an optimized foundation for **data modeling**, **admin interfaces**, **APIs**, and **development workflow**. Your team defines domain data as JSON stream configurations in `streams/`. Those definitions are version-controlled, reviewable, and shared across environments—the same way you already manage Laravel config and migrations. > Streams leans on domain-driven design. We call these domain abstractions **streams**. Working on **this repository**? Start with [This project](/docs/this-project). ## Who it is for Streams is built for **Laravel developers and teams** shipping real products: - Custom CMS and content sites - SaaS backends and tenant admin - Internal admin and product control panels - Headless APIs for mobile or SPA clients - Starter projects and composable addons You can adopt Core alone or compose **Core + UI + API + SDK + Testing** as your project requires. ## Core packages | Package | Role | |---------|------| | [Streams Core](/docs/core/introduction) | Data modeling, repositories, field types | | [Streams UI](/docs/ui/introduction) | Control panels, forms, tables, pages | | [Streams API](/docs/api/introduction) | REST endpoints for stream data | | [Streams SDK](/docs/sdk/introduction) | Scaffolding and dev workflow | | [Streams Testing](/docs/testing/introduction) | Test environment and helpers | ## Next steps - [Installation](/docs/installation) — new or existing Laravel projects - [Architecture](/docs/architecture) — how the pieces fit together - [Use cases](/docs/use-cases) — pick a path for your product type - [Configuration](/docs/configuration) — Laravel config and `streams/` layout ## Community - [Discord](https://discord.gg/vhz8NZC) - [GitHub](https://github.com/laravel-streams/streams) - [Stack Overflow](https://stackoverflow.com/search?q=laravel+streams) --- # Get started: Installation > Install Streams in a new or existing Laravel app: requirements, the version matrix, Composer constraints, and publishing config. Source: https://streams.dev/docs/installation ## Server requirements Streams requires a standard [Laravel-compatible environment](https://laravel.com/docs/deployment#server-requirements) with **PHP 8.2 or newer**. Core supports Laravel 10, 11, and 12, and this site (streams.dev) runs Laravel 12. The starter project and the `streams/testing` harness are still Laravel 10 only, and the SDK's MCP server needs Laravel 11.45+ or 12.41+. The [version matrix](#version-matrix) below has the details. For image handling, install GD or the Imagick PHP extension. See [Images](/docs/images) and [Core images](/docs/core/images). ## Version matrix | Package | Laravel 10 | Laravel 11 | Laravel 12 | Notes | |---------|:----------:|:----------:|:----------:|-------| | `streams/core` | Yes | Yes | Yes | Declares `^10\|^11\|^12`. Its CI runs on Laravel 10, through `streams/testing`. | | `streams/ui` | Yes | Yes | Yes | Follows Core. Requires Livewire 3. | | `streams/api` | Yes | Yes | Yes | Follows Core. | | `streams/sdk` generators and `streams:validate` | Yes | Yes | Yes | Its own test suite runs on Laravel 12. | | `streams/sdk` MCP server | No | 11.45+ | 12.41+ | Needs `laravel/mcp ^1.0`. On Laravel 10 it is skipped; use `streams:list --json` and `streams:validate --json`. | | `streams/testing` | Yes | Not yet | Not yet | Requires `orchestra/testbench ^8.36` (Laravel 10 only). Being widened to `^8.36\|^9.15\|^10.8` for Laravel 11 and 12. | | `streams/streams` starter | Yes | No | No | Pins `laravel/framework ^10.0`. | | streams.dev (this site) | No | No | Yes | Runs Laravel 12 on PHP `^8.2`, with Core (`rc/prep`), UI, and the SDK as a dev dependency. | Every PHP package needs **PHP 8.2 or newer**: Core, API, and SDK declare `php: ^8.2`, and UI and Testing require Core. Until `streams/testing` supports Laravel 11 and 12, the package test suites for Core, UI, and API run on Laravel 10 only, so treat 11 and 12 as supported but not yet verified in CI. See [Versions and support](/docs/versions) for install constraints and branches. ## New projects The fastest path is the official Streams starter: ```bash composer create-project streams/streams:1.0.x-dev my-app cd my-app php artisan serve ``` The starter ([laravel-streams/streams](https://github.com/laravel-streams/streams)) is a Laravel 10 application that requires Core, UI, and API. It has no tagged release yet, so the version is the `1.0` development branch. ## This repository (streams.dev) **streams.dev** is not the generic starter — it is the documentation site. Clone it to work on docs or reference patterns: ```bash git clone git@github.com:laravel-streams/streams.dev.git cd streams.dev composer install npm install && npm run dev php artisan serve ``` Production dependencies in this repo: - [streams/core](/docs/core/introduction) - [streams/ui](/docs/ui/introduction) Dev dependency: - [streams/sdk](/docs/sdk/introduction) **streams/api** is not required here. Add it when you need REST endpoints (see [API installation](/docs/api/installation)). See [This project](/docs/this-project) and [Local development](/docs/local-development). ## Existing Laravel projects Add only the packages you need: ```bash composer require streams/core:2.0.x-dev composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev composer require streams/core:2.0.x-dev streams/api:1.0.x-dev ``` Core is the only required package. UI and API are optional layers. Use the explicit `x-dev` constraints. None of these packages has a stable 2.x (Core) or 1.x (UI, API) tag yet, and a bare `composer require streams/core` installs the old **Core 1.10.4**. Alternatively, set `"minimum-stability": "dev"` and `"prefer-stable": true` in your `composer.json`. See [Versions and support](/docs/versions). ### Publish and configure Publishing is optional; Core works with its defaults. To change the config, publish it from Core's provider: ```bash php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=config ``` Core registers three tags: `config` (`config/streams/core.php`), `streams` (Core's own stream definitions, copied into `streams/`), and `public` (Core's public assets, into `public/vendor/streams/core`). UI and API also use the `config` tag, so pass `--provider` to publish only one package's file. See [Core installation](/docs/core/installation) and [Configuration](/docs/configuration). ### Team workflow Commit `streams/` and `streams/data/` to version control so stream definitions and filebase content stay in sync across developers and environments. Keep secrets in `.env` only. ## Local package development To contribute to Streams packages, use Composer path repositories pointing at local clones (see [Addons](/docs/addons) and [Project structure](/docs/project-structure)). ## Updating Update individual packages: ```bash composer update streams/core --with-dependencies composer update streams/ui --with-dependencies composer update streams/api --with-dependencies ``` Or update the full project: ```bash composer update ``` ## Related - [Versions and support](/docs/versions) - [Configuration](/docs/configuration) - [Architecture](/docs/architecture) - [Use cases](/docs/use-cases) --- # Get started: Use Cases > Starting paths for common product types with Streams. Source: https://streams.dev/docs/use-cases Pick the path closest to what your team is building. Each path lists packages to require and the first stream to define. ## Custom CMS or content site **Packages:** `streams/core`, `streams/ui` **First stream:** `pages` with filebase HTML or markdown entries and a `{path}` route. Your team edits content as flat files or entries under `streams/data/`. UI provides an editorial control panel when you need one. - [Content](/docs/content) - [Routing](/docs/routing) - [UI panels](/docs/ui/panels) ## SaaS product backend **Packages:** `streams/core`, `streams/ui`, `streams/api` **First stream:** a tenant-scoped domain model (e.g. `projects`, `subscriptions`) with Eloquent source. Use Laravel auth and policies as usual. Expose a JSON API for your SPA or mobile clients; use UI for internal admin or customer account settings. - [Users](/docs/users) - [API introduction](/docs/api/introduction) - [Databases](/docs/databases) ## Admin panel (internal ops) **Packages:** `streams/core`, `streams/ui` **First stream:** the primary entity your ops team manages (orders, users, inventory). Define a panel in UI configuration with navigation, tables, and forms generated from stream fields. - [Control panel](/docs/control-panel) - [Forms](/docs/forms) - [Tables](/docs/ui/tables) ## Product settings panel (customer-facing) **Packages:** `streams/core`, `streams/ui` **First stream:** settings or preferences owned by the authenticated user. Smaller scope than a full admin CP—focused pages and wizards for your product's configuration surface. - [UI pages](/docs/ui/pages) - [Components](/docs/components) ## Headless API only **Packages:** `streams/core`, `streams/api` **First stream:** the resource you expose publicly or to partners. Skip UI unless you later add an admin. Configure API interfaces, auth, and custom endpoints as needed. - [API introduction](/docs/api/introduction) - [Query parameters](/docs/api/query-parameters) - [Client library](/docs/client/introduction) ## Greenfield starter **Packages:** full starter via `composer create-project` (Core, UI, API, SDK) Use the starter to learn conventions, then strip packages you do not need. - [Installation](/docs/installation) - [Addons](/docs/addons) --- # Get started: Configuration > Laravel config, environment variables, and the streams directory. Source: https://streams.dev/docs/configuration ## Introduction Streams uses Laravel config files and environment variables alongside stream definitions in `streams/`. ### Streams directory Your team typically commits: ```files ├── streams/ │ ├── users.json │ └── pages.json ├── streams/data/ │ └── … entry files … ``` Stream JSON describes domain models; `streams/data/` holds entries when using filebase storage. ### Configuration files Published configuration files reside in `config/streams/`. ``` files ├── config/streams/ │ ├── core.php │ ├── api.php │ └── ui.php ``` ### Publishing Configuration Core, UI, and API all register a `config` tag, so this publishes every installed package's config file: ```bash php artisan vendor:publish --tag=config ``` To publish configuration for a specific package, name its provider: ```bash php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=config ``` The above commands will copy configuration files from their package location to the directory mentioned above so that you can modify them directly and commit them to your version control system. ## Environment Variables It is often helpful to have different configuration values based on the environment in which your application is running. For example, you may wish to enable "debug mode" on your local server but not your production server. ### The `.env` File Environmental variables are defined in the `.env` file in your project's root directory. In fresh installations, Composer will automatically rename the included `.env.example` file to `.env` for you. You can manually copy and rename, or use `php -r "copy('.env.example', '.env');"` if the file does not already exist. ### Environment Variable Types Variables in your `.env` files parse as strings. A couple specific values are worth noting: ```bash EXAMPLE_VAR= # (string) '' EXAMPLE_VAR=null # (null) null ``` If you need to define an environment variable value containing a space, you may enclose the value in double-quotes. ``` env APP_NAME="Spaghetti + Meatballs" ``` ### Retrieving Environment Variables All environmental variables are available in configuration files by using the `env()` helper function. An optional second argument allows you to pass a default value. ``` php // config/app.php 'debug' => env('APP_DEBUG', false), ``` Once passed into a config file, the variable is available using the `config()` helper function. Again, an optional second argument allows you to specify a default value. ``` php // Retrieve the above 'debug' value: config('app.debug', false) ``` ### Do not version your `.env` file The `.env` file **should not be committed to version control**. Each developer or server running your application may require a different environment configuration. It is also a security risk if a nefarious character gains access to your version control repository because sensitive data like credentials, API keys, and other configuration would be visible to them. ### Hiding Environment Variables from Debug Pages When an exception is uncaught, and the `APP_DEBUG` environment variable is `true`, the debug page will show all environment variables and their assigned values. You may obscure variables by updating the `debug_blacklist` option in your `config/app.php` file. ``` php return [ // ... 'debug_blacklist' => [ '_ENV' => [ 'APP_KEY', 'SECRET_API_KEY', 'BITCOIN_WALLET_PW', ], '_SERVER' => [ 'APP_KEY', 'DB_PASSWORD', ], '_POST' => [ 'password', ], ], ]; ``` Learn more about [environment configuration](https://laravel.com/docs/configuration#environment-configuration) in the Laravel docs. --- # Get started: Versions and support > Which Laravel and PHP versions each Streams package supports, and how to require them. Source: https://streams.dev/docs/versions Streams is pre-release. Most packages have **no tagged releases yet**, so you install them from their development branches. This page lists what each package declares in its `composer.json` (or `package.json`) and what it is tested against today. ## Support matrix | Package | Install constraint | Branch | Laravel | PHP | Notes | |---------|--------------------|--------|---------|-----|-------| | [streams/core](/docs/core/introduction) | `2.0.x-dev` | `2.0` | `^10\|^11\|^12` | `^8.2` | Stable tags exist only for the old 1.x line (latest `v1.10.4`). | | [streams/ui](/docs/ui/introduction) | `1.0.x-dev` | `1.0` | via Core | via Core | Requires Livewire `^3.0`. | | [streams/api](/docs/api/introduction) | `1.0.x-dev` | `1.0` | via Core | `^8.2` | | | [streams/sdk](/docs/sdk/introduction) | `1.0.x-dev` | `1.0` | via Core | `^8.2` | Install as a dev dependency. The optional [MCP server](/docs/mcp) needs `laravel/mcp`, so Laravel 11.45+ or 12.41+. | | [streams/testing](/docs/testing/introduction) | `1.0.x-dev` | `1.0` | **10 only, for now** | via Core (8.2+) | Requires `orchestra/testbench ^8.36`, which targets Laravel 10. Being widened to `^8.36\|^9.15\|^10.8` (Laravel 10 to 12). | | [streams/mongodb](/docs/core/sources-and-adapters) | `1.0.x-dev` | `1.0` | via Core | via Core | Experimental. Requires `mongodb/mongodb ^1.10`. | | [@laravel-streams/api-client](/docs/client/introduction) | `3.0.0` | `master` | n/a | n/a | npm package, zero runtime dependencies. | | `streams/streams` (starter) | `1.0.x-dev` | `1.0` | `^10.0` | `^8.0.2` declared (Core needs 8.2+) | Pins Laravel 10; requires Core, UI, and API. | "Via Core" means the package declares no framework or PHP constraint of its own and accepts whatever `streams/core ^2.0` accepts. Core, API, and SDK declare PHP `^8.2`, so every package needs PHP 8.2 or newer. The Laravel 10/11/12 [version matrix](/docs/installation#version-matrix) on the installation page shows the same support per Laravel version. ### What is actually tested - Core, UI, and API run their CI on **Laravel 10** (10.49). The shared harness, `streams/testing`, is built on testbench 8, which is Laravel 10 only. Widening it to testbench 9 and 10 is in progress. - Core **declares** Laravel 11 and 12 support, but the Core, UI, and API suites don't run on them until `streams/testing` is widened. Treat 11 and 12 as expected to work, not verified. - `streams/sdk` has its own harness and runs on **Laravel 12.41** with `laravel/mcp`, so the SDK and its MCP server are tested on Laravel 12. - This site (streams.dev) runs **Laravel 12** on PHP `^8.2`, with `streams/core` from `rc/prep` (aliased as `2.0.x-dev`), `streams/ui 1.0.x-dev`, and `streams/sdk` from `sdk/rc` as a dev dependency. It doesn't use `streams/testing`, so its own test suite runs on Laravel 12 and PHPUnit 11. ## Requiring the packages Because there are no stable 2.x (Core) or 1.x (everything else) tags, Composer's default `minimum-stability: stable` won't find them. Worse, a bare `composer require streams/core` silently installs **Core 1.10.4**, the previous major version. Require the development branches explicitly, and require Core alongside anything that depends on it: ```bash composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev composer require streams/core:2.0.x-dev streams/api:1.0.x-dev composer require --dev streams/sdk:1.0.x-dev ``` Or allow dev packages project-wide while still preferring stable releases of everything else: ```json { "minimum-stability": "dev", "prefer-stable": true } ``` After that, `^2.0` for Core and `^1.0` for the others resolve to the development branches. ## Versioning policy The plan is to adopt [semantic versioning](https://semver.org) and tag stable releases (Core 2.0, and 1.0 for UI, API, SDK, and Testing) as part of the release-candidate work. Until tags exist: - Development branches can change without notice. Pin a commit (`2.0.x-dev#abc1234`) if you need a fixed point. - Breaking changes are listed in the [Changelog](/docs/changelog) and explained in the [Upgrade guide](/docs/upgrading). - The JavaScript client is already semver-tagged on npm (`3.0.0`). ## Related - [Installation](/docs/installation) - [Upgrade guide](/docs/upgrading) - [Changelog](/docs/changelog) --- # Get started: Changelog > Notable changes to the Streams packages, newest first, grouped by month. Source: https://streams.dev/docs/changelog Streams packages are pre-release and installed from development branches, so this log is organized by month and branch rather than by version. Entries come from each repository's commit history. Changes that need action on your side are marked **Breaking** and explained in the [Upgrade guide](/docs/upgrading). ## September 2026 - **SDK (`1.0` release candidate):** Added a local-development [MCP server](/docs/mcp) (`php artisan mcp:start streams`, needs `laravel/mcp` on Laravel 11.45+ or 12.41+) with 14 tools, 2 resources, and the `design-stream` prompt. Added [`streams:validate`](/docs/sdk/commands#streamsvalidate) and [`streams:list --json`](/docs/sdk/commands#streamslist), and the [stream definition schema](/docs/sdk/stream-schema), served at `/schema/streams.schema.json`. `streams:livewire` now generates Livewire 3 components in `App\Livewire`. **Breaking:** `streams:admin` was removed, and `make:stream` and `streams:livewire` refuse to overwrite files without `--force`. The SDK now requires PHP 8.2. - **Core, API (release candidates):** Require PHP 8.2. Core supports the Laravel 11 and 12 filesystem contract and Carbon 3. - **Docs:** streams.dev is now the single source of truth for package documentation. Added [Versions and support](/docs/versions), this changelog, the [Upgrade guide](/docs/upgrading), `/llms.txt`, `/llms-full.txt`, and raw markdown for every page (append `.md` to any docs URL). Also added [Agents](/docs/agents), [MCP](/docs/mcp) (then planned; it has since shipped in `streams/sdk`), [Workflows](/docs/workflows), [Tenancy](/docs/tenancy), [Theming](/docs/ui/theming), and the [SDK command reference](/docs/sdk/commands). - **UI (`1.0`):** HTML attribute support on more components. ## August 2026 - **Core (`2.0`):** Added `elasticsearch` and `opensearch` source adapters built on a shared `AbstractSearchIndexAdapter`. Configure OpenSearch connections under `streams.core.opensearch`. The client libraries are suggested dependencies, not required. See [Sources and adapters](/docs/core/sources-and-adapters). - **UI (`1.0`):** **Breaking.** The OpenSearch adapter moved from UI to Core. - **UI (`1.0`):** Accessible `ChartWidget` with an empty state; table search is ANDed with active filters; modal and file-input close actions. ## July 2026 - **Core (`2.0`):** **Breaking.** `Criteria::with()` attaches eager-loaded relationships under the relation name (the field handle without a trailing `_id`, or the field's `relation` config) and leaves the foreign-key attribute as a scalar. - **API (`1.0`):** `with` / `with[]` eager loading matches relation names, following the Core change. - **UI (`1.0`):** Bulk actions, pages and navigation updates, vertical navigation style, `MenuItem` modal support, image builder, icon-only actions, and button size/radius options. ## June 2026 - **API (`1.0`):** **Breaking.** Endpoint builders with explicit route registration. Controllers under `Streams\Api\Http\Controller\…` became invokable endpoints under `Streams\Api\Endpoints\…`; routes are mounted per `ApiInterface`; access runs through the configurable `gate_middleware`. See [Custom interfaces](/docs/api/custom-interfaces). - **API (`1.0`):** `API::tenant()` and `ApiInterface::tenant()` for per-request tenant resolution. See [Tenancy](/docs/api/tenancy). - **Core (`2.0`):** Two-argument `where('field', $value)` handling fix. - **UI (`1.0`):** Table row selection fixes, row attributes, form spacing, required flags, and unified table filter state. ## May 2026 - **Core (`2.0`):** `Criteria::whereIn()`; `getIdAttribute()` on the Streams trait. - **UI (`1.0`):** FullCalendar builder, modal header and footer components, grid IDs, bulk action improvements, and form state path prefixes. ## January to April 2026 - **UI (`1.0`):** Timeline component, panel wizard, breadcrumbs, `Action` extends `MountableAction`, file uploads on pages, and table views. - **API (`1.0`):** Minor fixes to the entry show endpoint. - **Client (`3.0.0`):** Tagged January 7 and published to npm from GitHub Actions. Version 3 is a zero-dependency JavaScript rewrite of the TypeScript client. ## Related - [Upgrade guide](/docs/upgrading) - [Versions and support](/docs/versions) --- # Get started: Upgrade guide > How to stay current on the development branches, and what to change for each breaking change. Source: https://streams.dev/docs/upgrading Streams packages are installed from development branches until stable tags exist (see [Versions and support](/docs/versions)). Upgrading means pulling the latest commit on your branch and handling any breaking changes listed below. The [Changelog](/docs/changelog) has the full list. ## Updating ```bash composer update streams/core streams/ui streams/api --with-dependencies composer update streams/sdk streams/testing ``` Then clear cached config and views, and run your tests: ```bash php artisan optimize:clear php artisan test ``` Commit `composer.lock` so your team and CI run the same commits. ## Breaking changes ### SDK: `streams:admin` removed, `--force` required (September 2026) The `streams/sdk` release candidate changes its generators: - `streams:admin` is gone. Generate the index, form, and show components with `php artisan streams:livewire {stream}` and put them behind your own layout and routes. Files it generated before are yours and keep working. - `streams:livewire` writes Livewire 3 classes to `config('livewire.class_namespace')` (default `App\Livewire`, not `App\Http\Livewire`) and views to `resources/views/livewire/{stream}-{type}.blade.php`. It generates all three components unless you pass `--type`. - `make:stream` and `streams:livewire` stop instead of overwriting existing files. Pass `--force` to overwrite. - The SDK requires PHP 8.2. The optional MCP server needs `laravel/mcp`, which needs Laravel 11.45+ or 12.41+. See the [command reference](/docs/sdk/commands). ### OpenSearch adapter moved to Core (August 2026) The OpenSearch adapter was removed from `streams/ui` and added to `streams/core`, next to a new Elasticsearch adapter. - Streams using `"source": {"type": "opensearch"}` keep working once Core is updated. Core resolves `opensearch` and `elasticsearch` on its own. - If you referenced the class directly, change `Streams\Ui\Criteria\Adapter\OpenSearchAdapter` to `Streams\Core\Criteria\Adapter\OpenSearchAdapter`. - Move connection config from `streams.opensearch.*` to `streams.core.opensearch.*` (publish `config/streams/core.php` or set `OPENSEARCH_HOST`, `OPENSEARCH_USERNAME`, `OPENSEARCH_PASSWORD`, and `OPENSEARCH_SSL_VERIFICATION`). - Require `opensearch-project/opensearch-php` in your app. Core only suggests it. ### Eager loading uses relation names (July 2026) `Criteria::with()` now takes the **relation name** and attaches the related entry under that name. The foreign-key attribute stays a scalar. ```php // Before: the related entry replaced the foreign key $post = Streams::entries('posts')->with(['author_id'])->first(); $post->author_id; // Entry // After: ask for the relation name $post = Streams::entries('posts')->with(['author'])->first(); $post->author; // Entry $post->author_id; // 42 ``` The relation name is the field handle with a trailing `_id` removed, or the relationship field's `relation` config if set. API clients change `with=author_id` to `with=author` the same way. ### API endpoint builders and explicit interfaces (June 2026) `streams/api` was rebuilt around interfaces, resources, and invokable endpoints. | Before | After | |--------|-------| | `Streams\Api\Http\Controller\Entries\GetEntries` | `Streams\Api\Endpoints\Entries\ListEntries` | | `Streams\Api\Http\Controller\Streams\GetStreams` | `Streams\Api\Endpoints\Streams\ListStreams` | | Other `Streams\Api\Http\Controller\{Entries,Streams}\*` | Same class name under `Streams\Api\Endpoints\{Entries,Streams}\*` | | `SetUpInterface` middleware, alias `interface`, pushed into the `api` group | `SetUpApiInterface`, alias `api.interface`, applied to API routes only | | Route names always included the interface ID (`streams.api.api.entries.list`) | The default interface omits it (`streams.api.entries.list`); other interfaces keep it (`streams.api.v1.entries.list`) | | `enabled` config was informational | `enabled` is enforced by `gate_middleware`; API routes return 404 until `STREAMS_API_ENABLED=true` | To upgrade: 1. Set `STREAMS_API_ENABLED=true` wherever the API should respond. 2. Register routes with `API::routeCrud()` (or `routeEntries()` / `routeStreams()`) or `API::interface(...)` from a service provider's `boot()` method, not inside a prefixed route group. See [API installation](/docs/api/installation). 3. Move custom controllers that extended the old controllers onto the new endpoint classes. See [Custom endpoints](/docs/api/custom-endpoints). 4. Move authentication into interface middleware or your own gate. See [API authentication](/docs/api/authentication). ## Core 1.x to 2.0 Core 2.0 is a separate branch from the 1.x line (last tag `v1.10.4`) and is what every current Streams package requires. There is no written migration guide from 1.x yet. If you are on 1.x, start from the [Core introduction](/docs/core/introduction) and treat 2.0 as a new install. ## Related - [Changelog](/docs/changelog) - [Versions and support](/docs/versions) --- # Guides: Core Concepts: Architecture > How Streams packages fit together in a Laravel application. Source: https://streams.dev/docs/architecture ## Overview A Streams application is a normal Laravel project with additional conventions. Streams does not replace Laravel routing, config, or deployment—it extends them. ```mermaid flowchart TB subgraph git [In version control] StreamsJSON["streams/*.json"] StreamData["streams/data/"] LaravelConfig["config/ + .env"] end subgraph packages [Composer packages] Core[streams/core] UI[streams/ui] API[streams/api] end subgraph runtime [Runtime] Entries[Entries and repositories] CP[Control panel] REST[REST API] end StreamsJSON --> Core StreamData --> Core Core --> Entries Core --> UI Core --> API UI --> CP API --> REST ``` ## What your team commits | Path | Purpose | |------|---------| | `streams/*.json` | Stream definitions—fields, sources, routes, UI config | | `streams/data/` | Entry data when using filebase storage | | `config/streams/` | Published package configuration | | `app/` | Custom PHP when JSON is not enough | Environment-specific values stay in `.env`, not in stream JSON. ## Streams Core Core loads stream definitions, resolves data sources (filebase, Eloquent, collections, remote), and exposes repositories and criteria for querying entries. Hub guide: [Streams](/docs/streams) Reference: [Core documentation](/docs/core/introduction) ## Streams UI UI builds control panels, forms, tables, and pages from stream and panel configuration. It runs on Livewire and Tailwind—familiar Laravel frontend stack. Hub guide: [UI](/docs/ui) Reference: [UI documentation](/docs/ui/introduction) ## Streams API API registers REST routes for streams you expose. Response format follows the existing Streams API envelope (see [Responses](/docs/api/responses)). Hub guide: [API](/docs/api) Reference: [API documentation](/docs/api/introduction) ## Composing packages | Goal | Typical packages | |------|------------------| | Data layer only | `streams/core` | | Admin for your team | `streams/core`, `streams/ui` | | Headless + admin | `streams/core`, `streams/ui`, `streams/api` | | Faster scaffolding | Add `streams/sdk` (dev) | | CI and package tests | Add `streams/testing` (dev) | See [Use cases](/docs/use-cases) for concrete starting points. --- # Guides: Core Concepts: Streams > Get started with the stream modeling engine. Source: https://streams.dev/docs/streams ## Introduction The Streams platform leans heavily on domain-driven design (DDD). We call these domain abstractions `streams`, hence our namesake. An example could be configuring a domain model (a stream) for a website's pages, users of an application, or feedback submissions from a form. Streams describe your data structures. ## Defining Streams Using JSON files, you can define stream configurations in the `streams/` directory. The filenames serve as the stream's `id`. It is highly encouraged to use the plural form of a noun when naming Streams—for example, contacts and people. Also, naming conventions like `business_contacts` or `neat-people` work well. ```files ├── streams/ │ ├── users.json │ ├── pages.json │ └── contacts.json ``` ### The Basics To get started, you need only specify the `id`, which is the filename itself, and some `fields` to describe the domain object's structure. Let's create a little stream to hold information for a simple CRM. In `streams/contacts.json`: ```json { "name": "Contacts", "description": "A simple address book.", "config": { "source": { "type": "filebase", "path": "streams/data/contacts", "format": "json" }, "abstract": "Streams\\Core\\Entry\\Entry", "criteria": "Streams\\Core\\Criteria\\Criteria", "repository": "Streams\\Core\\Repository\\Repository", "collection": "Illuminate\\Support\\Collection" }, "fields": { "name": "string", "email": "email", "company": { "type": "relationship", "config": { "related": "companies" } } } } ``` ### Fields - [Fields](/docs/fields) - [Field Types](/docs/fields#field-types) **Fields** are an essential descriptor of the domain object. They describe what properties the domain object will have and how they work. Field **types** control things like accessors, data mutation, and casting. The **field configuration keys** serve as a `handle`, which you can use to reference the field later. So, for example, you may access the above contact fields like this: ```php $entry->email; $entry->company->email; ``` ### Stream Routes - [Stream Routes](/docs/routing#stream-routes) - [Route Options](/docs/core/routes) Streams can simplify **routing** by defining associated routes in their definition. In `streams/contacts.json`: ```json { "routes": { "index": "contacts", "view": "contacts/{id}" } } ``` You can also use an array to include other **route options**. In `streams/contacts.json`: ```json { "routes": { "contact": { "csrf": false, "uri": "form/{entry.email}" } } } ``` ### Stream Validation Streams simplifies **validation** by defining validation in their definition. - [Validation](/docs/core/validation) In `streams/contacts.json`: ```json { "rules": { "name": [ "required", "max:100" ], "email": [ "required", "email:rfc,dns" ], "company": "required|unique" } } ``` ### Security Specify the [Laravel policy](https://laravel.com/docs/authorization#creating-policies) class to use for the stream. There is no separate security guide. In `streams/contacts.json`: ```json { "policy": "App\\Contacts\\ContactPolicy" } ``` ### Caching Streams provides a touch-free caching system you can define in the configuration. - [Caching](/docs/core/caching) In `streams/contacts.json`: ```json { "config": { "cache": { "enabled": true, "ttl": 1800, "store": "file" } } } ``` Caching is off unless `config.cache.enabled` is `true`. `ttl` is in seconds (default 3600), and `store` defaults to your default cache store. ### Sources Sources define the source information for entry data which you can define in the configuration. - [Sources and adapters](/docs/core/sources-and-adapters) In `streams/contacts.json`: ```json { "source": { "type": "filebase", "format": "md" } } ``` ## Stream Entries Domain entities are called `entries` within the Streams platform. A stream defines entry attributes, or `fields`, that dictate the entry's properties, data-casting, and more. - [Entries](/docs/core/entries) ### Abstracts The **abstract** parameter defines the class to use when constructing entry instances. - [Entries](/docs/core/entries) In `streams/contacts.json`: ```json { "abstract": "App\\Contacts\\Contact" } ``` > When defining Elqouent stream sources, the sources model will be used as the abstract. ### Criteria The **criteria** parameter defines the class to use when building entry queries. - [Criteria](/docs/core/criteria) In `streams/contacts.json`: ```json { "criteria": "App\\Contacts\\ContactCriteria" } ``` ### Repositories The **repository** parameter defines the repository class to use for the stream entries. - [Repositories](/docs/core/repositories) In `streams/contacts.json`: ```json { "repository": "App\\Contacts\\ContactRepository" } ``` ## Advanced Streams ### JSON References You can use JSON file references within stream configurations to point to other JSON files using the `@` symbol followed by a relative path to the file. In this way, you can reuse various configuration information or tidy up larger files. **The referenced file's JSON data directly replaces the reference.** In `streams/contacts.json`: ```json { "name": "Contacts", "fields": "@streams/fields/contacts.json" } ``` In `streams/fields/contacts.json`: ```json { "name": "string", "email": "email", "company": { "type": "relationship", "stream": "company" } } ``` ### Extend a Stream A stream can `extend` another stream, which works like a recursive **merge**. In `streams/family.json`: ```json { "name": "Family Members", "extend": "contacts", "fields": { "relation": { "type": "select", "config": { "options": { "mother": "Mother", "father": "Father", "brother": "Brother", "sister": "Sister" } } } } } ``` In the above example, all `contacts` fields are available to you, as well as the new `relation` field. ```php $entry->email; // The email value. $entry->relation; // The relation value. ``` ### Stream Sources You can configure the flat-file database as well as other sources for storing data including any Laravel database. No code changes required. For full reference, see [Core — Streams](/docs/core/streams), [Core — Caching](/docs/core/caching), and [Core — Validation](/docs/core/validation). --- # Guides: Core Concepts: Fields > Fields, types, and inputs are documented here. Source: https://streams.dev/docs/fields ## Introduction Fields represent the type and characteristics of your stream data. For example a "name" field would likely be a **string** field type. Fields are strictly concerned with data. Please see the [UI package](/docs/ui/introduction) for configuring field [inputs](/docs/ui/inputs). ## Defining Fields Fields can be defined within the JSON [configuration for your streams](/docs/streams#defining-streams). You can get started by simply defining fields by `handle` and their `type` respectively. #### Basic Example In `streams/contacts.json`: ```json { "fields": [ { "handle": "title", "type": "string" } ] } ``` #### Full Example To define more information about the field use an array: In `streams/contacts.json`: ```json { "fields": [ { "handle": "title", "name": "Title", "description": "The title of the film.", "type": "string", "rules": ["min:4"], "config": { "default": "Untitled" }, "example": "Star Wars: The Force Awakens", "protected": false } ] } ``` ### Field Validation Define [Laravel validation rules](https://laravel.com/docs/validation#available-validation-rules) for fields and they will be merged the [stream validation rules](/docs/streams#stream-validation). In `streams/contacts.json`: ```json { "fields": [ { "handle": "name", "type": "string", "rules": ["required", "max:100"] }, { "handle": "email", "type": "email", "rules": ["required", "email:rfc,dns"] }, { "handle": "company", "type": "string", "rules": ["required", "unique"] } ] } ``` ## Basic Usage Values are stored as an [image source](/docs/images#image-sources) ```php Image::make($entry->profile_image)->url(); ``` ### Field Decorators Field decorators provide expanded function to entry attributes like a universal presenter. The below example demonstrates the `image` field decorator: ```php $entry->decorate('profile_image')->url(); ``` You may also use magic methods derived from "camel casing" the field's handle to invoke decoration. ```php $entry->profileImage()->url(); ``` ## Field Types The field type is responsible for validating, casting, and more for its specific data type. These are the 24 types registered by Core (`streams.core.field_types`); apps and addons can register more. Each example is a complete field in list form, so it validates against the [stream definition schema](/docs/sdk/stream-schema). ### String ```json { "handle": "title", "type": "string" } ``` ### URL ```json { "handle": "website", "type": "url" } ``` ### UUID `"default": true` in `config` generates a UUID when the attribute is missing. ```json { "handle": "id", "type": "uuid", "config": { "default": true } } ``` ### Hash ```json { "handle": "password", "type": "hash" } ``` ### Slug `config.separator` sets the word separator (default `-`). ```json { "handle": "slug", "type": "slug", "config": { "separator": "-" } } ``` ### Email ```json { "handle": "email", "type": "email" } ``` ### Encrypted ```json { "handle": "api_token", "type": "encrypted" } ``` ### Color Values are stored lowercase and must parse as a color. The decorator adds `hex()`, `rgb()`, `rgba()`, and the individual channels; `config.format` picks the default output (`hex`). ```json { "handle": "brand_color", "type": "color" } ``` ### Number ```json { "handle": "rating", "type": "number" } ``` ### Integer ```json { "handle": "age", "type": "integer" } ``` ### Decimal `config.precision` rounds to that many decimal places. ```json { "handle": "price", "type": "decimal", "config": { "precision": 2 } } ``` ### Boolean ```json { "handle": "published", "type": "boolean" } ``` ### Date `config.format` is the storage format (default `Y-m-d`). ```json { "handle": "birthday", "type": "date" } ``` ### Time `config.format` defaults to `H:i:s`; `config.timezone` defaults to `app.timezone`. ```json { "handle": "opens_at", "type": "time" } ``` ### Datetime `config.format` defaults to `Y-m-d H:i:s`; `config.timezone` defaults to `app.timezone`. ```json { "handle": "published_at", "type": "datetime", "config": { "timezone": "UTC" } } ``` ### Enum `enum` is an alias of `select`. Both need `config.options`. ```json { "handle": "size", "type": "enum", "config": { "options": { "sm": "Small", "lg": "Large" } } } ``` ### Select ```json { "handle": "status", "type": "select", "config": { "options": { "draft": "Draft", "live": "Live" } } } ``` ### Multiselect ```json { "handle": "tags", "type": "multiselect", "config": { "options": { "news": "News", "guides": "Guides" } } } ``` ### Array `config.items` lists the allowed item types. Each item must pass at least one of them. Set `config.enforce_items` to `false` to skip the check, or use `config.related` (or `config.stream`) to cast items to entries of a stream. ```json { "handle": "scores", "type": "array", "config": { "items": [ {"type": "string"}, {"type": "number"} ] } } ``` ### Object `config.allowed` lists the value types an object may be. Each item names a `stream` (an entry of that stream), a `generic` class, or a `prototype` class. Without `allowed`, any object is accepted. ```json { "handle": "address", "type": "object", "config": { "allowed": [ {"stream": "addresses"}, {"generic": "Illuminate\\Support\\Collection"} ] } } ``` ### File The value is a path string. The decorator adds file helpers. Core does not restrict file types; add Laravel `rules` for that. ```json { "handle": "attachment", "type": "file" } ``` ### Image A `file` whose decorator works with the [image manager](/docs/images). ```json { "handle": "profile_image", "type": "image" } ``` ### Relationship `config.related` names the related stream. `related` next to `type` is ignored. Add `"multiple": true` to store a list of keys, and `key_name` if the related stream is not keyed by `id`. ```json { "handle": "author_id", "type": "relationship", "config": { "related": "authors" } } ``` ### Polymorphic Stores a reference to an entry of any stream as `{"@stream": "posts", "id": "..."}` and restores the entry when read. `config.related` optionally documents the streams you expect; Core does not enforce it. ```json { "handle": "commentable", "type": "polymorphic", "config": { "related": ["posts", "pages"] } } ``` --- # Guides: The Basics: Files > Files and image handling. Source: https://streams.dev/docs/files ## Introduction Streams comes with a simple wrapper for the Laravel filesystem. ### Files Stream First, define a stream for the filesystem data: ```json { "name": "Files", "handle": "files", "description": "Basic filesystem cache.", "config": { "source": { "format": "json" } }, "fields": [ { "handle": "id", "type": "uuid", "required": true, "unique": true, "config": { "default": true } }, { "handle": "path", "type": "string", "required": true }, { "handle": "is_dir", "type": "boolean", "required": true }, { "handle": "disk", "type": "string", "required": true }, { "handle": "name", "type": "string", "required": true }, { "handle": "size", "type": "integer" }, { "handle": "mime_type", "type": "string" }, { "handle": "visibility", "type": "string" }, { "handle": "last_modified", "type": "datetime" }, { "handle": "extension", "type": "string" } ] } ``` ### Configuration To get started, define the data stream for the filesystem disk you wish to use. ```php // config/filesystems.php // ... 'disks' => [ 'local' => [ 'stream' => 'files', ], ], ``` ## Filesystem Wrapper The `StreamFilesystem` decorates Laravel's configured filesystem. In order to leverage streams fully you will need to use this wrapper to interact with the filesystem. The wrapper exists only to sync filesystem data to the configured stream. ```php $filesystem = Streams::filesystem(string $disk); // If you happen to have the stream. $filesystem = $stream->filesystem(string $disk); // Use as normal. $filesystem->put($path, $content); ``` ### Indexing Files Use the `index` method to index the filesystem to the configured stream. ```php $filesystem->index(string $path = '/'); ``` ### Filesystem Data After indexing, the filesystem data can be used normally. ```php $images = Streams::entries('files') ->where('extension', 'jpg') ->orderBy('size', 'asc') ->get(); ``` --- # Guides: The Basics: Content > Filebase pages and content fragments in Streams. Source: https://streams.dev/docs/content ## Introduction Streams stores content as **entries** in configured sources. This hub page covers common content patterns; field types and adapters are documented in [Core](/docs/core/introduction). ## Pages stream A typical pages stream uses the **filebase** source (default) with HTML or Markdown files: ```json { "id": "pages", "config": { "source": { "type": "filebase", "format": "html" } }, "routes": [ { "handle": "view", "uri": "{uri}", "parse": true, "view": "{layout}" } ], "fields": [ { "handle": "id", "type": "slug", "required": true, "unique": true }, { "handle": "title", "type": "string", "required": true }, { "handle": "uri", "type": "string", "required": true, "unique": true }, { "handle": "body", "type": "string" } ] } ``` ### Entry frontmatter Each file in `streams/data/pages/` carries YAML frontmatter plus a Blade/HTML body: ```html --- title: Welcome uri: / layout: blank --- @include('partials.topbar')

{{ $entry->title }}

``` | Key | Role | |-----|------| | `uri` | URL path for `parse: true` routes | | `layout` | Blade layout passed to `{layout}` in the route definition | | `title` | Stored field; available on `$entry` | See [Site pages](/docs/site-pages) for how this repo wires `/`, `/docs`, and `/addons`. ## Markdown documentation Doc streams set `format: md` and route to a shared view: ```json { "routes": [ { "uri": "docs/{id}", "view": "docs" } ] } ``` Files live in `streams/data/docs/` (or `{package}_docs/`). The entry `id` matches the filename without extension. ## Posts and structured content Blog or article streams follow the same pattern with different fields — for example `slug`, `published_at`, and a `relationship` to authors. Model fields in stream JSON; store entries in filebase or [database sources](/docs/core/sources-and-adapters). ## Blocks Block content is an array field. `config.stream` turns each item into an entry of a stream, either a stream ID or an inline definition, so every block gets fields, casting, and decorators: ```json { "handle": "content", "type": "array", "config": { "stream": { "id": "content_blocks", "fields": [ { "handle": "type", "type": "select", "config": { "options": ["text", "gallery"] } }, { "handle": "title", "type": "string" }, { "handle": "body", "type": "string" } ] } } } ``` An item saved from an existing entry keeps an `@stream` key (for example `{"@stream": "gallery_blocks", "id": "summer"}`) and is restored as that entry when read. To limit what items may be, list types in `config.items` (for example `[{"type": "string"}]`). Each block type can reference another stream or an inline field structure. ## Partials via Includes For reusable view fragments, use Core's [Includes](/docs/core/views-and-includes) API rather than duplicating Blade `@include` paths in JSON: ```php Includes::include('sidebar', 'partials.sidebar'); ``` ## Related - [Site pages](/docs/site-pages) - [Routing](/docs/routing) - [Core streams](/docs/core/streams) - [Core entries](/docs/core/entries) --- # Guides: The Basics: Routing > Routing your application. Source: https://streams.dev/docs/routing ## Introduction All requests to your application are handled by **Laravel** unless you create the routes using one of the specific methods described below. ## Defining Routes The Streams platform has a couple of ways it routes requests, which are listed below. Otherwise, [standard Laravel routing applies](https://laravel.com/docs/routing). ### Route Files You can configure routes just as you would in a regular Laravel application using the `routes/web.php` file. ### Streams Router The Streams platform provides a `Route::streams()` method for defining routes. *All streams-specific routing approaches pass through this method.* ```php // Options Route::streams('uri', [ 'foo' => 'bar', ]); // View Route::streams('uri', 'view'); // Controller Route::streams('uri', 'App\Http\Controller\Example@show'); // Controller and more Route::streams('uri', [ 'uses' => 'App\Http\Controller\Example@show' ]); ``` The first argument is the URI and the second is either: - The name of the [view](/docs/core/views-and-includes) to render. - A callable string. - An array of [route options](#route-options). ### Stream Routes Defining routes in your [stream configuration](/docs/streams#routing) makes it easy to automate naming and URL generation around your domain information and entities. Define stream routes using a `action => options` format, where `options` is again either the URI, controller and method string, or an array of [route options](#route-options). In `streams/contacts.json`: ```json { "routes": { "index": { "uri": "contacts", "view": "contacts" }, "view": { "uri": "contacts/{id}", "view": "contact" }, "profile": { "uri": "contacts/{id}", "view": "profile" } } } ``` #### Automatic Naming Unless a [route name](#named-routes) is specified, stream configured routes automatically name themselves like `streams::{stream}.{action}`. ```php $url = route('streams::contacts.index'); ``` #### Automatically Resolved Views Unless a view is specified, the associated requests will attempt to resolve a view automatically. In `streams/contacts.json`: ```json { "routes": { "index": { "uri": "contacts" }, "view": { "uri": "contacts/{id}" } } } ``` `EntryController` picks the first view that exists, in this order: 1. The route's `view` option. 2. A view with the same name as the route (`as`). 3. For a route that resolves an entry (`contacts/{id}`), the singular stream ID (`resources/views/contact.blade.php`). Otherwise, the plural (`resources/views/contacts.blade.php`). If none exists, no view is set. ## Route Parameters The Streams platform adds support for deep parameter variables using a dot notation when using the `URL::streams()` method to [generate URLs](#generating-urls). ```php URL::streams('uri/{foo.bar}', 'view'); ``` ### Stream Parameter You can specify the stream associated with the route using the [route option](#streams) or by using the `{stream}` URI segment variable in your URI pattern to resolve the stream by its handle. ```php Route::streams('address-book/{stream}', 'contacts'); ``` Consider locking down this routing pattern using a [parameter constraint](#parameter-constraints). ```php Route::streams('address-book/{stream}', [ 'view' => 'contacts.list', 'constraints' => [ 'stream' => '(businesses|family)' ], ]); ``` The resolved stream will be available within the view: ```blade @verbatim

{{ $stream->name }}

@endverbatim ``` ### Entry Parameters You can specify a stream entry associated with the route using the [route option](#entries) or by using the `{id}` URI segment variable in your URI pattern to resolve the entry by its ID or handle. ```php Route::streams('address-book/{stream}/{id}', 'contacts'); ``` You can also use `{entry.*}` parameters to query the entry by its field values. ```php // address-book/contacts/ryan@example.com Route::streams('address-book/{stream}/{entry.email}', 'contacts'); ``` The first matching entry will be available within the view: ```php @verbatim

{{ $entry->name }}

@endverbatim ``` A `404` error page will be displayed entry resolution is attempted, but no entry is found. ## Route Options All Streams platform-specific methods of registering routes support the following route options. All route options are parsed with [controller data](/docs/core/routes): ```php Route::streams('address-book/{stream}/{id}', [ 'view' => '{streams.handle}', ]); ``` ### View Use the `view` option to specify a [view](/docs/core/views-and-includes) to render: ```php Route::streams('uri', [ 'foo' => 'bar', 'view' => 'example', ]); ``` ### Stream Use the `stream` option to specify the stream associated with the request. [Stream configured routes](#stream-routes) will do this automatically. ```php Route::streams('uri', [ 'stream' => 'contacts', ]); ``` The stream is automatically injected into the view: ```blade @verbatim

{{ $stream->name }}

@endverbatim ``` ### Entry You can also specify a specific entry identifier: ```php Route::streams('uri', [ 'stream' => 'contacts', 'entry' => 'john_smith', ]); ``` The stream entry is automatically injected into the view: ```blade // uri/ryan_thompson @verbatim

{{ $entry->name }}

@endverbatim ``` You can use entry fields to query entries for the view. ```php // uri/ryan@example.com Route::streams('uri/{entry.email}', [ 'stream' => 'contacts', ]); ``` You can also hard code the entry ID or handle: ```php Route::streams('uri', [ 'stream' => 'contacts', 'entry' => 'ryan_thompson', ]); ``` The first result is automatically injected into the view: ```blade // uri/ryan_thompson @verbatim

{{ $entry->name }}

@endverbatim ``` ### Redirect Use the `redirect` and optional `status_code` option to specify a redirect: ```php Route::streams('uri/{entry.name}', [ 'redirect' => '/new/uri', 'status_code' => 301, // Default ]); ``` Redirects highlight a good use case to leverage the fact that route options are parsed with controller data: ```php Route::streams('uri/{entry.name}', [ 'redirect' => '/new/uri/{stream.id}/{entry.name}', 'status_code' => 301, // Default ]); ``` #### Native Redirects You can create [Laravel redirects](https://laravel.com/docs/routing#redirect-routes) in your `routes/web.php` using the `Route` facade as well: ``` php Route::redirect('/from', '/to'); Route::redirect('/from', '/to', 301); Route::permanentRedirect('/from', '/to'); ``` ### Named Routes Use the `as` option to specify the name of the route: ```php Route::streams('uri', [ 'view' => 'example', 'as' => 'login', ]); ``` You can refer to the route by name using the typical Laravel methods: ```php $url = route('login'); ``` ### HTTP Verbs Use the `verb` option to specify the HTTP verb the route should respond to: ```php Route::streams('uri', ['verb' => 'any']); Route::streams('uri', ['verb' => 'get']); // Default Route::streams('uri', ['verb' => 'put']); Route::streams('uri', ['verb' => 'post']); Route::streams('uri', ['verb' => 'patch']); Route::streams('uri', ['verb' => 'delete']); Route::streams('uri', ['verb' => 'options']); ``` ### Route Middleware Use the `middleware` option to assign additional middleware to the route: ```php Route::streams('uri', [ 'middleware' => ['first', 'second'] ]); ``` ### Parameter Constraints Use the `constraints` option to specify allowed parameter formatting for the route using regular expression: ```php Route::streams('uri/{name}', [ 'constraints' => ['name' => '[A-Za-z]+'] ]); ``` Laravel does not support dots in parameter names at this time. For this reason, `{entry.name}` type parameters transform into `{entry__name}`. ```php Route::streams('uri/{entry.name}', [ 'constraints' => ['entry__name' => '[A-Za-z]+'] ]); ``` ### Disabling CSRF You can disable CSRF protection using the **csrf** option. ```php Route::streams('uri', [ 'csrf' => false ]); ``` ### Deferring Routes Use the `defer` option to defer registering a route. ```php Route::streams('/{id}', [ // ... 'defer' => true, ]); ``` ## Generating URLs You may use the `URL::streams()` method to generate URLs for named routes, including those with dotted parameter variables. This method also supports parsing URL strings with parameter data. The `extra` data argument is appending as a query string. Use the `absolute` argument to control whether the resulting URL is absolute or not. ```php URL::streams($target, $parameters = [], $extra = [], $absolute = true); $entry = Streams::entries('contacts')->first(); // contacts/{entry.email}/{entry.id} $url = URL::streams('streams::contacts.view', ['entry' => $entry]); // contacts/{email}/{id} $url = URL::streams('streams::contacts.view', $entry); ``` You can also use [Laravel URL generation](https://laravel.com/docs/routing#named-routes) for named routes, though dotted parameters are not supported using Laravel methods: ```php // Generating URLs. $url = route('streams::contacts.index'); // Generating Redirects. return redirect()->route('streams::contacts.index'); ``` ## Error Pages Errors render views based on the status code of the error. For example, a `404` error will look a view in `resources/views/errors/{status_code}.blade.php`. Laravel will automatically render a `404` page for any unhandled routes. - [Laravel Custom Error Pages](https://laravel.com/docs/errors#custom-http-error-pages) --- # Guides: Frontend: Images > Read, resize, and render images with the Images facade, built on Intervention Image. Source: https://streams.dev/docs/images ## Introduction The Streams platform comes with a fluid and highly extensible image handling and manipulation tool that leans heavily on the fantastic [Intervention Image](https://github.com/Intervention/image). ## Reading Images To get started, use the `Images` facade to create a new image for working with. ```php use Streams\Core\Support\Facades\Images; $image = Images::make('img/foo.jpg'); ``` The facade is aliased for use in [views](/docs/core/views-and-includes) as well: ```blade @verbatim{!! Images::make('resources/img/foo.jpg') !!}@endverbatim ``` ### Image Sources The first and only argument should be the source image to display. The following sources are supported out of the box: #### Paths in the Filesystem Any image path relative to the application root may be used. ```blade @verbatim{!! Images::make('resources/img/foo.jpg') !!}@endverbatim ``` #### Configured Storage Disks You may use any configured storage location as an image source. ```blade @verbatim{!! Images::make('public://img/foo.jpg') !!}@endverbatim ``` If the file is not found relative to the base path of your application, the default public disk will be attempted. ```blade @verbatim{!! Images::make('img/foo.jpg') !!}@endverbatim ``` #### Remote URLs The URL of a remote image may also be used. The `allow_url_fopen` PHP directive must be enabled to use remote image sources. ```blade @verbatim{!! Images::make('https://example.com/img/foo.jpg') !!}@endverbatim ``` Remote images are cached locally. To use remote images without caching locally just use regular `` tags. ### Named Images Use named images to register image variables: #### Registering Images You may regiter iamges by name using the **register** method: ```php use Streams\Core\Support\Facades\Images; Images::register('logo.jpg', 'images/logo.jpg'); ``` ```blade @verbatim{!! Images::make('logo.jpg')->fit(300, 500)->quality(60) !!}@endverbatim ``` ## Editing Images After you initiat a new image instance with `Images::make()`, you can use the below manipulation methods. Chain methods together for more comple manipulations. ```php use Streams\Core\Support\Facades\Images; $image = Images::make('img/foo.jpg') ->fit(300, 500) ->quality(60) ->orientate(); ``` ```blade @verbatim{!! Images::make('resources/img/foo.jpg') ->fit(300, 500) ->quality(60) ->orientate() !!}@endverbatim ``` ### Resizing Images Use the following methods to resize images. - [resize()](http://image.intervention.io/api/resize) - [widen()](http://image.intervention.io/api/widen) - [heighten()](http://image.intervention.io/api/heighten) - [fit()](http://image.intervention.io/api/fit) - [crop()](http://image.intervention.io/api/crop) - [trim()](http://image.intervention.io/api/trim) ### Adjusting Images Use the following methods to adjust various aspects of images. - [encode()](http://image.intervention.io/api/encode) - [gamma()](http://image.intervention.io/api/gamma) - [brightness()](http://image.intervention.io/api/brightness) - [contrast()](http://image.intervention.io/api/contrast) - [colorize()](http://image.intervention.io/api/colorize) - [greyscale()](http://image.intervention.io/api/greyscale) - [invert()](http://image.intervention.io/api/invert) - [mask()](http://image.intervention.io/api/mask) - [flip()](http://image.intervention.io/api/flip) #### quality() Additionally, you may use the `quality` method to adjust the quality alone of JPG images. ```php Images::make('img/foo.jpg')->quality(60); ``` ### Applying Effects Use the following methods to apply effects to images. - [filter()](http://image.intervention.io/api/filter) - [pixelate()](http://image.intervention.io/api/pixelate) - [rotate()](http://image.intervention.io/api/rotate) - [blur()](http://image.intervention.io/api/blur) ### Drawing Use the following methods to draw on images. - [text()](http://image.intervention.io/api/text) - [pixel()](http://image.intervention.io/api/pixel) - [line()](http://image.intervention.io/api/line) - [rectangle()](http://image.intervention.io/api/rectangle) - [circle()](http://image.intervention.io/api/circle) - [ellipse()](http://image.intervention.io/api/ellipse) ### Macros Macros are a basic method of [extending the Streams platform](/docs/core/extending-core). #### Defining Macros You can define macros in a service provider. ```php use Streams\Core\Image\Image; Image::macro('thumbnail', function () { return $this->fit(148)->encode('jpg', 50); }); ``` #### Applying Macros ```php $thumbnail = Images::make('img/foo.jpg')->thumbnail(); ``` ## Outputting Images Use output methods to display image data from an image object. The `img` method is used by default. #### img() Use the `img` method to return an `` tag. ```blade @verbatim{!! Images::make('img/foo.jpg') !!}@endverbatim ``` The first parameter can be an `alt` tag or array of attributes. If an alt tag is provided, the attributes can still be provided as a second parameter. Note this is the default output method when used in Blade. ```php Images::make('img/foo.jpg')->img('Foo Bar Image', ['width' => '100']) ``` Note that unmatched methods will pass through to set attribute values. ```php Images::make('img/foo.jpg')->width(100)->img('Foo Bar Image') ``` #### url() Use the `url` method to output a URL to the image. The first argument may be an array of query string parameters to append. The second argument can be used to force secure URLs. If not specified, the URLs will use the protocol of the request. If ```php Images::make('img/foo.jpg')->url() // Append a manual version query parameter. Images::make('img/foo.jpg')->url(['version' => 'v1']) ``` #### inline() Use the `inline` method to return an `` tag with a **base64** encoded **src**. ```blade @verbatim{!! Images::inline('img/foo.jpg') !!}@endverbatim ``` The first parameter can be an `alt` tag or array of attributes. If an alt tag is provided, the attributes can still be provided as a second parameter. Note this is the default output method when used in Blade. ```php Images::make('img/foo.jpg')->inline('Foo Bar') ``` #### base64() Use the `base64` method to return a base64 encoded string. ```blade @verbatim@endverbatim ``` #### css() Use the `css` method to return a `url()` string for use in CSS backgrounds. ```blade @verbatim
@endverbatim ``` #### data() The `data` method will return the contents of the image as a string. ```php echo Images::make('img/foo.jpg')->data() ``` ### Responsive Images #### srcsets() ```php Images::make('img/foo.jpg') ->srcsets([ '1x' => [ 'resize' => 400, 'quality' => 60 ], '2x' => [ 'resize' => 800, 'quality' => 90 ], '640w' => [ 'resize' => 800, 'quality' => 90 ] ]); ``` #### srcset() ```php Images::make('img/foo.jpg') ->srcset([ '(min-width: 600px) 400px' => [ 'intrinsic' => 400, 'resize' => 400, 'quality' => 60 ], '(min-width: 1600px) 800px' => [ 'intrinsic' => 800, 'resize' => 800, 'quality' => 90 ] ])->img(); ``` #### picture() ```php Images::make('img/foo.jpg') ->resize(1800) // Fallback ->picture([ '(min-width: 600px)' => [ 'resize' => 400, 'quality' => 60 ], '(min-width: 1600px)' => [ 'resize' => 800, 'quality' => 90 ] ]); ``` --- # Guides: Frontend: Assets > Asset and image management overview. Source: https://streams.dev/docs/assets ## Overview Streams Core manages assets and images through stream field types and the asset registry. Use image fields for uploads; use the asset system for compiled or public vendor assets. ## Images See [Images](/docs/images) for manipulation, URLs, and storage disks. ## Reference Implementation detail lives in Core: - [Assets](/docs/core/assets) - [Fields — image types](/docs/core/fields) --- # Guides: Frontend: Components > Blade and Livewire UI patterns with Streams. Source: https://streams.dev/docs/components ## Overview Streams UI provides Blade components and Livewire-driven builders for tables, forms, and layout. Use them inside panels or embed in your own views. ## Patterns - **Panel pages** — full CP sections with navigation - **Embedded tables** — list stream entries with filters and actions - **Custom Livewire** — extend UI builders when JSON is not enough Streams remains Laravel-native: standard views, middleware, and authorization apply. ## Learn more - [UI introduction](/docs/ui/introduction) - [TALL components (SDK)](/docs/sdk/tall-components) - [Pages](/docs/ui/pages) --- # Guides: Frontend: Forms > When to use UI forms in Streams applications. Source: https://streams.dev/docs/forms ## Overview Streams UI generates forms from stream field definitions. Your team gets validation, field types, and layout without duplicating schema in Blade. Use forms in control panels, settings pages, and modals. ## When to build a form - Create or edit stream entries in the CP - Multi-step product settings - Nested or relationship fields Define fields once in stream JSON; UI renders inputs from field types. ## Learn more - [UI forms reference](/docs/ui/forms) - [Field types](/docs/core/fields) - [Control panel](/docs/control-panel) --- # Guides: Advanced: API > When and how to add a REST API to your Streams application. Source: https://streams.dev/docs/api ## When to add Streams API Add `streams/api` when your team needs to expose stream data to SPAs, mobile apps, partners, or webhooks—without writing CRUD controllers by hand. Keep business logic in Laravel as usual. The API reads your stream definitions at request time, so new fields show up without new controllers. ## Installation ```bash composer require streams/api:1.0.x-dev ``` Enable the gate and register routes from a service provider (not from `routes/api.php`): ```env STREAMS_API_ENABLED=true ``` ```php // app/Providers/AppServiceProvider.php use Streams\Api\Support\Facades\API; public function boot(): void { API::routeCrud(); } ``` See [API installation](/docs/api/installation). ## What you get Standard REST endpoints under `/api/streams/{stream}/entries`. Response shape follows the Streams envelope in [Responses](/docs/api/responses), not JSON:API. ## What you own - **Authentication.** The API ships none. Add middleware to the interface or extend the gate. See [API authentication](/docs/api/authentication). - **Tenancy.** Resolve a tenant with `API::tenant()` and scope criteria with endpoint callbacks. See [API tenancy](/docs/api/tenancy). - **Caching.** Opt in to ETags with the `ApiCache` middleware. See [API caching](/docs/api/caching). ## Learn more - [API introduction](/docs/api/introduction) - [Routes reference](/docs/api/routes) - [Query parameters](/docs/api/query-parameters) - [Custom interfaces](/docs/api/custom-interfaces) - [Custom endpoints](/docs/api/custom-endpoints) - [OpenAPI / Swagger](/docs/api/openapi) - [JavaScript client](/docs/client/introduction) For product paths, see [Use cases — Headless API](/docs/use-cases#headless-api-only). --- # Guides: Advanced: UI > Control panels and interface generation for Streams. Source: https://streams.dev/docs/ui ## When to use Streams UI Add `streams/ui` when your team needs an admin or product panel—forms, tables, navigation, and pages—built with Livewire and PHP builders on top of Core streams. You keep Laravel auth, policies, and middleware. Resources are PHP classes—not Blade helpers like `UI::form()`. ## Installation ```bash composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev ``` Register a panel in a service provider: ```php UI::panel( Panel::make('admin')->default()->path('admin')->middleware(['web']) ); ``` ## Typical workflow 1. Define streams in `streams/` (Core). 2. Create Resource classes with `form()` and `table()` builders. 3. Register resources on the panel and visit `/admin`. ## Learn more - [UI introduction](/docs/ui/introduction) - [Quick start](/docs/ui/quick-start) - [Panels](/docs/ui/panels) - [Resources](/docs/ui/resources) - [Forms](/docs/ui/forms) - [Tables](/docs/ui/tables) - [Control panel guide](/docs/control-panel) See [Use cases](/docs/use-cases) for admin and product panel paths. --- # Guides: Advanced: Control Panel > Building admin and product panels with Streams UI. Source: https://streams.dev/docs/control-panel ## Overview A control panel (CP) is the authenticated area where your team or customers manage stream data. Streams UI registers panel routes, navigation, and resources from configuration and PHP panel builders. ## Admin vs product panel | Type | Audience | Example | |------|----------|---------| | Admin panel | Internal ops | Manage users, orders, content | | Product panel | End customers | Account settings, project config | Same UI package; different panel registration and auth. ## Steps 1. Require `streams/ui` and configure middleware (typically `web`, `auth`). 2. Define a panel with path prefix (e.g. `/admin`). 3. Attach stream resources—tables and forms for each stream. 4. Customize navigation groups and pages as needed. ## Learn more - [UI panels](/docs/ui/panels) - [SDK Livewire generator](/docs/sdk/commands#streamslivewire) - [Use cases](/docs/use-cases) --- # Guides: Advanced: Users > Authentication and user streams in Streams applications. Source: https://streams.dev/docs/users ## Overview Streams does not replace Laravel authentication. Use Laravel's user model, sessions, Sanctum, or Passport as your team already does. Define a `users` stream when you want Streams repositories, CP resources, or API exposure for user records. ## Typical setup - Laravel `User` model for auth - Optional `users` stream with Eloquent source pointing at the same table - UI panel resources for admin user management - API routes only if you intentionally expose user endpoints ## Learn more - [Streams Core — entries](/docs/core/entries) - [UI panels](/docs/ui/panels) - [API authentication](/docs/api/authentication) --- # Guides: Advanced: Databases > Storage adapters and database-backed streams. Source: https://streams.dev/docs/databases ## Overview Streams is data-source agnostic. Configure each stream's `source` to use filebase files, Eloquent models, in-memory collections, or remote APIs. Most production apps use Eloquent for transactional data and filebase for content or config-like entries. ## Eloquent source Point a stream at an existing model: ```json { "config": { "source": { "type": "eloquent", "model": "App\\Models\\Post" } } } ``` Your team keeps migrations and models in Laravel; Streams adds repositories, criteria, and optional UI/API layers. ## Filebase source Store entries as JSON, YAML, Markdown, or HTML under `streams/data/`. Ideal for content sites and git-reviewed data. ## Learn more - [Streams Core — streams](/docs/core/streams) - [Repositories](/docs/core/repositories) - [Files](/docs/files) --- # Guides: Advanced: Localization > Internationalization with Streams and Laravel. Source: https://streams.dev/docs/localization ## Overview Use Laravel's localization for application strings (`lang/`, `__()`, `@lang`). Streams field labels and CP text can pull from translation files like any Blade view. For multi-locale **content**, model locales as separate streams, localized fields, or related translation entries— whichever fits your team's CMS pattern. ## Laravel strings Publish UI lang files when customizing control panel copy: ```bash php artisan vendor:publish --provider="Streams\\Ui\\UiServiceProvider" --tag=lang ``` ## Learn more - [Laravel localization](https://laravel.com/docs/localization) - [UI introduction](/docs/ui/introduction) - [Content modeling](/docs/content) --- # Guides: Advanced: Tenancy > Three separate mechanisms: Core applications matched by URL, an API tenant resolver you scope yourself, and a UI panel tenant that does not scope routes yet. Source: https://streams.dev/docs/tenancy Streams does not ship one tenancy product. Three packages each hold a piece, and none of them filters data unless you do. | Need | Use | |------|-----| | Different config, streams, or locale per host | Core [applications](/docs/core/applications) | | One tenant value per API request, then scope queries yourself | [API tenancy](/docs/api/tenancy) | | A tenant value available to a panel | UI `Panel::tenant()` — stored only, see below | ## Applications An application is an entry on the stream `config('streams.core.applications_id')` (default `applications`). Core loads that stream at boot, picks one entry by matching `match` patterns against the full request URL (`Str::is`), and activates it. If nothing matches, it uses the first entry with an empty `match`. If the stream is empty, it activates a synthetic `default` application. `StreamsServiceProvider::bootApplication()` then passes these attributes to `Integrator::integrate()` when they are non-empty: - `locale` — `App::setLocale()` - `config` — merged with `Config::set()` after dotting the array - `aliases`, `bindings`, `singletons` — container registrations - `streams` — each value is a file path passed to `Streams::load()`, or an array passed to `Streams::register()` The shipped `applications` stream schema only declares `handle`, `match`, and `config`. The other keys are still read off the entry when the JSON file contains them. `Integrator` can also merge `routes`, `assets`, `commands`, `listeners`, `policies`, `middleware`, `providers`, `schedules`, and `includes`. Boot does **not** pass those. Putting `routes` on an application entry does nothing until you call `Integrator::integrate()` yourself. ```json { "id": "spanish", "match": ["https://es.example.com/*"], "locale": "es", "config": { "app": {"name": "Corrientes"}, "streams": {"core": {"data_path": "streams/data/es"}} } } ``` `match` is compared with `Str::is` against `Request::fullUrl()`, which includes the scheme. A pattern of `es.example.com/*` does not match `https://es.example.com/…`. The example in Core's `applications.json` omits the scheme; include it. `Integrator::config()` flattens the array with `Arr::dot`, runs string values that contain `}` through `Str::parse`, then `Config::set()` on the dotted keys. Nested config in the file is the right shape. A flat key that already contains a dot, such as `"app.name"`, is also set as that config path. ```php use Streams\Core\Support\Facades\Applications; Applications::active(); // the entry chosen for this request Applications::activate($other); // replace it; does not re-run Integrator ``` Calling `activate()` later does not merge that entry's config again. Integration runs once during boot. ## API `API::tenant()` and `ApiInterface::tenant()` store a resolver. `API::getTenant()` calls it once per request and binds the result as `streams.api.tenant`. List and show endpoints do not apply it. You listen for their `apply` callback, or you enforce the tenant in gate middleware. Create, update, delete, and query are not covered by that callback. The working pattern is on [API tenancy](/docs/api/tenancy). ## UI panels `Panel::tenant($value)` stores a value or closure on the panel, and `Streams\Ui\UiManager::tenant()` stores a closure whose `getTenant()` result is available during the request. Resource and page route code that would put that tenant into route parameters is commented out, so a panel tenant does not change URLs or scope entry queries. Use applications or the API resolver when you need isolation. Use the panel method only when your own page code reads `UI::getTenant()` or `$panel->getTenant()`. ## Related - [Applications](/docs/core/applications) - [Integrator](/docs/core/integrator) - [API tenancy](/docs/api/tenancy) - [Authentication](/docs/api/authentication) --- # Guides: Developers: Caching > Caching options and automation. Source: https://streams.dev/docs/caching ## Introduction Streams Core provides a convenient API to link [Laravel cache](https://laravel.com/docs/cache) data to a Stream. When caching in this way, you can flush all cached items related to a Stream together. ### Configuration In `streams/examples.json`: ```json { "config": { "cache": { "enabled": true, "store": "default", "ttl": 3600 } } } ``` ## Cache Usage #### The Cache Instance To obtain a Stream-linked cache instance, you may use the `cache()` method on the desired Stream instance: ```php $cache = Streams::make('examples')->cache(); $cache->get('key'); ``` ### Retrieving Items Use the `get` method to retrieve items from the cache. If the item does not exist in the cache, `null` will be returned. You may pass a second argument specifying the default value to return if the item doesn't exist: ```php $cache = Streams::make('examples')->cache(); $value = $cache->get('key'); $value = $cache->get('key', 'default'); ``` You may also pass a `closure` as the default value. The get method will return the closure result if the specified item does not exist in the cache. Using a closure allows you to defer the retrieval of expensive default values until they are needed: ```php $stream = Streams::make('examples'); $value = $stream->cache()->get('key', function () use ($stream) { return $stream->entries()->all(); }); ``` #### Checking Items Use the `exists` method to check if an item exists in cache: ```php if (Streams::make('examples')->cache()->has('key')) { // We have it! } ``` #### Incrementing/Decrementing Values Use the `increment` and `decrement` methods to increment or decrement the value of cached integer value: ```php $cache = Streams::make('examples')->cache(); $cache->increment('key'); $cache->increment('key', $amount); $cache->decrement('key'); $cache->decrement('key', $amount); ``` #### Retrieve & Store Use the `remember` method to retrieve an item from the cache and store a default value if the requested item doesn't exist. ```php $value = Streams::make('examples')->cache()->remember('key', $seconds, function () { return Streams::entries('examples')->get(); }); ``` #### Retrieve & Delete Use the `pull` method to retrieve an item from the cache and then delete the item. ```php $value = Streams::make('examples')->cache()->pull('key'); ``` ### Storing Items Use the `put` method to store items in the cache: ```php Streams::make('examples')->cache()->put('key', 'value', $seconds); ``` You can also pass a `DateTime` instance instead of seconds: ```php Streams::make('examples')->cache()->put('key', 'value', now()->addMinutes(10)); ``` If the storage time is not passed to the put method, the item will be stored indefinitely: ```php Streams::make('examples')->cache()->put('key', 'value'); ``` #### Store If Not Present Use the `add` method to store items in the cache only if they do not already exist: ```php Streams::make('examples')->cache()->add('key', 'value', $seconds); ``` #### Store Forever Use the `forever` method to store items in the cache indefinitely: ```php Streams::make('examples')->cache()->forever('key', 'value'); ``` > Items that are stored "forever" may be removed under some circumstances. ### Removing Items Use the `forget` method to remove items from the cache: ```php Streams::make('examples')->cache()->forget('key'); ``` You can also remove items by providing a zero or negative number of seconds: ```php Streams::make('examples')->cache()->put('key', 'value', 0); Streams::make('examples')->cache()->put('key', 'value', -5); ``` > Stream entry changes automatically forget/flush cache items. #### Removing All Items You can clear the entire cache using the `flush` method: ```php Streams::make('examples')->cache()->flush(); ``` > The flush method only flushes linked cache. ## Related - [Core caching reference](/docs/core/caching) - [Core criteria](/docs/core/criteria) - [Laravel Cache](https://laravel.com/docs/cache) --- # Guides: Developers: Addons > Composing packages and local development with Streams. Source: https://streams.dev/docs/addons ## Overview Streams applications compose Composer packages. Your project may require Core only, or Core plus UI, API, SDK, and community addons. Browse published packages on [Addons](/addons). ## Path repositories For local development across packages, add path repositories in `composer.json`: ```json { "repositories": [ { "type": "path", "url": "../streams-core", "options": { "symlink": true } } ] } ``` Prefer `"preferred-install": { "streams/*": "source" }` when contributing to Streams packages. ## Documentation All package docs live on this site under `/docs/core`, `/docs/ui`, etc. Edit markdown in `streams/data/` in the streams.dev repository. ## Learn more - [Architecture](/docs/architecture) - [Installation](/docs/installation) - [Packages catalog](/addons) --- # Guides: Developers: Testing > Testing Streams applications and packages. Source: https://streams.dev/docs/testing ## Overview `streams/testing` provides a pre-configured Orchestra Testbench environment, sample streams, and helpers for testing Streams behavior in your app or package. ## Installation ```bash composer require --dev streams/testing:1.0.x-dev ``` Extend the package TestCase in your PHPUnit tests and use sample stream data for realistic scenarios. ## Learn more - [Testing introduction](/docs/testing/introduction) - [Writing tests](/docs/testing/writing-tests) - [Test data](/docs/testing/test-data) - [Troubleshooting](/docs/testing/troubleshooting) Laravel's own [testing documentation](https://laravel.com/docs/testing) applies for HTTP, database, and feature tests outside Streams specifics. --- # Guides: Developers: Agents > How agents work with Streams: llms.txt and raw markdown, the MCP server, the stream definition schema, and OpenAPI. Source: https://streams.dev/docs/agents Streams docs are meant to be read by coding agents as well as people. This site is the source of truth. Package repositories do not keep a second copy. ## Read the docs | URL | What you get | |-----|----------------| | [/llms.txt](/llms.txt) | An index of every docs page, as markdown links, following [llmstxt.org](https://llmstxt.org). Built on each request from the same index as search. | | [/llms-full.txt](/llms-full.txt) | The same pages, inlined. Use this when you need the text and cannot fetch each link. | | `/docs/{id}.md` and `/docs/{package}/{id}.md` | The source of one page, with its title as a heading. Drop `.md` for the HTML page. | | [/search/docs.json](/search/docs.json) | The Cmd+K index: title, description, URL, markdown URL, and an excerpt. | | [/docs/api/openapi.yaml](/docs/api/openapi.yaml) | The generic OpenAPI 3 description of `streams/api`, copied from the package. It is not generated from your app's streams. | | [/schema/streams.schema.json](/schema/streams.schema.json) | The JSON Schema for `streams/*.json` definition files. See [Stream definition schema](/docs/sdk/stream-schema). | HTML pages also emit `` pointing at the `.md` URL. These routes are dynamic. They read the docs streams through `App\Support\DocsSearchIndex` and cache the text for 15 minutes (`docs.llms.index`, `docs.llms.full`, `docs.search.index`). Adding a markdown file under `streams/data/{docs,core_docs,ui_docs,api_docs,sdk_docs,testing_docs,client_docs}/` publishes it on the next cache miss. There is no generate step. ## What to trust Documented behavior on a page marked `status: ready` was checked against package source. If a page and the code disagree, the code wins, and the page should be corrected here. Two names stay distinct until a later rename lands: - **SDK** (`streams/sdk`) is the PHP Artisan generators and the local [MCP server](/docs/mcp). See the [command reference](/docs/sdk/commands). - **Client** (`@laravel-streams/api-client`) is the JavaScript client for the REST API. ## Work inside an app Inside a Laravel app with `streams/sdk`, an agent doesn't have to read source files to learn the domain model: - **MCP server.** `php artisan mcp:start streams` exposes 14 tools (list and describe streams, entry and definition schemas, validate definitions, entry CRUD, docs search, and the `make-stream` and `make-addon` generators), two resources, and the `design-stream` prompt. It needs `laravel/mcp`, which needs Laravel 11.45+ or 12.41+. See [MCP server](/docs/mcp). - **CLI with JSON output.** On any supported Laravel version, `php artisan streams:list --json` lists the streams and `php artisan streams:validate --json` checks definition files. See the [command reference](/docs/sdk/commands). - **Definition schema.** `make:stream` writes `"$schema": "https://streams.dev/schema/streams.schema.json"`, and this site serves that file at [/schema/streams.schema.json](/schema/streams.schema.json). `streams:schema` writes a schema for the *entries* of each of your streams; that is a different file. ## Related - [MCP server](/docs/mcp) - [Workflows](/docs/workflows) - [Command reference](/docs/sdk/commands) - [Contributing documentation](/docs/contributing-docs) --- # Guides: Developers: MCP server > The local-development MCP server in streams/sdk: requirements, starting it with mcp:start streams, client setup, tools, resources, the design-stream prompt, and configuration. Source: https://streams.dev/docs/mcp The SDK includes a local-development [Model Context Protocol](https://modelcontextprotocol.io) server. MCP is the standard way AI agents call tools. With it, an agent working in your app can see the domain model (streams), read and write entries, check the stream definitions it writes, search the Streams docs, and run the SDK generators, all without guessing from source files. The server runs over stdio on your machine through Artisan. It is not an HTTP endpoint. ## Requirements - `streams/sdk` installed as a dev dependency (`composer require --dev streams/sdk:1.0.x-dev`). - [`laravel/mcp`](https://github.com/laravel/mcp) `^1.0`, which needs Laravel 11.45+ or 12.41+. ```bash composer require --dev laravel/mcp ``` On Laravel 10 the MCP server is not available and is skipped automatically. Agents can still use the CLI equivalents: `php artisan streams:list --json` and `php artisan streams:validate --json`. See the [version matrix](/docs/installation#version-matrix). ## Start the server ```bash php artisan mcp:start streams ``` You won't normally run this yourself. Your agent client starts it. To try the tools interactively, use the MCP Inspector that ships with `laravel/mcp`: ```bash php artisan mcp:inspector streams ``` ## Connect an agent Register the server with each client from the app root. Use an absolute path to `artisan` if the client does not start servers in the project directory. **Claude Code** (writes to `.mcp.json` with `--scope project`, so the team shares it): ```bash claude mcp add --scope project streams -- php artisan mcp:start streams ``` Or add it to `.mcp.json` yourself: ```json { "mcpServers": { "streams": { "command": "php", "args": ["artisan", "mcp:start", "streams"] } } } ``` **Cursor** (`.cursor/mcp.json`) uses the same `mcpServers` shape as `.mcp.json`. **VS Code** (`.vscode/mcp.json`): ```json { "servers": { "streams": { "type": "stdio", "command": "php", "args": ["artisan", "mcp:start", "streams"] } } } ``` **Codex** (`~/.codex/config.toml`): ```toml [mcp_servers.streams] command = "php" args = ["/absolute/path/to/app/artisan", "mcp:start", "streams"] ``` ## Tools | Tool | What it does | CLI equivalent | |------|--------------|----------------| | `list-streams` | Registered streams with ID, name, description, source, and field handles. | `streams:list --json` | | `describe-stream` | One stream's source, key, routes, definition file, original definition, and fields with resolved rules and config. | | | `entry-schema` | JSON Schema for a stream's entries. | `streams:schema` | | `definition-schema` | JSON Schema for `streams/*.json` definition files. | | | `validate-stream-definition` | Validate a draft definition, `streams/{id}.json`, or every definition. | `streams:validate --json` | | `list-entries` | Query entries with `where` constraints, ordering, and pagination. | | | `read-entry` | Read an entry by key. | | | `create-entry` | Create a validated entry. Fails if the key exists. | `make:entry` | | `update-entry` | Update some attributes; the merged entry is validated. | | | `delete-entry` | Delete an entry by key. Marked destructive. | | | `search-docs` | Keyword search over local Streams docs. | | | `read-doc` | Read a doc returned by `search-docs`. | | | `make-stream` | Write a validated definition to `streams/{id}.json` and register it. | `make:stream {id} [--force]` | | `make-addon` | Scaffold an addon package in `addons/{vendor}/{name}`. | `make:addon` | Tools annotate themselves as read-only, idempotent, or destructive, so clients can auto-approve safe calls and ask before writes. Protected fields (`"protected": true`) are left out of entry output. ### Resources and prompts - `streams://schemas/streams.schema.json`: the [stream definition JSON Schema](/docs/sdk/stream-schema) (also served at [/schema/streams.schema.json](/schema/streams.schema.json)). - `streams://streams/{id}`: a stream's description (same as `describe-stream`). - `design-stream` prompt: walks an agent through modeling a new stream (inspect, draft, validate, create, seed). ## Docs search `search-docs` indexes markdown under these paths, relative to the app root: - `docs` - `vendor/streams/*/docs` (docs shipped with installed Streams packages) - `streams/data/*docs` (docs streams, as on streams.dev) Change the list with `streams.sdk.mcp.docs.paths`. The index is built from local files only; the server makes no network requests. To search these docs from outside an app, use [/llms.txt](/llms.txt) and the raw `.md` pages described in [Agents](/docs/agents). ## Configuration Publish the config to change defaults: ```bash php artisan vendor:publish --tag=streams-sdk-config ``` | Key | Env | Default | Description | |-----|-----|---------|-------------| | `streams.sdk.mcp.enabled` | `STREAMS_MCP_ENABLED` | `true` | Register the server. | | `streams.sdk.mcp.allow_production` | `STREAMS_MCP_ALLOW_PRODUCTION` | `false` | Also register when `APP_ENV=production`. | | `streams.sdk.mcp.handle` | `STREAMS_MCP_HANDLE` | `streams` | Handle passed to `mcp:start`. | | `streams.sdk.mcp.read_only` | `STREAMS_MCP_READ_ONLY` | `false` | Hide `create-entry`, `update-entry`, `delete-entry`, `make-stream`, and `make-addon`. | | `streams.sdk.mcp.docs.paths` | | see above | Glob patterns for docs search. | ## Safety - The server is for local development. It is not registered in production unless you opt in, and the SDK should be a dev dependency anyway. - It acts with the full permissions of your app. Entry tools write to whatever source a stream uses (flat files in `streams/data` by default, or your database). - Flat-file writes land in your working tree, so review them with `git diff` like any other change. - Use `STREAMS_MCP_READ_ONLY=true` when you only want an agent to look. ## Troubleshooting - **"MCP Server with name [streams] not found"**: `laravel/mcp` is missing, the app is in production, or `STREAMS_MCP_ENABLED=false`. - **The client reports invalid JSON**: something in the app wrote to stdout during boot (for example `echo`, `dump`, or `dd` in a service provider). stdout carries the protocol, so send debugging output to the log. - **A new stream is missing**: streams are registered at boot. `make-stream` registers what it creates, but if you add a definition by hand, restart the server (most clients have a reconnect command). ## Related - [Agents](/docs/agents) - [Command reference](/docs/sdk/commands) - [Stream definition schema](/docs/sdk/stream-schema) - [Workflows](/docs/workflows) --- # Guides: Developers: Docs MCP server > Connect Cursor, Claude Code, or any MCP client to the hosted streams.dev docs over MCP: search, read pages, browse the navigation, and fetch the stream JSON Schema. Source: https://streams.dev/docs/docs-mcp streams.dev runs a public, read-only [Model Context Protocol](https://modelcontextprotocol.io) server over this documentation. Point your agent at it and it can search the docs, read whole pages as markdown, and fetch the stream JSON Schema without scraping HTML. This server is hosted by streams.dev and only reads these docs. It is separate from the SDK's local [MCP server](/docs/mcp) (`php artisan mcp:start streams`), which runs inside your own app and works with its streams and entries. Use both: the SDK server for your app, this one for the docs. ## Endpoint ```text https://streams.dev/mcp ``` Streamable HTTP transport (JSON-RPC over `POST`), no authentication. Requests are rate limited to 60 per minute per IP; over the limit the server answers `429` with a `Retry-After` header. ## Connect ### Cursor Add the server to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project): ```json { "mcpServers": { "streams-docs": { "url": "https://streams.dev/mcp" } } } ``` ### Claude Code ```bash claude mcp add --transport http streams https://streams.dev/mcp ``` ### Other clients Any client that speaks MCP over Streamable HTTP can use the URL above. To try it by hand, run the MCP Inspector: ```bash npx @modelcontextprotocol/inspector --cli https://streams.dev/mcp --transport http --method tools/list ``` ### Local, over stdio In a clone of the streams.dev repository the same server runs as an Artisan command, reading the docs from your checkout: ```bash php artisan mcp:start streams-docs ``` ```json { "mcpServers": { "streams-docs-local": { "command": "php", "args": ["artisan", "mcp:start", "streams-docs"], "cwd": "/path/to/streams.dev" } } } ``` ## Tools All tools are read-only. | Tool | Arguments | Returns | |------|-----------|---------| | `search_docs` | `query` (required), `package`, `section`, `limit` (default 10, max 50) | Ranked pages with title, slug, URL, markdown URL, package, section, tags, and a snippet | | `get_page` | `page`: a slug (`core/introduction`), docs path, or URL | The page as markdown with a frontmatter block (title, description, slug, url) | | `list_pages` | `package`, `section` | The navigation: guides by group, then each package, with title, slug, URL, and section | | `get_schema` | `name` (default `streams`) | The JSON Schema served at [/schema/streams.schema.json](/schema/streams.schema.json) | `package` is one of `guides`, `core`, `ui`, `api`, `sdk`, `testing`, `client`. `section` is the page's frontmatter section: `get-started`, `guides`, `concepts`, `reference`, `packages` (every package reference page), or `contributing`. A page's slug is its path under `/docs/`: `installation` for a hub guide, `core/introduction` for a package page. Pages are also exposed as resources: `streams-docs://pages/{package}/{page}` (for example `streams-docs://pages/core/introduction`), plus `streams-docs://llms.txt` for the index. ## Other ways to read the docs - [/llms.txt](/llms.txt) and [/llms-full.txt](/llms-full.txt) - Any docs page with `.md` appended, such as [/docs/core/streams.md](/docs/core/streams.md) - [/search/docs.json](/search/docs.json) ## Related - [Agents](/docs/agents) - [MCP server](/docs/mcp) --- # Guides: Developers: Workflows > Streams\Core\Support\Workflow runs named steps in order and fires before and after callbacks. Source: https://streams.dev/docs/workflows `Streams\Core\Support\Workflow` is a small ordered step runner in Core. It is not a job pipeline, a queue workflow, or an agent workflow. The Core README still lists replacing it with Laravel pipelines as an open question, so treat it as a stable class with an undecided future, not as a product surface you should build a framework on. Nothing in Core, UI, or the API dispatches a workflow for you. You subclass it when you want named steps and callbacks in your own code. ## Run steps Subclass `Workflow`, set `$steps`, and call `process()`. Each step is resolved through the container. A class name uses `handle`. A `Class@method` string or a `[Class::class, 'method']` pair calls that method. ```php use Streams\Core\Support\Workflow; class PublishPost extends Workflow { public array $steps = [ 'validate' => ValidatePost::class, 'notify' => NotifySubscribers::class.'@handle', ]; } (new PublishPost)->process(['post' => $post]); ``` `process()` returns `void`. It stores the array on `$workflow->payload` and passes that array to `App::call()` as named arguments, so a step method receives values whose parameter names match the array keys. It does not merge return values back into the payload. Share mutable state by passing an object. For each step named `validate`, the runner fires `before_validate`, runs the step, then fires `after_validate`. ## Add steps ```php $workflow->addStep('archive', ArchivePost::class); // append $workflow->doFirst('guard', GuardPost::class); // position 0 $workflow->doBefore('notify', 'audit', AuditPost::class); // before an existing name $workflow->doAfter('validate', 'stamp', StampPost::class); // after an existing name ``` `doBefore` and `doAfter` look the target up with `array_search` and pass that position to `addStep()`, which type-hints `?int`. A missing target makes `array_search` return `false`, and the call throws `TypeError`. Check that the target name exists. ## Callbacks `Workflow` uses Core's `FiresCallbacks` trait. ```php $workflow->addCallback('before_validate', function ($post) { // $post is the payload value whose key matches this parameter }); ``` `fire()` also calls a method on the workflow when one exists. The method name is `on` plus the callback name in camel case, so `before_validate` looks for `onBeforeValidate`. `passThrough($object)` forwards each callback to that object. The object's matching `on…` method runs first: ```php $workflow->passThrough($listener); // $listener->onBeforeValidate($post) ``` Global listeners use `Workflow::addCallbackListener('before_validate', $callback)`. The listener key is the class name plus the callback name, so register it on your subclass, not on `Workflow`, if you want it scoped to that subclass. ## Related - [Callbacks](/docs/core/callbacks) - [Agents](/docs/agents) - [Extending Core](/docs/core/extending-core) --- # Core reference: Introduction > JSON-defined streams, repositories, criteria, and Laravel integration. Source: https://streams.dev/docs/core/introduction Streams Core (`streams/core`) is the foundation package for the Streams ecosystem. It provides JSON-defined domain models, repository access, criteria-based queries, stream routing, assets, and images — all integrated with Laravel. ## When to read this page Start here if you are adding `streams/core` to a Laravel project or need an overview before diving into streams, entries, and repositories. ## Core concepts | Concept | Class / API | Purpose | |---------|-------------|---------| | Stream | `Streams\Core\Stream\Stream` | Domain model defined in JSON | | Entry | `Streams\Core\Entry\Entry` | Single record in a stream | | Repository | `Streams::repository()` | CRUD access to entries | | Criteria | `Streams::entries()` | Query builder for entries | Streams live in `streams/*.json`. Entry data lives in the configured source (filebase, database, self, etc.). ## Minimal example ```json { "id": "posts", "fields": [ { "handle": "title", "type": "string", "required": true }, { "handle": "body", "type": "string" } ] } ``` ```php $post = Streams::repository('posts')->create(['title' => 'Hello']); $post->save(); $posts = Streams::entries('posts')->where('title', 'Hello')->get(); ``` ## Package boundaries Core does not include admin UI (see [UI](/docs/ui/introduction)) or REST endpoints (see [API](/docs/api/introduction)). Validation is opt-in — call `$entry->validator()->validate()` before persisting when you need it. ## Related - [Installation](/docs/core/installation) - [Streams](/docs/core/streams) - [Repositories](/docs/core/repositories) - [Criteria](/docs/core/criteria) --- # Core reference: Installation > Composer, publish config/streams, env vars, and first stream. Source: https://streams.dev/docs/core/installation Install Streams Core in a Laravel 10, 11, or 12 application via Composer. Core declares `php: ^8.2` and `laravel/framework ^10|^11|^12`. This site runs Core on Laravel 12. Core's own test suite still runs on Laravel 10, because the `streams/testing` harness is Laravel 10 only until it is widened. See the [version matrix](/docs/installation#version-matrix). ## Require the package ```bash composer require streams/core:2.0.x-dev ``` Core 2.0 has no stable tag yet. Without the `2.0.x-dev` constraint (or `"minimum-stability": "dev"` in your `composer.json`), Composer installs the old Core 1.10.4 instead. Laravel auto-discovers `Streams\Core\StreamsServiceProvider`. ## Publish assets Nothing has to be published; Core runs on its defaults. Publish what you want to change: ```bash php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=config php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=streams php artisan vendor:publish --provider="Streams\Core\StreamsServiceProvider" --tag=public ``` | Tag | Copies | To | |-----|--------|----| | `config` | `resources/config/core.php` | `config/streams/core.php` | | `streams` | Core's own stream definitions (`resources/streams/`: `streams.json`, `applications.json`) | `streams/` | | `public` | `resources/public/` | `public/vendor/streams/core` | Always pass `--provider`. `streams/ui` and `streams/api` register a `config` tag too, so `--tag=config` on its own publishes every package's config. The `public` tag publishes nothing when the installed Core build ships no `resources/public` directory. ## Environment variables | Variable | Purpose | |----------|---------| | `STREAMS_DATA_PATH` | Filebase data directory (default `streams/data`) | | `STREAMS_SOURCE` | Default source adapter (default `filebase`) | | `STREAMS_DEFAULT_FORMAT` | Default file format (default `json`) | ## Verify installation Create `streams/posts.json` and query entries: ```php Streams::entries('posts')->count(); ``` ## Related - [Configuration](/docs/core/configuration) - [Streams](/docs/core/streams) - [Hub: Installation](/docs/installation) --- # Core reference: Configuration > config/streams/core.php — data path, sources, field types, and images. Source: https://streams.dev/docs/core/configuration Core configuration lives in `config/streams/core.php`, published from the package. Environment variables override defaults for deployment-specific paths and sources. ## Key settings | Key | Env | Default | Purpose | |-----|-----|---------|---------| | `streams_id` | — | `streams` | Meta-stream storing stream definitions | | `applications_id` | — | `applications` | Multi-app configuration stream | | `data_path` | `STREAMS_DATA_PATH` | `streams/data` | Filebase entry directory | | `default_source` | `STREAMS_SOURCE` | `filebase` | Default adapter when omitted in stream JSON | | `sources.filebase.default_format` | `STREAMS_DEFAULT_FORMAT` | `json` | Filebase format when a stream sets none | | `auto_alt` | `STREAMS_AUTO_ALT` | `true` | Generate image alt text when missing | | `version_images` | `STREAMS_VERSION_IMAGES` | `true` | Append cache-busting version to image URLs | ## OpenSearch connections The `opensearch` source adapter reads connections from `opensearch`: | Key | Env | Default | |-----|-----|---------| | `opensearch.default` | `OPENSEARCH_CONNECTION` | `default` | | `opensearch.connections.default.hosts` | `OPENSEARCH_HOST` | `https://localhost:9200` | | `opensearch.connections.default.username` | `OPENSEARCH_USERNAME` | none | | `opensearch.connections.default.password` | `OPENSEARCH_PASSWORD` | none | | `opensearch.connections.default.ssl_verification` | `OPENSEARCH_SSL_VERIFICATION` | `true` | ## Source formats Under `sources.filebase.formats`, Core registers parsers for filebase entries: | Format | Class | |--------|-------| | `json` | `Json` | | `yaml` | `Yaml` | | `html` | `Html` | | `md` | `Markdown` | | `tpl` | `Template` | Set per-stream via `config.source.format`. ## Field types The `field_types` array maps handle strings to PHP field type classes. Register custom types here when extending Core: ```php 'field_types' => [ 'string' => \Streams\Core\Field\Types\StringFieldType::class, // ... 'my_type' => \App\Streams\MyFieldType::class, ], ``` See [Fields](/docs/core/fields) for registered handles. ## Per-stream config Individual streams override global defaults in their JSON `config` block: ```json { "config": { "source": { "type": "filebase", "format": "md" }, "cache": { "enabled": true, "ttl": 3600, "store": "redis" } } } ``` See [Caching](/docs/core/caching) and [Sources and adapters](/docs/core/sources-and-adapters). ## Related - [Installation](/docs/core/installation) - [Streams](/docs/core/streams) - [Extending Core](/docs/core/extending-core) --- # Core reference: Streams > Stream JSON schema — fields, extends, imports, routes, and config. Source: https://streams.dev/docs/core/streams A **stream** is a domain model defined in JSON under `streams/`. Core loads definitions at boot and exposes them through the `Streams` facade. ## Stream file structure ```json { "id": "posts", "name": "Posts", "description": "Blog posts", "extends": "base_content", "config": { "source": { "type": "filebase", "format": "json" }, "cache": { "enabled": false } }, "fields": [ { "handle": "title", "type": "string", "required": true }, { "handle": "body", "type": "string" } ], "routes": [ { "uri": "blog/{id}", "view": "posts.show", "defer": true } ] } ``` | Key | Purpose | |-----|---------| | `id` | Stream handle (used in `Streams::make('posts')`) | | `extends` | Merge fields and config from a parent stream | | `config` | Source, repository, cache, abstract entry class | | `fields` | Field definitions (array or shorthand map) | | `routes` | Stream-managed Laravel routes | ## Field shorthand Fields can be a map of handle → type string: ```json { "fields": { "title": "string", "published": "boolean" } } ``` Or full objects with `handle`, `type`, `required`, `unique`, `protected`, and `rules`. ## `@` imports Values starting with `@` load JSON from a project path: ```json { "fields": "@streams/fields/common.json" } ``` Imports resolve with `base_path()` — only JSON files are supported for `@` imports. ## Abstract streams Set `config.abstract: true` for streams that define shared fields but are not instantiated directly. Child streams use `extends`. ## Meta-stream Stream definitions themselves are entries in the stream identified by `config('streams.core.streams_id')` (default `streams`). The `streams` stream uses a self or file source depending on your project layout. ## Accessing streams ```php $stream = Streams::make('posts'); $stream->fields(); $stream->repository(); $stream->entries(); // returns Criteria ``` ## Related - [Fields](/docs/core/fields) - [Routes](/docs/core/routes) - [Sources and adapters](/docs/core/sources-and-adapters) - [Hub: Streams concept](/docs/streams) --- # Core reference: Fields > Field handles, rules, shorthands, and registered types from core.php. Source: https://streams.dev/docs/core/fields Fields define entry attributes on a stream. Each field has a **handle**, **type**, and optional validation, defaults, and UI input config. ## Definition formats **Shorthand map:** ```json { "fields": { "title": "string", "published": "boolean" } } ``` **Full object** (list form): ```json { "fields": [ { "handle": "slug", "type": "slug", "required": true, "unique": true, "rules": ["alpha_dash"], "protected": false } ] } ``` **Import:** the whole `fields` value, or any one field, can be `"@path/to/file.json"`. Core replaces it with the decoded JSON file (relative to the app root) when it builds the stream: ```json { "fields": { "title": "string", "seo": "@streams/fields/seo.json" } } ``` ## Common field options | Option | Purpose | |--------|---------| | `required` | Adds required validation rule | | `unique` | Unique within stream (via StreamsPresenceVerifier) | | `rules` | Additional Laravel validation rules | | `protected` | Omit from `toArray()` / `toJson()` | | `config.default` | Default value via factory | | `input` | Admin UI hints (used by Streams UI) | ## Registered types From `config/streams/core.php` `field_types`: | Category | Types | |----------|-------| | Numbers | `number`, `integer`, `decimal` | | Strings | `string`, `url`, `uuid`, `hash`, `slug`, `email`, `encrypted` | | Boolean | `boolean` | | Dates | `datetime`, `date`, `time` | | Selection | `enum`, `select`, `multiselect` | | Structured | `array`, `object` | | Relations | `relationship`, `polymorphic` | | Media | `file`, `image` | | Other | `color` | `enum` is an alias of `select`. There is no `text`, `textarea`, `markdown`, `html`, or `multiple` type. Use `string` with an `input` hint (for example `{"type": "textarea"}`) for long content, `multiselect` for several options, and `"multiple": true` in a relationship's `config` for several related entries. ## Type-specific config These are the `config` keys Core reads. Put them inside `config`, never next to `type`. | Type | Key | Meaning | |------|-----|---------| | `relationship` | `related` (required) | Related stream ID | | `relationship` | `multiple`, `key_name`, `relation` | Store a list of keys; related key field (default `id`); relation name for eager loading (default: handle without `_id`) | | `select`, `enum`, `multiselect` | `options` (required) | Object of value => label, or a list of values | | `array` | `items` | List of allowed item types, each `{"type": "..."}`; `enforce_items: false` skips the check | | `array` | `stream`, `related` | Cast items to entries of a stream (ID or inline definition) or look them up by key | | `array`, `object` | `wrapper` | Class used to wrap the value | | `object` | `allowed` | List of `{"stream": ...}`, `{"generic": ...}`, or `{"prototype": ...}` objects | | `polymorphic` | `related` | Optional list of expected stream IDs (not enforced) | | `uuid` | `default: true` | Generate a UUID when missing | | `slug` | `separator` | Word separator (default `-`) | | `decimal` | `precision` | Decimal places | | `date`, `datetime`, `time` | `format`, `timezone` | Storage format; timezone (default `app.timezone`) | | `color` | `format` | Default decorator output (`hex`) | Examples of every type are in [Field types](/docs/fields#field-types). The [stream definition schema](/docs/sdk/stream-schema) checks these shapes. ## Accessing values ```php $entry->title; $entry->setAttribute('title', 'Hello'); $entry->decorate('body'); // formatted output ``` ## Related - [Field decorators](/docs/core/field-decorators) - [Validation](/docs/core/validation) - [Hub: Fields](/docs/fields) --- # Core reference: Field decorators > Decorate entry field values for display — markdown, HTML, and type decorators. Source: https://streams.dev/docs/core/field-decorators Field decorators transform raw stored values into display-ready output. Call `$entry->decorate()` to apply type-specific decorators registered on field types. ## When to use decorators Use decorators in views when you need formatted output (rendered markdown, humanized dates) without mutating the stored value. ## Basic usage ```php $html = $entry->decorate('body'); ``` Each field type may register a decorator that knows how to format its value. String fields with markdown configuration return parsed HTML through the decorator pipeline. ## Protected fields Fields marked `protected: true` are omitted from `toArray()` and `toJson()` but remain accessible on the entry object and through `decorate()` when authorized. ```json { "handle": "internal_notes", "type": "string", "protected": true } ``` ## Related - [Fields](/docs/core/fields) - [Entries](/docs/core/entries) - [Views and includes](/docs/core/views-and-includes) --- # Core reference: Entries > Entry lifecycle, attributes, factory, save/delete, and protected fields. Source: https://streams.dev/docs/core/entries An **entry** is a single record in a stream. Entries are instances of `Streams\Core\Entry\Entry` or a custom class set in stream `config.abstract`. ## Creating entries **Persist immediately** via repository or criteria: ```php $post = Streams::repository('posts')->create([ 'title' => 'Hello', ]); // create() calls save() internally ``` **In-memory only** via factory (defaults applied, not saved): ```php $post = Streams::make('posts')->factory()->create(['title' => 'Draft']); $post->save(); ``` Factory batch helpers: `collect($count)`, `state($callback)`. ## Reading and updating ```php $post = Streams::entries('posts')->find('my-post'); $post->title = 'Updated'; $post->save(); ``` ## Deleting ```php $post->delete(); ``` ## Validation Validation is **opt-in**. Core does not validate automatically on save: ```php $post->validator()->validate(); $post->save(); ``` See [Validation](/docs/core/validation). ## Protected fields Fields with `"protected": true` are excluded from serialization: ```php $post->toArray(); // omits protected fields ``` ## Callbacks Entry lifecycle hooks use Core callbacks (not Laravel events): `creating`, `created`, `saving`, `saved`, `deleting`. See [Callbacks](/docs/core/callbacks). ## Related - [Repositories](/docs/core/repositories) - [Validation](/docs/core/validation) - [Field decorators](/docs/core/field-decorators) --- # Core reference: Repositories > Repository CRUD API — repository() vs entries() and available methods. Source: https://streams.dev/docs/core/repositories Repositories provide direct CRUD access to stream entries through `Streams\Core\Repository\Repository`. ## repository() vs entries() | API | Returns | Use for | |-----|---------|---------| | `Streams::repository('posts')` | `Repository` | find, create, save, delete by id | | `Streams::entries('posts')` | `Criteria` | Query chains (where, orderBy, paginate) | Both operate on the same underlying adapter. Prefer `entries()` when filtering; prefer `repository()` for single-record CRUD. ```php $repo = Streams::repository('posts'); $post = $repo->find('hello-world'); Streams::entries('posts')->where('published', true)->get(); ``` ## Repository methods | Method | Purpose | |--------|---------| | `all()` | All entries | | `find($id)` | Entry by key or null | | `findBy($field, $value)` | First match | | `findAllWhere($field, $value)` | Collection of matches | | `count()` | Entry count | | `create($attributes)` | Create and persist | | `save($entry)` | Persist changes | | `delete($entry)` | Remove entry | | `truncate()` | Remove all entries | | `newInstance($attributes)` | Unsaved entry | | `newCriteria()` | Fresh criteria instance | ## Not available These methods do **not** exist on Repository: - `createMany()` — create entries in a loop or use criteria - `paginate()` — use `Streams::entries('posts')->paginate()` - `update()` — mutate the entry object and call `save()` ## Custom repositories Bind a custom class in stream config: ```json { "config": { "repository": "App\\Streams\\PostRepository" } } ``` See [Extending Core](/docs/core/extending-core). ## Related - [Criteria](/docs/core/criteria) - [Entries](/docs/core/entries) --- # Core reference: Criteria > Query API — where, orderBy, paginate, cache, chunk, and adapter forwarding. Source: https://streams.dev/docs/core/criteria `Streams::entries()` returns a `Streams\Core\Criteria\Criteria` instance — a fluent query API executed by the stream's source adapter. ## Basic queries ```php $posts = Streams::entries('posts') ->where('published', true) ->orderBy('created_at', 'desc') ->get(); $post = Streams::entries('posts')->find('my-slug'); $count = Streams::entries('posts')->where('published', true)->count(); ``` ## Pagination ```php $paginator = Streams::entries('posts')->paginate(15); // Or with options array $paginator = Streams::entries('posts')->paginate([ 'per_page' => 20, 'page' => 2, ]); ``` ## Cache When stream `config.cache.enabled` is true, `get()` and `count()` cache automatically. Override per query: ```php Streams::entries('posts')->cache(600)->get(); Streams::entries('posts')->fresh()->get(); // bypass cache ``` Mutations (`create`, `save`, `delete`, `truncate`) flush the stream cache. ## Chunk ```php Streams::entries('posts')->chunk(100, function ($entries, $page) { foreach ($entries as $entry) { // process } }); ``` ## Upsert helpers ```php Streams::entries('posts')->firstOrCreate(['slug' => 'hello'], ['title' => 'Hello']); Streams::entries('posts')->updateOrCreate(['slug' => 'hello'], ['title' => 'Updated']); ``` ## Adapter forwarding Unknown method calls on Criteria forward to the underlying adapter when supported. ## Related - [Repositories](/docs/core/repositories) - [Caching](/docs/core/caching) - [Sources and adapters](/docs/core/sources-and-adapters) --- # Core reference: Sources and adapters > filebase, file, self, database, eloquent, collection, and filesystem adapters. Source: https://streams.dev/docs/core/sources-and-adapters Each stream's `config.source.type` selects a **repository adapter** that reads and writes entries. Core resolves the adapter in `Streams\Core\Repository\Repository`. ## Adapter types | Type | Adapter | Typical config | |------|---------|----------------| | `filebase` | `FilebaseAdapter` | `path`, `format` (json, yaml, md, html) | | `file` | `FileAdapter` | `path` (single file) | | `self` | `SelfAdapter` | entries in stream JSON `data` array | | `database` | `DatabaseAdapter` | `table`, `connection` | | `eloquent` | `EloquentAdapter` | `model` (Eloquent class) | | `collection` | `CollectionAdapter` | `data` (inline array) | | `filesystem` | `FilesystemAdapter` | `disk` (Laravel Storage disk) | | `elasticsearch` | `ElasticsearchAdapter` | `index` (default: stream ID), `search_fields`, `scout_prefix`. Requires `elasticsearch/elasticsearch`. | | `opensearch` | `OpenSearchAdapter` | `index`, `search_fields`, `scout_prefix`; connections in `streams.core.opensearch`. Requires `opensearch-project/opensearch-php`. | Default type comes from `config('streams.core.default_source')` (`filebase`). ## Filebase (most common) ```json { "config": { "source": { "type": "filebase", "format": "md" } } } ``` Entries live under `config('streams.core.data_path')` in a directory named for the stream handle unless `path` is set. ## Self source Used when entries are embedded in the stream definition: ```json { "config": { "source": { "type": "self" } }, "data": [ { "id": "one", "title": "First" } ] } ``` ## Eloquent ```json { "config": { "source": { "type": "eloquent", "model": "App\\Models\\Post" } } } ``` ## Custom adapter Set `config.adapter` to a fully qualified adapter class, or `config.criteria` for a custom criteria class. See [Extending Core](/docs/core/extending-core). ## Related - [Streams](/docs/core/streams) - [Configuration](/docs/core/configuration) - [Hub: Databases](/docs/databases) --- # Core reference: Caching > Per-stream cache config, criteria cache(), fresh(), and flush on writes. Source: https://streams.dev/docs/core/caching Core caches criteria results per stream when enabled. Cache keys are prefixed with `streams.{handle}.` via `Streams\Core\Stream\StreamCache`. ## Stream config ```json { "config": { "cache": { "enabled": true, "ttl": 3600, "store": "redis" } } } ``` | Key | Purpose | |-----|---------| | `enabled` | Auto-cache `get()` and `count()` on criteria | | `ttl` | Default seconds for query cache | | `store` | Laravel cache store name | ## Query cache ```php // Explicit TTL and optional key Streams::entries('posts')->cache(600, 'posts.published')->get(); // Bypass cache for one query Streams::entries('posts')->fresh()->get(); ``` ## Stream-level remember ```php Streams::make('posts')->cache()->remember('meta', 3600, fn () => ...); ``` ## Flush behavior These operations flush the stream cache: - `Criteria::create()` - `$entry->save()` through criteria/repository - `$entry->delete()` - `truncate()` ## Related - [Criteria](/docs/core/criteria) - [Hub: Caching](/docs/caching) --- # Core reference: Validation > Manual validator() usage, field rules, and StreamsPresenceVerifier. Source: https://streams.dev/docs/core/validation Core validation is **opt-in**. Saving an entry does not run validation automatically — call `validate()` when your application requires it. ## Entry validation ```php $post = Streams::repository('posts')->newInstance([ 'title' => '', ]); $post->validator()->validate(); // throws ValidationException on failure $post->save(); ``` ## Stream validator Build a validator from raw data: ```php $validator = Streams::make('posts')->validator([ 'title' => 'Hello', ], $existingId); $validator->validate(); ``` Rules come from field definitions (`required`, `unique`, custom `rules` arrays) and stream-level `rules`. ## Unique rules `Streams\Core\Validation\StreamsPresenceVerifier` routes `unique` and `exists` rules through the stream's criteria adapter so uniqueness is checked against stream entries, not arbitrary database tables. ## Field rules Fields map JSON flags to Laravel rule strings during stream build: ```json { "handle": "slug", "type": "slug", "required": true, "unique": true } ``` Add explicit rules: ```json { "handle": "title", "type": "string", "rules": ["max:255"] } ``` ## Related - [Entries](/docs/core/entries) - [Fields](/docs/core/fields) --- # Core reference: Routes > Stream routes JSON, Route::streams, EntryController, parse and defer. Source: https://streams.dev/docs/core/routes Streams can register Laravel routes from JSON and through the `Route::streams()` macro. ## Stream JSON routes ```json { "routes": [ { "handle": "view", "uri": "{uri}", "parse": true, "view": "{layout}" }, { "uri": "blog/{id}", "view": "posts.show", "defer": true } ] } ``` | Option | Purpose | |--------|---------| | `parse` | Register one route per entry (bind `{field}` placeholders from entries) | | `defer` | Register routes on `App::booted()` instead of immediately | | `view` | Blade view name passed to `EntryController` | | `middleware`, `verb`, `csrf` | Standard Laravel route options | URI placeholders like `{entry.slug}` become criteria bindings (`entry__slug` internally). ## Route::streams macro ```php Route::streams('posts/{id}', [ 'stream' => 'posts', 'view' => 'posts.show', ]); ``` String routes without `@` default to `EntryController` with a `view` parameter. ## EntryController `Streams\Core\Http\Controller\EntryController` resolves: - The target stream - The entry (by route id or criteria parameters) - The view or redirect from route config Returns `Response::view()` or a redirect response. ## URL helper ```php URL::streams('posts/{id}', ['id' => 'hello']); ``` ## Related - [Hub: Routing](/docs/routing) - [Hub: Site pages](/docs/site-pages) - [Applications](/docs/core/applications) --- # Core reference: Applications > Multi-app URL matching, active application, and Integrator merge keys. Source: https://streams.dev/docs/core/applications Applications are entries in the stream identified by `config('streams.core.applications_id')` (default `applications`). They enable multi-tenant or multi-site setups where each application merges its own config and streams at boot. The site-level picture, including what boot does not merge, is the [Tenancy guide](/docs/tenancy). ## Application entries The stream schema declares `handle`, `match`, and `config`. Boot also reads these attributes when the entry JSON contains them: - `match` — URL patterns compared with `Str::is` against `Request::fullUrl()` (include the scheme) - `locale` - `config` — merged into Laravel config - `aliases`, `bindings`, `singletons` - `streams` — paths passed to `Streams::load()`, or arrays passed to `Streams::register()` `Integrator` can merge routes, but `bootApplication()` does not pass a `routes` key. Putting `routes` on the entry has no effect at boot. Core activates the matching application via `Streams\Core\Support\Facades\Applications`. ## Activation flow 1. `StreamsServiceProvider` loads the applications stream 2. `ApplicationManager` matches the incoming request URL 3. `Applications::activate()` sets the active application 4. `Integrator::integrate()` merges the application's configuration ## Accessing the active application ```php Applications::active(); Applications::activate($applicationEntry); ``` Application entries extend `Streams\Core\Application\Application` (subclass of `Entry`). ## Related - [Tenancy guide](/docs/tenancy) - [Integrator](/docs/core/integrator) - [Routes](/docs/core/routes) --- # Core reference: Integrator > Wire application entries into config, streams, routes, and providers at boot. Source: https://streams.dev/docs/core/integrator `Streams\Core\Support\Integrator` merges application (or addon) configuration into Laravel during boot. ## integrate() ```php Integrator::integrate([ 'locale' => 'en', 'config' => ['app.name' => 'Acme'], 'streams' => [ 'pages' => ['name' => 'Pages', 'fields' => []], ], 'routes' => function ($router) { // register routes }, ]); ``` ## Available merge keys | Key | Method | Purpose | |-----|--------|---------| | `locale` | `Integrator::locale()` | App locale | | `config` | `Integrator::config()` | Config values | | `assets` | `Integrator::assets()` | Asset paths | | `aliases` | `Integrator::aliases()` | Class aliases | | `bindings` | `Integrator::bindings()` | Container bindings | | `singletons` | `Integrator::singletons()` | Singleton bindings | | `commands` | `Integrator::commands()` | Artisan commands | | `listeners` | `Integrator::listeners()` | Event listeners | | `policies` | `Integrator::policies()` | Authorization policies | | `routes` | `Integrator::routes()` | Route closures | | `providers` | `Integrator::providers()` | Service providers | | `schedules` | `Integrator::schedules()` | Scheduled tasks | | `middleware` | `Integrator::middleware()` | Middleware aliases | | `streams` | `Integrator::streams()` | Stream definitions | | `includes` | `Integrator::includes()` | View includes | Applications call `Integrator::integrate()` automatically when activated. Addons use the same mechanism during package boot. ## Related - [Applications](/docs/core/applications) - [Addons](/docs/core/addons) --- # Core reference: Addons > streams-addon Composer discovery and the Addons facade. Source: https://streams.dev/docs/core/addons Addons are Composer packages tagged `streams-addon` that register streams, routes, and config through Core's Integrator at boot. ## Composer discovery Add to your addon's `composer.json`: ```json { "extra": { "laravel": { "providers": ["MyVendor\\MyAddon\\ServiceProvider"] }, "streams": { "addon": true } } } ``` The addon service provider typically calls `Integrator::integrate()` with streams and bindings. ## Addons facade ```php use Streams\Core\Support\Facades\Addons; Addons::register('my-addon', $paths); ``` Use the facade to inspect registered addon paths and assets. ## Related - [Integrator](/docs/core/integrator) - [Hub: Addons](/docs/addons) --- # Core reference: Assets > Asset registry, path namespaces, @assets Blade, and HTML helpers. Source: https://streams.dev/docs/core/assets Core provides an asset registry for grouping CSS, JS, and inline fragments referenced from Blade views. There is no built-in upload pipeline or CDN integration — register paths and emit tags yourself. ## Registering assets ```php use Streams\Core\Support\Facades\Assets; Assets::register('theme.css', 'resources/css/theme.css'); Assets::add('styles', 'theme.css'); ``` Collections group assets by name (`styles`, `scripts`, etc.). ## HTML helpers ```php Assets::url('theme.css'); Assets::tag('theme.css'); Assets::script('app.js'); Assets::style('theme.css'); Assets::inline('critical.css'); Assets::contents('public::img/logo.svg'); ``` ## @assets Blade directive ```blade @assets('styles', 'theme.css') @endassets ``` The directive captures the view path and registers inline content into the named collection. ## Path namespaces UI and addons register namespaces via `Assets::addPath()` — for example `ui` maps to package resources. ## Related - [Facades and helpers](/docs/core/facades-and-helpers) - [Hub: Assets](/docs/assets) --- # Core reference: Images > Images::make, alterations, picture/srcset, versioning, and auto-alt. Source: https://streams.dev/docs/core/images Core image handling wraps Intervention Image with stream-aware URL generation, alterations, and responsive output. ## Creating images ```php use Streams\Core\Support\Facades\Images; $url = Images::make('storage://uploads/photo.jpg') ->resize(800, 600) ->crop(400, 400) ->url(); ``` `Images::make()` accepts path strings or arrays and detects local, remote, and storage sources. ## Alterations Alterations chain via `__call()` — common methods include `resize`, `crop`, `fit`, `widen`, `heighten`, `blur`, `greyscale`, and `quality`. ## Responsive output ```php Images::make('photo.jpg')->picture(); Images::make('photo.jpg')->srcset(); Images::make('photo.jpg')->img(); ``` ## Configuration | Key | Purpose | |-----|---------| | `streams.core.auto_alt` | Generate alt text when missing | | `streams.core.version_images` | Append version query for cache busting | ## Related - [Assets](/docs/core/assets) - [Hub: Images](/docs/images) --- # Core reference: Views and includes > View namespaces, Includes slots, ViewTemplate, and Factory includes. Source: https://streams.dev/docs/core/views-and-includes Core extends Laravel views with named include slots and template parsing helpers. ## Includes facade ```php use Streams\Core\Support\Facades\Includes; Includes::include('sidebar', 'partials.sidebar'); echo Includes::render('sidebar', ['user' => $user]); ``` | Method | Purpose | |--------|---------| | `include($slot, $name, $include?)` | Register an include for a slot | | `slot($name)` | Get slot contents | | `render($slot, $payload)` | Render slot with data | ## Factory macros Blade `@include` parsing extensions via Factory macros: ```php Factory::parse($template, $data); Factory::include($name, $data); Factory::includes($includes); ``` ## ViewTemplate `ViewTemplate` tracks template paths for asset collection registration in `@assets` directives. ## Related - [Assets](/docs/core/assets) - [Macros](/docs/core/macros) --- # Core reference: Facades and helpers > Streams, Assets, Images, Applications facades and global helpers. Source: https://streams.dev/docs/core/facades-and-helpers Core registers facades and global helpers for common stream operations. ## Facades | Facade | Accessor | Purpose | |--------|----------|---------| | `Streams` | `streams` | Stream manager, make, load | | `Assets` | `assets` | Asset registry | | `Images` | `images` | Image alterations | | `Includes` | `includes` | View include slots | | `Applications` | `applications` | Multi-app activation | | `Addons` | `addons` | Addon registry | ## Global helpers Defined in `src/helpers.php`: ```php stream('posts'); // Stream instance entries('posts'); // Criteria repository('posts'); // Repository html_attributes($attrs); // HTML attribute string response_time(); // Debug helper memory_usage(); // Debug helper ``` Prefer `Streams::make()`, `Streams::entries()`, and `Streams::repository()` when facades are not imported. ## Related - [Repositories](/docs/core/repositories) - [Assets](/docs/core/assets) - [Macros](/docs/core/macros) --- # Core reference: Macros > Route::streams, Str::parse, Arr::make, Factory includes, and more. Source: https://streams.dev/docs/core/macros Core registers macros on Laravel classes during `StreamsServiceProvider::registerMacros()`. ## Route and URL ```php Route::streams('posts/{id}', ['stream' => 'posts', 'view' => 'posts.show']); URL::streams('posts/{id}', ['id' => 'hello']); ``` ## Str | Macro | Purpose | |-------|---------| | `Str::parse()` | Parse template strings with entry data | | `Str::purify()` | Sanitize HTML | | `Str::humanize()` | Human-readable strings | | `Str::truncate()` | Truncate with ellipsis | | `Str::isSerialized()` | Detect serialized PHP values | ## Arr | Macro | Purpose | |-------|---------| | `Arr::make()` | Normalize array input | | `Arr::parse()` | Parse nested structures | | `Arr::export()` | Export array to string | | `Arr::htmlAttributes()` | Build HTML attributes | ## Factory (View) ```php Factory::parse($content, $data); Factory::include($partial, $data); Factory::includes($map); ``` ## Translator ```php Translator::translate($key, $replace, $locale); ``` ## Related - [Routes](/docs/core/routes) - [Views and includes](/docs/core/views-and-includes) --- # Core reference: Callbacks > FiresCallbacks on streams, entries, repositories, and criteria. Source: https://streams.dev/docs/core/callbacks Core uses the `FiresCallbacks` trait for lifecycle hooks. These are **not** Laravel `Event::` events — they fire synchronously on the object that defines them. ## Trait API Classes using `Streams\Core\Support\Traits\FiresCallbacks`: | Method | Purpose | |--------|---------| | `addCallback($name, $callback)` | Register callback | | `addCallbackListener($name, $callback)` | Listen without replacing | | `observeCallbacks($object)` | Copy callbacks from another object | | `fire($name, $payload)` | Invoke callbacks | | `hasCallback($name)` | Check registration | ## Entry lifecycle callbacks | Callback | When | |----------|------| | `creating` | Before create persists | | `created` | After create persists | | `saving` | Before save | | `saved` | After save | | `deleting` | Before delete | ## Stream callbacks Streams fire callbacks such as `built` when the stream definition is assembled. ## Example ```php Streams::make('posts')->addCallbackListener('creating', function ($payload) { // inspect $payload['entry'] }); ``` ## Related - [Entries](/docs/core/entries) - [Repositories](/docs/core/repositories) --- # Core reference: Extending Core > Custom adapters, repositories, entries, and field types via config bindings. Source: https://streams.dev/docs/core/extending-core Extend Core by binding custom classes in stream JSON or Laravel service providers. ## Custom field types Register in `config/streams/core.php`: ```php 'field_types' => [ 'rating' => \App\Streams\Fields\RatingFieldType::class, ], ``` Implement a field type class extending Core's field type base with assignment, validation, and optional decorators. ## Custom repository ```json { "config": { "repository": "App\\Streams\\PostRepository" } } ``` Implement `Streams\Core\Repository\Contract\RepositoryInterface` or extend `Repository`. ## Custom entry class ```json { "config": { "abstract": "App\\Streams\\PostEntry" } } ``` Subclass `Streams\Core\Entry\Entry` for domain methods on entries. ## Custom adapter ```json { "config": { "adapter": "App\\Streams\\Adapters\\ApiAdapter" } } ``` Adapters implement criteria execution for your storage backend. ## Custom criteria class ```json { "config": { "criteria": "App\\Streams\\PostCriteria" } } ``` ## Service provider bindings Use Laravel's container in `AppServiceProvider` for cross-cutting bindings: ```php $this->app->bind(CustomInterface::class, CustomImplementation::class); ``` ## Related - [Configuration](/docs/core/configuration) - [Sources and adapters](/docs/core/sources-and-adapters) - [Fields](/docs/core/fields) --- # UI reference: Introduction > Livewire admin panels on Core — panels, resources, and builders. Source: https://streams.dev/docs/ui/introduction Streams UI (`streams/ui`) builds Livewire admin panels on top of Streams Core. You register a **panel**, define **resource** classes for each stream, and configure **forms** and **tables** with PHP builders. ## Architecture ```text Panel (UI::panel) └── Resources (PHP classes) ├── ListEntries → Table builder ├── CreateEntry → Form builder └── EditEntry → Form builder ``` There are no `UI::form()` or `UI::table()` Blade helpers. Builders are PHP objects wired through Livewire pages. ## Minimal setup ```php use Streams\Ui\Support\Facades\UI; use Streams\Ui\Builders\Panels\Panel; UI::panel( Panel::make('admin') ->default() ->path('admin') ->middleware(['web']) ->resources([PostResource::class]) ); ``` Visit `/admin` after registering at least one resource. ## Related - [Quick start](/docs/ui/quick-start) - [Installation](/docs/ui/installation) - [Panels](/docs/ui/panels) --- # UI reference: Installation > Composer, UI::panel(Panel::make()), publish tags, and middleware. Source: https://streams.dev/docs/ui/installation Install UI alongside Core in your Laravel application. ## Require the package ```bash composer require streams/core:2.0.x-dev streams/ui:1.0.x-dev ``` UI requires `streams/core ^2.0` and Livewire `^3.0`. Neither Core 2.0 nor UI 1.0 has a stable tag yet, so require both development branches explicitly (or set `"minimum-stability": "dev"` with `"prefer-stable": true`). See [Versions and support](/docs/versions). ## Register a panel In `AppServiceProvider::boot()`: ```php use Streams\Ui\Support\Facades\UI; use Streams\Ui\Builders\Panels\Panel; UI::panel( Panel::make('admin') ->default() ->path('admin') ->brandName('My App') ->middleware(['web']) ); ``` ## Publish tags From `UiServiceProvider`: ```bash php artisan vendor:publish --tag=config --provider="Streams\Ui\UiServiceProvider" php artisan vendor:publish --tag=laravel-streams --provider="Streams\Ui\UiServiceProvider" php artisan vendor:publish --tag=ui --provider="Streams\Ui\UiServiceProvider" ``` | Tag | Output | |-----|--------| | `config` | `config/streams/ui.php` | | `laravel-streams` | `streams/` scaffold | | `ui` | `resources/views/vendor/ui/` view overrides | Package views use the `ui::` namespace. Assets register via `Assets::addPath('ui', ...)`. ## Middleware UI registers middleware alias `panel` → `Streams\Ui\Http\Middleware\SetUpPanel`. Panel routes apply `panel:{id}` automatically. ## Related - [Quick start](/docs/ui/quick-start) - [Panels](/docs/ui/panels) --- # UI reference: Quick start > Register a panel, one Resource class, and visit /admin. Source: https://streams.dev/docs/ui/quick-start This walkthrough registers an admin panel with one resource backed by a Core stream. ## 1. Define a stream `streams/posts.json`: ```json { "id": "posts", "fields": [ { "handle": "title", "type": "string", "required": true }, { "handle": "body", "type": "string" } ] } ``` ## 2. Create a resource class `app/Ui/Posts/PostResource.php`: ```php namespace App\Ui\Posts; use Streams\Ui\Resources\Resource; use Streams\Ui\Builders\Forms\Form; use Streams\Ui\Builders\Tables\Table; use Streams\Ui\Builders\Forms\Layouts\Field; use Streams\Ui\Builders\Inputs\TextInput; use Streams\Ui\Builders\Inputs\TextareaInput; use Streams\Ui\Builders\Tables\Columns\TextColumn; use Streams\Ui\Livewire\Pages\ListEntries; use Streams\Ui\Livewire\Pages\CreateEntry; use Streams\Ui\Livewire\Pages\EditEntry; class PostResource extends Resource { protected static ?string $stream = 'posts'; public static function getPages(): array { return [ 'index' => ListEntries::route('/'), 'create' => CreateEntry::route('/create'), 'edit' => EditEntry::route('/{entry}/edit'), ]; } public static function form(Form $form): Form { return $form->components([ Field::make('title')->input(TextInput::make('title')), Field::make('body')->input(TextareaInput::make('body')), ]); } public static function table(Table $table): Table { return $table->columns([ TextColumn::make('title'), ]); } } ``` ## 3. Register the panel ```php UI::panel( Panel::make('admin') ->default() ->path('admin') ->middleware(['web']) ->resources([PostResource::class]) ); ``` ## 4. Visit the panel Open `/admin/posts` (resource slug derived from class name). ## Related - [Resources](/docs/ui/resources) - [Forms](/docs/ui/forms) - [Tables](/docs/ui/tables) --- # UI reference: Architecture > Panels → pages/resources → Livewire → routes flow. Source: https://streams.dev/docs/ui/architecture Streams UI follows a Filament-inspired architecture: panels contain resources, resources define pages, and pages host Livewire components that resolve form and table builders. ## Request flow ```mermaid flowchart LR Request --> Middleware Middleware --> SetUpPanel SetUpPanel --> LivewirePage LivewirePage --> Resource Resource --> FormOrTable FormOrTable --> CoreStream ``` 1. HTTP request hits `/admin/{resource}/...` 2. `SetUpPanel` middleware boots the current panel via `UI::bootCurrentPanel()` 3. Livewire page (`ListEntries`, `CreateEntry`, `EditEntry`) mounts 4. Page delegates to `Resource::table()` or `Resource::form()` 5. Builders query or persist via Core `Streams::entries()` ## Key classes | Layer | Namespace | |-------|-----------| | Panel | `Streams\Ui\Builders\Panels\Panel` | | Resource | `Streams\Ui\Resources\Resource` | | Form | `Streams\Ui\Builders\Forms\Form` | | Table | `Streams\Ui\Builders\Tables\Table` | | Pages | `Streams\Ui\Livewire\Pages\*` | ## Builders vs Livewire Builders (`Form`, `Table`, `Action`) are configuration objects. Livewire pages (`InteractsWithForms`, `InteractsWithTable`) bind builder state to HTTP requests. ## Related - [Panels](/docs/ui/panels) - [Routing](/docs/ui/routing) - [Livewire integration](/docs/ui/livewire) --- # UI reference: Panels > Path, middleware, default panel, branding, and SetUpPanel. Source: https://streams.dev/docs/ui/panels A **panel** is an admin area with its own path, middleware, navigation, resources, and pages. ## Register a panel ```php use Streams\Ui\Support\Facades\UI; use Streams\Ui\Builders\Panels\Panel; UI::panel( Panel::make('admin') ->default() ->path('admin') ->brandName('Acme') ->middleware(['web', 'auth']) ->resources([PostResource::class]) ->pages([DashboardPage::class]) ); ``` ## Panel options | Method | Purpose | |--------|---------| | `path()` | URL prefix (`/admin`) | | `domain()` / `domains()` | Restrict to hostnames | | `default()` | Mark as default panel for URL generation | | `middleware()` | Additional middleware (always includes `panel:{id}`) | | `brandName()`, logo traits | Branding | | `homeUrl()` | Panel home link | | `routes(Closure)` | Custom route registration | ## SetUpPanel middleware Alias `panel` maps to `Streams\Ui\Http\Middleware\SetUpPanel`. It boots the current panel before Livewire handles the request. ## Related - [Installation](/docs/ui/installation) - [Theming](/docs/ui/theming) - [Routing](/docs/ui/routing) - [Navigation](/docs/ui/navigation) --- # UI reference: Routing > Panel route registration, route names, and custom routes closure. Source: https://streams.dev/docs/ui/routing Panel routes register automatically when you call `UI::panel()`. Resource pages define paths relative to the resource slug. ## Route naming Pattern: `streams.ui.{panelId}.{resourceSlug}.{pageName}` ```php PostResource::getUrl('edit', ['entry' => $post->id]); // streams.ui.admin.posts.edit ``` ## Resource page routes ```php public static function getPages(): array { return [ 'index' => ListEntries::route('/'), 'create' => CreateEntry::route('/create'), 'edit' => EditEntry::route('/{entry}/edit'), ]; } ``` ## Panel path prefix ```php Panel::make('admin')->path('admin'); // /admin/posts, /admin/posts/create, ... ``` ## Custom routes Pass a closure to `->routes()` on the panel: ```php Panel::make('admin') ->path('admin') ->routes(function () { Route::get('/settings', SettingsPage::class); }); ``` ## Standalone pages Register page classes on the panel: ```php Panel::make('admin')->pages([DashboardPage::class]); ``` Standalone pages extend `Streams\Ui\Livewire\Pages\PanelPage` and define their own slug. ## Related - [Pages](/docs/ui/pages) - [Resources](/docs/ui/resources) --- # UI reference: Resources > PHP Resource subclasses — getPages(), table(), and form(). Source: https://streams.dev/docs/ui/resources A **resource** connects a Core stream to admin CRUD pages. Subclass `Streams\Ui\Resources\Resource` and implement `getPages()`, `form()`, and `table()`. ## Basic resource ```php class PostResource extends Resource { protected static ?string $stream = 'posts'; public static function getPages(): array { return [ 'index' => ListEntries::route('/'), 'create' => CreateEntry::route('/create'), 'edit' => EditEntry::route('/{entry}/edit'), ]; } public static function form(Form $form): Form { return $form->components([/* ... */]); } public static function table(Table $table): Table { return $table->columns([/* ... */]); } } ``` ## Registration ```php Panel::make('admin')->resources([PostResource::class]); ``` ## Navigation Override static properties or `getNavigationItems()`: ```php protected static ?string $navigationGroup = 'Content'; protected static ?string $navigationLabel = 'Posts'; protected static ?string $navigationIcon = 'heroicon-o-document'; ``` ## URL helpers ```php PostResource::getUrl('index'); PostResource::getUrl('edit', ['entry' => $id]); ``` ## Does not exist - `$panel->resource('posts', [...])` JSON registration - Auto-generated forms from stream JSON alone (you implement `form()` and `table()`) ## Related - [Pages](/docs/ui/pages) - [Forms](/docs/ui/forms) - [Tables](/docs/ui/tables) --- # UI reference: Pages > ListEntries, CreateEntry, EditEntry, and custom Livewire pages. Source: https://streams.dev/docs/ui/pages **Pages** are Livewire components rendered inside a panel layout. ## Resource CRUD pages | Class | Purpose | |-------|---------| | `ListEntries` | Table listing | | `CreateEntry` | Create form | | `EditEntry` | Edit form | Namespace: `Streams\Ui\Livewire\Pages` Register through resource `getPages()`: ```php return [ 'index' => ListEntries::route('/'), 'create' => CreateEntry::route('/create'), 'edit' => EditEntry::route('/{entry}/edit'), ]; ``` Each page sets `protected static string $resource = PostResource::class`. ## Custom pages Extend `Streams\Ui\Livewire\Pages\PanelPage` for standalone screens: ```php class DashboardPage extends PanelPage { protected static string $view = 'ui::pages.dashboard'; protected static ?string $navigationLabel = 'Dashboard'; } ``` Register on the panel: ```php Panel::make('admin')->pages([DashboardPage::class]); ``` ## Related - [Resources](/docs/ui/resources) - [Routing](/docs/ui/routing) - [Livewire integration](/docs/ui/livewire) --- # UI reference: Navigation > Navigation groups, items, and wiring resources into the sidebar. Source: https://streams.dev/docs/ui/navigation Panel sidebar navigation comes from registered resources, pages, and explicit navigation configuration. ## Resource navigation Set static properties on resource classes: ```php protected static ?string $navigationGroup = 'Content'; protected static ?string $navigationLabel = 'Posts'; protected static ?string $navigationIcon = 'heroicon-o-document-text'; protected static ?int $navigationSort = 10; ``` Or override `getNavigationItems()` for full control. ## Panel navigation groups ```php Panel::make('admin') ->navigationGroups([ NavigationGroup::make('Content'), NavigationGroup::make('Settings'), ]); ``` ## Navigation items ```php use Streams\Ui\Builders\Navigation\NavigationItem; Panel::make('admin')->navigationItems([ NavigationItem::make('Dashboard') ->url('/admin') ->icon('heroicon-o-home'), ]); ``` ## NavigationItem API | Method | Purpose | |--------|---------| | `group()` | Assign to a group label | | `icon()` | Icon name | | `url()` | Link target | | `sort()` | Sort order | | `badge()` | Optional badge | | `isActiveWhen()` | Active state closure | ## Related - [Panels](/docs/ui/panels) - [Resources](/docs/ui/resources) --- # UI reference: Forms > Form::make(), Form::for($livewire), components, state, and validation. Source: https://streams.dev/docs/ui/forms Forms are built with `Streams\Ui\Builders\Forms\Form` — a Livewire-aware view builder. ## Factory methods ```php $form = Form::make('post'); // named form $form = Form::for($livewire); // bound to Livewire component Form::register($livewire); // register default form on page $form = Form::resolve(); // resolve registered form ``` Resource pages call `Resource::form($form)` from `CreateEntry` and `EditEntry`. ## Schema API Use `->components()` (not `schema()`): ```php public static function form(Form $form): Form { return $form->components([ Field::make('title') ->input(TextInput::make('title')->required()), Field::make('body') ->input(TextareaInput::make('body')), ]); } ``` ## State Forms use `$statePath` (default derived from form name). Livewire pages expose public `$data` array synchronized with form state through `InteractsWithForms`. ## Validation Form builder uses `HandlesValidation` concern. Validation runs on save actions defined in CreateEntry/EditEntry page logic. ## Does not exist - `UI::form()` Blade helper - `->schema()` method on Form - Automatic form generation from stream JSON without PHP ## Related - [Form layouts](/docs/ui/form-layouts) - [Inputs](/docs/ui/inputs) - [Livewire integration](/docs/ui/livewire) --- # UI reference: Form layouts > Field, Fieldset, Container, Section, and Grid layout components. Source: https://streams.dev/docs/ui/form-layouts Form layouts organize inputs in the admin UI. All layouts use `::make()` and `->components([...])`. ## Field Wraps a single input: ```php Field::make('title') ->label('Title') ->input(TextInput::make('title')), ``` Namespace: `Streams\Ui\Builders\Forms\Layouts\Field` ## Fieldset Groups related fields: ```php Fieldset::make('Meta')->components([ Field::make('slug')->input(TextInput::make('slug')), ]), ``` ## Container Generic wrapper for nested components: ```php Container::make()->components([/* ... */]), ``` ## Section and Grid Use container builders from `Streams\Ui\Builders\Containers`: ```php Section::make('Details')->components([ Grid::make()->columns(2)->components([ Field::make('first_name')->input(TextInput::make('first_name')), Field::make('last_name')->input(TextInput::make('last_name')), ]), ]), ``` ## Tabs Tab navigation uses `Streams\Ui\Builders\Navigation\Tabs` for multi-section forms. ## Related - [Forms](/docs/ui/forms) - [Inputs](/docs/ui/inputs) --- # UI reference: Inputs > Input classes in Streams\\Ui\\Builders\\Inputs. Source: https://streams.dev/docs/ui/inputs Inputs define form controls. All extend `Streams\Ui\Builders\Inputs\Input` and use `InputClass::make('field_handle')`. ## Available inputs | Class | Purpose | |-------|---------| | `TextInput` | Text, email, url, number (via type) | | `TextareaInput` | Multi-line text | | `SelectInput` | Single select | | `CheckboxInput` | Checkbox | | `RadioInput` | Radio group | | `ToggleInput` | Boolean toggle | | `DateInput` | Date picker | | `DatetimeInput` | Datetime picker | | `TimeInput` | Time picker | | `FileInput` | File upload field | | `ColorInput` | Color picker | | `TagsInput` | Tag list | | `MarkdownInput` | Markdown editor | | `EditorInput` | Rich text editor | ## Example ```php TextInput::make('title') ->label('Title') ->required() ->maxLength(255), SelectInput::make('status') ->options(['draft' => 'Draft', 'published' => 'Published']), ``` ## Usage in forms Wrap inputs in `Field::make()` inside form `components()`: ```php Field::make('email')->input( TextInput::make('email')->type('email') ), ``` ## Related - [Forms](/docs/ui/forms) - [Form layouts](/docs/ui/form-layouts) --- # UI reference: Tables > Table::make($livewire), query, columns, filters, and pagination. Source: https://streams.dev/docs/ui/tables Tables list stream entries in admin resources via `Streams\Ui\Builders\Tables\Table`. ## Basic table ```php public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('title')->sortable(), TextColumn::make('created_at'), ]) ->filters([ SearchFilter::make('search'), ]) ->defaultSort('title'); } ``` ## Factory ```php Table::make($livewire); // used by ListEntries Table::for($livewire); // alias ``` `ListEntries` passes the Livewire component automatically: ```php public function table(Table $table): Table { return static::getResource()::table($table); } ``` ## Query Table uses `HasQuery` and `HasStream` concerns to build a Core criteria instance from the resource's stream. ## Pagination, sorting, filters Built-in concerns handle pagination, column sorting, and filter state. See [Columns](/docs/ui/columns), [Filters](/docs/ui/filters), and [Bulk actions](/docs/ui/bulk-actions). ## Does not exist - `UI::table()` Blade helper - Stream JSON `ui.tables` auto-wiring without PHP resource classes ## Related - [Columns](/docs/ui/columns) - [Filters](/docs/ui/filters) - [Resources](/docs/ui/resources) --- # UI reference: Columns > Text, Link, Image, Icon, Toggle, and other table columns. Source: https://streams.dev/docs/ui/columns Columns display entry values in resource tables. All extend `Streams\Ui\Builders\Tables\Columns\Column`. ## Available columns | Class | Purpose | |-------|---------| | `TextColumn` | Plain text | | `LinkColumn` | Clickable link | | `SelectColumn` | Select display | | `ToggleColumn` | Inline toggle | | `IconColumn` | Icon by value | | `ImageColumn` | Thumbnail image | | `ColorColumn` | Color swatch | | `BadgeColumn` | Badge label | | `ViewColumn` | Custom Blade view | ## Example ```php TextColumn::make('title') ->label('Title') ->sortable() ->searchable(), LinkColumn::make('title') ->url(fn ($entry) => PostResource::getUrl('edit', ['entry' => $entry])), ToggleColumn::make('published'), ``` ## Related - [Tables](/docs/ui/tables) - [Filters](/docs/ui/filters) --- # UI reference: Filters > Text, Select, Search, Toggle, and Criteria table filters. Source: https://streams.dev/docs/ui/filters Filters narrow table results. Register on the table builder via `->filters([...])`. ## Filter classes | Class | Purpose | |-------|---------| | `SearchFilter` | Global search across searchable columns | | `TextFilter` | Filter single field by text | | `SelectFilter` | Filter by select options | | `ToggleFilter` | Boolean filter | | `CriteriaFilter` | Custom criteria closure | Namespace: `Streams\Ui\Builders\Tables\Filters` ## Example ```php ->filters([ SearchFilter::make('search'), SelectFilter::make('status') ->options(['draft' => 'Draft', 'published' => 'Published']), ToggleFilter::make('featured'), ]) ``` ## Criteria filter For advanced queries, use `CriteriaFilter` to modify the Core criteria instance directly. ## Related - [Tables](/docs/ui/tables) - [Core criteria](/docs/core/criteria) --- # UI reference: Bulk actions > BulkAction, selection patterns, and grouped actions. Source: https://streams.dev/docs/ui/bulk-actions Bulk actions operate on selected table rows. Register via `->bulkActions()` or `->groupedBulkActions()` on the table builder. ## BulkAction ```php use Streams\Ui\Builders\Tables\BulkActions\BulkAction; BulkAction::make('delete') ->label('Delete selected') ->action(function (Collection $records) { $records->each->delete(); }), ``` Extends `Streams\Ui\Builders\Actions\Action`. ## Bulk action groups ```php use Streams\Ui\Builders\Tables\BulkActions\BulkActionGroup; ->groupedBulkActions([ BulkActionGroup::make('Export')->actions([ BulkAction::make('csv')->action(/* ... */), ]), ]) ``` ## Built-in action `DeleteSelectedEntries` is available for standard delete flows. ## Selection Table Livewire state tracks selected entry keys. Bulk actions receive the selected record collection when invoked. ## Related - [Tables](/docs/ui/tables) - [Actions](/docs/ui/actions) --- # UI reference: Actions > Action, modals, redirects, and table header/row action groups. Source: https://streams.dev/docs/ui/actions Actions are buttons that run closures, open modals, or redirect. Base class: `Streams\Ui\Builders\Actions\Action`. ## Basic action ```php use Streams\Ui\Builders\Actions\Action; Action::make('publish') ->label('Publish') ->icon('heroicon-o-check') ->action(function ($entry) { $entry->published = true; $entry->save(); }); ``` ## Table actions Register on the table builder: ```php ->actions([ Action::make('edit')->url(fn ($entry) => PostResource::getUrl('edit', ['entry' => $entry])), ]) ->headerActions([ Action::make('create')->url(fn () => PostResource::getUrl('create')), ]) ``` ## Modals and forms Actions support modal forms via `->form([...])` and redirect via `->redirect()` concerns on `MountableAction`. ## Action groups `ActionGroup` and table-specific action classes organize related actions in dropdown menus. ## Related - [Bulk actions](/docs/ui/bulk-actions) - [Tables](/docs/ui/tables) - [Livewire integration](/docs/ui/livewire) --- # UI reference: Livewire integration > InteractsWithTable, InteractsWithForms, and panel middleware. Source: https://streams.dev/docs/ui/livewire Streams UI admin pages are Livewire components. Traits connect page state to form and table builders. ## Page traits | Trait | Used by | Namespace | |-------|---------|-----------| | `InteractsWithTable` | `ListEntries` | `Streams\Ui\Livewire\Tables` | | `InteractsWithForms` | `CreateEntry`, `EditEntry` | `Streams\Ui\Livewire\Forms` | | `InteractsWithActions` | `Page` base | `Streams\Ui\Builders\Actions\Contracts` | ## ListEntries ```php class ListEntries extends PanelPage { use InteractsWithTable; public function table(Table $table): Table { return static::getResource()::table($table); } } ``` ## CreateEntry / EditEntry ```php class CreateEntry extends PanelPage { use InteractsWithForms; public function form(Form $form): Form { return static::getResource()::form($form); } } ``` ## Panel middleware `SetUpPanel` runs on every panel request and calls `UI::bootCurrentPanel()` so builders resolve the active panel, routes, and navigation. Livewire persistent middleware includes `SetUpPanel` for subsequent requests. ## Related - [Architecture](/docs/ui/architecture) - [Pages](/docs/ui/pages) - [Routing](/docs/ui/routing) --- # UI reference: Theming > Panel color palettes, brand name, logo, favicon, and layout. Source: https://streams.dev/docs/ui/theming Streams UI themes a panel by registering color palettes and sharing them as CSS variables. There is no site-wide theme switcher in Core. `streams/data/themes.json` in this docs site is leftover data; nothing in Core or UI reads it. ## Colors `Panel::boot()` registers the panel's colors and shares `--{name}-{shade}` variables with the panel layout. The layout writes them on `:root`. Defaults, from `ColorManager::getColors()`, before you set any: | Name | Palette | |------|---------| | `danger` | `Color::Red` | | `gray` | `Color::Zinc` | | `info` | `Color::Blue` | | `primary` | `Color::Amber` | | `success` | `Color::Green` | | `warning` | `Color::Amber` | Each palette is shades `50` through `950`. The values are `r, g, b` strings (for example `245, 158, 11`), not full `rgb()` functions. Override a name with a hex string or an `rgb()` string. `Color::hex()` and `Color::rgb()` generate the shade map from that color. Or pass the shade map yourself: ```php use Streams\Ui\Builders\Panels\Panel; use Streams\Ui\Colors\Color; use Streams\Ui\Support\Facades\UI; UI::panel( Panel::make('admin') ->path('admin') ->colors([ 'primary' => '#1d4ed8', 'gray' => Color::Slate, ]) ); ``` `colors()` merges by name. Names you omit keep the defaults. A hex string has to start with `#`. An `rgb()` string has to start with `rgb`. `Color::all()` lists the built-in palette names: slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose. ## Brand and favicon ```php Panel::make('admin') ->brandName('Acme') ->brandLogo(asset('img/logo.svg')) ->favicon(asset('favicon.png')); ``` `getBrandName()` falls back to `config('app.name')`. The panel layout's head prints the favicon only when `getFavicon()` returns a value. Pass a root-absolute or fully qualified URL; a relative path breaks on nested admin URLs. ## Layout `->layout('ui::layouts.app')` is the default. The view is a Blade view name, not a file path. Replace it when the panel should render inside your own shell. Color variables are shared with every view, but only a layout that prints `$cssVariables` (as `ui::layouts.partials.head` does) will emit them. ## Generated admins Components generated by `php artisan streams:livewire` are not panels and don't use these settings. Their styling is the Tailwind classes in the generated Blade views, which you own. See the [command reference](/docs/sdk/commands#streamslivewire). ## Related - [Panels](/docs/ui/panels) - [Command reference](/docs/sdk/commands) --- # API reference: Introduction > A criteria-scoped REST API for streams and entries. You opt in to routes and own authentication. Source: https://streams.dev/docs/api/introduction Streams API (`streams/api`) turns your stream definitions into a REST API. It builds JSON responses for stream definitions and entries from the same [criteria](/docs/core/criteria) you use in PHP, so an endpoint returns exactly what `Streams::entries('posts')->where(...)->get()` would. ## What it is - **Resources and endpoints.** `StreamsResource` (list, show, create, update, patch, delete) and `EntriesResource` (list, show, create, update, patch, delete, query). - **Interfaces.** An `ApiInterface` groups resources under a path, domain, and middleware stack. You can register several (for example `v1` and `admin`). - **A consistent envelope.** Every response has `data`, `meta`, `links`, and `errors` keys. See [Responses](/docs/api/responses). - **Self-describing.** Responses carry a `self` link and `meta` (the query, payload, route parameters, and stream handle), and `GET /api/streams` returns the stream definitions themselves, which is what lets the [JavaScript client](/docs/client/introduction) discover the API. - **Optional HTTP caching** through the `ApiCache` middleware (ETags and `Cache-Control`). See [Caching](/docs/api/caching). - **Schema commands.** `php artisan api:schema` and `php artisan api:documentation` for OpenAPI output. See [OpenAPI](/docs/api/openapi). ## Why it exists Streams already knows the shape of your data: fields, types, validation rules, and relationships live in `streams/*.json`. Writing CRUD controllers by hand repeats that knowledge and drifts from it. The API reads the stream definition at request time, so adding a field to a stream adds it to the API with no controller changes. ## What it does not do - **It does not authenticate anyone.** The package has no users, tokens, or policies. Your application owns the middleware. See [Authentication](/docs/api/authentication). - **It does not mount routes on its own.** Nothing is routed until you register an interface. - **It is not JSON:API.** Request bodies are flat field maps. See [Requests](/docs/api/requests). ## How to use it ```bash composer require streams/api:1.0.x-dev ``` ```env STREAMS_API_ENABLED=true ``` ```php // app/Providers/AppServiceProvider.php use Streams\Api\Support\Facades\API; public function boot(): void { API::routeCrud(); // streams + entries under /api } ``` `GET /api/streams/posts/entries` now lists entries. Protect it before you deploy: see [Authentication](/docs/api/authentication). ## How to extend it - Register more interfaces with their own paths and middleware: [Custom interfaces](/docs/api/custom-interfaces). - Add endpoints and resources: [Custom endpoints](/docs/api/custom-endpoints). - Resolve a tenant per request: [Tenancy](/docs/api/tenancy). ## Related - [Installation](/docs/api/installation) - [Routes](/docs/api/routes) - [Responses](/docs/api/responses) --- # API reference: Installation > Require streams/api, enable the gate, and register an interface from a service provider. Source: https://streams.dev/docs/api/installation Install the API alongside [Core](/docs/core/installation). ## Require the package ```bash composer require streams/api:1.0.x-dev ``` `streams/api` has no tagged release yet, so the version constraint is the `1.0` development branch. See [Versions and support](/docs/versions). Laravel auto-discovers `Streams\Api\ApiServiceProvider`. It registers the `API` facade, the `api:schema` and `api:documentation` commands, and two middleware aliases: `api.gate` (`EnsureApiIsEnabled`) and `api.interface` (`SetUpApiInterface`). ## Publish configuration (optional) ```bash php artisan vendor:publish --tag=config --provider="Streams\Api\ApiServiceProvider" ``` This writes `config/streams/api.php`. See [Configuration](/docs/api/configuration). ## Enable the gate Every API route runs through the gate middleware (`gate_middleware`, `EnsureApiIsEnabled` by default). It returns `404 Not Found` until the API is enabled: ```env STREAMS_API_ENABLED=true ``` ## Register routes No routes exist until you register an interface. Do it in a service provider's `boot()` method: ```php // app/Providers/AppServiceProvider.php use Streams\Api\Support\Facades\API; public function boot(): void { API::routeCrud(); // streams + entries // API::routeEntries(); // entries only // API::routeStreams(); // stream definitions only } ``` The helpers register the default interface (`STREAMS_API_DEFAULT_INTERFACE`, default `api`) at the configured prefix (`STREAMS_API_PREFIX`, default `api`) with the configured middleware group (`STREAMS_API_MIDDLEWARE`, default `api`). They do nothing if the default interface is already registered, so call only one of them: `routeEntries()` followed by `routeStreams()` registers the entry routes only. > Don't call these helpers from `routes/api.php` or inside a `Route::prefix()->group()`. Registration happens immediately, so the surrounding group's prefix and middleware stack on top of the interface's own and you get paths like `/api/api/streams`. For full control over the path, middleware, and resources, register an interface yourself: ```php use Streams\Api\ApiInterface; use Streams\Api\Resources\EntriesResource; use Streams\Api\Support\Facades\API; API::interface( ApiInterface::make('api') ->path('api') ->middleware(['auth:sanctum']) ->resources([EntriesResource::class]) ); ``` ## Verify ```bash php artisan route:list --name=streams.api curl -s http://localhost/api/streams/posts/entries ``` ## Related - [Configuration](/docs/api/configuration) - [Routes](/docs/api/routes) - [Authentication](/docs/api/authentication) --- # API reference: Configuration > Every key in config/streams/api.php: enabled, prefix, middleware, interface, and the gate. Source: https://streams.dev/docs/api/configuration Configuration merges into `streams.api`. Publish `config/streams/api.php` to change it (see [Installation](/docs/api/installation)). | Key | Env | Default | Purpose | |-----|-----|---------|---------| | `enabled` | `STREAMS_API_ENABLED` | `false` | Read by the default gate middleware. When `false`, API routes respond with `gate_status`. | | `prefix` | `STREAMS_API_PREFIX` | `api` | Path used by `API::routeCrud()`, `routeEntries()`, and `routeStreams()`. | | `default_interface` | `STREAMS_API_DEFAULT_INTERFACE` | `api` | ID of the default interface. Routes on other interfaces are named `streams.api.{id}.…`. | | `middleware` | `STREAMS_API_MIDDLEWARE` | `api` | Middleware applied first to every interface's routes. Set an array in the config file for more than one. | | `gate_middleware` | — | `EnsureApiIsEnabled::class` | Middleware applied second to every interface's routes. Point it at your own class to decide who gets in. | | `gate_status` | `STREAMS_API_GATE_STATUS` | `404` | Status returned when the gate denies a request. | | `gate_message` | `STREAMS_API_GATE_MESSAGE` | `Not Found` | Message returned when the gate denies a request. | | `gate_except` | — | `[]` | Request paths (`$request->is()` patterns) that bypass the `enabled` check. | ## Middleware order Every route on every interface gets this stack, in order: 1. `middleware` from config (Laravel's `api` group by default) 2. `gate_middleware` from config 3. `SetUpApiInterface`, which resolves the current interface from the route name and boots it 4. The interface's own `->middleware([...])` 5. Resource middleware (`ApiResource::$middleware`) and endpoint middleware ## Multiple middleware The env var holds a single value. `STREAMS_API_MIDDLEWARE=api,auth:sanctum` does **not** work: Laravel reads it as one middleware named `api,auth` with a `sanctum` parameter. Use an array in the published config file instead: ```php 'middleware' => ['api', 'auth:sanctum', 'throttle:api'], ``` Or keep the config minimal and add middleware per interface (see [Authentication](/docs/api/authentication)). ## Related - [Installation](/docs/api/installation) - [Authentication](/docs/api/authentication) - [Custom interfaces](/docs/api/custom-interfaces) --- # API reference: Routes > Full route table for streams and entries endpoints. Source: https://streams.dev/docs/api/routes Register routes with `API::routeCrud()` in a service provider's `boot()` method. It registers the default interface with both resources below. Paths below assume the default prefix `api`. `API::routeStreams()` and `API::routeEntries()` register the same default interface with only one resource each. Use one helper, not several: each helper does nothing if the default interface is already registered, so calling `routeStreams()` and then `routeEntries()` leaves you with the stream routes only. To get both, call `routeCrud()`, or register your own [interface](/docs/api/custom-interfaces) with both resources. ## Stream routes (`StreamsResource`) | Method | Path | Route name | |--------|------|------------| | GET | `/api/streams` | `streams.api.streams.list` | | POST | `/api/streams` | `streams.api.streams.create` | | GET | `/api/streams/{stream}` | `streams.api.streams.show` | | PUT | `/api/streams/{stream}` | `streams.api.streams.update` | | PATCH | `/api/streams/{stream}` | `streams.api.streams.patch` | | DELETE | `/api/streams/{stream}` | `streams.api.streams.delete` | Stream controllers operate on the meta-stream from `config('streams.core.streams_id')`. ## Entry routes (`EntriesResource`) | Method | Path | Route name | |--------|------|------------| | GET | `/api/streams/{stream}/entries` | `streams.api.entries.list` | | POST | `/api/streams/{stream}/entries` | `streams.api.entries.create` | | GET | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.show` | | PUT | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.update` | | PATCH | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.patch` | | DELETE | `/api/streams/{stream}/entries/{entry}` | `streams.api.entries.delete` | | POST | `/api/streams/{stream}/query` | `streams.api.entries.query` | The `{entry}` parameter accepts composite IDs (`where => ['entry' => '(.*)']`). ## Not valid There is no shorthand `/api/{stream}` path. Always use `/api/streams/{stream}/entries`. ## Related - [Stream endpoints](/docs/api/streams-endpoints) - [Entry endpoints](/docs/api/entry-endpoints) - [Query endpoint](/docs/api/query-endpoint) --- # API reference: Request format > Flat JSON field maps on create and update — not JSON:API. Source: https://streams.dev/docs/api/requests Create and update endpoints accept **flat JSON** (or form-encoded) field maps. The API does not use JSON:API resource objects or `data.attributes` envelopes for input. ## Create entry `POST /api/streams/{stream}/entries` ```bash curl -s -X POST "http://localhost/api/streams/films/entries" \ -H "Content-Type: application/json" \ -d '{"title":"Star Wars","director":"George Lucas"}' ``` The `CreateEntry` endpoint passes the JSON body to `newInstance()` on the stream's entries. ## Update entry **PUT** sets the JSON body on the entry with `setAttributes()`; **PATCH** assigns each field in the body one at a time. Both return **200** when they update and **201** when the entry didn't exist and was created. See [Entry endpoints](/docs/api/entry-endpoints#update-entry). ```bash curl -s -X PATCH "http://localhost/api/streams/films/entries/1" \ -H "Content-Type: application/json" \ -d '{"title":"Updated Title"}' ``` The route `{entry}` id is set on the payload from the URL segment. ## Create/update stream Same flat JSON shape for stream definition entries on `/api/streams` endpoints. ## Query endpoint (different shape) `POST /api/streams/{stream}/query` uses a `parameters` array — see [Query endpoint](/docs/api/query-endpoint). ## Related - [Entry endpoints](/docs/api/entry-endpoints) - [Responses](/docs/api/responses) - [Errors](/docs/api/errors) --- # API reference: Responses > Response envelope — data, errors, links, meta — and ApiResponse API. Source: https://streams.dev/docs/api/responses Successful API responses use a consistent JSON envelope from `Streams\Api\ApiResponse`. ## Envelope shape ```json { "data": { "id": "1", "title": "Star Wars" }, "errors": [], "links": { "self": "http://localhost/api/streams/films/entries/1" }, "meta": { "stream": "films", "query": {}, "parameters": { "stream": "films", "entry": "1" } } } ``` | Key | Purpose | |-----|---------| | `data` | Resource or collection (null when empty) | | `errors` | Array of error strings | | `links` | `self`, pagination links, create `location` | | `meta` | `stream`, `query`, `payload`, `parameters`, pagination keys | HTTP status is carried in the response header, not inside the JSON body. ## Status codes used `200`, `201`, `204`, `400`, `404`, `409` Validation failures return **409** with errors populated — not 422. ## Delete response Successful delete returns **204 No Content** with an empty body (no envelope). ## Related - [Errors](/docs/api/errors) - [Pagination](/docs/api/pagination) --- # API reference: Query parameters > where, constraint, order_by, limit, skip, and per_page on GET entries. Source: https://streams.dev/docs/api/query-parameters `GET /api/streams/{stream}/entries` accepts query parameters implemented in `Streams\Api\Endpoints\Entries\ListEntries::applyFilters()` (the pre-1.0 `GetEntries` controller was replaced by this endpoint class). ## Parameters | Parameter | Example | Purpose | |-----------|---------|---------| | `where[field]` | `where[director]=Lucas` | Equality filter | | `constraint[field]` | `constraint[director]=like` | Operator: `like`, `>`, `>=`, `<`, `<=`, `!=`, `<>`, `in`, `between` | | `order_by[field]` | `order_by[title]=ASC` | Sort direction | | `limit` | `limit=10` | Max results (with `skip`) | | `skip` | `skip=20` | Offset (default `0`) | | `per_page` | `per_page=20` | Page size (default **100** in code) | | `page` | `page=2` | Page number (default `1`) | | `with` | `with=author,tags` or `with[]=author` | Eager-load relationship fields (a trailing `_id` is stripped from the handle) | ## Example ```bash curl -s "http://localhost/api/streams/films/entries\ ?where[director]=George%20Lucas\ &constraint[director]=like\ &order_by[title]=ASC\ &per_page=20&page=1" ``` Array values on `where` fields support operator/operand pairs for advanced filters. ## Related - [Query endpoint](/docs/api/query-endpoint) - [Pagination](/docs/api/pagination) - [Core criteria](/docs/core/criteria) --- # API reference: Pagination > first_page, next_page, and meta keys from addPaginationMeta(). Source: https://streams.dev/docs/api/pagination Paginated list responses add keys to `links` and `meta` via `ApiResponse::addPaginationMeta()`. ## Meta keys ```json { "meta": { "total": 150, "per_page": 100, "last_page": 2, "current_page": 1 } } ``` ## Link keys ```json { "links": { "self": "...", "first_page": "...", "next_page": "...", "previous_page": "..." } } ``` When on the first page, `previous_page` may be null. When on the last page, `next_page` may be null. ## Request parameters Use `per_page` and `page` query parameters on `GET /api/streams/{stream}/entries`. Default `per_page` is **100** in source code. ## Related - [Query parameters](/docs/api/query-parameters) - [Responses](/docs/api/responses) --- # API reference: Errors > 409 validation, 404 JSON, 204 delete, and error envelope. Source: https://streams.dev/docs/api/errors Error responses populate the `errors` array in the JSON envelope (except 204 delete). ## Status codes | Code | When | Body | |------|------|------| | **409** | Validation failure on create/update/patch | Envelope with `errors` strings | | **404** | Stream or entry not found | Envelope with `errors` (e.g. `"Entry not found."`) | | **204** | Successful delete | Empty body, no envelope | | **400** | Bad request | Envelope with `errors` | Validation uses **409 Conflict**, not HTTP 422. ## Example validation error ```json { "data": null, "errors": ["The title field is required."], "links": { "self": "..." }, "meta": { "stream": "films" } } ``` ## Example not found `GET /api/streams/films/entries/missing` returns 404 with errors populated. ## Related - [Responses](/docs/api/responses) - [Request format](/docs/api/requests) --- # API reference: Stream endpoints > CRUD for stream definitions on /api/streams. Source: https://streams.dev/docs/api/streams-endpoints Stream endpoints manage entries in the meta-stream (`config('streams.core.streams_id')`) that store stream JSON definitions. ## List streams ```bash curl -s "http://localhost/api/streams" ``` Returns collection in `data`. ## Show stream ```bash curl -s "http://localhost/api/streams/posts" ``` ## Create stream ```bash curl -s -X POST "http://localhost/api/streams" \ -H "Content-Type: application/json" \ -d '{"id":"posts","name":"Posts","fields":[]}' ``` Returns **201** with `links.location`. ## Update stream ```bash curl -s -X PUT "http://localhost/api/streams/posts" \ -H "Content-Type: application/json" \ -d '{"name":"Blog Posts"}' ``` PATCH accepts partial updates. ## Delete stream ```bash curl -s -X DELETE "http://localhost/api/streams/posts" ``` Returns **204 No Content**. ## Related - [Routes](/docs/api/routes) - [Entry endpoints](/docs/api/entry-endpoints) --- # API reference: Entry endpoints > CRUD and upsert behavior for /api/streams/{stream}/entries. Source: https://streams.dev/docs/api/entry-endpoints Entry endpoints operate on any stream by handle. ## List entries ```bash curl -s "http://localhost/api/streams/films/entries" ``` Supports [query parameters](/docs/api/query-parameters) and [pagination](/docs/api/pagination). ## Show entry ```bash curl -s "http://localhost/api/streams/films/entries/1" ``` ## Create entry ```bash curl -s -X POST "http://localhost/api/streams/films/entries" \ -H "Content-Type: application/json" \ -d '{"title":"The Last Jedi","director":"Rian Johnson"}' ``` Returns **201** on success, **409** when validation fails. ## Update entry PUT sets the payload on the entry with `setAttributes()`; PATCH assigns each field in the payload one at a time. Both validate and save, and return **200** with the entry, or **409** when validation fails. If the entry doesn't exist yet, both create it (the `{entry}` key from the URL becomes its key) and return **201**, the same as a create: ```bash curl -s -X PATCH "http://localhost/api/streams/films/entries/4" \ -H "Content-Type: application/json" \ -d '{"title":"Patched Title"}' ``` ## Delete entry ```bash curl -s -X DELETE "http://localhost/api/streams/films/entries/4" ``` Returns **204** with empty body. ## Related - [Request format](/docs/api/requests) - [Query endpoint](/docs/api/query-endpoint) --- # API reference: Query endpoint > POST /api/streams/{stream}/query with parameters array. Source: https://streams.dev/docs/api/query-endpoint The query endpoint runs criteria operations from a JSON body instead of query string parameters. ## Request `POST /api/streams/{stream}/query` ```bash curl -s -X POST "http://localhost/api/streams/films/query" \ -H "Content-Type: application/json" \ -d '{ "parameters": [ {"where": ["director", "George Lucas"]}, {"orderBy": ["title", "asc"]}, {"limit": [20]} ] }' ``` Each item in `parameters` maps to a criteria method call: the key is the method, the value is its argument list. Results are paginated like the list endpoint; pass `per_page` and `page` in the query string or body. ## Response Same envelope as list endpoints: results in `data`, pagination in `meta` and `links`, and the request body echoed in `meta.payload`. ## Security The endpoint calls **any** criteria method named in the body except `delete` and `truncate`. That includes write methods such as `create`, `save`, `firstOrCreate`, and `updateOrCreate`. Treat access to this endpoint as write access to the stream: put it behind the same middleware as your create and update endpoints, or leave `EntriesResource` off public interfaces and register only the endpoints you want (see [Custom endpoints](/docs/api/custom-endpoints)). ## When to use Prefer GET with query parameters for simple filters. Use POST query when filter payloads are too large or complex for query strings. ## Related - [Query parameters](/docs/api/query-parameters) - [Core criteria](/docs/core/criteria) --- # API reference: Custom interfaces > Register several ApiInterface instances with their own path, domain, middleware, resources, and tenant. Source: https://streams.dev/docs/api/custom-interfaces An `ApiInterface` is one mounted API: a path (and optionally a domain), a middleware stack, and the resources and endpoints it serves. `API::routeCrud()` registers a single default interface for you. Register your own when you need versions, a separate admin API, or different auth for different consumers. ## Register an interface ```php use Streams\Api\ApiInterface; use Streams\Api\Resources\EntriesResource; use Streams\Api\Resources\StreamsResource; use Streams\Api\Support\Facades\API; API::interface( ApiInterface::make('v1') ->path('api/v1') ->middleware(['auth:sanctum', 'throttle:api']) ->resources([StreamsResource::class, EntriesResource::class]) ); ``` Call `API::interface()` from a service provider's `boot()` method. Routes are registered immediately, so don't wrap the call in a route group. ## Builder methods | Method | Purpose | |--------|---------| | `ApiInterface::make(?string $id)` | Create and configure an interface. The ID names routes and must be unique. | | `path(string $path)` | URL prefix. If empty, the ID is used. | | `domain(?string $domain)` / `domains(array $domains)` | Register the routes once per domain. | | `middleware(array $middleware)` | Append middleware for every route on the interface. | | `resources(array $classes)` | `ApiResource` classes to mount (for example `EntriesResource`). | | `endpoints(array $endpoints)` | Extra endpoints: `EndpointRouter` instances or `'uri' => action` pairs (registered as GET). | | `routes(?Closure $routes)` | A closure called with the interface once per domain. Its routes are registered **outside** the interface group, so they don't get the interface path or middleware; add those yourself. | | `tenant(mixed $tenant)` | A tenant value or resolver closure for this interface. See [Tenancy](/docs/api/tenancy). | All methods return the interface, and it also uses Laravel's `Conditionable` and `Tappable`, so `->when()` and `->tap()` work. ## Route names Routes on the default interface (`STREAMS_API_DEFAULT_INTERFACE`, default `api`) are named `streams.api.{resource}.{endpoint}`, for example `streams.api.entries.list`. Routes on any other interface include its ID: `streams.api.v1.entries.list`. `SetUpApiInterface` reads the route name on each request to decide which interface is current, so `API::currentApiInterface()` and `API::getTenant()` know which interface served the request. ## Configure every interface `ApiInterface` supports `configureUsing()`, which runs a closure against each interface as it's made: ```php use Streams\Api\ApiInterface; ApiInterface::configureUsing(function (ApiInterface $interface) { $interface->middleware(['throttle:api']); }); ``` ## Subclass for reuse Override `register()` or `boot()` to package an interface. `register()` runs when you pass it to `API::interface()`; `boot()` runs on the first request that hits one of its routes. ```php use Streams\Api\ApiInterface; use Streams\Api\Resources\EntriesResource; class PartnerApi extends ApiInterface { protected function setUp(): void { $this->path('api/partners') ->middleware(['auth:sanctum']) ->resources([EntriesResource::class]); } } API::interface(PartnerApi::make('partners')); ``` ## Related - [Authentication](/docs/api/authentication) - [Custom endpoints](/docs/api/custom-endpoints) - [Routes](/docs/api/routes) --- # API reference: Custom endpoints > Add endpoints to an interface, write ApiEndpoint classes, and group them in resources. Source: https://streams.dev/docs/api/custom-endpoints Custom endpoints live on an [interface](/docs/api/custom-interfaces), so they share its path, middleware, and gate. ## Quick endpoints String keys passed to `endpoints()` are registered as GET routes inside the interface group: ```php use Streams\Api\ApiInterface; use Streams\Api\Support\Facades\API; API::interface( ApiInterface::make('api') ->path('api') ->endpoints([ 'health' => fn () => response()->json(['status' => 'ok']), 'stats' => \App\Http\Controllers\StatsController::class, ]) ); // GET /api/health, GET /api/stats ``` ## Endpoint classes Extend `Streams\Api\Builders\Endpoints\ApiEndpoint`, implement `__invoke()`, and return `ApiResponse::make()`. The static `route()` method returns an `EndpointRouter` you can pass to `endpoints()` or a resource: ```php namespace App\Api; use Illuminate\Http\JsonResponse; use Streams\Api\ApiResponse; use Streams\Api\Builders\Endpoints\ApiEndpoint; class FeaturedPosts extends ApiEndpoint { public function __invoke(): JsonResponse { $response = new ApiResponse('posts'); $criteria = $response->stream->entries(); $this->fire('apply', compact('criteria')); return $response->make( $criteria->where('featured', true)->limit(10)->get() ); } } ``` ```php ->endpoints([ FeaturedPosts::route('posts/featured', 'get', 'posts.featured'), ]) ``` `route(string $path, string|array $methods = 'get', ?string $routeName = null, array $where = [])` registers the route when the interface mounts. Endpoint instances also accept `routeMiddleware()` and `withoutRouteMiddleware()`. Firing `apply` keeps your endpoint compatible with listeners such as [tenant scoping](/docs/api/tenancy). ## Resources Group related endpoints in an `ApiResource`. Endpoint names are appended to the resource's route name prefix: ```php namespace App\Api; use Streams\Api\ApiResource; class PostsResource extends ApiResource { protected static ?string $slug = 'posts'; protected static string|array $middleware = ['auth:sanctum']; public static function getEndpoints(): array { return [ 'featured' => FeaturedPosts::route('featured', 'get'), ]; } } ``` With `usesRoutePrefix()` returning `true` (the default), routes are prefixed with the slug: `GET /api/posts/featured`, named `streams.api.posts.featured`. `EntriesResource` and `StreamsResource` return `false` because their paths already start with `streams/`. ## Override a built-in endpoint Subclass a built-in endpoint and point a resource subclass at it: ```php use Streams\Api\Endpoints\Entries\CreateEntry; use Streams\Api\Resources\EntriesResource; class AuditedCreateEntry extends CreateEntry { public function __invoke(string $stream): \Illuminate\Http\JsonResponse { $response = parent::__invoke($stream); \Illuminate\Support\Facades\Log::info("API created an entry in {$stream}"); return $response; } } class AppEntriesResource extends EntriesResource { public static function getEndpoints(): array { return [ ...parent::getEndpoints(), 'create' => AuditedCreateEntry::route('streams/{stream}/entries', 'post'), ]; } } ``` ## Related - [Custom interfaces](/docs/api/custom-interfaces) - [Responses](/docs/api/responses) - [Routes](/docs/api/routes) --- # API reference: Authentication > The API ships no auth. Your app owns access through the gate middleware and interface middleware. Source: https://streams.dev/docs/api/authentication Streams API does not authenticate or authorize anyone. It has no users, tokens, or policies. **Your application owns access control**, and the package gives you three places to put it. This is deliberate: every app already has an auth system (Sanctum, Passport, session auth, signed URLs, an API gateway), and the API should use yours rather than add another. > Registered API routes are public unless you add middleware. `STREAMS_API_ENABLED=true` opens them; it does not protect them. ## 1. The gate: `gate_middleware` Every API route runs `config('streams.api.gate_middleware')`, which defaults to `Streams\Api\Http\Middleware\EnsureApiIsEnabled` (aliased as `api.gate`). The default gate only checks `streams.api.enabled` and `gate_except`, then aborts with `gate_status` (404) and `gate_message`. Extend it the way you extend Laravel's `VerifyCsrfToken`: ```php // app/Http/Middleware/EnsureApiIsEnabled.php namespace App\Http\Middleware; use Illuminate\Http\Request; use Streams\Api\Http\Middleware\EnsureApiIsEnabled as Middleware; class EnsureApiIsEnabled extends Middleware { protected function shouldEnable(Request $request): bool { if (! parent::shouldEnable($request)) { return false; } return $request->user()?->can('use-api') ?? false; } } ``` ```php // config/streams/api.php 'gate_middleware' => \App\Http\Middleware\EnsureApiIsEnabled::class, ``` Denied requests get `gate_status`, which is `404` by default so the API's existence isn't revealed. Set `STREAMS_API_GATE_STATUS=403` if you'd rather be explicit. Because the gate runs after the `middleware` group, `$request->user()` is available when that group authenticates (for example `['api', 'auth:sanctum']`). ## 2. Interface middleware Attach authentication to an interface. It applies to every resource and endpoint on that interface: ```php use Streams\Api\ApiInterface; use Streams\Api\Resources\EntriesResource; use Streams\Api\Resources\StreamsResource; use Streams\Api\Support\Facades\API; // Public, read-mostly API API::interface( ApiInterface::make('api') ->path('api') ->middleware(['throttle:api']) ->resources([EntriesResource::class]) ); // Authenticated admin API, including stream definition CRUD API::interface( ApiInterface::make('admin') ->path('api/admin') ->middleware(['auth:sanctum', 'can:manage-streams']) ->resources([StreamsResource::class, EntriesResource::class]) ); ``` Register interfaces in a service provider's `boot()` method, not inside a route group (see [Installation](/docs/api/installation)). ## 3. Resource and endpoint middleware For finer control, subclass a resource and set its middleware: ```php use Streams\Api\Resources\EntriesResource; class ProtectedEntriesResource extends EntriesResource { protected static string|array $middleware = ['auth:sanctum']; } ``` Endpoints built with the endpoint builder also accept `routeMiddleware()` and `withoutRouteMiddleware()`. ## Sanctum example ```php // config/streams/api.php 'middleware' => ['api', 'auth:sanctum'], ``` ```bash curl -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \ https://example.com/api/streams/posts/entries ``` With the [JavaScript client](/docs/client/introduction): ```javascript import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://example.com/api', middlewares: [new AuthorizationMiddleware({ token })], }); ``` ## Checklist before you deploy - Every interface you register has authentication, or you've decided it is public. - `StreamsResource` (create, update, and delete stream definitions) is only on an admin-only interface, if at all. - `POST /streams/{stream}/query` is protected like a write endpoint. It can call criteria write methods such as `create`. See [Query endpoint](/docs/api/query-endpoint). - Rate limiting (`throttle:...`) is in the stack. - The gate is your own class if access depends on more than a feature flag. ## Related - [Configuration](/docs/api/configuration) - [Custom interfaces](/docs/api/custom-interfaces) - [Tenancy](/docs/api/tenancy) --- # API reference: Tenancy > Resolve a tenant once per request with API::tenant() or ApiInterface::tenant(), then scope criteria with endpoint callbacks. Source: https://streams.dev/docs/api/tenancy The API can resolve **one tenant per request** and hand it to your code. It does not decide what a tenant is or filter data for you: you supply a resolver, then use the tenant to scope queries. For site-level multi-tenancy (different config, data paths, or streams per domain), see Core [Applications](/docs/core/applications) and the [Tenancy guide](/docs/tenancy). ## Resolve a tenant Register a global resolver in a service provider: ```php use Streams\Api\Support\Facades\API; API::tenant(fn () => request()->user()?->organization_id); ``` Or set one per interface. An interface resolver takes precedence over the global one for routes on that interface: ```php use Streams\Api\ApiInterface; API::interface( ApiInterface::make('partners') ->path('api/partners') ->middleware(['auth:sanctum']) ->tenant(fn () => request()->header('X-Partner')) ); ``` Read it anywhere during the request: ```php $tenant = API::getTenant(); ``` `API::getTenant()` calls the resolver once and binds the result in the container as `streams.api.tenant`, so later calls in the same request return the cached value. If no resolver is registered it returns `null`. ## Scope queries `ListEntries` and `ShowEntry` fire an `apply` callback with the criteria before they run the query (`ListEntries` also fires `applied` after filters). Add a listener to scope every request: ```php use Streams\Api\Endpoints\Entries\ListEntries; use Streams\Api\Endpoints\Entries\ShowEntry; use Streams\Api\Support\Facades\API; use Streams\Core\Criteria\Criteria; $scope = function (Criteria $criteria) { if ($tenant = API::getTenant()) { $criteria->where('organization_id', $tenant); } }; ListEntries::addCallbackListener('apply', $scope); ShowEntry::addCallbackListener('apply', $scope); ``` The callback receives the criteria as `$criteria`, because listeners are called through the container with named parameters. ## What is not scoped Only the list and show endpoints fire `apply`. Create, update, patch, delete, and query, plus all stream endpoints, do **not**. To enforce tenancy on writes: - Put tenant checks in your [gate middleware](/docs/api/authentication) or interface middleware, or - Replace the endpoints with your own subclasses (see [Custom endpoints](/docs/api/custom-endpoints)), or - Keep tenants in separate data sources with Core [Applications](/docs/core/applications). ## Related - [Authentication](/docs/api/authentication) - [Custom interfaces](/docs/api/custom-interfaces) - [Tenancy guide](/docs/tenancy) --- # API reference: Caching > HTTP caching with the ApiCache middleware: ETags, Cache-Control, and per-stream opt-out. Source: https://streams.dev/docs/api/caching `Streams\Api\Http\Middleware\ApiCache` adds HTTP caching to API responses: an `ETag`, public `Cache-Control` headers, and `304 Not Modified` for clients that already have the current version. It is **not applied by default**. Add it to the interfaces that should use it. Separately, `ApiResponse` adds a `Last-Modified` header on cacheable (GET/HEAD) requests when the response data has a `lastModified()` method. ## Why Stream definitions and most entry lists change far less often than they are read. ETags let clients and CDNs skip downloading unchanged data, and the optional server-side cache saves re-running criteria for clients that ask for it. ## Enable it ```php use Streams\Api\ApiInterface; use Streams\Api\Http\Middleware\ApiCache; use Streams\Api\Resources\EntriesResource; use Streams\Api\Support\Facades\API; API::interface( ApiInterface::make('api') ->path('api') ->middleware([ApiCache::class]) ->resources([EntriesResource::class]) ); ``` ## How it behaves For each request, the middleware: 1. **Skips** non-cacheable methods (anything but GET and HEAD), requests with `Cache-Control: no-cache` or `Pragma: no-cache`, routes with no `{stream}` parameter (such as `GET /api/streams`), and streams with `config.cache.enabled` set to `false`. 2. If the request sends `Cache-Control: max-age=N`, **caches the whole response server-side** for `N` seconds in the stream's cache store, keyed on URL, method, body, and input. The response is returned without ETag headers. 3. Otherwise, runs the request and sets: - `ETag` to a quoted MD5 of the response body - `Cache-Control: public, max-age=TTL, s-maxage=TTL`, where TTL is the stream's `config.cache.ttl` (default 3600 seconds) 4. If the request's `If-None-Match` matches the ETag, responds `304 Not Modified` with no body. ## Configure per stream The middleware reads the same `cache` block as Core's [query cache](/docs/core/caching): ```json { "config": { "cache": { "enabled": true, "ttl": 600, "store": "redis" } } } ``` | Key | Effect on `ApiCache` | |-----|----------------------| | `enabled` | `false` turns HTTP caching off for this stream. Any other value (including unset) leaves it on. | | `ttl` | `max-age` and `s-maxage` in seconds. Default `3600`. | | `store` | Laravel cache store for the `max-age` server-side cache. Default is your app's default store. | ## Things to know - Responses are marked `public`. Don't put `ApiCache` on interfaces that return per-user data behind shared caches or CDNs unless you also vary the cache key (for example with a `Vary: Authorization` header in your own middleware). - The ETag is computed after the response is built, so a 304 saves bandwidth, not server work. - The [JavaScript client](/docs/client/introduction) exports an `ETagMiddleware`, but in 3.0.0 it is a placeholder and does not send `If-None-Match` yet. ## Related - [Core caching](/docs/core/caching) - [Caching guide](/docs/caching) - [Custom interfaces](/docs/api/custom-interfaces) --- # API reference: OpenAPI > The generic OpenAPI reference, plus api:schema and api:documentation for your own app. Source: https://streams.dev/docs/api/openapi ## Reference spec The `streams/api` package ships an OpenAPI 3 description of its built-in endpoints in `resources/openapi/openapi.yaml`, and this site serves a copy at [/docs/api/openapi.yaml](/docs/api/openapi.yaml). It covers every route on `StreamsResource` and `EntriesResource` (the paths `API::routeCrud()` registers), the query parameters, and the response envelope. The package's own tests check that every registered route is documented and nothing else. Entry bodies are generic there because they depend on your streams. In your own app the file is at `vendor/streams/api/resources/openapi/openapi.yaml`. The copy on this site is re-synced from the package with `php scripts/sync-openapi.php`; its header comment names the branch and commit it came from. ## Your app's spec Generate an OpenAPI document with per-stream schemas from your application's stream definitions. `api:schema` builds a tag, a component schema, and the `/streams/{id}/entries` and `/streams/{id}/entries/{entry}` paths for every stream. It does not include the stream-definition endpoints, the query endpoint, or custom endpoints, and its `info` block (contact, license) is placeholder text you should edit before publishing. ## Dump schema ```bash php artisan api:schema ``` Writes OpenAPI YAML via `ApiSchema::create()`. Default output: `api.yaml` at project root. Custom path: ```bash php artisan api:schema storage/api/openapi.yaml ``` ## Swagger UI ```bash php artisan api:documentation ``` Copies Swagger UI to `public/swagger/` and runs `api:schema public/swagger/api.yaml`. View at: `http://your-app.test/swagger/index.html` ## Related - [Routes](/docs/api/routes) - [Examples](/docs/api/examples) --- # API reference: Testing > Test your API routes with Laravel HTTP tests, and the status codes to expect. Source: https://streams.dev/docs/api/testing Test your app's API routes with ordinary Laravel HTTP tests. `streams/api` has no test base class for apps: its `Streams\Api\Tests\ApiTestCase` is in the package's `autoload-dev`, so it isn't autoloaded when you install the package, and it extends the `streams/testing` harness, which is Laravel 10 only for now. ## HTTP test Routes exist once your app registers them, for example with `API::routeCrud()` in a service provider's `boot()` method (see [Installation](/docs/api/installation#register-routes)). Your normal `Tests\TestCase` boots the app, so the routes are there: ```php namespace Tests\Feature; use Illuminate\Support\Facades\URL; use Tests\TestCase; class FilmsApiTest extends TestCase { public function test_it_lists_films(): void { $this->getJson(URL::route('streams.api.entries.list', ['stream' => 'films'])) ->assertOk() ->assertJsonStructure(['data', 'errors', 'links', 'meta']); } public function test_it_creates_a_film(): void { $this->postJson(URL::route('streams.api.entries.create', ['stream' => 'films']), [ 'title' => 'The Last Jedi', ])->assertCreated(); } } ``` If you register the routes conditionally (for example only when an env flag is set), set that flag in `phpunit.xml` or call `API::routeCrud()` in your test's `setUp()`. Tests that create or delete entries write to the stream's source. Point the source at a temporary path in tests, or restore the data afterwards (see [Testing configuration](/docs/testing/configuration)). ## Status expectations | Operation | Expected status | |-----------|-----------------| | List/show | 200 | | Create | 201 | | Update (PUT/PATCH) an existing entry | 200 | | Update (PUT/PATCH) an entry that doesn't exist | 201 (it is created) | | Validation failure | 409 | | Not found (show/delete) | 404 | | Delete success | 204 | ## Related - [Errors](/docs/api/errors) - [Hub: Testing](/docs/testing) --- # API reference: Examples > curl recipes with correct paths and response shapes. Source: https://streams.dev/docs/api/examples Complete curl examples assuming default prefix `api` and local server at `http://127.0.0.1:8000`. ## List entries ```bash curl -s "http://127.0.0.1:8000/api/streams/films/entries" ``` ## Filter and paginate ```bash curl -s "http://127.0.0.1:8000/api/streams/films/entries\ ?where[director]=Lucas&per_page=10&page=1" ``` ## Create entry ```bash curl -s -X POST "http://127.0.0.1:8000/api/streams/films/entries" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"title":"A New Hope","director":"George Lucas"}' ``` ## Update entry ```bash curl -s -X PATCH "http://127.0.0.1:8000/api/streams/films/entries/1" \ -H "Content-Type: application/json" \ -d '{"title":"Star Wars: A New Hope"}' ``` ## Delete entry ```bash curl -s -o /dev/null -w "%{http_code}" \ -X DELETE "http://127.0.0.1:8000/api/streams/films/entries/1" # 204 ``` ## Query endpoint ```bash curl -s -X POST "http://127.0.0.1:8000/api/streams/films/query" \ -H "Content-Type: application/json" \ -d '{"parameters":[{"where":["director","George Lucas"]}]}' ``` ## Related - [Entry endpoints](/docs/api/entry-endpoints) - [Request format](/docs/api/requests) - [Responses](/docs/api/responses) --- # SDK reference: Introduction > Dev-only Artisan generators and checks for streams, entries, addons, and Livewire components, plus a local MCP server for agents. Source: https://streams.dev/docs/sdk/introduction `streams/sdk` is a dev dependency. It registers Artisan generators and publishes example stream JSON. It is not part of the runtime your application serves. Install it with Core already required: ```bash composer require --dev streams/sdk:1.0.x-dev ``` Commands are registered only when the app is running in the console. The ones `php artisan` can see today are `make:stream`, `make:entry`, `make:addon`, `streams:list`, `streams:validate`, `streams:schema`, and `streams:livewire`. Arguments, options, and the commands that exist in source but are not registered are in the [command reference](/docs/sdk/commands). There is no `streams:component` or `streams:crud` command. ## What it is for Use the SDK to write the first version of a stream file, an entry, an addon package, or a JSON schema export, to check definitions with `streams:validate`, and to generate Livewire index, form, and show components with `streams:livewire`. Configured control panels belong to [Streams UI](/docs/ui/introduction), not to these generators. `streams:admin` has been removed. The SDK also ships a local-development [MCP server](/docs/mcp) for AI agents. Start it with `php artisan mcp:start streams`. It needs `laravel/mcp`, which needs Laravel 11.45+ or 12.41+. On Laravel 10, agents use `streams:list --json` and `streams:validate --json` instead. The SDK is on the `1.0.x-dev` branch and has no tagged release yet. See [Versions and support](/docs/versions). ## What a stream file looks like `make:stream` writes a minimal file. A stream you actually use names its fields. Field behavior is Core's, documented in [Fields](/docs/core/fields): ```json { "$schema": "https://streams.dev/schema/streams.schema.json", "name": "Blog Posts", "fields": [ {"handle": "id", "type": "uuid"}, {"handle": "title", "type": "string", "required": true}, {"handle": "slug", "type": "slug", "unique": true}, {"handle": "author", "type": "relationship", "config": {"related": "users"}} ] } ``` The SDK also publishes the example streams in its `streams/` directory (`blog_posts`, `products`, `contacts`, `files`, and `docs`) with `php artisan vendor:publish --tag=examples`. ## How to extend it `make:entry` asks for missing fields through console inputs bound as `streams.console.inputs.{type}`. Replace that map in `config/streams/console.php` when you add a field type. Copy the defaults from `SdkServiceProvider::registerInputs()` first; a config value replaces the map rather than merging into it. Details are in the [command reference](/docs/sdk/commands). ## Related - [Command reference](/docs/sdk/commands) - [Core streams](/docs/core/streams) - [Core fields](/docs/core/fields) - [Stream definition schema](/docs/sdk/stream-schema) - [MCP server](/docs/mcp) - [Agents](/docs/agents) --- # SDK reference: Command reference > Every Artisan command in streams/sdk: what it writes, its arguments and options, JSON output for agents, and the command classes that are not registered. Source: https://streams.dev/docs/sdk/commands `streams/sdk` adds Artisan commands to generate and check streams, entries, addons, schemas, and Livewire components. Install it as a dev dependency: ```bash composer require --dev streams/sdk:1.0.x-dev ``` Commands are registered only when the app runs in the console. Run `php artisan list` to see what your installed version provides. ## Available commands | Command | What it does | |---------|--------------| | [`make:stream`](#makestream) | Writes a validated `streams/{id}.json` | | [`make:entry`](#makeentry) | Creates or updates an entry in the stream's source | | [`make:addon`](#makeaddon) | Scaffolds an addon package in `addons/{vendor}/{name}/` | | [`streams:list`](#streamslist) | Lists registered streams, as a table or JSON | | [`streams:validate`](#streamsvalidate) | Checks definitions against the JSON Schema and the running app | | [`streams:schema`](#streamsschema) | Writes a `{id}.schema.json` entry schema per stream | | [`streams:livewire`](#streamslivewire) | Generates Livewire 3 index, form, and show components | Agents can call the same operations through the [MCP server](/docs/mcp) (`php artisan mcp:start streams`), which needs `laravel/mcp` and Laravel 11.45+ or 12.41+. ### make:stream ```bash php artisan make:stream {id} {--name=} {--description=} {--force} ``` Writes `streams/{id}.json`. The ID must be snake_case (lowercase letters, numbers, and underscores, starting with a letter), and it is also the file name. `--name` defaults to the ID word-cased (`blog_posts` becomes "Blog Posts"). The file sets `config.source.format` to `json` without a `source.type`, so the app's default adapter is used, and it has one `id` field of type `uuid` with `config.default: true`. The definition is validated (the same checks as [`streams:validate`](#streamsvalidate)) before it is written, then registered. The command refuses to replace an existing file unless you pass `--force`, and it refuses an ID that the app or an addon already registers. ```bash php artisan make:stream blog_posts --description="Articles on the blog." # Stream created: streams/blog_posts.json ``` The file starts with `"$schema": "https://streams.dev/schema/streams.schema.json"`. That URL is served by this site at [/schema/streams.schema.json](/schema/streams.schema.json), so editors that understand `$schema` validate and autocomplete the file. ### make:entry ```bash php artisan make:entry {stream} {input?} {--update} ``` Creates an entry in `{stream}`. `input` is query-string formatted (`title=Hello&status=draft`). The command prompts for every field you didn't provide, validates the input against the stream's rules, then saves. With `--update`, validation treats the entry as existing, and if the input includes the stream's key (`key_name`, default `id`) and that entry exists, its attributes are updated. ```bash php artisan make:entry posts "title=Hello&status=draft" php artisan make:entry posts "id=hello&status=live" --update ``` Validation errors are printed and nothing is saved. On success the saved entry is printed as JSON. ### make:addon ```bash php artisan make:addon {vendor/name} {--description=} {--force} ``` Scaffolds a Composer package under `addons/{vendor}/{name}/` with a `composer.json` and a service provider at `src/{Name}Provider.php`. The name must be a valid Composer package name. Without `--description` the command asks for one. It refuses to overwrite an existing addon unless you pass `--force`. ```bash php artisan make:addon acme/reviews --description="Product reviews." # Created: addons/acme/reviews/composer.json # Created: addons/acme/reviews/src/ReviewsProvider.php ``` Add the directory as a Composer path repository to install it. See [Addons](/docs/addons). ### streams:list ```bash php artisan streams:list {--json} ``` Lists every registered stream, sorted by ID, with its name, source type, field count, and description. `--json` prints an array instead, which is what scripts and agents should read: ```json [ { "id": "posts", "name": "Posts", "description": "Blog posts.", "source": "filebase", "fields": ["id", "title", "status", "author_id"], "extends": null } ] ``` ### streams:validate ```bash php artisan streams:validate {paths?*} {--json} ``` Validates stream definitions. With no paths it checks every `streams/*.json`. Each file is checked in three steps, and later steps run only when earlier ones pass: 1. The [stream definition JSON Schema](/schema/streams.schema.json) (the same file `$schema` points at). 2. The running app: field types must be registered, `extends` must name a registered stream, adapter and model classes must exist, and `related` must sit inside `config`. A related stream that is not registered yet is a warning. So is an `@` import that does not resolve. 3. A real build with `Streams::build()`. ```bash php artisan streams:validate php artisan streams:validate streams/posts.json streams/authors.json --json ``` The table output ends with `N checked, M invalid.`. `--json` prints an object keyed by file, each with `valid`, `errors`, and `warnings`. The exit code is non-zero when any file is invalid, so the command works as a CI or pre-commit check. ### streams:schema ```bash php artisan streams:schema {--include=} {--exclude=} {--path=} ``` Writes a JSON schema (`{id}.schema.json`) for the *entries* of every registered stream, built from Core's `StreamSchema`: the stream's tag metadata merged with its object schema. This is not the definition schema that `$schema` points at. `--include` and `--exclude` take comma-separated stream IDs. `--path` is relative to the project root and must already exist; it defaults to the project root. ```bash mkdir -p storage/schemas php artisan streams:schema --include=posts,authors --path=storage/schemas ``` ### streams:livewire ```bash php artisan streams:livewire {stream} {--type=all} {--force} ``` Generates Livewire 3 components for a stream. `--type` is `index`, `form`, `show`, or `all` (the default). The components use the stream's repository and criteria, so they work with any source adapter. | Type | Class | What it does | |------|-------|--------------| | `index` | `{Stream}Index` | Paginated, sortable table with a delete action | | `form` | `{Stream}Form` | Create and edit form, validated with rules taken from the stream's fields | | `show` | `{Stream}Show` | Read-only view of one entry | Classes go in `config('livewire.class_namespace')` (default `App\Livewire`, so `app/Livewire/BlogPostsIndex.php`). Views go in `resources/views/livewire/`, named after the class in kebab case (`blog-posts-index.blade.php`). Protected fields are never rendered, and a generated `integer` or `uuid` key is left out of the form. The command stops without writing anything if any target file exists, unless you pass `--force`. It does not register routes. It prints the routes to add to `routes/web.php`: ```php Route::get('/blog-posts', \App\Livewire\BlogPostsIndex::class)->name('blog_posts.index'); Route::get('/blog-posts/create', \App\Livewire\BlogPostsForm::class)->name('blog_posts.create'); Route::get('/blog-posts/{entry}/edit', \App\Livewire\BlogPostsForm::class)->name('blog_posts.edit'); Route::get('/blog-posts/{entry}', \App\Livewire\BlogPostsShow::class)->name('blog_posts.show'); ``` The components are full-page, so they render inside your Livewire layout (`config('livewire.layout')`). Use [Streams UI](/docs/ui/introduction) instead when you want a configured control panel rather than files you own and edit. `streams:admin` has been removed. Generate the components with `streams:livewire` and put them behind your own layout and routes. ## Not registered These command classes exist in the SDK source but are **not registered**, so `php artisan` won't find them: | Command | Intended purpose | State | |---------|------------------|-------| | `streams:show` | Show one stream's attributes | Implemented, registration commented out. Use the MCP `describe-stream` tool. | | `entries:list` | Paginated table of a stream's entries | Implemented, registration commented out. Use the MCP `list-entries` tool. | | `entries:show` | Show one entry | Implemented, registration commented out. Use the MCP `read-entry` tool. | | `streams:describe` | Write `streams/{id}.json` by inspecting a URL, JSON, database table, or Eloquent model | Implemented, registration commented out | | `streams:tap` | Call a "tap" URL with query-string input | Not implemented (empty handler) | ## How to extend it `make:entry` prompts for each missing field through a console input bound as `streams.console.inputs.{type}`. `SdkServiceProvider::registerInputs()` binds string, boolean, select/enum, array, and object inputs. `number`, `decimal`, `date`, `time`, and `datetime` use the string input. `integer` is listed twice in that map; the later entry wins, so `integer` is also prompted as a string and `IntegerConsoleInput` is never bound. To prompt for a custom field type, set the full map in `config('streams.console.inputs')`. The config value replaces the default map, so copy the defaults from `registerInputs()` and add yours: ```php // config/streams/console.php return [ 'inputs' => [ // ...the SDK defaults... 'money' => \App\Console\Inputs\MoneyConsoleInput::class, ], ]; ``` ## Related - [SDK introduction](/docs/sdk/introduction) - [MCP server](/docs/mcp) - [Stream definition schema](/docs/sdk/stream-schema) - [Core streams](/docs/core/streams) --- # SDK reference: Stream definition schema > The JSON Schema for streams/*.json, served at /schema/streams.schema.json: what it checks, editor setup, and validating with streams:validate. Source: https://streams.dev/docs/sdk/stream-schema Stream definitions are the JSON files in `streams/` (for example `streams/posts.json`). `streams/sdk` ships a [JSON Schema](https://json-schema.org) (draft-07) for them in `vendor/streams/sdk/resources/schemas/streams.schema.json`, and this site serves the same file at: ```text https://streams.dev/schema/streams.schema.json ``` That URL is the schema's `$id`. It is also the `$schema` that `make:stream` and the `make-stream` MCP tool write into new definitions, so editors that follow `$schema` validate and autocomplete stream files with no setup. ## What it checks - Top-level keys: `id`, `name`, `description`, `extends`, `config`, `fields`, `rules`, `route`, `routes`, `data`, `ui`. - `config.source`: known source types (`filebase`, `file`, `self`, `database`, `eloquent`, `filesystem`, `collection`, `elasticsearch`, `opensearch`, or an adapter class) and formats (`json`, `yaml`, `md`, `html`, `tpl`, `csv`). - Both field forms: a list of field objects with `handle`, or an object keyed by handle whose values are a field object or a type string. - Imports: the whole `fields` value, or any single field, can be `"@path/to/file.json"`. Core replaces it with the decoded file, relative to the app root, when it builds the stream. - Field types: the core types are offered for completion. Other lowercase names are allowed because apps and addons can register their own types. - Type-specific config: `relationship` fields need `config.related` (one stream ID, or a list of IDs for polymorphic and multi-target fields). `select`, `enum`, and `multiselect` fields need `config.options`. `object` fields list `config.allowed` as `{"stream": ...}`, `{"generic": ...}`, or `{"prototype": ...}` objects. `eloquent` sources need `config.source.model`. - `data`: inline entries for a `self` source. - `input` on a field: a form hint for Streams UI. Core does not read it. Unknown top-level keys are allowed because addons extend definitions. Two shapes show up in older examples and are ignored by Core. `streams:validate` reports both: - `"related"` next to `"type"`. The value has to be `config.related`. - `"default"` next to `"type"`. The value has to be `config.default` (for a `uuid` field, `"default": true` generates a UUID). ## Validate from the command line ```bash php artisan streams:validate # every streams/*.json php artisan streams:validate streams/posts.json # specific files php artisan streams:validate --json # machine-readable output ``` The command exits non-zero if any file is invalid. Besides the schema, it checks the running app: - field types are registered, - `extends` streams exist, - `config.related` streams exist (a warning, since you may be creating them together), - `@` imports resolve (a warning), - model and adapter classes exist, and - Core can build the definition. Agents connected to the [MCP server](/docs/mcp) get the same checks from `validate-stream-definition`, and the schema from `definition-schema` or the `streams://schemas/streams.schema.json` resource. See the [command reference](/docs/sdk/commands#streamsvalidate). ## Use it in your editor Add `$schema` to a definition: ```json { "$schema": "https://streams.dev/schema/streams.schema.json", "name": "Posts", "fields": [] } ``` To validate offline, or against the exact version you have installed, map stream files to the local copy. In VS Code, add this to `.vscode/settings.json`: ```json { "json.schemas": [ { "fileMatch": ["**/streams/*.json"], "url": "./vendor/streams/sdk/resources/schemas/streams.schema.json" } ] } ``` ## Related - [Command reference](/docs/sdk/commands) - [MCP server](/docs/mcp) - [Core streams](/docs/core/streams) - [Core fields](/docs/core/fields) --- # SDK reference: Streams > Define streams with the SDK scaffolding workflow. Source: https://streams.dev/docs/sdk/streams Streams are the foundation of Laravel Streams - they define the structure and behavior of your data entities. Think of them as dynamic Eloquent models that can be configured through JSON. ## Basic Stream Structure ```json { "$schema": "https://streams.dev/schema/streams.schema.json", "name": "Blog Posts", "handle": "blog_posts", "description": "Content management for blog articles", "config": { "source": { "type": "eloquent", "model": "App\\Models\\BlogPost" } }, "fields": [ { "handle": "id", "type": "integer", "config": { "auto_increment": true } }, { "handle": "title", "type": "string", "name": "Title", "required": true, "config": { "max": 255 } } ], "routes": [ { "handle": "index", "uri": "blog", "view": "blog.index" } ] } ``` ## Stream Properties ### Required Properties - **name**: Human-readable name for the stream - **handle**: Unique identifier (snake_case recommended) - **fields**: Array of field definitions ### Optional Properties - **description**: Brief description of the stream's purpose - **config**: Configuration options for data source and behavior - **routes**: URL routing definitions - **validation**: Stream-level validation rules ## Data Sources ### Eloquent Models ```json { "config": { "source": { "type": "eloquent", "model": "App\\Models\\Product" } } } ``` ### File Storage (JSON/YAML) ```json { "config": { "source": { "format": "json", "path": "content/products" } } } ``` ### Database Tables ```json { "config": { "source": { "type": "database", "table": "products" } } } ``` ## Stream Configuration Options ### Pagination ```json { "config": { "pagination": { "per_page": 20, "page_name": "page" } } } ``` ### Sorting ```json { "config": { "sort": { "field": "created_at", "direction": "desc" } } } ``` ### Caching ```json { "config": { "cache": { "ttl": 3600, "tags": ["products", "catalog"] } } } ``` ## Routing Integration Streams can automatically generate routes for common operations: ```json { "routes": [ { "handle": "index", "uri": "products", "view": "products.index" }, { "handle": "show", "uri": "products/{id}", "view": "products.show" }, { "handle": "api", "uri": "api/products", "uses": "App\\Http\\Controllers\\Api\\ProductController" } ] } ``` ## AI Assistant Examples When creating streams, consider these common patterns: ### Content Management - Blog posts, pages, articles - Categories, tags, taxonomies - Media galleries, file management ### E-commerce - Products, variants, inventories - Orders, customers, payments - Reviews, ratings, wishlists ### User Management - Users, roles, permissions - Profiles, preferences, settings - Activity logs, notifications ### Business Applications - Contacts, companies, leads - Projects, tasks, timelines - Invoices, payments, reports ## Best Practices 1. **Use Descriptive Handles**: Choose clear, descriptive identifiers 2. **Plan Relationships**: Consider how streams connect to each other 3. **Think Mobile-First**: Design for responsive interfaces 4. **Consider Performance**: Add appropriate indexes and caching 5. **Validate Early**: Define validation rules in the stream definition 6. **Document Purpose**: Add clear descriptions for future developers --- # SDK reference: TALL Components > Tailwind, Alpine, Laravel, and Livewire patterns in the SDK. Source: https://streams.dev/docs/sdk/tall-components This guide covers how the Laravel Streams SDK generates and works with TALL stack components (Tailwind CSS, Alpine.js, Laravel, and Livewire). ## Overview The SDK automatically generates: - **Tailwind CSS**: Utility-first styling with pre-built component classes - **Alpine.js**: Lightweight JavaScript framework for interactivity - **Laravel**: Backend controllers, routes, and middleware - **Livewire**: Full-stack reactive components ## Generated Components ### Livewire Components #### Index Component (List View) ```php ['except' => ''], 'sortField' => ['except' => 'created_at'], 'sortDirection' => ['except' => 'desc'], ]; public function updatingSearch() { $this->resetPage(); } public function sortBy($field) { if ($this->sortField === $field) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortDirection = 'asc'; } $this->sortField = $field; } public function render() { $stream = Streams::make('blog_posts'); $entries = $stream->entries() ->when($this->search, function ($query) { $query->where('title', 'like', '%' . $this->search . '%') ->orWhere('content', 'like', '%' . $this->search . '%'); }) ->orderBy($this->sortField, $this->sortDirection) ->paginate($this->perPage); return view('livewire.blog-post-index', [ 'entries' => $entries, 'stream' => $stream, ]); } } ``` #### Form Component (Create/Edit) ```php 'required|min:3|max:255', 'slug' => 'required|unique:blog_posts,slug', 'content' => 'required|min:10', 'featured_image' => 'nullable|image|max:2048', 'status' => 'required|in:draft,published,archived', 'published_at' => 'nullable|date', ]; public function mount($entryId = null) { if ($entryId) { $this->entryId = $entryId; $this->loadEntry(); } } public function loadEntry() { $stream = Streams::make('blog_posts'); $entry = $stream->repository()->find($this->entryId); if ($entry) { $this->fill($entry->toArray()); } } public function updatedTitle() { if (!$this->entryId) { $this->slug = str()->slug($this->title); } } public function save() { $this->validate(); $stream = Streams::make('blog_posts'); $data = [ 'title' => $this->title, 'slug' => $this->slug, 'content' => $this->content, 'status' => $this->status, 'published_at' => $this->published_at, ]; if ($this->featured_image) { $data['featured_image'] = $this->featured_image->store('blog-images', 'public'); } if ($this->entryId) { $entry = $stream->repository()->find($this->entryId); $entry->update($data); } else { $entry = $stream->repository()->create($data); } session()->flash('message', 'Blog post saved successfully!'); return redirect()->route('blog-posts.index'); } public function render() { return view('livewire.blog-post-form'); } } ``` ### Blade Templates #### Index View ```blade {{-- resources/views/livewire/blog-post-index.blade.php --}}
{{-- Header --}}

Blog Posts

Manage your blog content

{{-- Filters --}}
{{-- Table --}}
@forelse($entries as $entry) @empty @endforelse
Title @if($sortField === 'title') @if($sortDirection === 'asc') @else @endif @endif
Status Published At Actions
@if($entry->featured_image) {{ $entry->title }} @endif
{{ $entry->title }}
{{ $entry->slug }}
{{ ucfirst($entry->status) }} {{ $entry->published_at ? $entry->published_at->format('M j, Y') : 'Not published' }}
View Edit

No blog posts

Get started by creating a new blog post.

{{-- Pagination --}}
{{ $entries->links() }}
``` #### Form View ```blade {{-- resources/views/livewire/blog-post-form.blade.php --}}
{{-- Header --}}

{{ $entryId ? 'Edit Blog Post' : 'Create Blog Post' }}

{{ $entryId ? 'Update your blog post information' : 'Create a new blog post' }}

{{-- Main Content --}}
{{-- Basic Information --}}

Basic Information

@error('title')

{{ $message }}

@enderror
@error('slug')

{{ $message }}

@enderror
@error('content')

{{ $message }}

@enderror
{{-- Sidebar --}}
{{-- Status --}}

Publishing

{{-- Featured Image --}}

Featured Image

@if($featured_image)
@endif
@error('featured_image')

{{ $message }}

@enderror
``` ## Alpine.js Enhancements ### Interactive Components ```html

Confirm Action

Are you sure you want to proceed?

``` ### Form Enhancements ```html
Saving... Saved!
/ characters
``` ## Tailwind CSS Utilities ### Component Classes ```css /* Custom component classes for streams */ .stream-card { @apply bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow; } .stream-table { @apply min-w-full divide-y divide-gray-200; } .stream-table th { @apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider bg-gray-50; } .stream-table td { @apply px-6 py-4 whitespace-nowrap text-sm text-gray-900; } .stream-form { @apply space-y-6; } .stream-form .form-group { @apply space-y-1; } .stream-form label { @apply block text-sm font-medium text-gray-700; } .stream-form input[type="text"], .stream-form input[type="email"], .stream-form input[type="password"], .stream-form textarea, .stream-form select { @apply mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm; } .btn-primary { @apply inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500; } .btn-secondary { @apply inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500; } .btn-danger { @apply inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500; } ``` ## Usage with SDK Commands `streams:livewire` is the only SDK command that generates components. It writes Livewire 3 classes and Blade views you then own and restyle with the patterns above: ```bash # Index, form, and show components for a stream php artisan streams:livewire blog_posts # Or one component at a time php artisan streams:livewire blog_posts --type=index php artisan streams:livewire blog_posts --type=form php artisan streams:livewire blog_posts --type=show ``` The command prints the routes to add to `routes/web.php`. There is no `streams:tall`, `streams:blade`, or `streams:admin` command. See the [command reference](/docs/sdk/commands#streamslivewire). --- # SDK reference: AI Prompts > Patterns and prompts for AI-assisted Streams development. Source: https://streams.dev/docs/sdk/ai-prompts This guide provides AI assistants with specific prompts, patterns, and examples for effectively using the Laravel Streams SDK to help developers build applications. ## AI Assistant Workflow When helping users with Laravel Streams, follow this structured approach: ### 1. Requirements Gathering Ask clarifying questions to understand the project: ```text Before we start building with Laravel Streams, I need to understand your requirements: 1. What type of application are you building? (blog, e-commerce, CRM, etc.) 2. What data entities do you need to manage? 3. Do you need an admin panel for content management? 4. What are the key relationships between your data? 5. Do you have any specific UI/UX requirements? 6. Will this be used by multiple user types/roles? ``` ### 2. Data Structure Analysis Help users design their stream structure: ```text Based on your requirements, I recommend these streams: For a blog application: - blog_posts: Main content with title, content, author, category - categories: Organize posts by topic - tags: Flexible labeling system - users: Authors and administrators Let me create the stream definitions for you. ``` ### 3. Implementation Steps Provide a clear roadmap: ```text Here's how we'll implement your application: 1. Create stream definitions with proper field types 2. Generate Livewire components for CRUD operations 3. Build an admin panel for content management 4. Customize the UI to match your design requirements 5. Add any custom business logic Let's start with step 1... ``` ## Common Prompts for Stream Creation ### Blog System ```bash # Create the main blog stream php artisan make:stream blog_posts # Generate the index, form, and show Livewire components php artisan streams:livewire blog_posts # Publish blog example for reference php artisan vendor:publish --tag=blog-example ``` ### E-commerce System ```bash # Create product catalog php artisan make:stream products # Generate the index and form components php artisan streams:livewire products --type=index php artisan streams:livewire products --type=form # Publish e-commerce example php artisan vendor:publish --tag=ecommerce-example ``` ### CRM System ```bash # Create contact management php artisan make:stream contacts # Generate CRM components (index, form, and show) php artisan streams:livewire contacts # Publish CRM example php artisan vendor:publish --tag=crm-example ``` ## Field Type Recommendations ### Content Management When users need content management, recommend these field types. Field behavior is Core's ([Fields](/docs/core/fields)): length and range limits go in `rules`, long text is a `string` with an `input` hint, and type options go in `config`: ```json { "fields": { "title": { "type": "string", "required": true, "rules": [ "max:255" ] }, "slug": { "type": "slug", "unique": true }, "content": { "type": "string", "required": true, "input": { "type": "editor" } }, "featured_image": { "type": "image" }, "status": { "type": "select", "config": { "options": { "draft": "Draft", "published": "Published", "archived": "Archived" } } } } } ``` ### E-commerce Fields For product catalogs, suggest: ```json { "fields": { "name": { "type": "string", "required": true }, "sku": { "type": "string", "unique": true, "required": true }, "price": { "type": "decimal", "required": true, "rules": [ "min:0" ], "config": { "precision": 2 } }, "stock_quantity": { "type": "integer", "rules": [ "min:0" ], "config": { "default": 0 } }, "is_featured": { "type": "boolean", "config": { "default": false } } } } ``` ### User Management For user-related streams: ```json { "fields": { "first_name": { "type": "string", "required": true, "rules": [ "max:100" ] }, "last_name": { "type": "string", "required": true, "rules": [ "max:100" ] }, "email": { "type": "email", "unique": true, "required": true }, "avatar": { "type": "image" }, "role": { "type": "select", "config": { "options": { "user": "User", "admin": "Administrator", "moderator": "Moderator" } } } } } ``` ## Customization Prompts ### UI Customization When users want to customize the interface: ```text I can help you customize the generated components. Here are common customizations: 1. **Styling**: Modify Tailwind CSS classes in the Blade templates 2. **Layout**: Change the grid layout or add/remove sections 3. **Functionality**: Add custom Alpine.js interactions 4. **Validation**: Enhance form validation rules 5. **Navigation**: Customize the admin panel navigation Which aspect would you like to customize first? ``` ### Business Logic For adding custom functionality: ```text To add custom business logic to your streams: 1. **Custom Methods**: Add methods to your Livewire components 2. **Event Listeners**: Set up event handling for real-time updates 3. **Validation Rules**: Create custom validation logic 4. **Relationships**: Define complex data relationships 5. **APIs**: Add API endpoints for external integrations Let me show you how to implement [specific feature]... ``` ## Troubleshooting Prompts ### Common Issues Help users resolve typical problems: ```text Let me help you troubleshoot this issue. Here are the most common problems and solutions: 1. **Missing Stream**: Make sure the stream definition exists in streams/ directory 2. **Component Errors**: Check that all required fields are defined 3. **Route Conflicts**: Verify routes are properly namespaced 4. **Permission Issues**: Ensure proper file permissions for generated files 5. **Asset Issues**: Run `npm run dev` to compile Tailwind CSS Can you share the specific error message you're seeing? ``` ### Performance Optimization When users need performance improvements: ```text Here are performance optimization strategies for your streams: 1. **Eager Loading**: Load related data efficiently 2. **Caching**: Cache expensive queries and computations 3. **Pagination**: Implement proper pagination for large datasets 4. **Indexing**: Add database indexes for frequently queried fields 5. **Image Optimization**: Compress and resize images automatically Let me show you how to implement these optimizations... ``` ## Advanced Patterns ### Multi-tenant Applications For SaaS applications: ```php // Add tenant filtering to streams public function render() { $entries = $this->stream->entries() ->where('tenant_id', auth()->user()->tenant_id) ->paginate($this->perPage); return view('livewire.blog-posts-index', compact('entries')); } ``` ### API Integration For headless/API-first applications: ```php // Add API endpoints Route::apiResource('blog-posts', BlogPostApiController::class); // In controller public function index() { $stream = Streams::make('blog_posts'); return BlogPostResource::collection( $stream->entries()->paginate(request('per_page', 15)) ); } ``` ### Real-time Updates For live data updates: ```php // Add broadcasting to Livewire components protected $listeners = ['postUpdated' => 'refreshPosts']; public function refreshPosts() { $this->emit('$refresh'); } // Broadcast events when data changes public function save() { // Save logic here broadcast(new PostUpdated($this->entry)); } ``` ## Code Generation Examples ### Complete Blog Setup ```bash # 1. Publish blog example php artisan vendor:publish --tag=blog-example # 2. Check the definition, then generate index, form, and show components php artisan streams:validate streams/blog_posts.json php artisan streams:livewire blog_posts # 3. Add the printed routes to routes/web.php ``` ### E-commerce Product Catalog ```bash # 1. Publish product example php artisan vendor:publish --tag=ecommerce-example # 2. Generate product management php artisan streams:livewire products --type=index php artisan streams:livewire products --type=form # 3. Add the printed routes to routes/web.php, then customize the # generated classes in app/Livewire and views in resources/views/livewire ``` ## Best Practices for AI Assistants ### 1. Always Ask Context Questions Before generating code, understand: - User's experience level - Project requirements - Existing codebase - Performance needs - Security requirements ### 2. Provide Complete Solutions Don't just generate partial code: - Include all necessary files - Add proper validation - Include error handling - Provide styling - Add documentation ### 3. Explain Choices When recommending field types or patterns: - Explain why you chose specific options - Mention alternatives - Discuss trade-offs - Provide examples ### 4. Follow Security Best Practices Always include: - Proper validation rules - CSRF protection - Authorization checks - Input sanitization - SQL injection prevention ### 5. Consider Performance Optimize generated code: - Use pagination for large datasets - Implement caching where appropriate - Add database indexes - Optimize images and assets - Use eager loading for relationships ## Sample Conversations ### Blog Setup Conversation ```text User: I want to create a blog with Laravel Streams. AI: I'd be happy to help you create a blog with Laravel Streams! Let me ask a few questions to ensure we build exactly what you need: 1. Will you have multiple authors or just yourself? 2. Do you need categories and tags for organizing posts? 3. Do you want an admin panel for content management? 4. Any specific design requirements or should we use the default TALL stack styling? Based on your answers, I'll generate the complete blog system for you. ``` ### E-commerce Conversation ```text User: I need to build a product catalog. AI: Great! A product catalog is perfect for Laravel Streams. To build the right solution, I need to understand your requirements: 1. What types of products will you sell? (physical, digital, or both) 2. Do you need inventory management? 3. Will you have product variants (size, color, etc.)? 4. Do you need categories and brands? 5. Any specific pricing features (sales, bulk pricing, etc.)? Once I understand your needs, I'll create the complete product management system with admin panel. ``` This guide ensures AI assistants can effectively help users build robust applications with the Laravel Streams SDK while following best practices and providing comprehensive solutions. --- # Testing reference: Introduction > Testing package for Streams with pre-configured environments and sample data. Source: https://streams.dev/docs/testing/introduction The Streams Testing package provides everything you need to test your Streams applications with confidence. Built on top of Orchestra Testbench, it offers a fully configured Laravel testing environment with sample data, automatic cleanup, and testing utilities specifically designed for Streams development. ## What's Included - **Pre-configured Laravel Environment**: A complete Laravel application setup ready for testing - **Sample Test Data**: Star Wars-themed sample streams for learning and testing - **Automatic Data Management**: Automatic backup and restoration of test data between tests - **Base TestCase**: A custom TestCase class with Streams-specific utilities - **PHPUnit 10 Support**: Modern testing framework integration ## Quick Example ```php get(); $this->assertCount(7, $films); } } ``` ## Who Should Use This Package? - **Streams Developers**: Anyone building applications with the Streams platform - **Package Authors**: Developers creating Streams addons and extensions - **Teams**: Organizations needing reliable testing infrastructure for Streams projects - **Learners**: Those exploring Streams capabilities in a safe testing environment ## Key Benefits ### 1. Zero Configuration The package comes pre-configured with a Laravel application, streams definitions, and sample data. You can start writing tests immediately without setup hassle. ### 2. Clean State Testing Every test runs with a fresh copy of the sample data. The TestCase automatically restores data after each test, ensuring no test pollution. ### 3. Real-World Examples Sample streams include films, people, planets, and more—providing realistic data structures to learn from and test against. ### 4. Laravel Integration Full access to Laravel's testing features, including database assertions, HTTP testing, and more. ## Getting Started Ready to start testing? Continue to the [Installation Guide](/docs/testing/installation) to set up the Streams Testing package in your project. --- # Testing reference: Installation > Install and configure the Streams Testing package. Source: https://streams.dev/docs/testing/installation This guide will walk you through installing and configuring the Streams Testing package for your project. ## Requirements Before installing, ensure your environment meets these requirements: - **PHP**: 8.2 or higher (required by Streams Core) - **Laravel**: 10.x only for now. The package requires `orchestra/testbench ^8.36`, which targets Laravel 10. Support is being widened to `^8.36|^9.15|^10.8` (Laravel 10, 11, and 12). Until then you can't install it in a Laravel 11 or 12 app; streams.dev, which runs Laravel 12, doesn't use it for that reason. See the [version matrix](/docs/installation#version-matrix). - **Streams Core**: ^2.0 - **Composer**: Latest version recommended ## Installing the Package Install the Streams Testing package via Composer as a development dependency: ```bash composer require --dev streams/testing:1.0.x-dev ``` This will install the package and all its dependencies, including Orchestra Testbench and PHPUnit. ## Verifying Installation After installation, verify everything is set up correctly: ```bash vendor/bin/phpunit --version ``` You should see PHPUnit 10.x listed. ## Basic Configuration ### 1. Create phpunit.xml If you don't already have a `phpunit.xml` file in your project root, create one: ```xml ./tests ./src ``` ### 2. Create Tests Directory Create a `tests` directory if it doesn't exist: ```bash mkdir tests ``` ### 3. Write Your First Test Create a test file `tests/ExampleTest.php`: ```php assertTrue(true); } public function test_streams_available() { $films = Streams::entries('films'); $this->assertNotNull($films); } } ``` ### 4. Run Your Tests Execute your test suite: ```bash vendor/bin/phpunit ``` You should see output indicating your tests passed: ```text PHPUnit 10.5.58 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:00.123, Memory: 24.00 MB OK (2 tests, 2 assertions) ``` ## Advanced Configuration ### Custom Test Namespace Update your `composer.json` to use a custom test namespace: ```json { "autoload-dev": { "psr-4": { "Tests\\": "tests/" } } } ``` Then run: ```bash composer dump-autoload ``` ### IDE Configuration #### PHPStorm / IntelliJ IDEA 1. Go to **Settings** → **PHP** → **Test Frameworks** 2. Click **+** and select **PHPUnit Local** 3. Set **Path to phpunit.phar** to `vendor/bin/phpunit` 4. Choose **Use Composer autoloader** 5. Set **Path to script** to `vendor/autoload.php` #### VS Code Install the "PHPUnit" extension and add to `.vscode/settings.json`: ```json { "phpunit.php": "/usr/local/bin/php", "phpunit.phpunit": "vendor/bin/phpunit", "phpunit.args": [ "--colors=always" ] } ``` ### CI/CD Integration #### GitHub Actions Create `.github/workflows/tests.yml`: ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.2 extensions: dom, curl, libxml, mbstring, zip coverage: none - name: Install Dependencies run: composer install --prefer-dist --no-interaction - name: Run Tests run: vendor/bin/phpunit ``` ## Troubleshooting ### "Class not found" Errors Regenerate the autoloader: ```bash composer dump-autoload ``` ### Memory Limit Issues Increase PHP memory limit in `phpunit.xml`: ```xml ``` ### Permission Errors Ensure test data directories are writable: ```bash chmod -R 775 vendor/streams/testing/laravel/streams ``` ## Next Steps Now that you have the package installed, learn about: - [Test Data](/docs/testing/test-data) — sample streams - [Writing Tests](/docs/testing/writing-tests) - [Configuration](/docs/testing/configuration) --- # Testing reference: Test Data > Sample streams and test data included in the package. Source: https://streams.dev/docs/testing/test-data The Streams Testing package includes a rich set of sample data based on the Star Wars universe. This data provides realistic examples for testing and learning Streams concepts. ## Available Streams The package includes these pre-configured streams: ### Films **Stream**: `films` **Entries**: 7 Star Wars films **Key Field**: `episode_id` ```php $films = Streams::entries('films')->get(); // Returns 7 film entries ``` #### Example Entry ```json { "episode_id": 1, "title": "A New Hope", "director": "George Lucas", "producer": "Gary Kurtz, Rick McCallum", "release_date": "1977-05-25", "opening_crawl": "It is a period of civil war..." } ``` #### Fields - `episode_id` (integer) - Unique episode number - `title` (string) - Film title - `director` (string) - Director name - `producer` (string) - Producer names - `release_date` (datetime) - Release date - `opening_crawl` (text) - Opening text - `created` (datetime) - Creation timestamp - `edited` (datetime) - Last edit timestamp ### People **Stream**: `people` **Entries**: Multiple characters **Key Field**: `id` ```php $characters = Streams::entries('people') ->where('homeworld', 'Tatooine') ->get(); ``` #### Example Entry ```json { "id": 1, "name": "Luke Skywalker", "height": "172", "mass": "77", "hair_color": "blond", "eye_color": "blue", "birth_year": "19BBY", "gender": "male", "homeworld": "Tatooine" } ``` ### Planets **Stream**: `planets` **Entries**: Various planets **Key Field**: `id` ```php $planets = Streams::entries('planets') ->where('climate', 'like', '%arid%') ->get(); ``` #### Example Entry ```json { "id": 1, "name": "Tatooine", "rotation_period": "23", "orbital_period": "304", "diameter": "10465", "climate": "arid", "gravity": "1 standard", "terrain": "desert", "population": "200000" } ``` ### Species **Stream**: `species` **Entries**: Various species **Key Field**: `id` ```php $species = Streams::entries('species') ->where('classification', 'mammal') ->get(); ``` ### Starships **Stream**: `starships` **Entries**: Various starships **Key Field**: `id` ```php $starships = Streams::entries('starships') ->where('manufacturer', 'like', '%Incom%') ->get(); ``` ### Vehicles **Stream**: `vehicles` **Entries**: Various vehicles **Key Field**: `id` ```php $vehicles = Streams::entries('vehicles') ->orderBy('cost_in_credits', 'desc') ->get(); ``` ### Files **Stream**: `files` **Entries**: File handling examples **Key Field**: `id` ```php $files = Streams::entries('files')->get(); ``` ## Using Test Data ### Querying Data ```php public function test_query_films_by_director() { $films = Streams::entries('films') ->where('director', 'George Lucas') ->get(); $this->assertGreaterThan(0, $films->count()); } ``` ### Accessing Relationships ```php public function test_character_homeworld() { $luke = Streams::entries('people') ->where('name', 'Luke Skywalker') ->first(); $this->assertEquals('Tatooine', $luke->homeworld); } ``` ### Counting Entries ```php public function test_all_films_present() { $count = Streams::entries('films')->count(); $this->assertEquals(7, $count); } ``` ### Sorting Data ```php public function test_films_chronological_order() { $films = Streams::entries('films') ->orderBy('release_date') ->get(); $this->assertEquals('A New Hope', $films->first()->title); } ``` ## Data Management ### Automatic Restoration The test data is automatically restored after each test. The TestCase handles this in the `tearDown()` method: ```php protected function tearDown(): void { $this->restoreStreamsData(); parent::tearDown(); } ``` This ensures: - Each test starts with clean data - Tests don't interfere with each other - You can modify data without permanent changes ### Manual Restoration If needed, you can manually restore data: ```php public function test_with_manual_restore() { // Modify some data Streams::make('films')->create([...]); // Manually restore $this->restoreStreamsData(); // Data is back to original state $this->assertCount(7, Streams::entries('films')->get()); } ``` ### Data Location Test data is stored in: - **Active**: `vendor/streams/testing/laravel/streams/` - **Backup**: `vendor/streams/testing/laravel/streams.bak/` ## Custom Test Data ### Adding Your Own Streams Create custom stream definitions in your test setup: ```php protected function setUp(): void { parent::setUp(); // Define a custom stream for testing Streams::build([ 'id' => 'custom_products', 'name' => 'Products', 'source' => [ 'type' => 'file', 'format' => 'json', ], 'fields' => [ 'id' => 'integer', 'name' => 'string', 'price' => 'decimal', ], ])->save(); } ``` ### Loading Custom Fixtures Load your own test data: ```php public function test_with_custom_data() { $products = [ ['id' => 1, 'name' => 'Widget', 'price' => 9.99], ['id' => 2, 'name' => 'Gadget', 'price' => 19.99], ]; foreach ($products as $product) { Streams::make('custom_products')->create($product); } $this->assertCount(2, Streams::entries('custom_products')->get()); } ``` ## Best Practices ### 1. Don't Rely on Specific IDs IDs may change. Use attributes instead: ```php // Bad $film = Streams::entries('films')->find(1); // Good $film = Streams::entries('films') ->where('title', 'A New Hope') ->first(); ``` ### 2. Use Factories for Complex Data Create factories for reusable test data: ```php class TestHelpers { public static function createFilm($attributes = []) { return Streams::make('films')->create(array_merge([ 'title' => 'Test Film', 'director' => 'Test Director', 'release_date' => now(), ], $attributes)); } } ``` ### 3. Test Data Integrity Verify expected data exists: ```php protected function setUp(): void { parent::setUp(); $this->assertGreaterThan(0, Streams::entries('films')->count(), 'Test data not loaded properly'); } ``` ## Next Steps - [Writing Tests](/docs/testing/writing-tests) - [Configuration](/docs/testing/configuration) - [Troubleshooting](/docs/testing/troubleshooting) --- # Testing reference: Writing Tests > Write effective tests for Streams applications. Source: https://streams.dev/docs/testing/writing-tests This guide covers everything you need to know about writing tests for Streams applications, from basic examples to advanced testing patterns. ## Basic Test Structure ### Your First Test Every test extends the `Streams\Testing\TestCase` class: ```php assertTrue(true); } } ``` ### Anatomy of a Test ```php public function test_descriptive_name() // 1. Test method { // 2. Arrange - Set up test data $stream = Streams::make('films'); // 3. Act - Perform the action $result = $stream->entries()->count(); // 4. Assert - Verify the result $this->assertEquals(7, $result); } ``` ## Testing Streams ### Creating Entries ```php public function test_creates_new_film() { $film = Streams::make('films')->create([ 'title' => 'New Film', 'director' => 'New Director', 'release_date' => '2025-01-01', ]); $this->assertNotNull($film->id); $this->assertEquals('New Film', $film->title); } ``` ### Reading Entries ```php public function test_finds_film_by_title() { $film = Streams::entries('films') ->where('title', 'A New Hope') ->first(); $this->assertNotNull($film); $this->assertEquals('George Lucas', $film->director); } ``` ### Updating Entries ```php public function test_updates_film_director() { $film = Streams::entries('films') ->where('title', 'A New Hope') ->first(); $film->update(['director' => 'Updated Director']); $updated = Streams::entries('films') ->where('title', 'A New Hope') ->first(); $this->assertEquals('Updated Director', $updated->director); } ``` ### Deleting Entries ```php public function test_deletes_film() { $initialCount = Streams::entries('films')->count(); $film = Streams::entries('films')->first(); $film->delete(); $finalCount = Streams::entries('films')->count(); $this->assertEquals($initialCount - 1, $finalCount); } ``` ## Testing Queries ### Simple Where Clauses ```php public function test_filters_by_director() { $films = Streams::entries('films') ->where('director', 'George Lucas') ->get(); $this->assertGreaterThan(0, $films->count()); foreach ($films as $film) { $this->assertEquals('George Lucas', $film->director); } } ``` ### Complex Queries ```php public function test_complex_film_query() { $films = Streams::entries('films') ->where('director', 'George Lucas') ->where('release_date', '>=', '1977-01-01') ->orderBy('release_date') ->limit(3) ->get(); $this->assertLessThanOrEqual(3, $films->count()); } ``` ### Using Like Operator ```php public function test_searches_film_titles() { $films = Streams::entries('films') ->where('title', 'like', '%Empire%') ->get(); $this->assertGreaterThan(0, $films->count()); } ``` ### Counting Results ```php public function test_counts_films_by_director() { $count = Streams::entries('films') ->where('director', 'George Lucas') ->count(); $this->assertGreaterThan(0, $count); } ``` ## Testing Stream Definitions ### Validating Stream Configuration ```php public function test_films_stream_configuration() { $stream = Streams::make('films'); $this->assertEquals('Films', $stream->name); $this->assertEquals('episode_id', $stream->config['key_name']); } ``` ### Testing Field Definitions ```php public function test_films_has_required_fields() { $stream = Streams::make('films'); $fields = $stream->fields; $this->assertArrayHasKey('title', $fields); $this->assertArrayHasKey('director', $fields); $this->assertArrayHasKey('release_date', $fields); } ``` ### Validating Field Types ```php public function test_field_types() { $stream = Streams::make('films'); $this->assertEquals('integer', $stream->fields['episode_id']->type); $this->assertEquals('string', $stream->fields['title']->type); $this->assertEquals('datetime', $stream->fields['release_date']->type); } ``` ## Testing Relationships ### One-to-Many Relationships ```php public function test_planet_has_residents() { $tatooine = Streams::entries('planets') ->where('name', 'Tatooine') ->first(); $residents = Streams::entries('people') ->where('homeworld', $tatooine->name) ->get(); $this->assertGreaterThan(0, $residents->count()); } ``` ### Many-to-Many Relationships ```php public function test_character_appears_in_films() { $luke = Streams::entries('people') ->where('name', 'Luke Skywalker') ->first(); // Assuming a films relationship if (isset($luke->films)) { $this->assertGreaterThan(0, count($luke->films)); } } ``` ## Using Setup and Teardown ### Setup Method Run code before each test: ```php protected function setUp(): void { parent::setUp(); // Create test data that all tests need $this->testFilm = Streams::make('films')->create([ 'title' => 'Test Film', 'director' => 'Test Director', ]); } ``` ### Teardown Method The parent TestCase handles data restoration automatically. Add custom cleanup if needed: ```php protected function tearDown(): void { // Custom cleanup here parent::tearDown(); // This restores test data } ``` ## Data Providers Test the same logic with different data: ```php /** * @dataProvider filmDirectorProvider */ public function test_finds_films_by_director($director, $expectedMinimum) { $films = Streams::entries('films') ->where('director', $director) ->get(); $this->assertGreaterThanOrEqual($expectedMinimum, $films->count()); } public static function filmDirectorProvider() { return [ 'George Lucas' => ['George Lucas', 1], 'Irvin Kershner' => ['Irvin Kershner', 1], 'Richard Marquand' => ['Richard Marquand', 1], ]; } ``` ## Testing Exceptions ### Expecting Exceptions ```php public function test_throws_exception_for_invalid_stream() { $this->expectException(\Exception::class); Streams::make('nonexistent_stream'); } ``` ### Testing Validation Errors ```php public function test_validates_required_fields() { $this->expectException(\Illuminate\Validation\ValidationException::class); Streams::make('films')->create([ // Missing required 'title' field 'director' => 'Test Director', ]); } ``` ## Common Assertions ### Equality Assertions ```php $this->assertEquals($expected, $actual); $this->assertNotEquals($unexpected, $actual); $this->assertSame($expected, $actual); // Strict comparison ``` ### Boolean Assertions ```php $this->assertTrue($condition); $this->assertFalse($condition); $this->assertNull($value); $this->assertNotNull($value); ``` ### Collection Assertions ```php $this->assertCount($expectedCount, $collection); $this->assertEmpty($collection); $this->assertNotEmpty($collection); $this->assertContains($needle, $collection); ``` ### Array Assertions ```php $this->assertArrayHasKey($key, $array); $this->assertArrayNotHasKey($key, $array); $this->assertIsArray($value); ``` ### String Assertions ```php $this->assertStringContainsString($needle, $haystack); $this->assertStringStartsWith($prefix, $string); $this->assertStringEndsWith($suffix, $string); $this->assertMatchesRegularExpression($pattern, $string); ``` ### Numeric Assertions ```php $this->assertGreaterThan($expected, $actual); $this->assertGreaterThanOrEqual($expected, $actual); $this->assertLessThan($expected, $actual); $this->assertLessThanOrEqual($expected, $actual); ``` ## Testing Best Practices ### 1. One Assertion Per Test (When Possible) ```php // Good public function test_film_has_title() { $film = Streams::entries('films')->first(); $this->assertNotEmpty($film->title); } public function test_film_has_director() { $film = Streams::entries('films')->first(); $this->assertNotEmpty($film->director); } ``` ### 2. Use Descriptive Test Names ```php // Bad public function test_film() { } // Good public function test_creates_film_with_all_required_fields() { } ``` ### 3. Follow the AAA Pattern ```php public function test_updates_film_release_date() { // Arrange $film = Streams::entries('films')->first(); $newDate = '2025-12-25'; // Act $film->update(['release_date' => $newDate]); // Assert $this->assertEquals($newDate, $film->fresh()->release_date); } ``` ### 4. Test Edge Cases ```php public function test_handles_empty_query_results() { $films = Streams::entries('films') ->where('director', 'Nonexistent Director') ->get(); $this->assertCount(0, $films); } ``` ### 5. Keep Tests Independent Each test should work in isolation: ```php public function test_first_test() { $film = Streams::make('films')->create(['title' => 'Test']); $this->assertNotNull($film); } public function test_second_test() { // Don't rely on data from test_first_test $count = Streams::entries('films')->count(); $this->assertEquals(7, $count); // Original test data } ``` ## Debugging Tests ### Using dump() and dd() ```php public function test_debugging_example() { $films = Streams::entries('films')->get(); dump($films); // Output without stopping // dd($films); // Dump and die $this->assertNotEmpty($films); } ``` ### Verbose Output Run tests with verbose output: ```bash vendor/bin/phpunit --testdox --verbose ``` ### Running Single Tests Test specific methods: ```bash vendor/bin/phpunit --filter test_specific_method ``` ## Next Steps - [Configuration](/docs/testing/configuration) - [Test Data](/docs/testing/test-data) - [Troubleshooting](/docs/testing/troubleshooting) --- # Testing reference: Configuration > Configure the Streams Testing environment. Source: https://streams.dev/docs/testing/configuration Learn how to configure the Streams Testing package to match your project's needs, from basic PHPUnit settings to advanced test environment customization. ## PHPUnit Configuration ### Basic phpunit.xml The minimal configuration for Streams Testing: ```xml ./tests ``` ### Recommended Configuration A more comprehensive setup with best practices: ```xml ./tests/Unit ./tests/Feature ./src ./src/Console ./src/TestServiceProvider.php ``` ### Configuration Options Explained #### Test Execution ```xml stopOnError="false" stopOnFailure="false" stopOnWarning="false" stopOnRisky="false" beStrictAboutOutputDuringTests="true" > ``` #### Failure Handling ```xml failOnRisky="true" failOnDeprecation="false" failOnNotice="false" > ``` #### Display Options ```xml displayDetailsOnTestsThatTriggerWarnings="true" displayDetailsOnTestsThatTriggerDeprecations="true" displayDetailsOnTestsThatTriggerNotices="true" > ``` ## Environment Variables ### Laravel Configuration Configure Laravel behavior during tests: ```xml ``` ### Streams Configuration Configure Streams-specific settings: ```xml ``` ### PHP Settings Adjust PHP behavior: ```xml ``` ## Custom TestCase ### Extending the Base TestCase Create your own base TestCase with custom functionality: ```php artisan('cache:clear'); } protected function tearDown(): void { // Custom cleanup parent::tearDown(); } /** * Create a test film entry */ protected function createFilm(array $attributes = []): object { return Streams::make('films')->create(array_merge([ 'title' => 'Test Film', 'director' => 'Test Director', 'release_date' => now()->format('Y-m-d'), ], $attributes)); } } ``` ### Using Your Custom TestCase ```php createFilm(['title' => 'My Film']); $this->assertEquals('My Film', $film->title); } } ``` ## Test Organization ### Directory Structure Organize tests by type: ```text tests/ ├── Feature/ # Integration tests │ ├── FilmTest.php │ └── PlanetTest.php ├── Unit/ # Unit tests │ ├── StreamTest.php │ └── EntryTest.php ├── Fixtures/ # Test data │ └── films.json └── Support/ # Test helpers └── Helpers.php ``` ### Autoloading Test Helpers Add to `composer.json`: ```json { "autoload-dev": { "psr-4": { "Tests\\": "tests/" }, "files": [ "tests/Support/helpers.php" ] } } ``` ## Code Coverage ### Enable Coverage Install Xdebug or PCOV, then configure coverage: ```xml ./src ./src/Console ./src/TestServiceProvider.php ``` ### Generate Coverage Report ```bash # HTML report vendor/bin/phpunit --coverage-html coverage # Text report vendor/bin/phpunit --coverage-text # Clover XML (for CI) vendor/bin/phpunit --coverage-clover coverage.xml ``` ## Test Filtering ### Run Specific Test Suites ```bash # Run only unit tests vendor/bin/phpunit --testsuite Unit # Run only feature tests vendor/bin/phpunit --testsuite Feature ``` ### Run Tests by Group Add groups to tests: ```php /** * @group films * @group integration */ public function test_film_creation() { // Test code } ``` Run by group: ```bash # Run only films tests vendor/bin/phpunit --group films # Exclude slow tests vendor/bin/phpunit --exclude-group slow ``` ### Run Tests by Filter ```bash # Run tests matching pattern vendor/bin/phpunit --filter FilmTest # Run specific test method vendor/bin/phpunit --filter test_creates_film ``` ## Performance Optimization ### Parallel Test Execution Install ParaTest: ```bash composer require --dev brianium/paratest ``` Run tests in parallel: ```bash vendor/bin/paratest --processes=4 ``` ### Process Isolation For tests that need isolation: ```xml ``` Or per-test: ```php /** * @runInSeparateProcess */ public function test_isolated() { // Test code } ``` ### Cache Configuration Enable result caching: ```xml ``` Add to `.gitignore`: ```text .phpunit.cache/ coverage/ ``` ## Database Configuration ### SQLite In-Memory Fast testing with SQLite: ```xml ``` ### MySQL Testing Database ```xml ``` ## Continuous Integration ### GitHub Actions `.github/workflows/tests.yml`: ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: php: [8.2, 8.3] steps: - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip coverage: xdebug - name: Install Dependencies run: composer install --prefer-dist --no-interaction - name: Run Tests run: vendor/bin/phpunit --coverage-clover coverage.xml - name: Upload Coverage uses: codecov/codecov-action@v3 with: files: ./coverage.xml ``` ### GitLab CI `.gitlab-ci.yml`: ```yaml test: image: php:8.2 before_script: - apt-get update -y - apt-get install -y git unzip - curl -sS https://getcomposer.org/installer | php - php composer.phar install script: - vendor/bin/phpunit artifacts: reports: junit: phpunit-report.xml ``` ## Next Steps - [Writing Tests](/docs/testing/writing-tests) - [Troubleshooting](/docs/testing/troubleshooting) --- # Testing reference: Troubleshooting > Common issues when testing with Streams Testing. Source: https://streams.dev/docs/testing/troubleshooting This guide helps you resolve common issues when using the Streams Testing package. ## Installation Issues ### Class Not Found **Problem**: `Class 'Streams\Testing\TestCase' not found` **Solution**: ```bash # Regenerate autoload files composer dump-autoload # Clear any cached autoload files rm -rf vendor/composer composer install ``` ### PHPUnit Not Found **Problem**: `vendor/bin/phpunit: No such file or directory` **Solution**: ```bash # Reinstall dev dependencies composer install --dev # Or explicitly require PHPUnit composer require --dev phpunit/phpunit ``` ### Orchestra Testbench Missing **Problem**: `Class 'Orchestra\Testbench\TestCase' not found` **Solution**: ```bash # Install compatible version composer require --dev orchestra/testbench:^8.0 ``` ## Configuration Issues ### No Tests Found **Problem**: PHPUnit reports `No tests executed!` **Solutions**: 1. **Check phpunit.xml test suite configuration**: ```xml ./tests ``` 2. **Verify test file naming**: - Files must end with `Test.php` (e.g., `FilmTest.php`) - Test methods must start with `test` or use `@test` annotation 3. **Check test directory exists**: ```bash ls -la tests/ ``` ### Wrong PHPUnit Version **Problem**: PHPUnit schema errors or deprecated features **Solution**: Check PHPUnit version matches configuration: ```bash vendor/bin/phpunit --version ``` Update `phpunit.xml` schema: ```xml xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" ``` ### Missing APP_KEY **Problem**: `No application encryption key has been specified` **Solution**: Add to `phpunit.xml`: ```xml ``` Or generate a new one: ```bash php artisan key:generate --show ``` ## Test Execution Issues ### Memory Limit Exceeded **Problem**: `Fatal error: Allowed memory size exhausted` **Solutions**: 1. **Increase memory limit in phpunit.xml**: ```xml ``` 2. **Run with more memory**: ```bash php -d memory_limit=512M vendor/bin/phpunit ``` 3. **Use process isolation**: ```xml ``` ### Tests Timeout **Problem**: Tests hang or timeout **Solutions**: 1. **Increase max execution time**: ```xml ``` 2. **Check for infinite loops**: ```php // Add timeouts to potentially long operations $films = Streams::entries('films') ->timeout(30) ->get(); ``` 3. **Run with verbose output to identify hanging test**: ```bash vendor/bin/phpunit --verbose --debug ``` ### Permission Denied Errors **Problem**: `Permission denied` when accessing test data **Solution**: ```bash # Make streams directories writable chmod -R 775 vendor/streams/testing/laravel/streams chmod -R 775 vendor/streams/testing/laravel/streams.bak # Or change ownership sudo chown -R $USER:$USER vendor/streams/testing/laravel/streams ``` ## Test Data Issues ### Test Data Not Loading **Problem**: Sample streams return empty results **Solutions**: 1. **Verify test data exists**: ```bash ls -la vendor/streams/testing/laravel/streams/ ``` 2. **Check backup data**: ```bash ls -la vendor/streams/testing/laravel/streams.bak/ ``` 3. **Manually restore data**: ```php public function setUp(): void { parent::setUp(); $this->restoreStreamsData(); } ``` 4. **Verify stream files are valid JSON**: ```bash php -r "json_decode(file_get_contents('vendor/streams/testing/laravel/streams/films.json'));" ``` ### Data Not Resetting Between Tests **Problem**: Modified data persists across tests **Solutions**: 1. **Ensure parent tearDown is called**: ```php protected function tearDown(): void { // Your cleanup code here parent::tearDown(); // Must call this! } ``` 2. **Check file permissions**: ```bash # Ensure test can write and delete chmod -R 755 vendor/streams/testing/laravel/streams ``` 3. **Manually verify restoration**: ```php public function test_data_resets() { $initialCount = Streams::entries('films')->count(); Streams::make('films')->create(['title' => 'New Film']); $this->restoreStreamsData(); $finalCount = Streams::entries('films')->count(); $this->assertEquals($initialCount, $finalCount); } ``` ### Invalid JSON in Stream Files **Problem**: `JSON decode error` when loading streams **Solution**: Validate and fix JSON: ```bash # Validate JSON cat vendor/streams/testing/laravel/streams/films.json | python -m json.tool # Or use jq jq . vendor/streams/testing/laravel/streams/films.json ``` ## Assertion Issues ### Unexpected Test Failures **Problem**: Tests fail unexpectedly **Debug Steps**: 1. **Add debug output**: ```php public function test_debug_example() { $films = Streams::entries('films')->get(); dump($films->count()); // Check actual count dd($films->toArray()); // Inspect full data $this->assertCount(7, $films); } ``` 2. **Use verbose assertions**: ```php $this->assertEquals( $expected, $actual, "Expected $expected but got $actual" ); ``` 3. **Run single test**: ```bash vendor/bin/phpunit --filter test_specific_method --testdox ``` ### Type Comparison Issues **Problem**: `Expected integer but got string` **Solution**: Use appropriate assertions: ```php // Loose comparison $this->assertEquals(7, $count); // Strict comparison $this->assertSame(7, $count); // Type checking $this->assertIsInt($count); $this->assertEquals(7, (int) $count); ``` ### Collection vs Array Confusion **Problem**: Assertions fail on collections **Solution**: Convert collections appropriately: ```php // Get collection $films = Streams::entries('films')->get(); // For counting $this->assertCount(7, $films); // For array assertions $this->assertIsArray($films->toArray()); // For iteration foreach ($films as $film) { $this->assertNotNull($film); } ``` ## Laravel Integration Issues ### Route Not Found in Tests **Problem**: `Route [name] not defined` **Solution**: Ensure routes are loaded in TestCase: ```php protected function getPackageProviders($app) { return [ \Your\Package\ServiceProvider::class, ]; } ``` ### Config Not Loading **Problem**: Configuration values not available **Solution**: Define config in TestCase: ```php protected function defineEnvironment($app) { $app['config']->set('streams.path', __DIR__ . '/streams'); } ``` ### Service Provider Not Loaded **Problem**: Services not registered **Solution**: Register providers explicitly: ```php protected function getPackageProviders($app) { return [ \Streams\Core\StreamsServiceProvider::class, \Streams\Testing\TestServiceProvider::class, ]; } ``` ## IDE Issues ### PHPStorm Not Running Tests **Problem**: Can't run tests from IDE **Solutions**: 1. **Configure PHPUnit**: - Settings → PHP → Test Frameworks - Add PHPUnit Local - Use Composer autoloader: `vendor/autoload.php` 2. **Mark directories**: - Right-click `tests/` → Mark Directory As → Test Sources Root 3. **Refresh configuration**: - File → Invalidate Caches / Restart ### VS Code Not Recognizing Tests **Problem**: Test runner doesn't find tests **Solutions**: 1. **Install PHPUnit extension**: - Better PHPUnit by calebporzio 2. **Configure settings.json**: ```json { "phpunit.phpunit": "vendor/bin/phpunit", "phpunit.args": ["--colors=always"] } ``` 3. **Reload window**: - Cmd/Ctrl + Shift + P → Reload Window ## Performance Issues ### Tests Running Slowly **Solutions**: 1. **Use process isolation sparingly**: ```xml ``` 2. **Disable coverage when not needed**: ```bash vendor/bin/phpunit --no-coverage ``` 3. **Run tests in parallel**: ```bash composer require --dev brianium/paratest vendor/bin/paratest ``` 4. **Use SQLite in-memory for database tests**: ```xml ``` ### High Memory Usage **Solutions**: 1. **Clear data after tests**: ```php protected function tearDown(): void { $this->clearData(); parent::tearDown(); } ``` 2. **Use fewer fixtures**: ```php // Load only needed data $this->loadFixtures(['films']); // Not all streams ``` 3. **Garbage collection**: ```php protected function tearDown(): void { gc_collect_cycles(); parent::tearDown(); } ``` ## Getting Help ### Diagnostic Information When reporting issues, include: ```bash # PHP version php -v # Composer info composer show # PHPUnit version vendor/bin/phpunit --version # Streams core version composer show streams/core # Run tests with verbose output vendor/bin/phpunit --verbose --debug ``` ### Enable Debug Mode Add to `phpunit.xml`: ```xml ``` ### Community Resources - **Documentation**: https://streams.dev/docs/testing/introduction - **GitHub Issues**: https://github.com/laravel-streams/streams-testing/issues - **Discord**: Join the Streams community - **Stack Overflow**: Tag with `laravel-streams` ## Quick Checklist When tests aren't working, check: - [ ] `composer dump-autoload` executed - [ ] `phpunit.xml` exists and is valid - [ ] Test files end with `Test.php` - [ ] Test methods start with `test` - [ ] Extending `\Streams\Testing\TestCase` - [ ] `parent::setUp()` and `parent::tearDown()` called - [ ] File permissions correct on stream data - [ ] APP_KEY set in phpunit.xml - [ ] Correct PHP version (8.2+) - [ ] Dependencies installed with `composer install` Still having issues? Check the [GitHub issues](https://github.com/laravel-streams/streams-testing/issues) or create a new one with the diagnostic information above. --- # Client reference: Introduction > A zero-dependency JavaScript client for the Streams REST API. Source: https://streams.dev/docs/client/introduction `@laravel-streams/api-client` is a JavaScript client for the [Streams API](/docs/api/introduction). It wraps the API's stream and entry endpoints in a small fluent interface, with a Laravel-style criteria builder and a request/response middleware pipeline. ## What it is - **Zero runtime dependencies.** It uses the native `fetch` API, and ships ESM (`dist/module.js`) and CommonJS (`dist/index.js`) builds. - **Two resources.** `client.streams` for stream definitions and `client.entries` for entry CRUD. - **Criteria builder.** `where`, `orWhere`, `orderBy`, `limit`, and `paginate`, compiled into the API's query parameters. - **Middleware.** Request, response, and error hooks with priorities. The defaults serialize request bodies, compile criteria, append query strings, and unwrap the response envelope. It is the JavaScript counterpart to the PHP [Streams SDK](/docs/sdk/introduction), but they are different things: the SDK is Artisan tooling for building a Streams app, and the client is a runtime library for consuming a Streams API from a browser or Node. ## Why it exists The Streams API describes itself: every response carries links to related resources, and stream definitions are available over HTTP. The client lets front ends and Node services use that API without hand-writing `fetch` calls, query-string encoding, or envelope parsing, and it keeps the query vocabulary (`where`, `orderBy`, `paginate`) the same as the [criteria](/docs/core/criteria) you already use in PHP. ## How to use it Install from npm (the current release is **3.0.0**): ```bash npm install @laravel-streams/api-client ``` Point the client at your API prefix and query a stream: ```javascript import { Client, Criteria } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://example.com/api' }); const streams = await client.streams.get(); const posts = await client.entries.get('posts', { criteria: new Criteria() .where('status', 'published') .orderBy('created_at', 'desc') .limit(10), }); await client.entries.patch('posts', 1, { title: 'Updated title' }); ``` The server side needs [streams/api](/docs/api/installation) installed and enabled. The API has no built-in authentication; your app decides who can call it (see [API authentication](/docs/api/authentication)). ## How to extend it Add behavior by writing a middleware class and registering it with `client.use()`: ```javascript import { Middleware } from '@laravel-streams/api-client'; class TenantHeader extends Middleware { async request(request, client) { request.headers.set('X-Tenant', 'acme'); return request; } } client.use(new TenantHeader()); ``` `AuthorizationMiddleware` sets a bearer token for you. See [Middleware](/docs/client/middleware) for priorities and the error hook. ## Where to next - [Installation](/docs/client/installation) - [Quick start](/docs/client/quickstart) - [Client configuration](/docs/client/client) - [Criteria query builder](/docs/client/criteria) --- # Client reference: Installation > Installing and setting up the Streams API Client Source: https://streams.dev/docs/client/installation ## Requirements - Node.js 14.0 or higher - npm, yarn, or pnpm ## Installing via NPM Install the package using your preferred package manager: ### NPM ```bash npm install @laravel-streams/api-client ``` ### Yarn ```bash yarn add @laravel-streams/api-client ``` ### PNPM ```bash pnpm add @laravel-streams/api-client ``` ## Installing from Source ### Cloning with Git ```bash git clone git@github.com:laravel-streams/api-client.git cd api-client npm install npm run build ``` ### Building from Source ```bash # Install dependencies npm install # Build the distribution files npm run build # Run tests to verify npm test ``` ## Package Contents After installation, the package includes: ```text node_modules/@laravel-streams/api-client/ ├── dist/ │ ├── index.js # CommonJS build │ └── module.js # ES Module build ├── src/ │ ├── Client.js │ ├── Criteria.js │ ├── Entries.js │ ├── Streams.js │ └── middleware/ └── package.json ``` ## Importing the Client ### ES Modules (Recommended) ```javascript import { Client, Criteria } from '@laravel-streams/api-client'; ``` ### CommonJS ```javascript const { Client, Criteria } = require('@laravel-streams/api-client'); ``` ### Browser (ES Modules) ```html ``` ## Updating Update to the latest version: ```bash npm update @laravel-streams/api-client ``` Or update to a specific version: ```bash npm install @laravel-streams/api-client@3.0.0 ``` ## Verifying Installation Create a simple test file to verify the installation: ```javascript // test-install.mjs import { Client, Criteria } from '@laravel-streams/api-client'; console.log('✓ Client imported:', typeof Client); console.log('✓ Criteria imported:', typeof Criteria); const client = new Client({ baseURL: 'http://localhost/api' }); console.log('✓ Client created successfully'); ``` Run it: ```bash node test-install.mjs ``` Expected output: ```text ✓ Client imported: function ✓ Criteria imported: function ✓ Client created successfully ``` ## Next Steps - [Quick Start Guide](/docs/client/quickstart) - Get up and running quickly - [Client Configuration](/docs/client/client) - Configure the client for your needs - [Working with Entries](/docs/client/entries) - Start interacting with your data --- # Client reference: Quick Start > Get started with the Streams API Client in minutes Source: https://streams.dev/docs/client/quickstart Get up and running with the Streams API Client in just a few minutes. ## Basic Setup ```javascript import { Client } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'http://localhost/api' }); ``` ## Working with Streams ### Get All Streams ```javascript const response = await client.streams.get(); console.log(response.data); // Array of streams ``` ### Get a Single Stream ```javascript const response = await client.streams.find('posts'); console.log(response.data); // Stream object ``` ## Working with Entries ### Get Entries ```javascript // Get all entries from a stream const response = await client.entries.get('posts'); console.log(response.data); // Array of entries ``` ### Find a Specific Entry ```javascript const response = await client.entries.find('posts', 1); console.log(response.data); // Single entry object ``` ### Create an Entry ```javascript const response = await client.entries.post('posts', { title: 'My First Post', content: 'Hello, world!', status: 'published' }); console.log(response.data); // Created entry with ID ``` ### Update an Entry ```javascript // Partial update (PATCH) const response = await client.entries.patch('posts', 1, { title: 'Updated Title' }); // Full update (PUT) const response = await client.entries.put('posts', 1, { title: 'New Title', content: 'New content', status: 'draft' }); ``` ### Delete an Entry ```javascript await client.entries.delete('posts', 1); ``` ## Using Criteria for Queries ```javascript import { Criteria } from '@laravel-streams/api-client'; // Build a query const criteria = new Criteria() .where('status', 'published') .where('views', '>', 100) .orderBy('created_at', 'desc') .limit(10); // Execute the query const response = await client.entries.get('posts', { criteria: criteria }); console.log(response.data); // Filtered and sorted entries ``` ## Adding Authentication ```javascript import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client'; // Create auth middleware const auth = new AuthorizationMiddleware({ token: 'your-api-token', type: 'Bearer' }); // Add to client const client = new Client({ baseURL: 'http://localhost/api', middlewares: [auth] }); // All requests will now include: Authorization: Bearer your-api-token ``` ## Error Handling ```javascript try { const response = await client.entries.find('posts', 999); console.log(response.data); } catch (error) { if (error.status === 404) { console.log('Entry not found'); } else { console.error('Error:', error.message); } } ``` ## Complete Example Here's a complete example putting it all together: ```javascript import { Client, Criteria, AuthorizationMiddleware } from '@laravel-streams/api-client'; // Setup client with authentication const auth = new AuthorizationMiddleware({ token: process.env.API_TOKEN, type: 'Bearer' }); const client = new Client({ baseURL: 'https://api.example.com', middlewares: [auth] }); // Fetch and display published posts async function getPublishedPosts() { try { const criteria = new Criteria() .where('status', 'published') .orderBy('created_at', 'desc') .limit(10); const response = await client.entries.get('posts', { criteria }); response.data.forEach(post => { console.log(`${post.title} - ${post.created_at}`); }); } catch (error) { console.error('Failed to fetch posts:', error.message); } } // Create a new post async function createPost(data) { try { const response = await client.entries.post('posts', data); console.log('Post created with ID:', response.data.id); return response.data; } catch (error) { console.error('Failed to create post:', error.message); } } // Run examples await getPublishedPosts(); await createPost({ title: 'Hello World', content: 'This is my first post!', status: 'draft' }); ``` ## Next Steps - [Client Configuration](/docs/client/client) - Learn about all configuration options - [Criteria Query Builder](/docs/client/criteria) - Master the query builder - [Middleware](/docs/client/middleware) - Extend functionality with middleware - [Examples](/docs/client/examples) - See more real-world examples --- # Client reference: Client Configuration > Configure the API client for your application Source: https://streams.dev/docs/client/client The `Client` class is the main entry point for interacting with the Streams API. ## Basic Configuration ```javascript import { Client } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'http://localhost/api' }); ``` ## Configuration Options ### baseURL (required) The base URL for your API endpoint: ```javascript const client = new Client({ baseURL: 'https://api.example.com' }); ``` ### request Default request configuration applied to all requests: ```javascript const client = new Client({ baseURL: 'http://localhost/api', request: { mode: 'cors', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Content-Type': 'application/json' }, credentials: 'include', // Send cookies cache: 'no-cache' } }); ``` ### middlewares Array of middleware to apply to requests: ```javascript import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'http://localhost/api', middlewares: [ new AuthorizationMiddleware({ token: 'api-token', type: 'Bearer' }) ] }); ``` ## Default Middlewares The client includes these middlewares by default: - `RequestDataMiddleware` - Handles request body serialization - `CriteriaMiddleware` - Processes Criteria query parameters - `QueryMiddleware` - Converts query parameters to query strings - `ResponseDataMiddleware` - Parses response data You can customize the default middlewares: ```javascript const client = new Client({ baseURL: 'http://localhost/api', defaultMiddlewares: [], // Disable default middlewares middlewares: [ // Add your custom middlewares ] }); ``` ## Adding Middleware Dynamically You can add middleware after client creation: ```javascript const client = new Client({ baseURL: 'http://localhost/api' }); // Add authentication later const auth = new AuthorizationMiddleware({ token: getUserToken() }); client.use(auth); ``` ## Environment-Based Configuration ```javascript const config = { development: { baseURL: 'http://localhost:8000/api', request: { mode: 'cors', credentials: 'include' } }, production: { baseURL: 'https://api.production.com', request: { mode: 'cors', credentials: 'same-origin' } } }; const env = process.env.NODE_ENV || 'development'; const client = new Client(config[env]); ``` ## Resources The client automatically initializes resource instances: ### Streams Resource ```javascript client.streams.get() client.streams.find(stream) client.streams.post(data) client.streams.patch(stream, data) client.streams.put(stream, data) client.streams.delete(stream) ``` ### Entries Resource ```javascript client.entries.get(stream, config) client.entries.find(stream, id, config) client.entries.post(stream, data, config) client.entries.patch(stream, id, data, config) client.entries.put(stream, id, data, config) client.entries.delete(stream, id, config) ``` ## Request Method For custom requests, use the `request` method directly: ```javascript const response = await client.request('GET', '/custom/endpoint', { query: { filter: 'active' }, headers: { 'X-Custom-Header': 'value' } }); ``` ## Complete Example ```javascript import { Client, AuthorizationMiddleware, ETagMiddleware } from '@laravel-streams/api-client'; // Custom logging middleware class LoggingMiddleware { constructor() { this.options = { priority: { request: 10, response: 90, error: 50 } }; } async request(request, client) { console.log(`→ ${request.method} ${request.url}`); return request; } async response(response, client) { console.log(`← ${response.status} ${response.statusText}`); return response; } async error(error, client) { console.error(`✗ ${error.message}`); throw error; } } // Create client with full configuration const client = new Client({ baseURL: process.env.API_URL || 'http://localhost/api', request: { mode: 'cors', credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }, middlewares: [ new LoggingMiddleware(), new AuthorizationMiddleware({ token: process.env.API_TOKEN, type: 'Bearer' }), new ETagMiddleware() ] }); export default client; ``` ## Next Steps - [Working with Streams](/docs/client/streams) - [Working with Entries](/docs/client/entries) - [Middleware](/docs/client/middleware) --- # Client reference: Streams > Working with stream resources and CRUD operations Source: https://streams.dev/docs/client/streams Streams represent collections or resource types in the Laravel Streams API. The `Streams` class provides methods for managing stream resources. ## Getting Streams ### Get All Streams ```javascript const response = await client.streams.get(); console.log(response.data); // Array of stream objects ``` ### Get Specific Stream ```javascript const response = await client.streams.find('posts'); console.log(response.data); // Single stream object ``` ### With Criteria ```javascript import { Criteria } from '@laravel-streams/api-client'; const criteria = new Criteria() .where('type', 'content') .orderBy('name', 'asc'); const response = await client.streams.get({ criteria }); ``` ## Creating Streams ### Basic Creation ```javascript const stream = { id: 'posts', name: 'Posts', description: 'Blog posts stream', fields: { title: { type: 'string', required: true }, content: { type: 'text', required: true }, author: { type: 'relationship', related: 'users' }, published_at: { type: 'datetime', nullable: true } } }; const response = await client.streams.post(stream); ``` ### With Validation ```javascript const stream = { id: 'products', name: 'Products', rules: { title: 'required|string|max:255', price: 'required|numeric|min:0', sku: 'required|unique:products,sku' }, fields: { title: { type: 'string' }, price: { type: 'decimal' }, sku: { type: 'string' } } }; const response = await client.streams.post(stream); ``` ## Updating Streams ### Full Update (PUT) ```javascript const updatedStream = { id: 'posts', name: 'Blog Posts', description: 'Updated description', fields: { title: { type: 'string', required: true }, content: { type: 'text', required: true }, excerpt: { type: 'text' }, // New field published_at: { type: 'datetime' } } }; const response = await client.streams.put('posts', updatedStream); ``` ### Partial Update (PATCH) ```javascript const updates = { description: 'Updated description only', fields: { featured: { type: 'boolean', default: false } } }; const response = await client.streams.patch('posts', updates); ``` ## Deleting Streams ```javascript const response = await client.streams.delete('posts'); console.log(response.status); // 204 No Content ``` ## Stream Structure ### Basic Stream Definition ```javascript const stream = { // Identity id: 'posts', // Required: Unique identifier name: 'Posts', // Required: Human-readable name description: 'Blog posts', // Optional: Description // Configuration source: { type: 'eloquent', // Storage type model: 'App\\Models\\Post' // Model class }, // Fields fields: { title: { type: 'string', required: true, rules: 'max:255' }, content: { type: 'text', required: true } }, // Validation rules: { title: 'required|string|max:255', content: 'required|string' } }; ``` ### Field Types ```javascript const stream = { id: 'examples', name: 'Examples', fields: { // Text fields string_field: { type: 'string' }, text_field: { type: 'text' }, markdown_field: { type: 'markdown' }, // Number fields integer_field: { type: 'integer' }, decimal_field: { type: 'decimal', decimals: 2 }, // Boolean boolean_field: { type: 'boolean', default: false }, // Date/Time date_field: { type: 'date' }, datetime_field: { type: 'datetime' }, timestamp_field: { type: 'timestamp' }, // Relationships user: { type: 'relationship', related: 'users', relationship_type: 'belongsTo' }, // JSON metadata: { type: 'object' }, tags: { type: 'array' }, // Files image: { type: 'image' }, file: { type: 'file' } } }; ``` ## Real-World Examples ### Blog Stream ```javascript const blogStream = { id: 'posts', name: 'Blog Posts', description: 'Blog content management', fields: { title: { type: 'string', required: true, rules: 'max:255' }, slug: { type: 'string', required: true, unique: true }, excerpt: { type: 'text', rules: 'max:500' }, content: { type: 'markdown', required: true }, featured_image: { type: 'image' }, author: { type: 'relationship', related: 'users', relationship_type: 'belongsTo' }, category: { type: 'relationship', related: 'categories', relationship_type: 'belongsTo' }, tags: { type: 'relationship', related: 'tags', relationship_type: 'belongsToMany' }, status: { type: 'string', default: 'draft', rules: 'in:draft,published,archived' }, published_at: { type: 'datetime', nullable: true } }, rules: { title: 'required|string|max:255', slug: 'required|unique:posts,slug', content: 'required', status: 'required|in:draft,published,archived' } }; const response = await client.streams.post(blogStream); ``` ### E-commerce Product Stream ```javascript const productStream = { id: 'products', name: 'Products', description: 'E-commerce product catalog', fields: { name: { type: 'string', required: true }, sku: { type: 'string', required: true, unique: true }, description: { type: 'text' }, price: { type: 'decimal', decimals: 2, required: true }, sale_price: { type: 'decimal', decimals: 2, nullable: true }, cost: { type: 'decimal', decimals: 2 }, stock: { type: 'integer', default: 0 }, images: { type: 'array', items: { type: 'image' } }, category: { type: 'relationship', related: 'categories' }, attributes: { type: 'object' }, is_active: { type: 'boolean', default: true }, weight: { type: 'decimal', decimals: 2 }, dimensions: { type: 'object' } }, rules: { name: 'required|string|max:255', sku: 'required|unique:products,sku', price: 'required|numeric|min:0', stock: 'integer|min:0' } }; const response = await client.streams.post(productStream); ``` ### User Management Stream ```javascript const userStream = { id: 'users', name: 'Users', description: 'User accounts and profiles', fields: { email: { type: 'email', required: true, unique: true }, name: { type: 'string', required: true }, username: { type: 'string', required: true, unique: true }, password: { type: 'password', required: true }, avatar: { type: 'image', nullable: true }, bio: { type: 'text', nullable: true }, role: { type: 'relationship', related: 'roles' }, status: { type: 'string', default: 'active', rules: 'in:active,inactive,banned' }, email_verified_at: { type: 'datetime', nullable: true }, last_login_at: { type: 'datetime', nullable: true } }, rules: { email: 'required|email|unique:users,email', name: 'required|string|max:255', username: 'required|unique:users,username|alpha_dash', password: 'required|min:8', status: 'in:active,inactive,banned' } }; const response = await client.streams.post(userStream); ``` ## Listing and Managing Streams ### Get All Streams with Details ```javascript const response = await client.streams.get(); response.data.forEach(stream => { console.log(`Stream: ${stream.name} (${stream.id})`); console.log(`Fields: ${Object.keys(stream.fields).length}`); console.log(`Description: ${stream.description}`); }); ``` ### Filter Streams ```javascript import { Criteria } from '@laravel-streams/api-client'; // Get only content-type streams const criteria = new Criteria() .where('source.type', 'eloquent') .orderBy('name', 'asc'); const response = await client.streams.get({ criteria }); ``` ### Update Stream Configuration ```javascript // Add new field to existing stream const updates = { fields: { view_count: { type: 'integer', default: 0 } } }; await client.streams.patch('posts', updates); ``` ## Response Structure ```javascript // Success response { data: { id: 'posts', name: 'Posts', description: 'Blog posts', fields: { /* ... */ }, rules: { /* ... */ } }, status: 200, headers: { /* ... */ } } // List response { data: [ { id: 'posts', name: 'Posts', /* ... */ }, { id: 'users', name: 'Users', /* ... */ } ], status: 200, headers: { /* ... */ } } ``` ## Next Steps - [Working with Entries](/docs/client/entries) - CRUD operations on stream entries - [Criteria](/docs/client/criteria) - Query builder for filtering and sorting - [Examples](/docs/client/examples) - Real-world usage patterns --- # Client reference: Working with Entries > CRUD operations for stream entries Source: https://streams.dev/docs/client/entries Entries represent the data within your configured Streams. Use the Entries API to interact with stream data. ## Get Entries Return entries from a configured stream: **Method:** `client.entries.get(stream, config)` ```javascript const response = await client.entries.get('posts'); console.log(response.data); // Array of entries ``` ### With Pagination ```javascript const response = await client.entries.get('posts', { query: { per_page: 25, page: 2 } }); ``` ### With Criteria ```javascript import { Criteria } from '@laravel-streams/api-client'; const criteria = new Criteria() .where('status', 'published') .orderBy('created_at', 'desc') .limit(10); const response = await client.entries.get('posts', { criteria }); ``` ### Response Structure ```javascript { "data": [ { "id": "1", "title": "Hello World", "status": "published" } ], "meta": { "total": 100, "per_page": 25, "current_page": 1, "last_page": 4 }, "links": { "self": "http://api.example.com/streams/posts/entries", "first": "http://api.example.com/streams/posts/entries?page=1", "next": "http://api.example.com/streams/posts/entries?page=2" } } ``` ## Find Entry Return a single entry by ID: **Method:** `client.entries.find(stream, id, config)` ```javascript const response = await client.entries.find('posts', 1); console.log(response.data); // Single entry object ``` ### Response Structure ```javascript { "data": { "id": "1", "title": "Hello World", "content": "This is my first post", "status": "published", "created_at": "2024-01-01T00:00:00.000Z" }, "links": { "self": "http://api.example.com/streams/posts/entries/1", "stream": "http://api.example.com/streams/posts" } } ``` ## Create Entry Create a new entry in a stream: **Method:** `client.entries.post(stream, data, config)` ```javascript const response = await client.entries.post('posts', { title: 'My New Post', content: 'This is the content', status: 'draft' }); console.log(response.data.id); // ID of created entry ``` ### Response Structure ```javascript { "data": { "id": "123", "title": "My New Post", "content": "This is the content", "status": "draft", "created_at": "2024-01-01T12:00:00.000Z" }, "links": { "self": "http://api.example.com/streams/posts/entries/123", "location": "http://api.example.com/streams/posts/entries/123" } } ``` ## Update Entry (Partial) Update specific fields of an entry: **Method:** `client.entries.patch(stream, id, data, config)` ```javascript // Only update the title const response = await client.entries.patch('posts', 1, { title: 'Updated Title' }); // Other fields remain unchanged console.log(response.data); ``` ## Update Entry (Full) Replace all fields of an entry: **Method:** `client.entries.put(stream, id, data, config)` ```javascript // Replace entire entry const response = await client.entries.put('posts', 1, { title: 'New Title', content: 'New content', status: 'published' }); // Omitted fields will be set to null/default ``` ### PATCH vs PUT - **PATCH**: Updates only the provided fields, keeps others unchanged - **PUT**: Replaces the entire entry, omitted fields become null ```javascript // Original entry { id: 1, title: 'Hello', content: 'World', status: 'draft' } // PATCH { title: 'Hi' } { id: 1, title: 'Hi', content: 'World', status: 'draft' } // PUT { title: 'Hi' } { id: 1, title: 'Hi', content: null, status: null } ``` ## Delete Entry Delete an entry from a stream: **Method:** `client.entries.delete(stream, id, config)` ```javascript await client.entries.delete('posts', 1); // Returns empty response on success ``` ## Filtering Results Use Criteria to filter entries: ```javascript import { Criteria } from '@laravel-streams/api-client'; const criteria = new Criteria() .where('status', 'published') .where('created_at', '>=', '2024-01-01') .where('views', '>', 100); const response = await client.entries.get('posts', { criteria }); ``` ### Available Operators ```javascript // Comparison .where('views', '>', 100) .where('views', '>=', 100) .where('views', '<', 100) .where('views', '<=', 100) .where('status', '==', 'published') .where('status', '!=', 'draft') // Logical .where('tags', 'IN', ['tech', 'news']) .where('content', 'LIKE', '%tutorial%') ``` ## Sorting Results ```javascript const criteria = new Criteria() .orderBy('created_at', 'desc') .orderBy('title', 'asc'); const response = await client.entries.get('posts', { criteria }); ``` ## Pagination ```javascript // Simple pagination const criteria = new Criteria() .paginate(25, 2); // 25 per page, page 2 const response = await client.entries.get('posts', { criteria }); console.log(response.meta.total); console.log(response.meta.current_page); console.log(response.meta.last_page); ``` ## Error Handling ```javascript try { const response = await client.entries.find('posts', 999); } catch (error) { if (error.status === 404) { console.log('Entry not found'); } else if (error.status === 422) { console.log('Validation error:', error.response.data.errors); } else { console.error('Error:', error.message); } } ``` ## Real-World Examples ### Blog Post Management ```javascript // Get recent published posts async function getRecentPosts(limit = 10) { const criteria = new Criteria() .where('status', 'published') .orderBy('published_at', 'desc') .limit(limit); return await client.entries.get('posts', { criteria }); } // Publish a draft async function publishPost(id) { return await client.entries.patch('posts', id, { status: 'published', published_at: new Date().toISOString() }); } // Search posts async function searchPosts(query) { const criteria = new Criteria() .where('title', 'LIKE', `%${query}%`) .orWhere('content', 'LIKE', `%${query}%`) .where('status', 'published') .orderBy('relevance', 'desc'); return await client.entries.get('posts', { criteria }); } ``` ### User Management ```javascript // Get active users async function getActiveUsers() { const criteria = new Criteria() .where('status', 'active') .where('last_login', '>=', getLastWeek()) .orderBy('last_login', 'desc'); return await client.entries.get('users', { criteria }); } // Update user profile async function updateProfile(userId, data) { return await client.entries.patch('users', userId, { name: data.name, email: data.email, bio: data.bio, updated_at: new Date().toISOString() }); } ``` ## Next Steps - [Criteria Query Builder](/docs/client/criteria) - Learn advanced querying - [Middleware](/docs/client/middleware) - Add custom functionality - [Examples](/docs/client/examples) - See more examples --- # Client reference: Criteria Query Builder > PHP Laravel-style query building for filtering and sorting Source: https://streams.dev/docs/client/criteria The Criteria class provides a fluent, PHP Laravel-style interface for building queries. ## Basic Usage ```javascript import { Criteria } from '@laravel-streams/api-client'; const criteria = new Criteria() .where('status', 'published') .orderBy('created_at', 'desc') .limit(10); const response = await client.entries.get('posts', { criteria }); ``` ## Creating Criteria ### Static Factory ```javascript const criteria = Criteria.make() .where('status', 'active'); ``` ### Constructor ```javascript const criteria = new Criteria(); criteria.where('status', 'active'); ``` ## Where Clauses ### Basic Where ```javascript // Simple equality criteria.where('status', 'published'); // Generates: WHERE status == 'published' // With operator criteria.where('views', '>', 100); // Generates: WHERE views > 100 ``` ### Available Operators #### Comparison Operators ```javascript criteria.where('views', '>', 100); // Greater than criteria.where('views', '>=', 100); // Greater than or equal criteria.where('views', '<', 100); // Less than criteria.where('views', '<=', 100); // Less than or equal criteria.where('status', '==', 'active'); // Equal criteria.where('status', '!=', 'deleted'); // Not equal criteria.where('value', '<>', 0); // Not equal (alternative) ``` #### Logical Operators ```javascript criteria.where('status', 'IN', ['published', 'featured']); criteria.where('title', 'LIKE', '%tutorial%'); criteria.where('content', 'NOT', null); criteria.where('tags', 'ALL', ['javascript', 'tutorial']); criteria.where('categories', 'ANY', ['tech', 'news']); ``` ### OR Where Clauses ```javascript const criteria = new Criteria() .where('status', 'published') .orWhere('status', 'featured'); // WHERE status == 'published' OR status == 'featured' ``` ### Chaining Where Clauses ```javascript const criteria = new Criteria() .where('status', 'published') .where('views', '>', 100) .where('category', 'technology') .where('created_at', '>=', '2024-01-01'); // WHERE status == 'published' // AND views > 100 // AND category == 'technology' // AND created_at >= '2024-01-01' ``` ## Ordering Results ### Order By ```javascript // Descending (default) criteria.orderBy('created_at'); // Ascending criteria.orderBy('title', 'asc'); // Descending criteria.orderBy('views', 'desc'); ``` ### Multiple Order By ```javascript const criteria = new Criteria() .orderBy('priority', 'desc') // First by priority .orderBy('created_at', 'desc') // Then by date .orderBy('title', 'asc'); // Then by title ``` ## Limiting Results ### Limit ```javascript criteria.limit(10); // Return maximum 10 results ``` ### First ```javascript criteria.first(); // Same as limit(1) ``` ### Find by ID ```javascript criteria.find(123); // Same as: where('id', 123).limit(1) ``` ## Pagination ```javascript // paginate(perPage, page) criteria.paginate(25, 1); // 25 per page, page 1 // Default pagination (100 per page, page 1) criteria.paginate(); ``` ## Complex Queries ### Combining Conditions ```javascript const criteria = new Criteria() // Published or featured .where('status', 'published') .orWhere('status', 'featured') // With minimum views .where('views', '>=', 100) // In specific categories .where('category', 'IN', ['tech', 'tutorial']) // Recent posts .where('created_at', '>=', '2024-01-01') // Sort by popularity, then date .orderBy('views', 'desc') .orderBy('created_at', 'desc') // Paginate .paginate(20, 1); ``` ### Search Functionality ```javascript function searchPosts(query) { return new Criteria() .where('title', 'LIKE', `%${query}%`) .orWhere('content', 'LIKE', `%${query}%`) .orWhere('tags', 'LIKE', `%${query}%`) .where('status', 'published') .orderBy('relevance', 'desc') .limit(50); } const criteria = searchPosts('javascript'); const results = await client.entries.get('posts', { criteria }); ``` ## Working with Parameters ### Get Parameters ```javascript const criteria = new Criteria() .where('status', 'published') .orderBy('created_at', 'desc') .limit(10); const params = criteria.getParameters(); console.log(params); // [ // { name: 'where', value: ['status', '==', 'published', null] }, // { name: 'orderBy', value: ['created_at', 'desc'] }, // { name: 'limit', value: 10 } // ] ``` ### Set Parameters ```javascript const criteria = new Criteria(); criteria.setParameters([ { name: 'where', value: ['status', '==', 'published', null] }, { name: 'limit', value: 10 } ]); ``` ### Add Parameter ```javascript const criteria = new Criteria(); criteria.addParameter('where', ['status', '==', 'published', null]); ``` ### Standardize Parameters ```javascript const criteria = new Criteria() .where('status', 'published') .limit(10); const standardized = criteria.standardizeParameters(); console.log(standardized); // { // where: ['status', '==', 'published', null], // limit: 10 // } ``` ## Real-World Examples ### Blog Post Filtering ```javascript // Featured posts from last month const criteria = new Criteria() .where('featured', true) .where('published_at', '>=', getLastMonth()) .where('status', 'published') .orderBy('published_at', 'desc') .limit(5); // Posts by category with minimum engagement const criteria = new Criteria() .where('category', 'javascript') .where('likes', '>', 50) .where('comments', '>', 10) .orderBy('engagement_score', 'desc') .paginate(20, 1); ``` ### E-commerce Product Filtering ```javascript // Products in price range, in stock const criteria = new Criteria() .where('price', '>=', 10) .where('price', '<=', 100) .where('stock', '>', 0) .where('category', 'electronics') .orderBy('popularity', 'desc') .paginate(24, 1); // Sale items const criteria = new Criteria() .where('on_sale', true) .where('discount', '>=', 20) .where('stock', '>', 0) .orderBy('discount', 'desc') .orderBy('price', 'asc'); ``` ### User Management ```javascript // Active users who logged in recently const criteria = new Criteria() .where('status', 'active') .where('last_login', '>=', getLastWeek()) .where('email_verified', true) .orderBy('last_login', 'desc') .limit(100); // Premium users expiring soon const criteria = new Criteria() .where('subscription', 'premium') .where('expires_at', '<=', getNextMonth()) .where('expires_at', '>=', getToday()) .orderBy('expires_at', 'asc'); ``` ## Builder Pattern The Criteria class uses the builder pattern, allowing you to chain methods: ```javascript const criteria = new Criteria() .where('a', 1) // Returns this .where('b', 2) // Returns this .orderBy('c') // Returns this .limit(10); // Returns this // Same as: const criteria = new Criteria(); criteria.where('a', 1); criteria.where('b', 2); criteria.orderBy('c'); criteria.limit(10); ``` ## Reusable Criteria ```javascript // Create base criteria const publishedPosts = () => new Criteria() .where('status', 'published') .where('deleted_at', null); // Extend it const recentPublishedPosts = publishedPosts() .where('created_at', '>=', getLastWeek()) .orderBy('created_at', 'desc'); const popularPublishedPosts = publishedPosts() .where('views', '>', 1000) .orderBy('views', 'desc'); ``` ## Next Steps - [Working with Entries](/docs/client/entries) - Apply criteria to entry queries - [Examples](/docs/client/examples) - See more complex examples --- # Client reference: Middleware System > Request/response middleware and custom middleware creation Source: https://streams.dev/docs/client/middleware The middleware system allows you to intercept and modify requests and responses. Middleware can transform data, add authentication, handle errors, and more. ## Built-in Middleware ### AuthorizationMiddleware Adds Bearer token authentication to requests. ```javascript import { Client, AuthorizationMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new AuthorizationMiddleware({ token: 'your-api-token' }) ] }); ``` **What it does:** - Adds `Authorization: Bearer {token}` header to all requests - Enables authenticated API access ### CriteriaMiddleware Converts Criteria objects to query parameters. ```javascript import { Client, CriteriaMiddleware, Criteria } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new CriteriaMiddleware() ] }); // Criteria is automatically converted to query params const criteria = new Criteria() .where('status', 'published') .limit(10); const response = await client.entries.get('posts', { criteria }); ``` **What it does:** - Transforms `Criteria` objects into URL query parameters - Enables fluent query building with Laravel-style syntax ### QueryMiddleware Adds query parameters to requests. ```javascript import { Client, QueryMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new QueryMiddleware() ] }); // Add query params via options const response = await client.get('/posts', { query: { status: 'published', limit: 10 } }); // GET /posts?status=published&limit=10 ``` **What it does:** - Converts `query` option to URL query string - Handles URL encoding automatically ### RequestDataMiddleware Transforms request data based on content type. ```javascript import { Client, RequestDataMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new RequestDataMiddleware() ] }); // JSON (default) await client.post('/posts', { data: { title: 'Hello', content: 'World' } }); // Content-Type: application/json // Body: {"title":"Hello","content":"World"} // Form data await client.post('/upload', { data: { file: fileBlob }, headers: { 'Content-Type': 'multipart/form-data' } }); // Content-Type: multipart/form-data // Body: FormData object ``` **What it does:** - JSON: Stringifies data and sets `Content-Type: application/json` - Form Data: Converts to FormData and sets `Content-Type: multipart/form-data` - URL Encoded: Converts to URLSearchParams and sets `Content-Type: application/x-www-form-urlencoded` ### ResponseDataMiddleware Parses response data based on content type. ```javascript import { Client, ResponseDataMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new ResponseDataMiddleware() ] }); const response = await client.get('/posts'); // Automatically parses JSON response console.log(response.data); // Parsed object ``` **What it does:** - Parses JSON responses automatically - Handles text responses - Returns raw response for other content types ### ETagMiddleware Implements HTTP caching with ETags. ```javascript import { Client, ETagMiddleware } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new ETagMiddleware() ] }); // First request - stores ETag const response1 = await client.get('/posts/1'); // Response includes: ETag: "abc123" // Second request - sends If-None-Match const response2 = await client.get('/posts/1'); // Request includes: If-None-Match: "abc123" // If not modified: 304 status, returns cached data ``` **What it does:** - Stores ETags from responses - Adds `If-None-Match` header on subsequent requests - Returns cached data for 304 (Not Modified) responses - Reduces bandwidth and improves performance ## Creating Custom Middleware ### Basic Middleware ```javascript import { Middleware } from '@laravel-streams/api-client'; class LoggingMiddleware extends Middleware { async handle(request, next) { console.log('Request:', request.url); const response = await next(request); console.log('Response:', response.status); return response; } } // Use it const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new LoggingMiddleware() ] }); ``` ### Request Transformation ```javascript class ApiVersionMiddleware extends Middleware { constructor(version = 'v1') { super(); this.version = version; } async handle(request, next) { // Add version to URL request.url = `/api/${this.version}${request.url}`; // Add version header request.headers.set('X-API-Version', this.version); return next(request); } } const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new ApiVersionMiddleware('v2') ] }); await client.get('/posts'); // GET https://api.example.com/api/v2/posts // Headers: X-API-Version: v2 ``` ### Response Transformation ```javascript class DataWrapperMiddleware extends Middleware { async handle(request, next) { const response = await next(request); // Unwrap nested data structure if (response.data && response.data.data) { response.data = response.data.data; } return response; } } ``` ### Error Handling ```javascript class RetryMiddleware extends Middleware { constructor(maxRetries = 3) { super(); this.maxRetries = maxRetries; } async handle(request, next) { let lastError; for (let i = 0; i < this.maxRetries; i++) { try { return await next(request); } catch (error) { lastError = error; // Only retry on network errors or 5xx status if (error.status < 500) { throw error; } // Wait before retry await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)) ); } } throw lastError; } } ``` ### Request/Response Logging ```javascript class DetailedLoggingMiddleware extends Middleware { async handle(request, next) { const startTime = Date.now(); console.log('→ Request:', { method: request.method, url: request.url, headers: Object.fromEntries(request.headers.entries()), body: request.body }); try { const response = await next(request); const duration = Date.now() - startTime; console.log('← Response:', { status: response.status, statusText: response.statusText, duration: `${duration}ms`, headers: Object.fromEntries(response.headers.entries()), data: response.data }); return response; } catch (error) { const duration = Date.now() - startTime; console.error('✗ Error:', { duration: `${duration}ms`, message: error.message, status: error.status }); throw error; } } } ``` ## Real-World Examples ### API Key Authentication ```javascript class ApiKeyMiddleware extends Middleware { constructor(apiKey) { super(); this.apiKey = apiKey; } async handle(request, next) { request.headers.set('X-API-Key', this.apiKey); return next(request); } } const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new ApiKeyMiddleware('your-api-key-here') ] }); ``` ### Request Throttling ```javascript class ThrottleMiddleware extends Middleware { constructor(requestsPerSecond = 10) { super(); this.delay = 1000 / requestsPerSecond; this.lastRequest = 0; } async handle(request, next) { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequest; if (timeSinceLastRequest < this.delay) { await new Promise(resolve => setTimeout(resolve, this.delay - timeSinceLastRequest) ); } this.lastRequest = Date.now(); return next(request); } } const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new ThrottleMiddleware(5) // Max 5 requests per second ] }); ``` ### Response Caching ```javascript class CacheMiddleware extends Middleware { constructor(ttl = 60000) { // 60 seconds default super(); this.cache = new Map(); this.ttl = ttl; } getCacheKey(request) { return `${request.method}:${request.url}`; } async handle(request, next) { // Only cache GET requests if (request.method !== 'GET') { return next(request); } const cacheKey = this.getCacheKey(request); const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.ttl) { console.log('Cache hit:', cacheKey); return cached.response; } const response = await next(request); this.cache.set(cacheKey, { response: response, timestamp: Date.now() }); return response; } } const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new CacheMiddleware(30000) // Cache for 30 seconds ] }); ``` ### Request/Response Transformation ```javascript class TimestampMiddleware extends Middleware { async handle(request, next) { // Add timestamp to requests if (request.body && typeof request.body === 'object') { request.body.timestamp = Date.now(); } const response = await next(request); // Parse date strings in response if (response.data) { this.parseDates(response.data); } return response; } parseDates(obj) { const dateFields = ['created_at', 'updated_at', 'published_at']; for (const key in obj) { if (dateFields.includes(key) && typeof obj[key] === 'string') { obj[key] = new Date(obj[key]); } else if (typeof obj[key] === 'object') { this.parseDates(obj[key]); } } } } ``` ## Middleware Execution Order Middleware executes in the order it's added: ```javascript const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new LoggingMiddleware(), // 1. Logs request new AuthMiddleware(), // 2. Adds auth new CriteriaMiddleware(), // 3. Converts criteria new RequestDataMiddleware(), // 4. Transforms request data // --- Request sent --- // --- Response received --- new ResponseDataMiddleware(), // 5. Parses response data new DataWrapperMiddleware() // 6. Unwraps data ] }); ``` The execution flow: 1. Request goes through middleware from top to bottom 2. Response comes back through middleware from bottom to top 3. Each middleware can modify request before calling `next()` 4. Each middleware can modify response after calling `next()` ## Best Practices ### Keep Middleware Focused ```javascript // Good: Single responsibility class AuthMiddleware extends Middleware { async handle(request, next) { request.headers.set('Authorization', `Bearer ${this.token}`); return next(request); } } // Bad: Too many responsibilities class MegaMiddleware extends Middleware { async handle(request, next) { // Adds auth, transforms data, logs, caches, retries... // Too much in one middleware! } } ``` ### Error Handling ```javascript class SafeMiddleware extends Middleware { async handle(request, next) { try { // Your middleware logic return await next(request); } catch (error) { // Handle or re-throw console.error('Middleware error:', error); throw error; } } } ``` ### Configuration ```javascript class ConfigurableMiddleware extends Middleware { constructor(options = {}) { super(); this.options = { enabled: true, timeout: 5000, ...options }; } async handle(request, next) { if (!this.options.enabled) { return next(request); } // Use this.options... return next(request); } } ``` ## Next Steps - [Examples](/docs/client/examples) - See middleware in real-world scenarios - [Client Configuration](/docs/client/client) - Learn more about client setup --- # Client reference: Examples > Real-world usage patterns and complete examples Source: https://streams.dev/docs/client/examples Complete examples demonstrating real-world usage patterns. ## Basic Blog Application ### Setup ```javascript import { Client, AuthorizationMiddleware, CriteriaMiddleware, Criteria } from '@laravel-streams/api-client'; const client = new Client({ baseURL: 'https://api.example.com', middlewares: [ new AuthorizationMiddleware({ token: 'your-api-token' }), new CriteriaMiddleware() ] }); ``` ### List Blog Posts ```javascript async function getBlogPosts(page = 1, perPage = 10) { const criteria = new Criteria() .where('status', 'published') .orderBy('published_at', 'desc') .paginate(perPage, page); const response = await client.entries.get('posts', { criteria }); return { posts: response.data, page: page, perPage: perPage }; } // Usage const { posts } = await getBlogPosts(1, 10); posts.forEach(post => { console.log(`${post.title} - ${post.published_at}`); }); ``` ### Get Single Post ```javascript async function getPost(id) { const response = await client.entries.find('posts', id); return response.data; } // Usage const post = await getPost(123); console.log(post.title); console.log(post.content); ``` ### Create Post ```javascript async function createPost(data) { const post = { title: data.title, slug: data.slug || slugify(data.title), content: data.content, excerpt: data.excerpt, author_id: data.authorId, category_id: data.categoryId, status: 'draft', published_at: null }; const response = await client.entries.post('posts', post); return response.data; } // Usage const newPost = await createPost({ title: 'Getting Started with Laravel Streams', content: 'This is a comprehensive guide...', excerpt: 'Learn the basics of Laravel Streams', authorId: 1, categoryId: 5 }); ``` ### Update Post ```javascript async function updatePost(id, updates) { const response = await client.entries.patch('posts', id, updates); return response.data; } // Usage await updatePost(123, { title: 'Updated Title', content: 'Updated content...' }); ``` ### Publish Post ```javascript async function publishPost(id) { const updates = { status: 'published', published_at: new Date().toISOString() }; return await updatePost(id, updates); } // Usage await publishPost(123); ``` ### Delete Post ```javascript async function deletePost(id) { await client.entries.delete('posts', id); } // Usage await deletePost(123); ``` ### Search Posts ```javascript async function searchPosts(query, filters = {}) { const criteria = new Criteria() // Search in title and content .where('title', 'LIKE', `%${query}%`) .orWhere('content', 'LIKE', `%${query}%`) // Always published .where('status', 'published'); // Optional filters if (filters.category) { criteria.where('category_id', filters.category); } if (filters.author) { criteria.where('author_id', filters.author); } if (filters.tag) { criteria.where('tags', 'LIKE', `%${filters.tag}%`); } criteria.orderBy('published_at', 'desc').limit(20); const response = await client.entries.get('posts', { criteria }); return response.data; } // Usage const results = await searchPosts('javascript', { category: 5, tag: 'tutorial' }); ``` ### Posts by Category ```javascript async function getPostsByCategory(categoryId, page = 1) { const criteria = new Criteria() .where('category_id', categoryId) .where('status', 'published') .orderBy('published_at', 'desc') .paginate(10, page); const response = await client.entries.get('posts', { criteria }); return response.data; } // Usage const techPosts = await getPostsByCategory(5); ``` ## E-commerce Application ### Product Catalog ```javascript async function getProducts(filters = {}) { const criteria = new Criteria() .where('is_active', true); // Price range if (filters.minPrice) { criteria.where('price', '>=', filters.minPrice); } if (filters.maxPrice) { criteria.where('price', '<=', filters.maxPrice); } // Category if (filters.category) { criteria.where('category_id', filters.category); } // In stock only if (filters.inStock) { criteria.where('stock', '>', 0); } // On sale if (filters.onSale) { criteria.where('sale_price', '!=', null); } // Sorting const sortBy = filters.sortBy || 'created_at'; const sortOrder = filters.sortOrder || 'desc'; criteria.orderBy(sortBy, sortOrder); criteria.paginate(24, filters.page || 1); const response = await client.entries.get('products', { criteria }); return response.data; } // Usage const products = await getProducts({ category: 10, minPrice: 10, maxPrice: 100, inStock: true, sortBy: 'price', sortOrder: 'asc', page: 1 }); ``` ### Shopping Cart ```javascript class ShoppingCart { constructor(client) { this.client = client; } async addItem(productId, quantity = 1) { const item = { product_id: productId, quantity: quantity }; const response = await this.client.entries.post('cart_items', item); return response.data; } async updateQuantity(itemId, quantity) { const response = await this.client.entries.patch('cart_items', itemId, { quantity: quantity }); return response.data; } async removeItem(itemId) { await this.client.entries.delete('cart_items', itemId); } async getItems() { const criteria = new Criteria() .where('user_id', this.getCurrentUserId()) .orderBy('created_at', 'desc'); const response = await this.client.entries.get('cart_items', { criteria }); return response.data; } async clear() { const items = await this.getItems(); for (const item of items) { await this.removeItem(item.id); } } getCurrentUserId() { // Get from your auth system return 1; } } // Usage const cart = new ShoppingCart(client); await cart.addItem(123, 2); await cart.updateQuantity(456, 3); const items = await cart.getItems(); ``` ### Order Management ```javascript async function createOrder(cartItems) { // Calculate totals const subtotal = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0 ); const tax = subtotal * 0.1; // 10% tax const total = subtotal + tax; // Create order const order = { subtotal: subtotal, tax: tax, total: total, status: 'pending', items: cartItems.map(item => ({ product_id: item.product_id, quantity: item.quantity, price: item.price })) }; const response = await client.entries.post('orders', order); return response.data; } async function getOrderHistory(userId, page = 1) { const criteria = new Criteria() .where('user_id', userId) .orderBy('created_at', 'desc') .paginate(10, page); const response = await client.entries.get('orders', { criteria }); return response.data; } // Usage const order = await createOrder(cartItems); const orders = await getOrderHistory(1); ``` ## User Management ### Authentication ```javascript class AuthService { constructor(client) { this.client = client; this.token = localStorage.getItem('auth_token'); } async login(email, password) { const response = await this.client.post('/auth/login', { data: { email, password } }); this.token = response.data.token; localStorage.setItem('auth_token', this.token); // Update client with new token this.client.middleware.forEach(middleware => { if (middleware instanceof AuthorizationMiddleware) { middleware.token = this.token; } }); return response.data.user; } async logout() { await this.client.post('/auth/logout'); this.token = null; localStorage.removeItem('auth_token'); } async register(userData) { const response = await this.client.post('/auth/register', { data: userData }); return response.data; } async getCurrentUser() { const response = await this.client.get('/auth/me'); return response.data; } } // Usage const auth = new AuthService(client); const user = await auth.login('user@example.com', 'password'); ``` ### User Profiles ```javascript async function getUserProfile(userId) { const response = await client.entries.find('users', userId); return response.data; } async function updateProfile(userId, updates) { const response = await client.entries.patch('users', userId, updates); return response.data; } async function uploadAvatar(userId, file) { const formData = new FormData(); formData.append('avatar', file); const response = await client.post(`/users/${userId}/avatar`, { data: formData, headers: { 'Content-Type': 'multipart/form-data' } }); return response.data.avatar_url; } // Usage const profile = await getUserProfile(1); await updateProfile(1, { bio: 'Updated bio' }); await uploadAvatar(1, avatarFile); ``` ## Content Management ### Media Library ```javascript class MediaLibrary { constructor(client) { this.client = client; } async upload(file, metadata = {}) { const formData = new FormData(); formData.append('file', file); formData.append('metadata', JSON.stringify(metadata)); const response = await this.client.post('/media/upload', { data: formData, headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; } async getFiles(filters = {}) { const criteria = new Criteria(); if (filters.type) { criteria.where('mime_type', 'LIKE', `${filters.type}/%`); } if (filters.search) { criteria.where('filename', 'LIKE', `%${filters.search}%`); } criteria.orderBy('created_at', 'desc') .paginate(filters.perPage || 20, filters.page || 1); const response = await this.client.entries.get('media', { criteria }); return response.data; } async delete(fileId) { await this.client.entries.delete('media', fileId); } } // Usage const media = new MediaLibrary(client); const file = await media.upload(imageFile, { alt: 'Product image' }); const images = await media.getFiles({ type: 'image' }); await media.delete(123); ``` ### Multi-language Content ```javascript async function getTranslations(streamId, entryId) { const criteria = new Criteria() .where('translatable_type', streamId) .where('translatable_id', entryId); const response = await client.entries.get('translations', { criteria }); return response.data; } async function setTranslation(streamId, entryId, locale, field, value) { const translation = { translatable_type: streamId, translatable_id: entryId, locale: locale, field: field, value: value }; const response = await client.entries.post('translations', translation); return response.data; } // Usage await setTranslation('posts', 123, 'es', 'title', 'Título en Español'); await setTranslation('posts', 123, 'es', 'content', 'Contenido en Español'); const translations = await getTranslations('posts', 123); ``` ## Analytics and Reporting ### View Tracking ```javascript async function trackView(streamId, entryId) { await client.post('/analytics/view', { data: { stream: streamId, entry: entryId, timestamp: Date.now(), user_agent: navigator.userAgent } }); } async function getPopularPosts(days = 7) { const since = new Date(); since.setDate(since.getDate() - days); const criteria = new Criteria() .where('status', 'published') .where('published_at', '>=', since.toISOString()) .orderBy('views', 'desc') .limit(10); const response = await client.entries.get('posts', { criteria }); return response.data; } // Usage await trackView('posts', 123); const popular = await getPopularPosts(7); ``` ## Error Handling ### Comprehensive Error Handling ```javascript async function safeApiCall(operation) { try { return await operation(); } catch (error) { if (error.status === 401) { // Unauthorized - redirect to login console.error('Authentication required'); window.location.href = '/login'; } else if (error.status === 403) { // Forbidden console.error('Access denied'); throw new Error('You do not have permission to perform this action'); } else if (error.status === 404) { // Not found console.error('Resource not found'); throw new Error('The requested resource was not found'); } else if (error.status === 422) { // Validation error console.error('Validation failed:', error.data); throw new Error('Please check your input and try again'); } else if (error.status >= 500) { // Server error console.error('Server error:', error); throw new Error('A server error occurred. Please try again later.'); } else { // Other errors console.error('API error:', error); throw error; } } } // Usage const post = await safeApiCall(() => getPost(123)); ``` ## Next Steps - [Client Configuration](/docs/client/client) - Advanced client setup - [Criteria](/docs/client/criteria) - Query building techniques - [Middleware](/docs/client/middleware) - Custom middleware patterns --- # Contributing: This project > What streams.dev is and how it uses Streams Core and UI. Source: https://streams.dev/docs/this-project ## What this repository is **streams.dev** is the public documentation site and reference implementation for the Streams ecosystem. It is a Laravel 12 application (PHP 8.2 or newer) that ships with **streams/core** and **streams/ui** in production, plus **streams/sdk** as a dev dependency. Unlike the generic `composer create-project streams/streams` starter, this repo is purpose-built to: - Serve documentation from flat files in `streams/data/` - Demonstrate stream-driven pages (`/`, `/docs`, `/addons`) without custom controllers - Link to package source via Composer path repositories for local development ## What is in git | Path | Role | |------|------| | `streams/*.json` | Stream definitions (pages, docs, packages, categories) | | `streams/data/` | Filebase content: markdown docs, HTML pages, package catalog data | | `resources/views/` | Blade layouts and partials used by pages and docs | | `app/Providers/AppServiceProvider.php` | Registers the admin panel via `UI::panel()` | | `composer.json` | Requires Core + UI; path repos for sibling package clones | There is minimal application code. Most behavior comes from stream configuration and package service providers. ## Packages in this project Production dependencies: - [streams/core](/docs/core/introduction) — data modeling, repositories, routing - [streams/ui](/docs/ui/introduction) — Livewire admin panel at `/admin` Dev-only: - [streams/sdk](/docs/sdk/introduction) — scaffolding helpers **streams/api** is available as a path repository for local work but is **not** required in this project's `composer.json`. Add it when you need REST endpoints. ## Related - [Project structure](/docs/project-structure) — directory map - [Local development](/docs/local-development) — run the site locally - [Installation](/docs/installation) — add Streams to your own Laravel app --- # Contributing: Project structure > Where stream definitions, content, views, and app code live. Source: https://streams.dev/docs/project-structure ## Top-level layout ```text streams.dev/ ├── app/ # Minimal Laravel app code ├── streams/ # Stream JSON definitions ├── streams/data/ # Filebase content for streams ├── resources/views/ # Blade templates ├── routes/ # Laravel routes (mostly defaults) ├── config/ # Laravel + streams config └── composer.json # Core, UI, path repos ``` ## `streams/` — configuration Each `.json` file defines one stream. Examples in this repo: | File | Stream handle | Purpose | |------|---------------|---------| | `pages.json` | `pages` | Site pages at `/`, `/docs`, `/addons` | | `docs.json` | `docs` | Hub documentation at `/docs/{id}` | | `docs_categories.json` | `docs_categories` | Sidebar and index groupings | | `packages.json` | `packages` | Add-on catalog (`type: self`) | | `core_docs.json` | `core_docs` | Core reference at `/docs/core/{id}` | | `ui_docs.json` | `ui_docs` | UI reference at `/docs/ui/{id}` | | `api_docs.json` | `api_docs` | API reference at `/docs/api/{id}` | Stream JSON holds fields, routes, source adapters, and optional UI admin config. See [Streams](/docs/core/streams) for the full schema. ## `streams/data/` — content Filebase entries live beside stream definitions: ```text streams/data/ ├── docs/ # Hub guides (*.md) ├── core_docs/ # Core package docs ├── ui_docs/ # UI package docs ├── api_docs/ # API package docs ├── pages/ # HTML pages (*.html) └── packages/ # Package catalog entries (if used) ``` Entry filenames become entry IDs (for example `introduction.md` → `/docs/introduction`). ## `resources/views/` | Path | Used by | |------|---------| | `docs.blade.php` | All documentation routes | | `page.blade.php`, `blank.blade.php` | Site pages stream | | `partials/sidebar.blade.php` | Docs sidebar navigation | | `partials/topbar.blade.php` | Site header | ## Application code `AppServiceProvider` registers the admin panel: ```php UI::panel( Panel::make('admin') ->default() ->path('admin') ->brandName('Streams') ->middleware(['web']) ); ``` Most domain logic lives in the packages under `vendor/streams/`, which Composer installs as git clones from GitHub. ## Composer repositories `composer.json` points each Streams package at its GitHub repository and pins the branch: ```json { "require": { "streams/core": "dev-rc/prep as 2.0.x-dev", "streams/ui": "1.0.x-dev" }, "repositories": [ { "type": "vcs", "url": "https://github.com/laravel-streams/streams-core.git", "no-api": true } ] } ``` The `as 2.0.x-dev` alias lets packages that require `streams/core ^2.0` accept the branch. For local package work, `php scripts/composer-local.php` writes a gitignored `composer.local.json` with symlinked path repositories; see [Local development](/docs/local-development). ## Related - [Site pages](/docs/site-pages) — how `pages.json` drives URLs - [Content](/docs/content) — filebase formats and frontmatter - [Contributing documentation](/docs/contributing-docs) --- # Contributing: Site pages > How pages.json and HTML entries drive site URLs without controllers. Source: https://streams.dev/docs/site-pages ## Overview Public site URLs (`/`, `/docs`, `/addons`) are served by the **pages** stream. No route files or controllers define these paths — the stream's `routes` block and entry frontmatter do. Read this page when you need to add or change a marketing or landing page in this repository. ## Stream definition From `streams/pages.json`: ```json { "routes": [ { "handle": "view", "uri": "{uri}", "parse": true, "view": "{layout}" } ], "fields": [ { "handle": "id", "type": "slug", "required": true, "unique": true }, { "handle": "title", "type": "string", "required": true }, { "handle": "uri", "type": "string", "required": true, "unique": true }, { "handle": "body", "type": "string" } ] } ``` Key options: - **`parse: true`** — registers one Laravel route per entry using each entry's `uri` field - **`{uri}`** — route parameter bound to the entry's `uri` value - **`{layout}`** — resolves the Blade layout from entry frontmatter ## Page entry format Pages are HTML files in `streams/data/pages/` with YAML frontmatter: ```html --- title: Documentation uri: docs layout: page sort_order: 1 --- @include('partials.topbar')

{{ $entry->title }}

``` | Frontmatter key | Purpose | |-----------------|---------| | `uri` | URL path (`/` for homepage uses `uri: /` or root value) | | `layout` | Blade layout name (`page`, `blank`, etc.) | | `title` | Entry title, available as `$entry->title` in the body | The homepage (`welcome.html`) uses `uri: /` and `layout: blank`. ## How routing works 1. At boot, Core reads `pages.json` and registers routes for each entry because `parse` is true. 2. A request to `/docs` matches the entry whose `uri` is `docs`. 3. `EntryController` renders the entry body through the layout named in frontmatter. For programmatic routes outside the pages stream, use Laravel's `Route` facade or [Route::streams()](/docs/core/routes). ## Documentation vs site pages | Stream | URL pattern | Content | |--------|-------------|---------| | `pages` | `/`, `/docs`, `/addons` | HTML with Blade | | `docs` | `/docs/{id}` | Markdown hub guides | | `core_docs` | `/docs/core/{id}` | Markdown package reference | The `/docs` **index** is a pages entry (`docs.html`). Individual guide pages use the `docs` stream. ## Related - [Content](/docs/content) — filebase formats - [Routing](/docs/routing) — Laravel vs stream routes - [Core routes reference](/docs/core/routes) --- # Contributing: Package catalog > How packages.json powers the add-ons page and homepage cards. Source: https://streams.dev/docs/package-catalog ## Overview The add-on catalog on `/addons` comes from the **packages** stream. Entries describe Streams ecosystem packages (Core, UI, API, SDK, and community addons) with Composer metadata and marketing copy. ## Stream source `streams/packages.json` uses a **self** source — entries are stored inline in the JSON file under `data`, not as separate files: ```json { "config": { "source": { "type": "self" } } } ``` Each entry includes fields such as `name`, `type`, `enabled`, and a `composer` object with package coordinates. ## Entry types The `type` field groups catalog entries: | Type | Examples | |------|----------| | `starter` | Official starter projects | | `example` | Demo applications | | `database` | Database adapters | | `client` | API clients | | `tools` | Dev tooling | Only entries with `enabled: true` should appear in public listings (filter in your Blade/views as needed). ## Using entries in views Pages can query the stream in Blade: ```blade @foreach (Streams::entries('packages')->where('enabled', true)->get() as $package)

{{ $package->name }}

@endforeach ``` The homepage and `/addons` page include package cards that link to documentation or external repositories. ## Adding a catalog entry 1. Edit `streams/packages.json` and add an object to the `data` array. 2. Set a unique `id`, `name`, `type`, and `enabled`. 3. Add `composer` JSON with `name`, `description`, and repository URL if applicable. 4. Link to `/docs/{package}/introduction` when package docs exist on this site. ## Related - [Addons](/docs/addons) — Composer addon discovery in Core - [Core addons reference](/docs/core/addons) - [Architecture](/docs/architecture) --- # Contributing: Local development > Install dependencies, run the dev server, and edit content locally. Source: https://streams.dev/docs/local-development ## Prerequisites - PHP 8.2+ with the extensions required by [Laravel 12](https://laravel.com/docs/12.x/deployment#server-requirements) (this site runs Laravel 12) - Composer 2 - Node.js 20.19+ or 22.12+ and npm (for Vite and Tailwind) - Git (Composer clones the Streams packages from GitHub) ## First-time setup ```bash git clone https://github.com/laravel-streams/streams.dev.git --branch develop cd streams.dev composer install cp .env.example .env php artisan key:generate npm install composer dev ``` `composer dev` serves the app on `http://127.0.0.1:8427`, tails the application log with `php artisan pail`, and runs the Vite dev server (with hot reload) on port 5427. Both ports are strict: if one is taken the command stops instead of picking another. No database is needed. Run the tests with `php artisan test` and `npm run test:js`. ## Editing content Documentation and pages are flat files — no database required for content changes. | Content | Edit path | Preview URL | |---------|-----------|-------------| | Hub guide | `streams/data/docs/{id}.md` | `/docs/{id}` | | Core doc | `streams/data/core_docs/{id}.md` | `/docs/core/{id}` | | Site page | `streams/data/pages/{id}.html` | entry `uri` | | Stream config | `streams/{handle}.json` | after cache clear if cached | In local environment, doc pages show an **Edit this page** link to that file on GitHub (`laravel-streams/streams.dev`, branch `develop`). ## Admin panel Streams UI registers a panel at `/admin` from `AppServiceProvider`. Use it to browse and edit stream entries when you prefer a UI over flat files. ## Local package development `composer.json` installs `streams/core` (branch `rc/prep`), `streams/ui` (`1.0`) and `streams/sdk` (`sdk/rc`) from their GitHub repositories as git clones in `vendor/streams/`. To work on a package and see the change in this site, check it out next to this repository and link it in without editing `composer.json`: ```bash composer local ``` `composer local` runs `scripts/composer-local.php`, which finds checkouts at `../_packages/streams-` or `../streams-` (or the path in `STREAMS__PATH`), writes a gitignored `composer.local.json` and `composer.local.lock` that symlink them, and then runs `composer update "streams/*"` against that file. `composer.json` and `composer.lock` stay as committed. Edits in the checkout then show up immediately. The script warns when a checkout does not contain the branch `composer.json` asks for (for example `_packages/streams-core` on `2.0` while the site needs `rc/prep`). To link release-candidate worktrees from `../_rc/streams-` instead, ask for them by name: ```bash composer local -- --rc=core,sdk # or: STREAMS_LOCAL_RC=core,sdk composer local composer local -- --rc # every package from ../_rc ``` To only write `composer.local.json`, run `php scripts/composer-local.php` and then `COMPOSER=composer.local.json composer update "streams/*"` yourself. Run a plain `composer install` to switch back to the GitHub packages. Run package tests in the package directory; run `php artisan test` here for the site. ## Assets Front-end assets compile through Vite: ```bash npm run dev # watch mode npm run build # production build ``` Styles live in `resources/css/` (`app.css` imports the tokens and components). Tailwind v4 runs through the `@tailwindcss/vite` plugin. The built files in `public/build` are committed, so run `npm run build` after changing CSS or JavaScript. ## Branches and deploys Work on `develop` (or a short-lived branch off it that you merge back). When `develop` is ready, merge it into `master` and push both. `master` is the live site and GitHub's default branch, and `envoy run deploy` fast-forwards the server to it. The old `next`, `integration/rc` and `production` branches no longer exist. ## Related - [Contributing documentation](/docs/contributing-docs) - [This project](/docs/this-project) - [Configuration](/docs/configuration) --- # Contributing: Contributing documentation > How to add or edit documentation on streams.dev. Source: https://streams.dev/docs/contributing-docs ## Where docs live All documentation for streams.dev is in **this repository** under `streams/data/`. Package repos link here; they do not maintain parallel doc trees. | Section | Path | URL | |---------|------|-----| | Hub guides | `streams/data/docs/` | `/docs/{id}` | | Core | `streams/data/core_docs/` | `/docs/core/{id}` | | UI | `streams/data/ui_docs/` | `/docs/ui/{id}` | | API | `streams/data/api_docs/` | `/docs/api/{id}` | See [STYLE.md](https://github.com/laravel-streams/streams.dev/blob/develop/STYLE.md) for voice, frontmatter, and visual guidelines. ## Frontmatter Every page requires YAML frontmatter: ```yaml --- title: 'Core: Installation' # required; unique across the site (the page H1) nav_title: Installation # short sidebar and browser-tab label description: One sentence, plain text, for indexes and meta. section: packages # get-started | guides | concepts | reference | packages | contributing category: getting-started # hub docs only: a key in streams/docs_categories.json package: core # core | ui | api | sdk | testing | client | site | all order: 20 # sidebar order within the stream (hub docs: within the category); step by 10 tags: [core, installation] status: ready # draft | review | ready | deprecated --- ``` The layout renders `title` as the page H1, so don't start the body with a `#` heading. Give every code fence a language, link to other pages with root-relative URLs (`/docs/core/fields`), and keep `json` blocks valid JSON. `tests/Feature/DocsContentTest.php` checks all of this. **Status workflow:** `draft` → `review` → `ready` (and `deprecated` for pages kept only for old versions). Mark pages `ready` only after verifying claims against package source code. ## Hub vs section rules - **Hub docs** explain workflows, architecture, and when to use a feature. Link to section docs for API depth. - **Section docs** hold package-specific reference: classes, methods, configuration keys, examples. - Do not duplicate long reference material in both places. ## Adding a hub page 1. Create `streams/data/docs/{slug}.md` with frontmatter including `category`. 2. Assign `order` within the category. 3. Add the page to a category in `streams/docs_categories.json` if introducing a new category. 4. Preview at `/docs/{slug}`. ## Adding a package reference page 1. Create `streams/data/{package}_docs/{slug}.md`. 2. Set `order` for sidebar ordering (no `category` field needed). 3. Sidebar picks up the page automatically from the doc stream entries. ## Accuracy standard Document **what works today**. Do not describe APIs that are planned or commented out unless labeled as deferred. Before marking `status: ready`, trace documented classes and methods to the package source. In this app, `vendor/streams/core`, `vendor/streams/ui`, and `vendor/streams/sdk` are symlinks into the package checkouts. `streams/api`, `streams/testing`, and the JavaScript client are not installed under `vendor/streams` here; read those checkouts directly. Do not edit files through `vendor/streams/*`. The `submodules/` directory in this repo is empty. ## Related - [STYLE.md](https://github.com/laravel-streams/streams.dev/blob/develop/STYLE.md) - [Project structure](/docs/project-structure) - [Local development](/docs/local-development)