orchid/cms
Orchid is a Laravel package for rapidly building admin panels and CMS-style backends. It offers screens, flexible layouts, form fields, filters, and CRUD tooling, letting developers create rich, data-driven management interfaces with minimal boilerplate.
Prerequisites. Laravel with orchid/platform already installed and working (admin user, config/platform.php, routes/platform.php, storage link). Press is a domain module on top of Orchid — it will not install usefully into a plain Blade, Filament, or Nova admin, and it adds no delivery layer of its own.
Minimal install sequence.
# 1. Read the actual composer.json constraints before committing
composer show orchid/platform
composer require orchidcommunity/press
# 2. Discover the provider name; don't guess it
grep -A5 '"autoload"' vendor/orchidcommunity/press/composer.json
ls vendor/orchidcommunity/press/src
Then:
bootstrap/providers.php; Laravel 10-: config/app.php.php artisan vendor:publish --provider="…PressServiceProvider" then php artisan migrate. Inspect the published migrations before running them — check foreign keys to users and any assumptions about the attachments table from orchid/platform.php artisan orchid:admin [email protected] password.php artisan route:list --path=platform to confirm the screen routes registered.Where to look first, in this order:
| Look at | Why |
|---|---|
vendor/…/Press/src/Providers/*ServiceProvider.php |
Defines menu(), permissions(), and often routes; this is the package's whole integration contract |
vendor/…/Press/src/Screens/ |
Orchid Screen classes — query(), layout(), commandBar(); this is where you extend |
database/migrations/ (published) |
The real content model; the schema is the source of truth |
composer.json of the package |
Orchid/Laravel/PHP constraints and the last-touched date — the abandonment signal |
First use case. Do a read-only spike: install into a throwaway app on your target Orchid version, create one post through the admin screen, attach one image, and render it manually from a closure route:
Route::get('/news/{post:slug}', fn ($post) => view('news.show', ['post' => $post]));
If the screen renders, the CRUD works, and the attachment URL resolves, you have the two facts that matter: it runs on your stack, and the model is queryable. If it needs patching before the first screen loads, stop and price an in-house module instead.
Pattern 1 — fork before first commit. You are adopting code, not buying a product. Vendor it from day one:
"repositories": [{ "type": "vcs", "url": "[email protected]:yourorg/press.git" }],
"require": { "orchidcommunity/press": "dev-main#<pinned-sha>" }
Pin the Orchid major in the same composer.json and commit the lockfile. Keeping your diff against upstream small is the whole game — prefer adding your own screens over editing vendor screens.
Pattern 2 — extend by composing screens, not editing them. Add your own Orchid Screen that reuses the package model and appends fields:
class PostEditScreen extends Screen
{
public $post;
public function query(Post $post): iterable
{
return ['post' => $post->load('categories', 'attachment')];
}
public function layout(): iterable
{
return [
Layout::rows([
Input::make('post.title')->title('Title')->required(),
Slug::make('post.slug')->from('post.title'),
Markdown::make('post.body'), // or TinyMCE via Orchid's HTML field
Relation::make('post.categories.')
->fromModel(Category::class, 'name')->multiple(),
Input::make('post.published_at')->type('datetime-local'),
Attach::make('post.attachment_id'),
]),
];
}
}
Register it with Route::screen('press/posts/{post}/edit', PostEditScreen::class)->name('platform.press.posts.edit'); in routes/platform.php. Your screens survive Orchid upgrades far better than patched vendor screens.
Pattern 3 — add fields via migration, not model edits. The package won't expose a configurable model, so add columns in your migrations and swap in a subclass if you need casts/fillable:
Schema::table('press_posts', function (Blueprint $t) {
$t->string('seo_title')->nullable();
$t->string('canonical_url')->nullable();
$t->index(['published_at', 'status']);
});
Override the model binding in your screen (query(Post $post) against your subclass via a Route::model binding) rather than editing vendor/.
Pattern 4 — build a thin delivery layer with explicit scopes. Press is admin-only; public routes are yours. Centralise published-state logic:
// App\Models\Post extends OrchidCommunity\Press\Models\Post
public function scopePublished($q)
{
return $q->whereNotNull('published_at')->where('published_at', '<=', now());
}
Then cache at the controller level and invalidate with an observer rather than sprinkling Cache::forget everywhere:
static::saved(fn () => Cache::forget('press.index'));
static::deleted(fn () => Cache::forget('press.index'));
Pattern 5 — permissions and menu are provider-driven. permissions() and menu() in the package provider are the integration surface. After any change there, re-run your permission sync and re-check the role: Orchid permissions resolve at boot, and a missing permission manifests as a blank/403 screen, not an error message. Grant explicitly:
$admin->roles()->sync([Dashboard::modelClass(Role::class)::where('slug','admin')->value('id')]);
Pattern 6 — CI smoke test that actually catches breakage. The failure mode is framework drift, so test boot + render, not just models:
public function test_press_index_renders(): void
{
$this->actingAs(User::factory()->create())
->get(route('platform.press.posts'))
->assertOk();
}
Run it against the pinned matrix (Orchid major × Laravel × PHP) on every bump. This single test is your upgrade tripwire.
Pattern 7 — seed for local dev. Add a factory for the package model in your app (database/factories) so you can populate a hundred posts and see how the list screen, pagination, and filters behave at realistic volume — not with the three rows a spike creates.
Screen and field APIs have broken across majors, and a zero-adoption package won't have tracked them. Before anything else, run the install against your exact Orchid major. "Composer resolves" ≠ "screens render."bootstrap/providers.php / config/app.php before debugging Orchid. This is the single most common first-install failure.php artisan optimize:clear first, then verify the user's role has the package's permissions. Orchid fails silently here.users table and Orchid's attachments table exist. Run Orchid's migrations first; if you see foreign-key errors, the published migration ordering is off — rename the timestamp in your fork.php artisan orchid:link (or storage:link), and confirm which disk Orchid's Attachment config points at. If the package hard-codes public, plan a fork-level change rather than fighting it in config.{post:slug} route.->with() in the screen's query(); Orchid gives you no automatic eager-loading.dump() inside query() is faster than log-diving; if the screen 500s before rendering, check storage/logs/laravel.log for a field-DSL TypeError — that's usually API drift, not your code.Layout/Field composition. Avoid: overriding package screens globally via container bindings, or editing vendor files you'll have to re-apply on every pull.MAINTAINERS.md next to the fork pinning the Orchid major and naming who absorbs upgrade work. Zero stars and zero dependents means no upstream fix will arrive on any timeline you can plan against — "are we prepared to maintain this?" is the question that decides adoption, not "does it work?"How can I help you explore Laravel packages today?