Search code examples
phplaravellaravel-bladelaravel-requestlaravel-resource

Laravel edit shallow nested resource


I'm using the following resource using shallow nesting

Route::resource('organizations.emaildomains', 'OrganizationEmailDomainController', ['except' => ['show']])->shallow();

Got the following table with two records which is the result of an index.blade.php

enter image description here

Let's say we want to edit tiagoperes.eu to stackoverflow.com. I'd go to the edit view

enter image description here

change the respective field and click the save button. This is the result

enter image description here

As you can see, the record isn't updated.

Checking the $request->all() in the update() of the controller

enter image description here

and the form data in the network tab

enter image description here

and the data is posted to

http://localhost/app/public/emaildomains/7

which matches the URI in shallow nesting.


In edit.blade.php I have the following form to handle the updates

<form method="post" action="{{ route('emaildomains.update', ['emaildomain' => $email_domain->id]) }}" autocomplete="off">
    @csrf
    @method('put')

    <h6 class="heading-small text-muted mb-4">{{ __('Email Domain information') }}</h6>
    <div class="pl-lg-4">
        <div class="form-group{{ $errors->has('organization_id') ? ' has-danger' : '' }}">
            <label class="form-control-label" for="input-organization_id">{{ __('Organization') }}</label>
            <select name="organization_id" id="input-organization" class="form-control{{ $errors->has('organization_id') ? ' is-invalid' : '' }}" placeholder="{{ __('Organization') }}" required>
                @foreach ($organizations as $organization)
                    <option value="{{ $organization->id }}" {{ $organization->id == old('organization_id', $email_domain->organization->id) ? 'selected' : '' }}>{{ $organization->name }}</option>
                @endforeach
            </select>
            @include('alerts.feedback', ['field' => 'organization_id'])
        </div>

        <div class="form-group{{ $errors->has('email_domain') ? ' has-danger' : '' }}">
            <label class="form-control-label" for="input-email_domain">{{ __('Email Domain') }}</label>
            <input type="text" name="email_domain" id="input-email_domain" class="form-control{{ $errors->has('email_domain') ? ' is-invalid' : '' }}" placeholder="{{ __('Email Domain') }}" value="{{ old('email_domain', $email_domain->email_domain) }}" required autofocus>

            @include('alerts.feedback', ['field' => 'email_domain'])
        </div>

        <div class="text-center">
            <button type="submit" class="btn btn-success mt-4">{{ __('Save') }}</button>
        </div>
    </div>
</form>

and here's both the edit and update methods in the OrganizationEmailDomainController

/**
 * Show the form for editing the specified resource.
 *
 * @param  \App\OrganizationEmailDomain  $email_domain
 * @return \Illuminate\Http\Response
 */
public function edit(Request $request, OrganizationEmailDomain $email_domain, Organization $model)
{
    $path = $request->path();

    $id = (int)explode('/', $path)[1];

    $emailDomain = OrganizationEmailDomain::find($id);

    return view('organizations.emaildomains.edit', ['email_domain' => $emailDomain->load('organization'), 'organizations' => $model::where('id', $emailDomain->organization_id)->get(['id', 'name'])]);        

}

/**
 * Update the specified resource in storage.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \App\OrganizationEmailDomain  $email_domain
 * @return \Illuminate\Http\Response
 */
public function update(Request $request, OrganizationEmailDomain $email_domain)
{

    $email_domain->update($request->all());

    $organization_id = (int)$request->all()['organization_id'];

    return redirect()->route('organizations.emaildomains.index', ['organization' => $organization_id])->withStatus(__("Org's email domain successfully updated."));
}

and this is the model (notice I'm using a table with a different name than the expected by default - protected $table = 'email_domains';)

class OrganizationEmailDomain extends Model
{
    protected $fillable = [
        'email_domain', 'organization_id'
    ];

    protected $table = 'email_domains';

    /**
     * Get the organization
     *
     * @return \Organization
     */
    public function organization()
    {
        return $this->belongsTo(Organization::class);
    }

}

Solution

  • When injecting a model ID to a route or controller action, you will often query the database to retrieve the model that corresponds to that ID. Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a OrganizationEmailDomain's ID, you can inject the entire OrganizationEmailDomain model instance that matches the given ID.

    Laravel automatically resolves Eloquent models defined in routes or controller actions whose type-hinted variable names match a route segment name. For example:

    use App\OrganizationEmailDomain;
    
    Route::put('emaildomains/{emailDomain}', [OrganizationEmailDomainController::class, 'update']);
    

    Then the following should be in your controller

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\OrganizationEmailDomain  $emailDomain
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, OrganizationEmailDomain $emailDomain)
    {
    
        $emailDomain->update($request->all());
    
        return redirect()->route('organizations.emaildomains.index', [
                'organization' => $request->organization_id
        ])->withStatus(__("Org's email domain successfully updated."));
    }
    

    Note that if the variable name $emailDomain is different from the route segment {emailDomain}, then laravel will not be able to resolve the model. Therefore you will get an empty OrganizationEmailDomain model and it will not update any data. So make sure to put the same name defined in the route.

    To check which is the correct name of the route run the command php artisan route:list and you will see the route and the name of the segment.


    Edit

    In order to solve the problem, I ran

    php artisan route:list
    

    which showed

    organizations/{organization}/emaildomains/{emaildomain} | organizations.emaildomains.update

    So, changing the OrganizationEmailDomainController.php to

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\OrganizationEmailDomainRequest  $request
     * @param  \App\OrganizationEmailDomain  $emaildomain
     * @return \Illuminate\Http\Response
     */
    public function update(OrganizationEmailDomainRequest $request, OrganizationEmailDomain $emaildomain)
    {
    
        $emaildomain->update($request->all());
    
        return redirect()->route('organizations.emaildomains.index', ['organization' => $request->organization_id])->withStatus(__("Org's email domain successfully updated."));
    }
    

    was enough.

    enter image description here

    Notice that the only needed change was in the controller from $email_domain to $emaildomain yet also removed the unnecessary bit to get the organization_id and used instead the suggsted above through the $request.