Search code examples
phplaraveleloquent

Laravel 11 api resource PUT request No query results for model


I am working on a API project which uses Laravel 11. As mentioned when I send a PUT request on a certain endpoints I am getting the error:

"No query results for model"

Below you can find routes in the api.php code:

Route::prefix('area')->middleware('client')->group(function () {

        Route::delete('country', [CountryController::class, 'destroyMultiple']);
        Route::delete('city', [CityController::class, 'destroyMultiple']);
        Route::delete('district', [DistrictController::class, 'destroyMultiple']);
        Route::delete('neighborhood', [NeighborhoodController::class, 'destroyMultiple']);

        Route::apiResource('country', CountryController::class);
        Route::apiResource('city', CityController::class);
        Route::apiResource('district', DistrictController::class);
        Route::apiResource('neighborhood', NeighborhoodController::class);
    });

I get this error for every PUT route except CountryController PUT route. Firstly this is a working endpoint in CountryController:

public function update(UpdateCountryRequest $request, Country $country)
    {
        DB::transaction(function () use ($request, $country) {
            $country->update($request->only(['country_abbreviation', 'country_code', 'logo', 'status']));

            $delete = [];

            foreach ($request->languages as $language) {
                $countryName = $country->CountryNames()->updateOrCreate([
                    'lang_id' => $language['lang_id'],
                    'country_id' => $country->id,
                ], $language);

                $delete[] = $countryName->id;
            }

            CountryLanguage::where('country_id', $country->id)->whereNotIn('id', $delete)->delete();
        });

        return response()->json([
            'message' => 'Country updated successfully',
            'status' => 'success',
        ]);
    }

And also the UpdateCountryRequest file as below:

public function authorize(): bool
{
    if ($this->user()->isAdmin) {
        return true;
    }

    return false;
}


public function rules(): array
{
    $country = $this->route('country');

    return [
        'country_abbreviation' => ['required', 'string', Rule::unique('countries')->ignore($country->id)], // keeping the same country_abbreviation seperately
        'country_code' => ['required', 'string', 'max:5', Rule::unique('countries')->ignore($country->id)],
        'logo' => ['required', 'string', 'max:255'],
        'status' => ['required', 'boolean'],
        'languages' => ['required', 'array'],
        'languages.*.lang_id' => ['required', 'integer', 'exists:languages,id'],
        'languages.*.country_name' => ['required', 'string'],
    ];
}

And Postman PUT Request:

enter image description here

But for other controllers when I send PUT request I get the mentioned error:

"No query results for model"

For example let me give you the same codes for DistrictController:

Update metot inside controller file:

 public function update(UpdateDistrictRequest $request, District $district)
    {
        DB::transaction(function () use ($request, $district) {
            $district->update($request->validated());
        });

        return response()->json(['message' => 'District updated successfully'], 200);
    }

UpdateDistrictRequest code:

public function authorize(): bool
{
    if ($this->user()->isAdmin) {
        return true;
    }

    return false;
}


public function rules(): array
{
    return [
        'service_id' => 'required|integer',
        'city_id' => 'required|integer|exists:cities,id',
        'name' => 'required|string|max:255',
        'status' => 'required|boolean',
    ];
}

And also the Postman PUT request:

enter image description here

I have checked if district exist with given id and it exist on database.

enter image description here

Below you can find migrations:

Country:

Schema::create('countries', function (Blueprint $table) {
            $table->id();
            $table->string('country_abbreviation');
            $table->string('country_code');
            $table->string('logo');
            $table->tinyInteger('status');
            $table->timestamps();
        });

District:

Schema::create('districts', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('service_id')->nullable();
            $table->unsignedBigInteger('city_id');
            $table->foreign('city_id')->on('cities')->references('id')->onDelete('cascade');
            $table->string('name');
            $table->boolean('status')->default(1);
            $table->timestamps();
        });

And also models:

Country:

use HasFactory;

    protected $guarded = [];

    protected $fillable = [
        'country_abbreviation',
        'country_code',
        'logo',
        'status'
    ];

// ... Relationship methods

District:

protected $guarded = [];

    protected $fillable = [
        'service_id',
        'city_id',
        'name',
        'status'
    ];

// ... Relationship methods

When I run "route:list" method with artisan I can see the all put endpoints listed properly.

I have tried changing route definitions inside api.php. For example moving DistrictCotroller line to the top of it but it stills gives same error at the same time when I do that CountryController request still works properly.

The exception is throwned by this method inside src > Illuminate > Routing > ImlicitRouteBinding.php

