framework: (regression) Impossilbe to use JsonResource::collection with paginated query (not eloquent)

  • Laravel Version: 5.8.35
  • PHP Version: 7.2.5
  • Database Driver & Version: Mysq 5.7

Description:

Laravel 5.8.34:

function actionWithoutPaginate() {
    $result = DB::query(...difficult query where is not possible to use eloquent...)->get(); 

    return CustomResource::collection($result);
}

function actionWithPaginate() {
    $result = DB::query(...dificult query where is not possible to use eloquent...)->paginate();

    return CustomResource::collection($result);
}

class CustomResource extends Resource {
    /**
     * Transform the resource into an array.
     *
     * @param \Illuminate\Http\Request
     *
     * @return array
     */
    public function toArray($request) {
        $image = get_object_vars($this->resource);

        if (isset($image['last_changed'])) {
            $service = app()->make(MyService::class);
            $format  = $service->getConnection()->getQueryGrammar()->getDateFormat();
            $carbon  = Carbon::createFromFormat($format, $image['last_changed']);

            $image['last_changed'] = $carbon;
        }

        return $image;
    }
}

Since 5.8.35:

  • actionWithoutPaginate - works fine
  • actionWithPaginate - broken
    [2019-09-09 08:08:55] testing.ERROR: Cannot use object of type stdClass as array {"exception":"[object] (Symfony\\Component\\Debug\\Exception\\FatalThrowableError(code: 0): Cannot use object of type stdClass as array at vendor\\laravel\\framework\\src\\Illuminate\\Http\\Resources\\DelegatesToResource.php:53)
    [stacktrace]
    #0 vendor\\laravel\\framework\\src\\Illuminate\\Support\\Arr.php(148): Illuminate\\Http\\Resources\\Json\\JsonResource->offsetExists('resource')
    #1 vendor\\laravel\\framework\\src\\Illuminate\\Support\\helpers.php(524): Illuminate\\Support\\Arr::exists(Object(Yggdrasil\\Http\\Http\\Resources\\ObjectResource), 'resource')
    #2 vendor\\laravel\\framework\\src\\Illuminate\\Support\\Arr.php(387): data_get(Object(Yggdrasil\\Http\\Http\\Resources\\ObjectResource), Array)
    #3 vendor\\laravel\\framework\\src\\Illuminate\\Support\\Collection.php(1107): Illuminate\\Support\\Arr::pluck(Array, Array, NULL)
    #4 vendor\\laravel\\framework\\src\\Illuminate\\Support\\Traits\\ForwardsCalls.php(23): Illuminate\\Support\\Collection->pluck('resource')
    #5 vendor\\laravel\\framework\\src\\Illuminate\\Pagination\\AbstractPaginator.php(647): Illuminate\\Pagination\\AbstractPaginator->forwardCallTo(Object(Illuminate\\Support\\Collection), 'pluck', Array)
    #6 vendor\\laravel\\framework\\src\\Illuminate\\Http\\Resources\\Json\\PaginatedResourceResponse.php(28): Illuminate\\Pagination\\AbstractPaginator->__call('pluck', Array)
    #7 vendor\\laravel\\framework\\src\\Illuminate\\Support\\helpers.php(1124): Illuminate\\Http\\Resources\\Json\\PaginatedResourceResponse->Illuminate\\Http\\Resources\\Json\\{closure}(Object(Illuminate\\Http\\JsonResponse))
    #8 vendor\\laravel\\framework\\src\\Illuminate\\Http\\Resources\\Json\\PaginatedResourceResponse.php(31): tap(Object(Illuminate\\Http\\JsonResponse), Object(Closure))
    #9 vendor\\laravel\\framework\\src\\Illuminate\\Http\\Resources\\Json\\ResourceCollection.php(71): Illuminate\\Http\\Resources\\Json\\PaginatedResourceResponse->toResponse(Object(Illuminate\\Http\\Request))
    #10 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(733): Illuminate\\Http\\Resources\\Json\\ResourceCollection->toResponse(Object(Illuminate\\Http\\Request))
    #11 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(720): Illuminate\\Routing\\Router::toResponse(Object(Illuminate\\Http\\Request), Object(Yggdrasil\\Http\\Http\\Resources\\ResourceCollection))
    #12 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(680): Illuminate\\Routing\\Router->prepareResponse(Object(Illuminate\\Http\\Request), Object(Yggdrasil\\Http\\Http\\Resources\\ResourceCollection))
    #13 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(30): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #14 app\\Http\\Middleware\\CheckClientCredentials.php(29): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #15 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): App\\Http\\Middleware\\CheckClientCredentials->handle(Object(Illuminate\\Http\\Request), Object(Closure), 'marinexprocurem...')
    #16 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #17 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Middleware\\SubstituteBindings.php(41): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #18 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Routing\\Middleware\\SubstituteBindings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #19 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #20 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Middleware\\ThrottleRequests.php(58): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #21 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Routing\\Middleware\\ThrottleRequests->handle(Object(Illuminate\\Http\\Request), Object(Closure), 600, '1')
    #22 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #23 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(104): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #24 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(682): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
    #25 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(657): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
    #26 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(623): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
    #27 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php(612): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
    #28 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Kernel.php(176): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
    #29 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(30): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
    #30 vendor\\fideloper\\proxy\\src\\TrustProxies.php(57): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #31 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Fideloper\\Proxy\\TrustProxies->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #32 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #33 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest.php(21): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #34 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #35 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #36 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest.php(21): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #37 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #38 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #39 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize.php(27): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #40 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #41 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #42 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode.php(62): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #43 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #44 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #45 vendor\\laravel\\framework\\src\\Illuminate\\Http\\Middleware\\FrameGuard.php(18): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #46 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Illuminate\\Http\\Middleware\\FrameGuard->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #47 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #48 packages\\yggdrasil-core\\src\\Http\\Http\\Middleware\\Cors.php(25): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #49 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Yggdrasil\\Http\\Http\\Middleware\\Cors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #50 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #51 packages\\yggdrasil-core\\src\\Http\\Http\\Middleware\\ForceWantsJson.php(22): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #52 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Yggdrasil\\Http\\Http\\Middleware\\ForceWantsJson->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #53 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #54 vendor\\itsgoingd\\clockwork\\Clockwork\\Support\\Laravel\\ClockworkMiddleware.php(27): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #55 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(163): Clockwork\\Support\\Laravel\\ClockworkMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #56 vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
    #57 vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php(104): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
    #58 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Kernel.php(151): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
    #59 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Kernel.php(116): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
    #60 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Testing\\Concerns\\MakesHttpRequests.php(375): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
    #61 vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Testing\\Concerns\\MakesHttpRequests.php(347): Illuminate\\Foundation\\Testing\\TestCase->call('GET', '/api/v1/MarineX...', Array, Array, Array, Array, '[]')
    #62 tests\\Feature\\Api\\v1\\MarineXProcurement\\RecipesControllerTest.php(75): Illuminate\\Foundation\\Testing\\TestCase->json('GET', '/api/v1/MarineX...')
    #63 vendor\\phpunit\\phpunit\\src\\Framework\\TestCase.php(1154): Tests\\Feature\\Api\\v1\\MarineXProcurement\\RecipesControllerTest->testIndex()
    #64 vendor\\phpunit\\phpunit\\src\\Framework\\TestCase.php(842): PHPUnit\\Framework\\TestCase->runTest()
    #65 vendor\\phpunit\\phpunit\\src\\Framework\\TestResult.php(693): PHPUnit\\Framework\\TestCase->runBare()
    #66 vendor\\phpunit\\phpunit\\src\\Framework\\TestCase.php(796): PHPUnit\\Framework\\TestResult->run(Object(Tests\\Feature\\Api\\v1\\MarineXProcurement\\RecipesControllerTest))
    #67 vendor\\phpunit\\phpunit\\src\\Framework\\TestSuite.php(746): PHPUnit\\Framework\\TestCase->run(Object(PHPUnit\\Framework\\TestResult))
    #68 vendor\\phpunit\\phpunit\\src\\TextUI\\TestRunner.php(652): PHPUnit\\Framework\\TestSuite->run(Object(PHPUnit\\Framework\\TestResult))
    #69 vendor\\phpunit\\phpunit\\src\\TextUI\\Command.php(206): PHPUnit\\TextUI\\TestRunner->doRun(Object(PHPUnit\\Framework\\TestSuite), Array, true)
    #70 vendor\\phpunit\\phpunit\\src\\TextUI\\Command.php(162): PHPUnit\\TextUI\\Command->run(Array, true)
    #71 vendor\\phpunit\\phpunit\\phpunit(61): PHPUnit\\TextUI\\Command::main()
    #72 {main}
    "} 
    

Related to #29858, #29860

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 3
  • Comments: 20 (13 by maintainers)

Commits related to this issue

Most upvoted comments

I can confirm this issue is still present.

PHP 7.2 Mysql 5.7 Laravel 6.1.0

return SearchUserResource::collection(
    DB::table(DB::raw("weird query with unions"))->paginate(10)
);

We ended up adding this lines to our Resource

/**
 * This method is necessary just to preserve backward compatibility with non-Eloquent resources.
 * The objects inside a JsonResource now must implement ArrayAccess. This means you cannot create a Resource
 * of a Collection of plain objects like we're doing here with a RAW Query.
 * 
 * We're applying the changes suggested by Graham Campbell in #29860.
 * 
 * More information 
 *      https://github.com/laravel/framework/issues/29916
 *      https://github.com/laravel/framework/issues/29858
 *      https://github.com/laravel/framework/pull/29860 
 */
public function offsetExists($offset) 
{
    return (is_array($offset) || $offset instanceof ArrayAccess) && isset($this->resource[$offset]);
}

Any workaround?

For php 7.2/7.3 you can just copy offsetExists from v5.8.34:

class CustomResource extends Resource {
    public function toArray(...) {}

    public function offsetExists($offset) {
        return array_key_exists($offset, $this->resource);
    }
}

Why only PHP 7.2 & 7.3 though?

Seems array_key_exists deprecated for objects in 7.4 (but I’m on 7.2 now and not 100% sure…)

@Nilanth See the comment directly above yours. Nothing to fix but could be discussed in the ideas repo.

@Lloople we didn’t add an notice to the upgrade guide because there weren’t breaking changes.

If they were compatible with non-eloquent objects before, why removing that compatibility?

We had to make some adjustments for PHP 7.4 (see https://github.com/laravel/framework/pull/29842). Unfortunately that removed the accidental support for the query builder.

Btw, nothing says that we can’t look into making it compatible again but that’s a discussion for the ideas repo.

@LastDragon-ru I just realized this can’t possible work because resources are eloquent specific. We don’t support using it with the query builder. Because stdClass doesn’t implements array access it doesn’t work like that. See https://laravel.com/docs/6.x/eloquent-resources

@stsepelin @Lloople Please see the above note.

This works for me on a fresh Laravel 5.8 installation:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class CustomResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return parent::toArray($request);
    }
}
Route::get('/', function () {
    $result = \DB::table('users')->paginate();

    return \App\Http\Resources\CustomResource::collection($result);
});

Since this has been fixed as well in Laravel 6.0 I want to ask you all to upgrade please. Thanks.