# Craft CMS
## Introduction
Craft CMS is a flexible, self-hosted PHP content management system built on the Yii 2 framework. It provides a clean-slate approach to content modeling without making assumptions about the structure of your content. The system features an intuitive control panel for content creators, a powerful Twig-based templating engine, and an auto-generated GraphQL API for headless applications. Craft's architecture supports multi-site deployments, custom field types, and extensibility through a robust plugin ecosystem.
The core system is organized around elements (entries, assets, users, categories, tags), each with customizable field layouts. Craft uses a service-oriented architecture where business logic resides in service classes accessed via `Craft::$app`, controllers handle HTTP requests, and the database layer uses Yii's ActiveRecord pattern. The framework includes over 47 built-in services managing everything from content operations to authentication, search, and GraphQL schema generation.
---
## Core APIs and Functions
### Query and Retrieve Entries
Query entries from a section with filtering, ordering, and eager loading support.
```php
// Query published blog entries with author information
$entries = \craft\elements\Entry::find()
->section('blog')
->type('article')
->status('live')
->orderBy('postDate DESC')
->with(['author'])
->limit(10)
->all();
// Access entry data and custom fields
foreach ($entries as $entry) {
echo $entry->title;
echo $entry->slug;
echo $entry->postDate->format('Y-m-d H:i:s');
echo $entry->author->email;
// Custom fields via magic properties
echo $entry->customTextField;
echo $entry->customImageField->one()->url;
}
// Query with custom field filtering
$filtered = \craft\elements\Entry::find()
->section('products')
->customPrice(['>', 100])
->customCategory('electronics')
->all();
```
### Create and Save Entry Elements
Create new entries programmatically with field values and validation.
```php
use craft\elements\Entry;
use Craft;
// Create a new entry
$entry = new Entry();
$entry->sectionId = 5;
$entry->typeId = 3;
$entry->siteId = 1;
$entry->title = 'My New Article';
$entry->slug = 'my-new-article';
// Set author
$entry->authorId = Craft::$app->getUser()->getId();
// Set custom field values
$entry->setFieldValue('bodyContent', '
Article body text here
');
$entry->setFieldValue('featuredImage', [42]); // Asset IDs
$entry->setFieldValue('tags', [10, 11, 12]); // Tag IDs
// Set dates
$entry->postDate = new \DateTime('2025-01-15 10:00:00');
$entry->expiryDate = new \DateTime('2025-12-31 23:59:59');
// Save the entry
if (Craft::$app->elements->saveElement($entry)) {
echo "Entry saved successfully with ID: {$entry->id}";
} else {
echo "Failed to save entry:";
print_r($entry->getErrors());
}
// Alternative: Save with validation disabled (use with caution)
Craft::$app->elements->saveElement($entry, false);
```
### GraphQL API Query Endpoint
Execute GraphQL queries via HTTP POST to retrieve content.
```bash
# Query entries with nested relations
curl -X POST https://example.com/api \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"query": "query { entries(section: \"blog\", limit: 5) { id title slug postDate @formatDateTime(format: \"Y-m-d\") author { id fullName email } ... on blog_article_Entry { bodyContent featuredImage { id url width height } tags { id title } } } }"
}'
# Response structure
# {
# "data": {
# "entries": [
# {
# "id": "123",
# "title": "Article Title",
# "slug": "article-title",
# "postDate": "2025-01-15",
# "author": {
# "id": "1",
# "fullName": "John Doe",
# "email": "john@example.com"
# },
# "bodyContent": "
"
featuredImage: [42]
tags: [10, 11]
) {
id
title
slug
dateCreated @formatDateTime(format: "Y-m-d H:i:s")
errors {
attribute
message
}
}
}
```
### Register Custom Element Type via Plugin
Extend Craft with custom element types for specialized content structures.
```php
use craft\events\RegisterComponentTypesEvent;
use craft\services\Elements;
use yii\base\Event;
// In your plugin's init() method
Event::on(
Elements::class,
Elements::EVENT_REGISTER_ELEMENT_TYPES,
function(RegisterComponentTypesEvent $event) {
$event->types[] = \myplugin\elements\Product::class;
}
);
// Custom element class example
namespace myplugin\elements;
use craft\base\Element;
use craft\elements\db\ElementQueryInterface;
class Product extends Element
{
public static function displayName(): string
{
return 'Product';
}
public static function hasContent(): bool
{
return true;
}
public static function hasTitles(): bool
{
return true;
}
public static function find(): ElementQueryInterface
{
return new ProductQuery(static::class);
}
protected static function defineSources(string $context): array
{
return [
[
'key' => '*',
'label' => 'All Products',
],
];
}
}
```
### Twig Template Rendering
Render Twig templates with variables for frontend or control panel views.
```php
// In a controller action
public function actionView(): \yii\web\Response
{
$entry = \craft\elements\Entry::find()
->section('blog')
->slug('my-article')
->one();
if (!$entry) {
throw new \yii\web\NotFoundHttpException('Entry not found');
}
return $this->renderTemplate('blog/_detail.twig', [
'entry' => $entry,
'relatedEntries' => $entry->relatedPosts->limit(3)->all(),
'siteUrl' => \craft\helpers\UrlHelper::siteUrl(),
]);
}
```
```twig
{# In blog/_detail.twig template #}
{% extends "_layout" %}
{% block content %}
{{ entry.title }}
By {{ entry.author.fullName }}
{{ entry.bodyContent|raw }}
{% if entry.featuredImage|length %}
{% endif %}
{% if relatedEntries|length %}
{% endif %}
{% endblock %}
```
### Asset Upload and Management
Upload files to asset volumes and manipulate images.
```php
use craft\elements\Asset;
use craft\helpers\Assets;
use Craft;
// Upload a file from form submission
$volumeId = 2; // Target volume
$folderId = 5; // Target folder within volume
$file = $_FILES['upload'];
$filename = Assets::prepareAssetName($file['name']);
$asset = new Asset();
$asset->tempFilePath = $file['tmp_name'];
$asset->filename = $filename;
$asset->newFolderId = $folderId;
$asset->volumeId = $volumeId;
$asset->avoidFilenameConflicts = true;
// Set custom fields on asset
$asset->setFieldValue('altText', 'Description of image');
$asset->setFieldValue('photographer', 'John Smith');
if (Craft::$app->getElements()->saveElement($asset)) {
echo "Asset uploaded: {$asset->url}";
// Generate image transform
$transform = Craft::$app->imageTransforms->getTransformByHandle('thumbnail');
$transformUrl = $asset->getUrl($transform);
echo "Thumbnail URL: {$transformUrl}";
} else {
print_r($asset->getErrors());
}
// Query existing assets
$images = Asset::find()
->volumeHandle('images')
->kind('image')
->filename('*.jpg')
->limit(20)
->all();
```
### User Authentication and Registration
Authenticate users and manage user accounts programmatically.
```php
use craft\elements\User;
use Craft;
// Register a new user
$user = new User();
$user->username = 'newuser';
$user->email = 'newuser@example.com';
$user->firstName = 'John';
$user->lastName = 'Doe';
$user->newPassword = 'securePassword123!';
// Assign to user group
$group = Craft::$app->userGroups->getGroupByHandle('members');
if ($group) {
Craft::$app->users->assignUserToGroups($user->id, [$group->id]);
}
if (Craft::$app->elements->saveElement($user)) {
// Activate the user account
Craft::$app->users->activateUser($user);
// Send activation email
Craft::$app->users->sendActivationEmail($user);
echo "User created with ID: {$user->id}";
} else {
print_r($user->getErrors());
}
// Authenticate user login
$username = 'newuser';
$password = 'securePassword123!';
if (Craft::$app->getUser()->login(
Craft::$app->users->getUserByUsernameOrEmail($username),
3600 * 24 * 30 // 30 days
)) {
echo "User logged in successfully";
} else {
echo "Invalid credentials";
}
// Check current user permissions
$currentUser = Craft::$app->getUser()->getIdentity();
if ($currentUser && $currentUser->can('editEntries:5')) {
echo "User can edit entries in section 5";
}
```
### Plugin Configuration and Settings
Create a plugin with custom settings and service components.
```php
namespace myplugin;
use craft\base\Plugin;
use myplugin\services\MyService;
use myplugin\models\Settings;
class MyPlugin extends Plugin
{
public static function config(): array
{
return [
'components' => [
'myService' => ['class' => MyService::class],
],
];
}
public function init()
{
parent::init();
// Register event listeners
\yii\base\Event::on(
\craft\elements\Entry::class,
\craft\elements\Entry::EVENT_AFTER_SAVE,
function(\craft\events\ElementEvent $event) {
$entry = $event->element;
// Custom logic after entry save
$this->myService->processEntry($entry);
}
);
}
protected function createSettingsModel(): ?\craft\base\Model
{
return new Settings();
}
protected function settingsHtml(): ?string
{
return \Craft::$app->view->renderTemplate(
'my-plugin/settings',
['settings' => $this->getSettings()]
);
}
}
// Access plugin instance and services
$plugin = \myplugin\MyPlugin::getInstance();
$result = $plugin->myService->someMethod();
// Get plugin settings
$settings = $plugin->getSettings();
echo $settings->apiKey;
```
### Register GraphQL Queries and Types
Extend the GraphQL schema with custom queries and types.
```php
use craft\events\RegisterGqlQueriesEvent;
use craft\events\RegisterGqlTypesEvent;
use craft\services\Gql;
use yii\base\Event;
// Register custom query
Event::on(
Gql::class,
Gql::EVENT_REGISTER_GQL_QUERIES,
function(RegisterGqlQueriesEvent $event) {
$event->queries['myProducts'] = [
'type' => \GraphQL\Type\Definition\Type::listOf(
\myplugin\gql\types\Product::getType()
),
'args' => [
'limit' => \GraphQL\Type\Definition\Type::int(),
'category' => \GraphQL\Type\Definition\Type::string(),
],
'resolve' => function($source, $args) {
return \myplugin\elements\Product::find()
->category($args['category'] ?? null)
->limit($args['limit'] ?? 10)
->all();
},
];
}
);
// Register custom type
Event::on(
Gql::class,
Gql::EVENT_REGISTER_GQL_TYPES,
function(RegisterGqlTypesEvent $event) {
$event->types[] = \myplugin\gql\types\Product::class;
}
);
// Query usage
// {
// myProducts(limit: 5, category: "electronics") {
// id
// title
// price
// }
// }
```
### Element Queries with Relations
Eager load related elements to optimize database queries.
```php
// Eager load multiple levels of relations
$entries = \craft\elements\Entry::find()
->section('blog')
->with([
'author',
'featuredImage',
['relatedProducts', ['manufacturer']],
])
->all();
// Access eager-loaded relations without additional queries
foreach ($entries as $entry) {
echo $entry->title;
echo $entry->author->fullName; // No query
if ($entry->featuredImage) {
foreach ($entry->featuredImage->all() as $image) {
echo $image->url; // No query
}
}
foreach ($entry->relatedProducts->all() as $product) {
echo $product->title; // No query
echo $product->manufacturer->one()->name; // No query
}
}
// Matrix field eager loading
$entries = \craft\elements\Entry::find()
->section('pages')
->with(['contentBlocks.image'])
->all();
foreach ($entries as $entry) {
foreach ($entry->contentBlocks->all() as $block) {
echo $block->heading;
if ($block->image->one()) {
echo $block->image->one()->url;
}
}
}
```
### Database Migrations
Create database migrations for plugin installation and updates.
```php
namespace myplugin\migrations;
use Craft;
use craft\db\Migration;
class Install extends Migration
{
public function safeUp()
{
// Create custom table
$this->createTable('{{%myplugin_products}}', [
'id' => $this->primaryKey(),
'name' => $this->string()->notNull(),
'price' => $this->decimal(10, 2)->notNull(),
'sku' => $this->string(50)->notNull(),
'stock' => $this->integer()->defaultValue(0),
'dateCreated' => $this->dateTime()->notNull(),
'dateUpdated' => $this->dateTime()->notNull(),
'uid' => $this->uid(),
]);
// Create index
$this->createIndex(
'myplugin_products_sku_idx',
'{{%myplugin_products}}',
'sku',
true // unique
);
// Create foreign key
$this->addForeignKey(
'myplugin_products_id_fk',
'{{%myplugin_products}}',
'id',
'{{%elements}}',
'id',
'CASCADE'
);
return true;
}
public function safeDown()
{
$this->dropTableIfExists('{{%myplugin_products}}');
return true;
}
}
// Run migrations programmatically
$migrator = Craft::$app->contentMigrator;
$migrator->up();
```
### Template Fragment Caching
Cache expensive template operations for performance optimization.
```twig
{# Cache for 1 hour (3600 seconds) #}
{% cache for 1 hour %}
{% set products = craft.entries()
.section('products')
.limit(20)
.all() %}
{% for product in products %}
{{ product.title }} - {{ product.price }}
{% endfor %}
{% endcache %}
{# Cache with dynamic keys based on segment and site #}
{% cache using key "blog-list-#{currentUser.id}" for 30 minutes %}
{% set entries = craft.entries()
.section('blog')
.authorId(currentUser.id)
.all() %}
{% for entry in entries %}
{{ entry.title }}
{{ entry.summary }}
{% endfor %}
{% endcache %}
{# Cache with conditions #}
{% cache unless currentUser if craft.app.request.getQueryParam('nocache') is empty %}
{# Expensive operations here #}
{% endcache %}
```
### Field Type Definition
Create custom field types for specialized content input.
```php
namespace myplugin\fields;
use Craft;
use craft\base\Field;
use craft\base\ElementInterface;
class ColorPicker extends Field
{
public static function displayName(): string
{
return 'Color Picker';
}
public static function icon(): string
{
return '@myplugin/icon.svg';
}
public function getContentColumnType(): string
{
return \yii\db\Schema::TYPE_STRING;
}
public function normalizeValue($value, ElementInterface $element = null)
{
if (is_string($value) && preg_match('/^#[0-9a-f]{6}$/i', $value)) {
return $value;
}
return null;
}
public function getInputHtml($value, ElementInterface $element = null): string
{
return Craft::$app->getView()->renderTemplate(
'my-plugin/_fields/color-picker/input',
[
'name' => $this->handle,
'value' => $value,
'field' => $this,
]
);
}
public function getSearchKeywords($value, ElementInterface $element): string
{
return (string) $value;
}
}
// Register the field type
use craft\events\RegisterComponentTypesEvent;
use craft\services\Fields;
use yii\base\Event;
Event::on(
Fields::class,
Fields::EVENT_REGISTER_FIELD_TYPES,
function(RegisterComponentTypesEvent $event) {
$event->types[] = \myplugin\fields\ColorPicker::class;
}
);
```
### URL Routing Configuration
Define custom URL routes for controllers and templates.
```php
// In config/routes.php
return [
'blog' => 'templates/render',
'blog/' => 'templates/render',
'api/products' => 'my-plugin/api/products',
'api/products/' => 'my-plugin/api/product-detail',
];
// Dynamic route registration in plugin
use craft\events\RegisterUrlRulesEvent;
use craft\web\UrlManager;
use yii\base\Event;
Event::on(
UrlManager::class,
UrlManager::EVENT_REGISTER_SITE_URL_RULES,
function(RegisterUrlRulesEvent $event) {
$event->rules['search'] = 'my-plugin/search/index';
$event->rules['search/'] = 'my-plugin/search/results';
}
);
// Controller action
namespace myplugin\controllers;
use craft\web\Controller;
class SearchController extends Controller
{
protected array|bool|int $allowAnonymous = ['index', 'results'];
public function actionResults(string $query): \yii\web\Response
{
$results = \craft\elements\Entry::find()
->search($query)
->limit(20)
->all();
return $this->renderTemplate('my-plugin/search/results', [
'query' => $query,
'results' => $results,
]);
}
}
```
---
## Main Use Cases and Integration Patterns
Craft CMS excels in scenarios requiring flexible content modeling without rigid schemas. Content teams benefit from the intuitive control panel to manage entries, assets, and users across multiple sites and languages. The element-query system enables developers to build complex content relationships with efficient eager loading, while custom field types extend the platform to handle specialized data like color pickers, address fields, or embedded media. The GraphQL API auto-generates schemas based on configured sections and fields, making it ideal for headless deployments powering mobile apps, SPAs, or IoT devices.
For integration patterns, plugins extend functionality through event listeners, custom services, and new element types. The migration system ensures database changes are version-controlled and deployable. Template caching and query optimization features handle high-traffic scenarios. Multi-environment configuration uses environment variables parsed via `App::parseEnv()` for database credentials, API keys, and feature flags. Project Config YAML files sync content structure across environments via Git. The service layer (accessed via `Craft::$app->serviceName`) provides a consistent API for all operations, from saving elements to sending emails, making Craft highly testable and maintainable for large-scale applications.