anomaly/streams-platform
Streams Platform is a Laravel-based modular application engine for building custom apps and addons. It provides a powerful streams and fields system for defining data structures, a flexible addon and extension architecture, and serves as the foundation of PyroCMS.
Pin your Laravel version first — Streams is tightly coupled to the framework release it was built for, so check the package's composer.json against your app before anything else.
composer require anomaly/streams-platform on a fresh or compatible Laravel app.php artisan streams:install on most versions; check the README for your tag). This publishes config/streams.php, runs the core migrations, and registers the bundled addons. Make sure the addons/ directory at the project root is writable and that its path is set in config/streams.php.php artisan addon:list (or addon:list equivalent) to confirm the core addons booted. If the list is empty, your addon path or PSR-4 registration is wrong — fix that before writing any code.Where to look first: read a core addon end to end (the Pages module is the canonical example). Streams is opinionated and the fastest way to learn its conventions is to copy the structure of a module that already ships with the platform: src/, resources/streams/, resources/views/, routes.php, migrations/.
First real use case: scaffold one addon and one stream. php artisan addon:make acme.module.blog (older versions: addon:make), then add resources/streams/posts.php returning a config array with name, slug, namespace, fields, and optional assignments. Follow it with php artisan streams:compile and you have a working table, entry repository, and admin CRUD surface — no hand-written migration or model required.
Addons are the unit of work. Everything — module, extension, plugin, theme, field type — is an addon under addons/{vendor}-{type}-{name} with a matching namespace (Acme\BlogModule\). Keep domain logic inside the addon, not in app/; that is what makes the flat-file model portable.
Streams are schema-as-code. The stream config is the table definition: fields, field type configs, and assignments. Commit these files. Treat the compiler as your migration tool for greenfield streams, and drop to a real migration when you need to alter an existing table.
Query through repositories, not raw SQL. The idiomatic pattern is:
$posts = stream('posts')->getRepository();
$post = $posts->findBySlug('hello-world');
$recent = $posts->where('published', true)->orderBy('created_at', 'desc')->paginate(10);
Criteria methods (where, orderBy, limit, paginate, first, findBy*) keep business logic readable and consistent.
Switch to Eloquent mode when you need relations. Setting 'config' => ['eloquent' => true] (or the equivalent per-stream flag) gives you a real model, eager loading, and relationships — worth it as soon as you're doing joins across assignments. Otherwise stick with the repository for plain reads.
Assignments model relationships between streams. Attach a categories stream to posts via the assignments key and the platform wires up pivot handling and admin UI for you. Use this instead of a bespoke pivot table.
Lean on events for decoupling. Entry lifecycle events (EntryWasCreated, EntryWasUpdated, etc.) are the clean seam for cache invalidation, search indexing, or notifications. Listen or subscribe in the addon's service provider rather than stuffing side effects into controllers.
Reuse standard Laravel inside addons. Routes, migrations, views, policies, form requests, and console commands all work normally; the addon is just a package with a service provider. Don't reinvent them.
Custom field types are the main extension point. Extend the base FieldType class, register the addon type, and you get admin form rendering and validation for free.
php artisan streams:compile (and clear the app cache if things look stale). A surprising number of "why isn't my field showing up" bugs are just an uncompiled definition.acme-module-blog ↔ Acme\BlogModule ↔ stream slug posts). A mismatch will silently fail to boot the addon.posts streams in different addons, resolve with the fully qualified identifier rather than the bare slug, and verify what actually loaded with the addon list command.->with([...])) or a batch criteria call; looping an entry and calling $entry->categories per row is the classic slow path here.storage/logs/laravel.log as boot/registry errors rather than clean exceptions.addon:list; fields missing → streams:compile; routes 404 → check the addon's routes.php is loaded by the service provider; stale output → php artisan cache:clear and view:clear.How can I help you explore Laravel packages today?