Skip to content

Preview breaks for modules with BelongsTo browser relationships #2799

@baeroe

Description

@baeroe

Description

When using a browser field with a BelongsTo relationship (e.g., max(1)), the preview functionality breaks because hydrateBrowser() in HandleRevisions trait always sets the relationship as a Collection, even for BelongsTo relationships that expect a single model.

Steps to Reproduce

  1. Create a module with a BelongsTo relationship (e.g., Blog belongs to Category)
  2. Configure the browser in the repository:
    protected $browsers = ['category'];
  3. Define the relationship in the model:
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }
  4. In the frontend view, access the relationship:
    $category = $item->category;
    echo $category->title; // Works normally, breaks in preview
  5. Try to preview the item in Twill admin

Expected Behavior

Preview should work the same as the normal frontend view - $item->category should return a single Category model (or null).

Actual Behavior

Preview throws an error like:

Property [title] does not exist on this collection instance.

Because $item->category returns a Collection instead of a single model during preview hydration.

Root Cause

In HandleRevisions::hydrateBrowser(), the method unconditionally calls hydrateOrderedBelongsToMany(), which always sets the relationship as a Collection:

public function hydrateBrowser(...): void {
    $this->hydrateOrderedBelongsToMany($object, $fields, $relationship, $positionAttribute, $model);
}

However, in HandleBrowsers::updateBrowser() (used for saving), there's a proper check for BelongsTo:

if ($object->$relationship() instanceof BelongsTo) {
    // Handles single item correctly
    $foreignKey = $object->$relationship()->getForeignKeyName();
    $id = Arr::get($relatedElements, '0.id');
    $object->$foreignKey = $id;
    // ...
}

This check is missing in hydrateBrowser().

Proposed Fix

Add the same BelongsTo check to hydrateBrowser():

public function hydrateBrowser(
    TwillModelContract $object,
    array $fields,
    string $relationship,
    string $positionAttribute = 'position',
    null|TwillModelContract|string $model = null
): void {
    $fieldsHasElements = isset($fields['browsers'][$relationship]) && !empty($fields['browsers'][$relationship]);
    $relatedElements = $fieldsHasElements ? $fields['browsers'][$relationship] : [];

    $relationRepository = $this->getModelRepository($relationship, $model);

    // Handle BelongsTo relationships - set single model instead of collection
    if (method_exists($object, $relationship) && $object->$relationship() instanceof BelongsTo) {
        $relatedItem = !empty($relatedElements) ? $relationRepository->getById($relatedElements[0]['id']) : null;
        $object->setRelation($relationship, $relatedItem);
        return;
    }

    // Existing logic for BelongsToMany relationships
    $this->hydrateOrderedBelongsToMany($object, $fields, $relationship, $positionAttribute, $model);
}

Environment

  • Twill version: 3.5.2
  • Laravel version: 11.x
  • PHP version: 8.2

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions