Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Patch85's avatar

Breeze, Inertia & Vue 3 - Updating a Polymorphic Relationship

I have a Laravel 10 project going where I'm learning to use Inertia and Vue 3. Using the Breeze starter pack, I have created a new Model, Address, and I'm starting by defining the relationship between Users and Addresses, but Addresses will eventually have relationships to other models as well.

A user can have 1 address, a one-to-one polymorphic relationship defined as such:

    public function address(): MorphOne
    {
        return $this->morphOne(Address::class, 'addressable');
    }

And of course, the addressable() relationship is defined on the address as well. An Address record can only belong to one other model, so:

    public function addressable(): MorphTo
    {
        return $this->morphTo();
    }

Given that I'm using Breeze, I decided that I would create an AddressForm vue component that could be used wherever I might need to create or edit an address. I'm including it in js/Pages/Profile/Edit.vue :

<script setup>
import { Head } from '@inertiajs/vue3';
import AddressForm from '@/Components/AddressForm.vue';

...

<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
    <AddressForm class="max-w-xl" :address="$page.props.address" />
</div>

I'm passing the Address model as a prop from ProfileController::edit()

The AddressForm component looks like this:

<script setup>
import { useForm } from '@inertiajs/vue3';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';

let props = defineProps({
    address: {
        type: Object,
    },
});

const form = useForm({
    description: props.address.description,
    attention: props.address.attention,
    street1: props.address.street_1,
    street2: props.address.street_2,
    city: props.address.city,
    state: props.address.state,
    postalCode: props.address.postal_code,
    countryCode: props.address.country_code,
});

const submit = () => {
    form.post(route('address.store'), {
        onFinish: () => form.reset(),
    });
};
</script>

<template>
    <section class="space-y-6">

        <header>
            <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Address</h2>
        </header>

        <form @submit.prevent="submit">
            <div>
                <InputLabel for="description"
                            value="Description" />

                <TextInput id="description"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.description"
                           required />

                <InputError class="mt-2"
                            :message="form.errors.description" />
            </div>

            <div>
                <InputLabel for="attention"
                            value="Attention" />

                <TextInput id="attention"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.attention"
                           required />

                <InputError class="mt-2"
                            :message="form.errors.attention" />
            </div>

            <div>
                <InputLabel for="street1"
                            value="Street Address" />

                <TextInput id="street1"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.street1"
                           required />

                <InputError class="mt-2"
                            :message="form.errors.street1" />
            </div>

            <div>
                <InputLabel for="street2"
                            value="Street Address: Line 2" />

                <TextInput id="street2"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.street2" />

                <InputError class="mt-2"
                            :message="form.errors.street2" />
            </div>

            <div>
                <InputLabel for="city"
                            value="City" />

                <TextInput id="city"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.city"
                           required />

                <InputError class="mt-2"
                            :message="form.errors.city" />
            </div>

            <div>
                <InputLabel for="state"
                            value="State" />

                <TextInput id="state"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.state" />

                <InputError class="mt-2"
                            :message="form.errors.state" />
            </div>

            <div>
                <InputLabel for="postalCode"
                            value="Postal Code" />

                <TextInput id="postalCode"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.postalCode"
                           required />

                <InputError class="mt-2"
                            :message="form.errors.postalCode" />
            </div>

            <div>
                <InputLabel for="countryCode"
                            value="Country Code" />

                <TextInput id="v"
                           type="text"
                           class="block w-full mt-1"
                           v-model="form.countryCode"
                           required />

                <InputError class="mt-2"
                            :message="form.errors.countryCode" />
            </div>

            <div class="flex justify-end mt-4">
                <PrimaryButton class="ml-4"
                               :class="{ 'opacity-25': form.processing }"
                               :disabled="form.processing">
                    Save
                </PrimaryButton>
            </div>
        </form>
    </section>
</template>

So far, so good. My form as the data I've seeded for the authenticated user's address. Now, I want to be able to edit that data and submit the form. I have an AddressController::store method that is a work in progress, but my expectation is that I'll have to be able to pass the address_id to update an existing address, and addressable_type and addressable_id when I'm creating a new address. How can I submit this with my form(s) so that the AddressController methods can do their thing?

0 likes
1 reply
martinbean's avatar

but my expectation is that I'll have to be able to pass the address_id to update an existing address, and addressable_type and addressable_id when I'm creating a new address. How can I submit this with my form(s) so that the AddressController methods can do their thing?

@patch85 I tend to create separate nests resource controllers to handle relationships like this. Yes, it may be some duplication, but I much prefer that duplication than exposing my database structure, or passing IDs as hidden form inputs that can be manipulated.

So, in the case of addresses, if an address can be “owned” by a user then I would create a UserAddressController nested resource controller:

class UserAddressController extends Controller
{
    public function store(StoreAddressRequest $request, User $user)
    {
        $address = $user->addresses()->create($request->validated());

        // Return response...
    }
}

You could use “shallow” resources so that your nested resources have methods to list and store addresses for a user, but once the address is created, it’s then updated/deleted by its own resource controller:

class AddressController extends Controller
{
    public function update(UpdateAddressRequest $request, Address $address)
    {
        $address->update($request->validated());

        // Return response...
    }

    public function destroy(Address $address)
    {
        $address->delete();

        // Return response...
    }
}

You’d then end up with routes like this:

GET     addresses/{address}
GET     addresses/{address}/edit
PUT     addresses/{address}
DELETE  addresses/{address}

GET     users/{user}/addresses
GET     users/{user}/addresses/create
POST    users/{user}/addresses
1 like

Please or to participate in this conversation.