When building complex integrations, custom migrations, or front-end submission forms in Craft CMS, you will eventually need to save Matrix data programmatically. While Craft's UI makes managing content easy, handling the underlying data structure via PHP requires a clear understanding of how the Element system processes field values.

In this guide, you will learn the standard approach for creating entries with Matrix blocks, how to append new blocks to existing fields without overwriting data, and the specific changes introduced in more recent versions like Craft 4. Understanding these patterns will help you write cleaner, more efficient code for your custom modules and plugins.

Creating a New Entry with Matrix Blocks

To save Matrix data programmatically, you first need to define the entry and its properties. In Craft 3 and beyond, the content refactoring has made this process significantly more intuitive. Instead of manually creating block elements one by one, you can pass a structured array to the setFieldValues() method.

Here is a complete example of creating a new entry in a specific section and adding multiple Matrix blocks to it:

use craft\elements\Entry;
use craft\elements\MatrixBlock;
use Craft;

// 1. Identify the section and entry type
$section = Craft::$app->sections->getSectionByHandle('news');
$entryTypes = $section->getEntryTypes();
$entryType = reset($entryTypes);

// 2. Instantiate the Entry
$entry = new Entry([
    'sectionId' => $section->id,
    'typeId' => $entryType->id,
    'fieldLayoutId' => $entryType->fieldLayoutId,
    'authorId' => 1,
    'title' => 'My Programmatic Entry',
    'slug' => 'my-programmatic-entry',
    'postDate' => new \DateTime(),
]);

// 3. Define the custom field values, including the Matrix data
$entry->setFieldValues([
    'summary' => 'This is a standard text field value.',
    'matrixFieldHandle' => [
        'new1' => [
            'type' => 'textBlock',
            'fields' => [
                'textFieldHandle' => 'Hello world!',
            ],
        ],
        'new2' => [
            'type' => 'imageBlock',
            'fields' => [
                'caption' => 'A beautiful landscape',
                // For assets, provide an array of IDs
                'imageField' => [42],
            ],
        ],
    ],
]);

// 4. Save the element
if (Craft::$app->elements->saveElement($entry)) {
    // Success!
} else {
    // Handle errors
    $errors = $entry->getErrors();
}

Understanding the Array Keys

In the example above, you'll notice the keys 'new1' and 'new2'. These are temporary IDs used to tell Craft that these are brand-new blocks. If you were updating existing blocks, you would use their actual block IDs as the keys instead. This structure allows Craft to distinguish between adding new content and updating what is already there.

Appending Blocks to an Existing Matrix Field

A common challenge developers face is adding a new block to a Matrix field that already contains data. If you simply pass a new array to setFieldValues(), you might accidentally overwrite all existing blocks. To avoid this, you must retrieve the current data first.

The Craft 4+ Approach

In Craft 4, a sortOrder key was introduced to simplify the management of block order and persistence. Here is how you can safely append a new block to an existing element:

// 1. Get the existing block IDs
$existingIds = $element->getFieldValue('matrixFieldHandle')->ids();

// 2. Add a temporary ID for the new block to our order array
$existingIds[] = 'new1';

// 3. Set the field value with the new sort order and the new block data
$element->setFieldValue('matrixFieldHandle', [
    'sortOrder' => $existingIds,
    'blocks' => [
        'new1' => [
            'type' => 'blockTypeHandle',
            'fields' => [
                'fieldHandle' => 'New content value',
            ]
        ]
    ]
]);

// 4. Save the parent element
Craft::$app->elements->saveElement($element);

The Legacy Craft 3 Approach (Pre-3.6)

If you are working on an older Craft 3 site, you might need to use the serializeValue method. This converts the Matrix query (which is what getFieldValue returns) into a format that can be easily manipulated as an array.

/** @var \craft\fields\Matrix $field */
$field = Craft::$app->getFields()->getFieldByHandle('matrixFieldHandle');

// Get the existing Matrix Query
$existingMatrixQuery = $element->getFieldValue('matrixFieldHandle');

// Serialize to an array
$serializedMatrix = $field->serializeValue($existingMatrixQuery, $element);

// Append the new block
$serializedMatrix['new1'] = [
    'type' => 'blockTypeHandle',
    'fields' => [
        'fieldHandle' => 'New content',
    ]
];

$element->setFieldValue('matrixFieldHandle', $serializedMatrix);
Craft::$app->elements->saveElement($element);

Best Practices for Programmatic Matrix Operations

When you save Matrix data programmatically, you are bypassing the standard control panel validation checks that a content editor would trigger. Keep these best practices in mind to ensure data integrity:

  1. Validate the Parent Element: Always check the return value of saveElement(). If it returns false, use $entry->getErrors() to debug what went wrong.
  2. Check Block Type Handles: Ensure the type key in your array matches the handle of the Matrix Block Type exactly. A typo here will result in the block not being created.
  3. Handle Assets Correctly: If your Matrix blocks contain Asset fields, remember that Craft expects an array of IDs, even if the field is limited to a single selection.
  4. Consider Performance: Saving elements is a resource-intensive task. If you are importing hundreds of entries, consider wrapping your logic in a transaction or using a background job (Queue).

Frequently Asked Questions

How do I delete a Matrix block programmatically?

To delete a block, you simply omit its ID from the array passed to setFieldValues(). If you are using the Craft 4 sortOrder method, removing an ID from the sortOrder array will result in that block being deleted when the parent element is saved.

Can I save Matrix blocks inside a Super Table field?

Yes, but the nesting becomes deeper. You would apply the same logic: get the Super Table data, find the Matrix field within it, and structure your array to match the nested hierarchy. Most developers find it easier to use serializeValue() when dealing with deeply nested fields.

Why is my Matrix data not saving even though the entry saves?

This usually happens if the block type handle is incorrect or if the field handles inside the fields array don't match the handles defined in the Craft CP. Double-check your handles and ensure the user ID assigned as the author has the correct permissions to save content in that section.

Wrapping Up

Saving Matrix data programmatically in Craft CMS is a powerful skill for any developer. By using the setFieldValues() method and understanding how to structure the data array with temporary IDs like new1, you can automate complex content workflows with ease.

Whether you are building a custom API integration or migrating data from a legacy system, remember to account for the version of Craft you are using. While Craft 3 relies on simple array structures, Craft 4 provides more control via the sortOrder key. Always validate your elements after saving to ensure your data is clean and consistent.