earmsby's avatar

Importer for relationship with pivot value

I'm working on a project that has a model for Work:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Work extends Model
{
    protected $fillable = [
        'work_number',
        'title',
        'year_composed',
        'commit_status',
        ];

    public function authors(): BelongsToMany
    {
        return $this->belongsToMany(Account::class, 'account_work_authors')->withPivot('author_role');
    }
}

And another model for Account:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Account extends Model
{
    protected $fillable = [
        'account_number',
        'name_title',
        'first_name',
        'middle_name',
        [more contact info fields...]
    ];

    public function contracts(): BelongsToMany
    {
        return $this->belongsToMany(Contract::class);
    }

    public function authoredWorks(): BelongsToMany
    {
        return $this->belongsToMany(Work::class, 'account_work_authors')->withPivot('author_role');
    }
}

I want to create an importer that would accept a csv like this:

Work ID, Account ID, Author Role
W-47EZC18X, A-TPWQEW1Q, Composer
W-R53MSVXI, A-8VHMAJ6I, Arranger
etc.

I would want the importer to look up the Work from the Work ID and attach the Account (finding it by the ID) with the pivot of author_role.

I've worked with other Filament importers but they generally just used the standard resolveRecord function to write the data. I have used the ->relationship(resolveUsing: 'account_number') functionality to import relations that don't have a pivot field.

My scenario above seems a bit more complicated and I'm just not quite sure how to get started on it. Appreciate any suggestions.

0 likes
5 replies
LaryAI's avatar
Level 58

Certainly! Here's how you can approach importing relationships with additional pivot values in Laravel, specifically for your Work ↔ Account many-to-many with author_role in the pivot.

Steps:

  1. Read the CSV and parse it row by row.
  2. Find the Work using the work_number (e.g., W-47EZC18X).
  3. Find the Account using the account_number (e.g., A-TPWQEW1Q).
  4. Attach the Account to the Work with the author_role as pivot value.

Below is an example method you might put in a custom import command or job:

use App\Models\Work;
use App\Models\Account;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

// assuming $data is an array of rows from your csv, each with 'work_id', 'account_id', 'author_role'
// If you're using something like Laravel Excel, this will come naturally

foreach ($data as $row) {
    // Find Work by work_number
    $work = Work::where('work_number', $row['Work ID'])->first();
    if (!$work) {
        Log::warning("Work not found: " . $row['Work ID']);
        continue;
    }

    // Find Account by account_number
    $account = Account::where('account_number', $row['Account ID'])->first();
    if (!$account) {
        Log::warning("Account not found: " . $row['Account ID']);
        continue;
    }

    // Attach with pivot (prevents duplicates by default if entries are unique)
    $work->authors()->syncWithoutDetaching([
        $account->id => ['author_role' => $row['Author Role']]
    ]);
}

Key Points:

  • syncWithoutDetaching ensures you don’t overwrite existing authors for the work, and it also updates the pivot value if the combination already exists.
  • Replace direct array access ($row['Work ID']) with trimmed/lowercased indices if your CSV headings vary.
  • Logging is optional but recommended for debugging missing records.

If using Filament Importers

If you are customizing a Filament resource importer, you’d typically override the logic in your importer:

public static function resolveRecord(array $row): ?Model
{
    $work = Work::where('work_number', $row['Work ID'])->first();
    $account = Account::where('account_number', $row['Account ID'])->first();

    if ($work && $account) {
        $work->authors()->syncWithoutDetaching([
            $account->id => ['author_role' => $row['Author Role']]
        ]);
    }

    // You can choose to return $work or null, depending on Filament's expectations
    return $work;
}

Summary

  1. Find both Work and Account by their human-readable keys.
  2. Attach the account to the work with the pivot value, using syncWithoutDetaching.
  3. For Filament or other importer systems, override the appropriate record/relationship resolution method.

You do NOT need to manually insert into the pivot table; Eloquent relationships handle this.

Let me know if you need a more concrete example for your specific importer tool or more context on the CSV parsing!

imrandevbd's avatar

Yeah, LaryAI completely missed the mark on how Filament v3's Importer architecture actually works. You shouldn't be hijacking resolveRecord to handle relationship logic, nor should you return a Work model if this specific CSV row represents a pivot link rather than a new Work record.

Since you're using Filament's native Import action, you have two clean ways to handle this depending on how you want to structure it.

Create a dedicated "Author Import" instead

If your CSV is strictly mapping relationships, it’s much cleaner to map the importer to the pivot table itself, or treat the assignment as the primary record.

However, since account_work_authors is a pivot, you can create a pivot model or just use an ImportColumn hook. The cleanest way inside a Filament Importer class is to use the after() hook on a column or the life-cycle hooks of the importer.

earmsby's avatar

Does it matter that I'm not using Filament v3 but rather v.4?

earmsby's avatar

Thanks for your input. I'm a little confused, however.

You mentioned 2 clean ways to handle this. #1 is presumably "Create a dedicated Author import" What is #2?

"it’s much cleaner to map the importer to the pivot table" -> how do I do that?

"just use an ImportColumn hook" -> how do I do that?

"The cleanest way inside a Filament Importer class is to use the after() hook on a column or the life-cycle hooks of the importer." -> how do I use the "after() hook on a column"?

earmsby's avatar

Well, Larry AI's solution worked ok with a few changes. Here's what I used:

The main tweak was not referencing $row but referencing $this->data instead. I could probably return NULL instead of $work. I may try that or leave it as is.

Please or to participate in this conversation.