public static function resolveForRoute($container, $route)
{
    $parameters = $route->parameters();

    $route = static::resolveBackedEnumsForRoute($route, $parameters);

    foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {


        if (!$parameterName = static::getParameterName($parameter->getName(), $parameters)) {
            continue;
        }

        $parameterValue = $parameters[$parameterName];

        if ($parameterValue instanceof UrlRoutable) {
            continue;
        }

        $instance = $container->make(Reflector::getParameterClassName($parameter));

        $parent = $route->parentOfParameter($parameterName);

        $routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
            ? 'resolveSoftDeletableRouteBinding'
            : 'resolveRouteBinding';

        if (
            $parent instanceof UrlRoutable &&
            !$route->preventsScopedBindings() &&
            ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))
        ) {
            $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
                ? 'resolveSoftDeletableChildRouteBinding'
                : 'resolveChildRouteBinding';

            if (
                !$model = $parent->{$childRouteBindingMethod}(
                    $parameterName,
                    $parameterValue,
                    $route->bindingFieldFor($parameterName)
                )
            ) {
                throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
            }
        } elseif (!$model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
            // EXCEPTION
            throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
        }

        $route->setParameter($parameterName, $model);
    }
}

I changed update methods in both CountryController and DistrictController as below and noticed that on DistrictController, querying database is just not working.

// CountryController update
public function update(UpdateCountryRequest $request, int $id)
    {
        dd($id, District::find($id)); // 1, District {#1444 ...}

// DistrictController update
public function update(UpdateDistrictRequest $request, int $id)
    {
        dd($id, District::find($id)); // 1, null

I double checked the namespaces in both controllers and they are identical.

enter image description here

It seems problem caused by Global Scopes which I've overlooked. On our models we have boot functions as below for registering Global Scopes to service container I believe :

// District Model
protected static function boot()
{
    parent::boot();

    static::addGlobalScope(new FilterBy('App\Services\V1\Area\DistrictFilters', request()->all()));
}

Also below you can find FilterBy scope file codes :

class FilterBy implements Scope
{
    protected $namespace;
    protected $filters;

    public function __construct($namespace, $filters)
    {
        $this->namespace = $namespace;
        $this->filters = $filters;
    }
    /**
     * Apply the scope to a given Eloquent query builder.
     */
    public function apply(Builder $builder, Model $model): void
    {
        $filter = new FilterBuilder($builder, $this->filters, $this->namespace);
        $builder = $filter->apply();
    }
}

And also in FilterBuilder class we are applying filters as below :

public function apply()
    {
        foreach ($this->filters as $name => $value) {
            $normailizedName = ucfirst($name);
            $class = $this->namespace . "\\{$normailizedName}";

            if (! class_exists($class)) {
                continue;
            }

            if (strlen($value)) {
                (new $class($this->query))->handle($value);
            } else {
                (new $class($this->query))->handle();
            }
        }

        return $this->query;
    }

And finally we have Name.php and CityId.php files inside the given App\Services\V1\Area\DistrictFilters namespace. They have handle methods inside them like below :

public function handle($value = ""): void
    {
        // Name.php
        $this->query->where('name', 'ilike','%'. $value .'%');
    }

So If I understand it correctly, when I send PUT request to let's say DistrictController for example, because of these QueryFilters unless I send the 'Name' field as it's saved in database, it just can't find the model.

For example I created a Distrcit record which has an Id with 974 as below :

enter image description here

If I send a PUT request as below with same 'Name', it manages to find the Model and updates it :

{
    "service_id" : 1,
    "city_id" : 1,
    "name" : "District",
    "status" : 1
}

But If i send the same request with updated name something like 'District 123' it can't find Model.

So I updated the apply function inside FilterBy as below to prevent adding QueryFilters and this seems solved the problem :

public function apply(Builder $builder, Model $model): void
    {
        if (collect(['PUT', 'PATCH'])->contains(request()->method())) {
            return;
        }
        
        $filter = new FilterBuilder($builder, $this->filters, $this->namespace);
        $builder = $filter->apply();
    }

It would be faster to find the problem if I have shared the whole District Model code. It's my bad. Thank you everyone for trying to help.


Solution

  • We encountered an error like this in previous projects. If you are using Scope, these scopes may affect the put and patch select queries.

    To see a preview of the queries;

    //DistrictController
    
    public function update(UpdateDistrictRequest $request, int $id)
    {
        District::dd();
    

    If the problem is caused by scopes, you can skip scopes for put and patch request methods.