Skip to content

Commit 2918ca8

Browse files
committed
Element saves, persisting custom attributes
1 parent 45c31b5 commit 2918ca8

File tree

1 file changed

+63
-17
lines changed

1 file changed

+63
-17
lines changed

docs/5.x/extend/element-types.md

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,8 @@ Event::on(
452452

453453
`mandatory` here means that the layout element _must_ be present in the field layout, not that a value is required. Even if a developer has not customized the field layout, Craft will ensure this layout element is added to the first tab. The `TitleField` layout element is mandatory by default, and assumes the title comes from a `title` attribute, so there’s nothing to customize!
454454

455+
Field layout elements that map to native attributes on your element should be [declared as “safe”](guide:structure-models#safe-attributes) so that they can be mass-assigned by the `elements/save` action. If you maintain your own controller, you will need to manually assign each attribute (i.e: `$element->price = Craft::$app->getRequest()->getRequiredBody('price'))`).
456+
455457
Take a look at the existing [field layout element types](repo:craftcms/cms/blob/5.x/src/fieldlayoutelements) to see which makes the most sense for your attribute. Field layout elements that exist only to provide feedback or information should extend <craft5:craft\fieldlayoutelements\BaseUiElement>.
456458

457459
::: tip
@@ -462,7 +464,7 @@ Custom fields are handled for you, automatically—they’ll appear just below y
462464

463465
### Saving Custom Field Values
464466

465-
When saving values to a custom field, you may use the [`setFieldValue()`](craft5:craft\base\ElementInterface::setFieldValue()) and [`setFieldValues()`](craft5:craft\base\ElementInterface::setFieldValues()) methods or assign directly to a property corresponding to its handle.
467+
When saving values to a custom field attached to your element, can use any combination of [`setFieldValue()`](craft5:craft\base\ElementInterface::setFieldValue()), [`setFieldValues()`](craft5:craft\base\ElementInterface::setFieldValues()), and direct assignment to a property corresponding to its instance handle.
466468

467469
::: code
468470
```php Single Value
@@ -489,6 +491,8 @@ $product->setFieldValues([
489491
```
490492
:::
491493

494+
When using Craft’s default `elements/save` controller action, field values are automatically assigned from POST data.
495+
492496
#### Validating Required Custom Fields
493497

494498
Required custom fields are only enforced when the element is saved using the `live` [validation scenario](guide:structure-models#scenarios). Set the scenario before calling `saveElement()`:
@@ -532,12 +536,17 @@ Elements that support multiple sites will have their `afterSave()` method called
532536
All that is required to support [element indexes](../system/elements.md#indexes) is a route that points to a template containing this:
533537

534538
```twig
539+
{# This template is provided by Craft: #}
535540
{% extends '_layouts/elementindex.twig' %}
541+
542+
{# Set a page title: #}
536543
{% set title = 'Products'|t('my-plugin') %}
537-
{% set elementType = 'ns\\prefix\\elements\\Product' %}
544+
545+
{# Let Craft know what element type the index is for: #}
546+
{% set elementType = 'mynamespace\\elements\\Product' %}
538547
```
539548

540-
To create a new element from this page, define an `actionButton` block:
549+
To let authors create new elements from this page, define an `actionButton` block:
541550

542551
```twig
543552
{% block actionButton %}
@@ -555,7 +564,7 @@ To create a new element from this page, define an `actionButton` block:
555564
Some element types may require more information to properly initialize, or will enforce permissions based on initial configuration. Entries, for example, are always created in a particular _section_—and a user may not be [permitted](#permissions) to create them in every section they’re allowed to view or publish in. In these cases, you may need to define extra params in the action URL, or <badge vertical="baseline" type="verb">POST</badge> to your own controller.
556565
:::
557566

558-
Your route should be registered via [`EVENT_REGISTER_CP_URL_RULES`](craft5:craft\web\UrlManager::EVENT_REGISTER_CP_URL_RULES):
567+
Your index’s route should be registered via [`EVENT_REGISTER_CP_URL_RULES`](craft5:craft\web\UrlManager::EVENT_REGISTER_CP_URL_RULES):
559568

560569
```php
561570
use craft\events\RegisterUrlRulesEvent;
@@ -571,6 +580,10 @@ Event::on(
571580
);
572581
```
573582

583+
A controller is not necessary, here—the route maps directly to a template.
584+
585+
At this point, your element index will be empty. You can skip down to the [Editing Elements](#editing-elements) section to learn more about populating and persisting elements!
586+
574587
### Sources
575588

576589
Element sources are sets of criteria that form the basis of how users interact with your index and [relation fields](#relation-field). Default sources can be defined by your element type by implementing the protected static [defineSources()](craft5:craft\base\Element::defineSources()) method:
@@ -948,7 +961,11 @@ Craft makes editing elements frictionless by providing turn-key edit screens as
948961

949962
To give your elements dedicated edit pages, you must define a route that agrees with their `getCpEditUrl()` method. Collocate this rule with the one that defines your [index](#element-index):
950963

951-
```php
964+
```php{5}
965+
// Index:
966+
$event->rules['products'] = ['template' => 'my-plugin/products/_index.twig'];
967+
968+
// Edit:
952969
$event->rules['products/<elementId:\d+>'] = 'elements/edit';
953970
```
954971

@@ -994,7 +1011,7 @@ If you are interested in rendering context-agnostic views for your element (or o
9941011

9951012
### Saving
9961013

997-
If your element does not require any special processing, you may be able to use the generic `elements/save` controller action. Craft will use the <badge vertical="baseline" type="verb">POST</badge> body to bulk-assign and typecast [safe attributes](guide:structure-models#safe-attributes) with <craft5:craft\base\Model::setAttributes()>, populate custom fields with <craft5:craft\base\Element::setFieldValues()>, check permissions, validate, and eventually save the element.
1014+
Most element types can use the generic `elements/save` controller action to persist data. Craft will use the <badge vertical="baseline" type="verb">POST</badge> body to bulk-assign and typecast [safe attributes](guide:structure-models#safe-attributes) with <craft5:craft\base\Model::setAttributes()>, populate custom fields with <craft5:craft\base\Element::setFieldValues()>, check permissions, validate, and eventually save the element.
9981015

9991016
Any time you _do_ require some special handling (or need to do more sophisticated authorization than is encapsulated by the element’s [`canSave()`](#permissions) method), you should implement a custom [controller](./controllers.md) action. To programmatically save an element, you will need to do at least the following:
10001017

@@ -1031,7 +1048,7 @@ return $this->asModelSuccess(
10311048
);
10321049
```
10331050

1034-
The elements service will in turn call your element’s [`beforeSave()` and `afterSave()` methods](#save-hooks), in which you must persist any additional custom properties.
1051+
The elements service will in turn call your element’s [`beforeSave()` and `afterSave()` methods](#save-hooks), in which you must persist any native attributes.
10351052

10361053
::: tip
10371054
This process is discussed in greater depth in the [controllers documentation](./controllers.md#model-lifecycle).
@@ -1052,7 +1069,7 @@ Create Drafts | [`canCreateDraft()`](craft5:craft\base\Element::canCreateDraft()
10521069
::: warning
10531070
The default implementation of these methods in <craft5:craft\base\Element> is typically _restrictive_, meaning users will be _denied_ access by default.
10541071

1055-
Your element should always call the parent method to ensure that events are emitted when a permission check is taking place.
1072+
Your element should always call the parent method to ensure that [events](events.md) are emitted when a permission check is taking place.
10561073
:::
10571074

10581075
If your element would benefit from a user-manageable permissions structure, you must [register each relevant permission](./user-permissions.md) and check them in the corresponding methods—or as part of custom [controller actions](#saving).
@@ -1063,7 +1080,7 @@ If your element would benefit from a user-manageable permissions structure, you
10631080

10641081
You can give your element its own relation field by creating a new [field type](field-types.md) that extends <craft5:craft\fields\BaseRelationField>.
10651082

1066-
That base class does most of the grunt work for you, so you can get your field up and running by implementing three simple methods:
1083+
That base class does most of the grunt work for you, so you can get your field up and running by implementing three methods:
10671084

10681085
```php
10691086
<?php
@@ -1091,11 +1108,13 @@ class Products extends BaseRelationField
10911108
}
10921109
```
10931110

1111+
Be sure and [register your field type](field-types.md#registering-custom-field-types), so that developers can select it!
1112+
10941113
## Eager-Loading
10951114

1096-
If your element type has its own [relation field](#relation-field), it is already eager-loadable through that. Furthermore, if you have declared support for [content](#fields--content), any elements that are selected as relations via other relation fields will be eager-loadable from your element.
1115+
If your element type has its own [relation field](#relation-field), it is already eager-loadable through that. Furthermore, if you have declared support for [content](#fields--content), any elements that are selected as relations via other relational fields will be eager-loadable from your element.
10971116

1098-
The only case where eager-loading support is _not_ provided for free is if your element type has any “hard-coded” relations with other elements. For example, entries have authors (users), but those relations are defined in a dedicated `authorId` column in the `entries` table—not the `relations` table.
1117+
Eager-loading support is _not_ provided automatically for “hard-coded” relations with other elements. For example, entries have authors (user elements), but those relationships are stored in a separate `entries_authors` table.
10991118

11001119
If your elements maintain this kind of relationship to other elements, make them eager-loadable by adding an `eagerLoadingMap()` method to your element class:
11011120

@@ -1137,27 +1156,54 @@ public static function eagerLoadingMap(array $sourceElements, string $handle): a
11371156
}
11381157
```
11391158

1140-
This function takes an array of already-queried elements (the “source” elements) and an eager-loading handle. It returns a map of which _source_ element IDs should eager-load which _target_ element IDs.
1159+
This function takes an array of already-queried elements (the “source” elements) and an eager-loading handle. It returns a map of which _source_ element IDs should eager-load which _target_ element IDs. The structure of that array should look something like this:
1160+
1161+
```php
1162+
[
1163+
'elementType' => User::class,
1164+
'map' => [
1165+
// Source: Product ID; Target: User ID
1166+
['source' => 8735, 'target' => 385],
1167+
['source' => 7319, 'target' => 1388],
1168+
['source' => 6684, 'target' => 139],
1169+
['source' => 6693, 'target' => 139],
1170+
// ...
1171+
]
1172+
]
1173+
```
1174+
1175+
Note that user ID `139` appears twice, for product IDs `6684` and `6693`! That’s to be expected—Craft knows to only load that element _once_, but will make it available on both of those products.
11411176

11421177
::: tip
1143-
You may be able to create the source-target map without another database query, if the target IDs have already been loaded along with the elements!
1178+
You may be able to create the source-target map without another database query, if the target IDs have already been loaded along with the elements! In the example, if products are owned by a single vendor, the vendor ID could be stored an [selected](#element-query-class) such that the map can be built in memory:
1179+
1180+
```php
1181+
$map = array_map(function($src) {
1182+
return [
1183+
'source' => $src->id,
1184+
'target' => $src->vendorId,
1185+
];
1186+
}, $sourceElements);
1187+
```
11441188
:::
11451189

1146-
If you need to override where eager-loaded elements are stored, add a `setEagerLoadedElements()` method to your element class as well:
1190+
To assign eager-loaded elements to a specific attribute (or process the value in some other way), add a `setEagerLoadedElements()` method to your element class:
11471191

11481192
```php
11491193
public function setEagerLoadedElements(string $handle, array $elements): void
11501194
{
11511195
// The handle can be anything, so long as it matches what is used in `eagerLoadingMap()`:
1152-
if ($handle === 'author') {
1153-
$author = $elements[0] ?? null;
1154-
$this->setAuthor($author);
1196+
if ($handle === 'vendor') {
1197+
$vendor = $elements[0] ?? null;
1198+
$this->setVendor($vendor);
11551199
} else {
11561200
parent::setEagerLoadedElements($handle, $elements);
11571201
}
11581202
}
11591203
```
11601204

1205+
Otherwise, Craft stashes the results in a generic `_eagerLoadedElements` array by handle, which you must retrieve later. Be sure and handle each eager-loadable native attribute in this method.
1206+
11611207
## Advanced Topics
11621208

11631209
### Reference Tags

0 commit comments

Comments
 (0)