1

Following the docs on the offical CraftCMS website i'm not able to get "Native Layout Elements" saved. My field body is always empty. Any idea what i'm missing here?

Relevant Part of Plugin base class

<?php

namespace tkf\craftfirebasemessaging;

use Craft;
use craft\base\Model;
use craft\base\Plugin;
use craft\events\RegisterComponentTypesEvent;
use craft\events\RegisterUrlRulesEvent;
use craft\services\Elements;
use craft\web\UrlManager;
use craft\models\FieldLayout;
use craft\events\DefineFieldLayoutFieldsEvent;
use craft\fieldlayoutelements\TitleField;
use craft\fieldlayoutelements\TextareaField;
use tkf\craftfirebasemessaging\elements\Notification;
use tkf\craftfirebasemessaging\models\Settings;
use tkf\craftfirebasemessaging\services\Messaging;
use tkf\craftfirebasemessaging\services\Tokens;
use yii\base\Event;

class FirebaseMessaging extends Plugin
{
    public string $schemaVersion = '1.0.0';
    public bool $hasCpSettings = true;

    public static function config(): array
    {
        return [
            'components' => ['tokens' => Tokens::class, 'messaging' => Messaging::class],
        ];
    }

    public function init(): void
    {
        parent::init();

        // Defer most setup tasks until Craft is fully initialized
        Craft::$app->onInit(function() {
            $this->attachEventHandlers();
            // ...
        });
    }

    protected function createSettingsModel(): ?Model
    {
        return Craft::createObject(Settings::class);
    }

    protected function settingsHtml(): ?string
    {
        return Craft::$app->view->renderTemplate('firebase-messaging/_settings.twig', [
            'plugin' => $this,
            'settings' => $this->getSettings(),
        ]);
    }

    private function attachEventHandlers(): void
    {
        Event::on(
            UrlManager::class,
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
            function (RegisterUrlRulesEvent $event) {
                $event->rules['api/v1/firebase-token'] = 'firebase-messaging/tokens/index';
            }
        );
        Event::on(
            Elements::class,
            Elements::EVENT_REGISTER_ELEMENT_TYPES,
            function (RegisterComponentTypesEvent $event) {
                $event->types[] = Notification::class;
            }
        );
        Event::on(
            UrlManager::class,
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
            function (RegisterUrlRulesEvent $event) {
                $event->rules['notifications'] = ['template' => 'firebase-messaging/notifications/_index.twig'];
                $event->rules['notifications/<elementId:\\d+>'] = 'elements/edit';
            }
        );

        Event::on(
            FieldLayout::class,
            FieldLayout::EVENT_DEFINE_NATIVE_FIELDS,
            function(DefineFieldLayoutFieldsEvent $event) {
                /** @var FieldLayout $fieldLayout */
                $fieldLayout = $event->sender;

                $event->fields[] = new TextareaField([
                    'attribute' => 'body',
                    'required' => true,
                    'mandatory' => true,
                    'label' => 'Body',
                    'instructions' => 'Enter a description.',
                ]);
            }
        );
    }
}

Relevant Part of my Element Class

<?php

namespace tkf\craftfirebasemessaging\elements;

use Craft;
use craft\base\Element;
use craft\elements\User;
use craft\elements\conditions\ElementConditionInterface;
use craft\elements\db\ElementQueryInterface;
use craft\helpers\UrlHelper;
use craft\helpers\Db;
use craft\web\CpScreenResponseBehavior;
use tkf\craftfirebasemessaging\elements\conditions\NotificationCondition;
use tkf\craftfirebasemessaging\elements\db\NotificationQuery;
use yii\web\Response;
use craft\models\FieldLayout;

/**
 * Notification element type
 */
class Notification extends Element
{
    public static function displayName(): string
    {
        return Craft::t('firebase-messaging', 'Notification');
    }

    public static function lowerDisplayName(): string
    {
        return Craft::t('firebase-messaging', 'notification');
    }

    public static function pluralDisplayName(): string
    {
        return Craft::t('firebase-messaging', 'Notifications');
    }

    public static function pluralLowerDisplayName(): string
    {
        return Craft::t('firebase-messaging', 'notifications');
    }

    public static function refHandle(): ?string
    {
        return 'notification';
    }

    public static function trackChanges(): bool
    {
        return false;
    }

    public static function hasContent(): bool
    {
        return false;
    }

    public static function hasTitles(): bool
    {
        return true;
    }

    public static function hasUris(): bool
    {
        return false;
    }

    public static function isLocalized(): bool
    {
        return false;
    }

    public static function hasStatuses(): bool
    {
        return true;
    }

    public ?string $body = null;

    public function getFieldLayout(): FieldLayout
    {
        return \Craft::$app->getFields()->getLayoutByType(self::class);
    }

    public static function find(): ElementQueryInterface
    {
        return Craft::createObject(NotificationQuery::class, [static::class]);
    }

