From statamic-dev
This skill should be used when the user works with "Entry facade", "query builder", "Statamic events", "query scope", "view model", "computed values", "PHP API", "Entry::query", "Collection::computed", or uses Statamic facades for data retrieval, content manipulation, event handling, or controller queries.
How this skill is triggered — by the user, by Claude, or both
Slash command
/statamic-dev:statamic-php-apiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use facades from `Statamic\Facades` to interact with all content types. The primary facades are:
Use facades from Statamic\Facades to interact with all content types. The primary facades are:
use Statamic\Facades\Entry;
use Statamic\Facades\Collection;
use Statamic\Facades\Term;
use Statamic\Facades\GlobalSet;
use Statamic\Facades\Asset;
use Statamic\Facades\User;
use Statamic\Facades\Nav;
use Statamic\Facades\Blueprint;
use Statamic\Facades\Site;
Each facade provides find(), query(), and make() methods as entry points.
Retrieve individual items by ID or URI:
$entry = Entry::find('abc123');
$entry = Entry::findByUri('/blog/my-post');
$entry = Entry::findByUri('/blog/my-post', 'french'); // Site-specific lookup
find() accepts a string ID (the filename stem from the flat-file content). findByUri() accepts the URI path and returns the entry matching that route. Pass an optional site handle as the second argument for multi-site lookups. Both methods return null when no match is found.
For other content types, use the equivalent facade methods:
$term = Term::find('tags::featured');
$user = User::findByEmail('[email protected]');
$asset = Asset::find('images::hero.jpg'); // container::path format
$nav = Nav::findByHandle('main');
Build queries using the fluent query builder accessed via ::query():
$entries = Entry::query()
->where('collection', 'blog')
->where('status', 'published')
->where('date', '>=', now()->subDays(30))
->orderBy('date', 'desc')
->limit(10)
->get();
where($field, $operator, $value) -- filter by field valuewhereBetween($field, [$min, $max]) -- range filterwhereIn($field, $values) -- match any value in arraywhereNull($field) / whereNotNull($field) -- null checkswhereDate($field, $value), whereMonth, whereDay, whereYear, whereTime -- date component filterswhereJsonContains($field, $value) -- JSON field searchwhereJsonLength($field, $operator, $value) -- JSON array lengthwhereColumn($col1, $operator, $col2) -- compare columnswhen($condition, $callback) / unless($condition, $callback) -- conditional clausesorderBy($field, $direction) -- sort resultslimit($n) -- cap result countget() -- execute and return collectionpaginate($perPage) -- paginate resultsStandard operators: =, <>, !=, like, not like, regexp, not regexp, >, <, >=, <=.
Closure-based queries for complex grouping:
Entry::query()->where(function ($query) {
$query->where('status', 'published')
->orWhere('featured', true);
})->get();
See references/events-and-operators.md for complete operator reference with examples.
$entry = Entry::make()
->collection('blog')
->slug('new-post')
->published(true)
->data(['title' => 'New Post', 'content' => 'Hello!']);
$entry->save();
$entry->set('title', 'Updated Title');
$entry->save();
$entry->saveQuietly();
Use saveQuietly() to suppress all EntrySaving and EntrySaved events. Useful in migrations, imports, or batch operations where event side-effects are undesirable.
When creating or updating multiple entries, wrap operations to avoid excessive event firing and Stache rebuilds:
$items = [['title' => 'Post A', 'slug' => 'post-a'], ['title' => 'Post B', 'slug' => 'post-b']];
foreach ($items as $item) {
Entry::make()
->collection('blog')
->slug($item['slug'])
->data(['title' => $item['title']])
->saveQuietly();
}
$entry = Entry::find('abc123');
$entry->delete();
Deletion fires EntryDeleting (preventable) and EntryDeleted events.
Statamic distinguishes between augmented (processed) and raw (stored) values:
$entry->title; // Augmented value
$entry->content; // Augmented (e.g., Markdown converted to HTML)
$entry->get('title'); // Raw stored value
$entry->value('title'); // Raw with inheritance (falls back to collection defaults)
$entry->data(); // All raw data as array
$entry->toAugmentedArray(); // All values, augmented
Augmented values pass through fieldtype processing. A Markdown field returns HTML. A relationship field returns entry objects. Use augmented values for display.
Raw values return exactly what is stored in the YAML file. Use raw values for comparisons, conditions, and data manipulation.
Inherited values via value() check the entry first, then fall back to the collection's inject defaults.
Access global variable sets:
$footer = GlobalSet::findByHandle('footer')->inDefaultSite();
$copyright = $footer->get('copyright');
Call inDefaultSite() or inCurrentSite() to get the localized variables object, then access data via get().
Statamic fires 86+ events in the Statamic\Events namespace covering the full content lifecycle. Key events:
EntrySaving / EntrySaved / EntryCreated / EntryDeletedTermSaved / AssetUploaded / FormSubmittedUserSaved / BlueprintSaved / CollectionSavedNavTreeSaved / GlobalVariablesSavedSearchIndexUpdated / StacheWarmed / UrlInvalidatedReturn false from a Saving event listener to prevent the save:
use Statamic\Events\EntrySaving;
class PreventDraftPublishing
{
public function handle(EntrySaving $event)
{
if ($event->entry->get('needs_review')) {
return false; // Prevents the save
}
}
}
Register listeners in EventServiceProvider as with any Laravel event.
See references/events-and-operators.md for the full event catalog.
Hooks allow tapping into tag pipelines to modify results. Use the hook() method on tag classes:
use Statamic\Tags\Collection;
Collection::hook('fetched-entries', function ($entries, $next) {
return $next($entries->take(3));
});
The hook receives the current value and a $next closure. Always call $next() with the modified (or unmodified) value to continue the pipeline. Register hooks in a service provider's boot() method.
$next($value) to pass data to the next hook in the chain. Omitting this call stops all downstream hooks.boot() method of a service provider, not in register().Collection::hook('fetched-entries', function ($entries, $next) {
$entries->each(function ($entry) {
$entry->setSupplement('external_score', fetchScore($entry->slug()));
});
return $next($entries);
});
Use setSupplement() to attach transient data to entries without modifying stored content. Supplements are available in templates alongside regular fields.
Define reusable query constraints as scope classes:
namespace App\Scopes;
use Statamic\Query\Scopes\Scope;
class Featured extends Scope
{
public function apply($query, $values)
{
$query->where('featured', true);
}
}
Register scopes in a service provider:
use App\Scopes\Featured;
use Statamic\Facades\Scope;
Scope::register('featured', Featured::class);
{{ collection:blog query_scope="featured" }}
The $values parameter receives any additional parameters passed from the tag. Pass extra parameters from Antlers and access them via $values->get('param_name') inside the scope's apply() method.
Extend Statamic\Query\Scopes\Filter instead of Scope to create interactive filter UI in the control panel listings. Filter scopes define fieldItems() for the filter form, apply() for the query logic, badge() for the active filter label, and visibleTo() to control which listing pages show the filter. See references/events-and-operators.md for a full Filter scope example.
Define computed fields that derive values at runtime rather than storing them:
// app/Providers/AppServiceProvider.php boot()
use Statamic\Facades\Collection;
Collection::computed('articles', 'read_time', function ($entry, $value) {
return ceil(str_word_count(strip_tags($entry->content)) / 200);
});
Arguments: collection handle, field handle, closure receiving the entry and current value.
Set the field visibility to computed in the blueprint to make it read-only in the control panel:
fields:
-
handle: read_time
field:
type: integer
visibility: computed
Computed values are available in templates and the API like any other field. They are recalculated each time the entry is accessed, so keep computations lightweight or cache expensive results.
Register multiple computed values for the same or different collections:
Collection::computed('articles', 'read_time', function ($entry, $value) {
return ceil(str_word_count(strip_tags($entry->content)) / 200);
});
Collection::computed('articles', 'excerpt_auto', function ($entry, $value) {
return Str::limit(strip_tags($entry->content), 160);
});
Collection::computed('products', 'price_formatted', function ($entry, $value) {
return '$' . number_format($entry->get('price') / 100, 2);
});
Use view models to inject derived data into templates without cluttering entries:
namespace App\ViewModels;
use Statamic\View\ViewModel;
class ArticleStats extends ViewModel
{
public function data(): array
{
$content = strip_tags($this->cascade->get('content'));
$words = str_word_count($content);
return [
'word_count' => $words,
'read_time' => ceil($words / 200),
];
}
}
Configure in the collection YAML:
inject:
view_model: App\ViewModels\ArticleStats
The data() return values merge into the template cascade and are accessible as variables alongside entry data. Access the full cascade (including entry data, globals, and site variables) via $this->cascade within the view model.
Use computed values for single derived fields tied to a specific collection (e.g., read time, formatted price). Use view models when injecting multiple related variables, when the computation needs access to the full cascade (not just the entry), or when the logic is complex enough to warrant its own class.
Use Statamic facades in custom Laravel controllers for full control over routing and rendering:
use Statamic\Facades\Entry;
class BlogController extends Controller
{
public function index()
{
$entries = Entry::query()
->where('collection', 'blog')
->where('status', 'published')
->orderBy('date', 'desc')
->paginate(15);
return (new \Statamic\View\View)
->template('blog.index')
->layout('layout')
->with(['entries' => $entries]);
}
}
Use \Statamic\View\View instead of Laravel's view() helper to render through the Statamic view layer with Antlers support, cascade injection, and layout handling. Pass data via with().
For API-style endpoints, return augmented data directly:
public function show($slug)
{
$entry = Entry::query()
->where('collection', 'blog')
->where('slug', $slug)
->first();
if (! $entry) {
abort(404);
}
return response()->json($entry->toAugmentedArray());
}
Register routes in routes/web.php as standard Laravel routes. Statamic's route handling coexists with Laravel routes -- explicit Laravel routes take priority over Statamic's content routing.
npx claudepluginhub dontfreakout/claude-statamic-pluginGuides Craft CMS 5 content architecture: sections, entry types, fields, Matrix, relations, eager loading, multi-site propagation, and project config. Use when planning content structure or configuring the Entries index.
Guides Astro rendering strategy selection, islands architecture, content collections, view transitions, and Cloudflare/Vercel deployments with configuration examples.
Helps build and configure EmDash CMS, a full-stack TypeScript CMS on Astro and Cloudflare. Scaffold projects, create content types, build plugins, and migrate from WordPress.