Cruorzy's avatar
Level 14

Looking for a nicer way to crate a morph relation that also depends on a hasMany relation

I find it a bit hard to properly explain this so here we go.

I do have a relation from Accounts to Transactions.

Account Model

class Account extends Model
{
    protected $fillable = [
        'name',
    ];

    /**
     * Get the transactions that belong to the account.
     */
    public function transactions(): HasMany
    {
        return $this->hasMany(\App\Models\Transaction::class);
    }

    /**
     * Get the incoming transactions that the Account belongs to.
     */
    public function incomingTransactions(): MorphMany
    {
        return $this->morphMany(\App\Models\Transaction::class, 'transactionable');
    }
}

Transaction Model

class Transaction extends Model
{
    protected $fillable = [
        'name',
        'price',
        'external_credit',
    ];

    /**
     * Get the account that the Transaction belongs to
     */
    public function account(): BelongsTo
    {
        return $this->belongsTo(\App\Models\Account::class);
    }

    /**
     * Get the transactions that the Transaction morphs to
     */
    public function transactionable(): MorphTo
    {
        return $this->morphTo();
    }
}

Transactions table

Schema::create('transactions', function (Blueprint $table) {
	$table->uuid('id')->primary();
	$table->foreignUuid('account_id')->constrained()->restrictOnDelete();
	$table->uuidMorphs('transactionable');
	$table->string('name');
	$table->integer('price');
	$table->boolean('external_credit');
	$table->timestamps();
});

The Transactions can morph to Account and PiggyBank

PiggyBank Model

class PiggyBank extends Model
{
    protected $fillable = [
        'name',
        'description',
    ];

    /**
     * Get the incoming transactions that the PiggyBank belongs to
     */
    public function incomingTransactions(): MorphMany
    {
        return $this->morphMany(\App\Models\Transaction::class, 'transactionable');
    }
}

My 'issue' is when you just have a morph model you can easly create a new row by calling

$account->incomingTransactions->create(); // Create a morph for the account
$piggyBank->incomingTransactions->create(); // Create a morph for the piggy bank

But now I want to make a transaction on the account and morph it to the selected model. But I'm not sure how to do this quite nicely.

$account->transactions->create(); // Will not fill the morphable fields, somehow need to pass them to the create method

I tried trying to find a way to override the create method, but I am not sure if that is the best way. I can also add those fields to be fillable but also that does not feel too good. Hopefully someone found a decent way to do this.

I can make this work to be clear, just want it to be clean.

0 likes
3 replies
LaryAI's avatar
Level 58

It seems like you want to create a transaction that is associated with an account and also morphs to a specific model (either an Account or a PiggyBank). To achieve this in a clean way, you can use the create method on the transactions relationship and then manually set the morphable attributes.

Here's how you can do it:

// Assuming you have an Account instance and a morphable model instance (Account or PiggyBank)
$account = Account::find($accountId);
$morphableModel = PiggyBank::find($piggyBankId); // or another Account

// Create the transaction for the account
$transaction = $account->transactions()->create([
    'name' => 'Transaction Name',
    'price' => 100,
    'external_credit' => false,
    // You don't need to set 'account_id' as it's automatically set by the `transactions()` relationship
]);

// Now manually set the morphable attributes
$transaction->transactionable()->associate($morphableModel);
$transaction->save();

This way, you're explicitly setting the morphable model after creating the transaction. The associate method is used to set the transactionable_type and transactionable_id fields on the Transaction model.

If you find yourself doing this often, you could encapsulate this logic into a method on the Account model to keep your code DRY:

class Account extends Model
{
    // ... existing methods ...

    /**
     * Create a transaction and associate it with a morphable model.
     */
    public function createTransactionWithMorphableModel(array $transactionData, Model $morphableModel): Transaction
    {
        $transaction = $this->transactions()->create($transactionData);
        $transaction->transactionable()->associate($morphableModel);
        $transaction->save();

        return $transaction;
    }
}

Then you can use this method like so:

$account = Account::find($accountId);
$morphableModel = PiggyBank::find($piggyBankId); // or another Account

$transactionData = [
    'name' => 'Transaction Name',
    'price' => 100,
    'external_credit' => false,
];

$transaction = $account->createTransactionWithMorphableModel($transactionData, $morphableModel);

This approach keeps your controllers or services clean and delegates the responsibility of creating the transaction with the morphable model to the Account model.

Cruorzy's avatar
Level 14

@LaryAI Damn the AI is fast and good.

Was not aware of ->associate($morphableModel) sounds good. But how the AI explains it is when I call

$transaction = $account->transactions()->create([
    'name' => 'Transaction Name',
    'price' => 100,
    'external_credit' => false,
    // You don't need to set 'account_id' as it's automatically set by the `transactions()` relationship
]);

I can after the creation associate it with the transaction.

But the Morphable fields are not nullables, so it seems to me this will throw an error.

Schema::create('transactions', function (Blueprint $table) {
	$table->uuid('id')->primary();
	$table->foreignUuid('account_id')->constrained()->restrictOnDelete();
	$table->uuidMorphs('transactionable');
	$table->string('name');
	$table->integer('price');
	$table->boolean('external_credit');
	$table->timestamps();
});

Please or to participate in this conversation.