    public static function createCondition(): ElementConditionInterface
    {
        return Craft::createObject(NotificationCondition::class, [static::class]);
    }

    protected static function defineSources(string $context): array
    {
        return [
            [
                'key' => '*',
                'label' => Craft::t('firebase-messaging', 'All notifications'),
            ],
        ];
    }

    protected static function defineActions(string $source): array
    {
        // List any bulk element actions here
        return [];
    }

    protected static function includeSetStatusAction(): bool
    {
        return true;
    }

    protected static function defineSortOptions(): array
    {
        return [
            'title' => Craft::t('app', 'Title'),
            [
                'label' => Craft::t('app', 'Date Created'),
                'orderBy' => 'elements.dateCreated',
                'attribute' => 'dateCreated',
                'defaultDir' => 'desc',
            ],
            [
                'label' => Craft::t('app', 'Date Updated'),
                'orderBy' => 'elements.dateUpdated',
                'attribute' => 'dateUpdated',
                'defaultDir' => 'desc',
            ],
            [
                'label' => Craft::t('app', 'ID'),
                'orderBy' => 'elements.id',
                'attribute' => 'id',
            ],
        ];
    }

    protected static function defineTableAttributes(): array
    {
        return [
            'id' => ['label' => Craft::t('app', 'ID')],
            'dateCreated' => ['label' => Craft::t('app', 'Date Created')],
            'dateUpdated' => ['label' => Craft::t('app', 'Date Updated')],
        ];
    }

    protected static function defineDefaultTableAttributes(string $source): array
    {
        return [
            'link'
        ];
    }

    protected function defineRules(): array
    {
        return array_merge(parent::defineRules(), [
            // ...
        ]);
    }

    public function getUriFormat(): ?string
    {
        // If notifications should have URLs, define their URI format here
        return null;
    }

    protected function previewTargets(): array
    {
        $previewTargets = [];
        $url = $this->getUrl();
        if ($url) {
            $previewTargets[] = [
                'label' => Craft::t('app', 'Primary {type} page', [
                    'type' => self::lowerDisplayName(),
                ]),
                'url' => $url,
            ];
        }
        return $previewTargets;
    }

    protected function route(): array|string|null
    {
        // Define how notifications should be routed when their URLs are requested
        return [
            'templates/render',
            [
                'template' => 'site/template/path',
                'variables' => ['notification' => $this],
            ]
        ];
    }

    public function canView(User $user): bool
    {
        if (parent::canView($user)) {
            return true;
        }
        // todo: implement user permissions
        return $user->can('viewNotifications');
    }

    public function canSave(User $user): bool
    {
        if (parent::canSave($user)) {
            return true;
        }
        // todo: implement user permissions
        return $user->can('saveNotifications');
    }

    public function canDuplicate(User $user): bool
    {
        if (parent::canDuplicate($user)) {
            return true;
        }
        // todo: implement user permissions
        return $user->can('saveNotifications');
    }

    public function canDelete(User $user): bool
    {
        if (parent::canSave($user)) {
            return true;
        }
        // todo: implement user permissions
        return $user->can('deleteNotifications');
    }

    public function canCreateDrafts(User $user): bool
    {
        return true;
    }

    protected function cpEditUrl(): ?string
    {
        return sprintf('notifications/%s', $this->getCanonicalId());
    }

    public function getPostEditUrl(): ?string
    {
        return UrlHelper::cpUrl('notifications');
    }

    public function prepareEditScreen(Response $response, string $containerId): void
    {
        /** @var Response|CpScreenResponseBehavior $response */
        $response->crumbs([
            [
                'label' => self::pluralDisplayName(),
                'url' => UrlHelper::cpUrl('notifications'),
            ]
        ]);
    }

    public function afterSave(bool $isNew): void
    {
        if (!$this->propagating) {
            Db::upsert('{{%firebase-messaging_notifications}}', [
                'id' => $this->id,
            ], [
                'body' => $this->body,
            ]);
        }

        parent::afterSave($isNew);
    }
}

1 Answer 1

2

I'm not certain what the issue is but a few things that have got me in the past that I can note:

While you can use EVENT_DEFINE_NATIVE_FIELDS to register native fields, if you control the element and are the only one deciding if the native field should exist on the element, you can also just do this in the Element::getFieldLayout() method. Your current code might be adding your field to all field layouts as mandatory. You can check the Event sender to only add it where you want, or just lean on the Element to only act in the context you want.

I believe fields may also need to pass validation before being added to the model, so you might need to add your field to the validation rules. A simple way to do that is to just define your field as safe:

protected function defineRules(): array
{
    $rules = parent::defineRules();

    $rules[] = [['myCustomField'], 'safe'];
 
    return $rules;
}
2
  • Ben is exactly right—Yii will only see "safe" attributes as mass-assignable, which is how Craft passes incoming request params to your model in an element-agnostic way! Sep 26 at 19:14
  • Thanks that did the trick
    – user14370
    Sep 27 at 6:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.