core icon indicating copy to clipboard operation
core copied to clipboard

Sub resources are lazy loaded (this throws a Laravel error)

Open Barbapapazes opened this issue 9 months ago • 5 comments

API Platform version(s) affected: 4.1

Description

In a Laravel application, if the strict mode (https://laravel-news.com/shouldbestrict) is applied to models, the collection endpoints throws the following error:

Attempted to lazy load [...] on model [App\\Models\\...] but lazy loading is disabled.

How to reproduce

Create a Laravel application, add a model, a second model with a foreign key and add Model::shouldBeStrict() within the boot method of the service provider.

Possible Solution

Trying to not lazy load the sub resources?

Additional Context

Barbapapazes avatar May 22 '25 08:05 Barbapapazes

I'm not sure how we could not lazy load, do you have a stack trace?

soyuka avatar May 22 '25 08:05 soyuka

Hey,

The stack trace:

[2025-05-22 08:08:40] local.ERROR: Attempted to lazy load [executables] on model [App\Models\Package] but lazy loading is disabled. {"exception":"[object] (Illuminate\\Database\\LazyLoadingViolationException(code: 0): Attempted to lazy load [executables] on model [App\\Models\\Package] but lazy loading is disabled. at /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:595)
[stacktrace]
#0 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php(554): Illuminate\\Database\\Eloquent\\Model->handleLazyLoadingViolation()
#1 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php(485): Illuminate\\Database\\Eloquent\\Model->getRelationValue()
#2 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(2260): Illuminate\\Database\\Eloquent\\Model->getAttribute()
#3 /home/esteban/dev/p/maiamix/vendor/api-platform/laravel/Eloquent/PropertyAccess/PropertyAccessor.php(54): Illuminate\\Database\\Eloquent\\Model->__get()
#4 /home/esteban/dev/p/maiamix/vendor/api-platform/serializer/AbstractItemNormalizer.php(688): ApiPlatform\\Laravel\\Eloquent\\PropertyAccess\\PropertyAccessor->getValue()
#5 /home/esteban/dev/p/maiamix/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(197): ApiPlatform\\Serializer\\AbstractItemNormalizer->getAttributeValue()
#6 /home/esteban/dev/p/maiamix/vendor/api-platform/serializer/AbstractItemNormalizer.php(155): Symfony\\Component\\Serializer\\Normalizer\\AbstractObjectNormalizer->normalize()
#7 /home/esteban/dev/p/maiamix/vendor/api-platform/jsonld/Serializer/ItemNormalizer.php(134): ApiPlatform\\Serializer\\AbstractItemNormalizer->normalize()
#8 /home/esteban/dev/p/maiamix/vendor/symfony/serializer/Serializer.php(152): ApiPlatform\\JsonLd\\Serializer\\ItemNormalizer->normalize()
#9 /home/esteban/dev/p/maiamix/vendor/api-platform/hydra/Serializer/CollectionNormalizer.php(92): Symfony\\Component\\Serializer\\Serializer->normalize()
#10 /home/esteban/dev/p/maiamix/vendor/api-platform/serializer/AbstractCollectionNormalizer.php(94): ApiPlatform\\Hydra\\Serializer\\CollectionNormalizer->getItemsData()
#11 /home/esteban/dev/p/maiamix/vendor/api-platform/hydra/Serializer/CollectionFiltersNormalizer.php(76): ApiPlatform\\Serializer\\AbstractCollectionNormalizer->normalize()
#12 /home/esteban/dev/p/maiamix/vendor/api-platform/hydra/Serializer/PartialCollectionViewNormalizer.php(53): ApiPlatform\\Hydra\\Serializer\\CollectionFiltersNormalizer->normalize()
#13 /home/esteban/dev/p/maiamix/vendor/symfony/serializer/Serializer.php(152): ApiPlatform\\Hydra\\Serializer\\PartialCollectionViewNormalizer->normalize()
#14 /home/esteban/dev/p/maiamix/vendor/symfony/serializer/Serializer.php(131): Symfony\\Component\\Serializer\\Serializer->normalize()
#15 /home/esteban/dev/p/maiamix/vendor/api-platform/state/Processor/SerializeProcessor.php(74): Symfony\\Component\\Serializer\\Serializer->serialize()
#16 /home/esteban/dev/p/maiamix/vendor/api-platform/state/Processor/WriteProcessor.php(51): ApiPlatform\\State\\Processor\\SerializeProcessor->process()
#17 /home/esteban/dev/p/maiamix/vendor/api-platform/hydra/State/HydraLinkProcessor.php(58): ApiPlatform\\State\\Processor\\WriteProcessor->process()
#18 /home/esteban/dev/p/maiamix/vendor/api-platform/laravel/Controller/ApiPlatformController.php(96): ApiPlatform\\Hydra\\State\\HydraLinkProcessor->process()
#19 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): ApiPlatform\\Laravel\\Controller\\ApiPlatformController->__invoke()
#20 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(44): Illuminate\\Routing\\Controller->callAction()
#21 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Route.php(266): Illuminate\\Routing\\ControllerDispatcher->dispatch()
#22 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Route.php(212): Illuminate\\Routing\\Route->runController()
#23 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Router.php(808): Illuminate\\Routing\\Route->run()
#24 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(170): Illuminate\\Routing\\Router->{closure:Illuminate\\Routing\\Router::runRouteWithinStack():807}()
#25 /home/esteban/dev/p/maiamix/vendor/api-platform/laravel/ApiPlatformMiddleware.php(47): Illuminate\\Pipeline\\Pipeline->{closure:Illuminate\\Pipeline\\Pipeline::prepareDestination():168}()
#26 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): ApiPlatform\\Laravel\\ApiPlatformMiddleware->handle()
#27 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(127): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#28 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Router.php(807): Illuminate\\Pipeline\\Pipeline->then()
#29 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Router.php(786): Illuminate\\Routing\\Router->runRouteWithinStack()
#30 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Router.php(750): Illuminate\\Routing\\Router->runRoute()
#31 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Routing/Router.php(739): Illuminate\\Routing\\Router->dispatchToRoute()
#32 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(201): Illuminate\\Routing\\Router->dispatch()
#33 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(170): Illuminate\\Foundation\\Http\\Kernel->{closure:Illuminate\\Foundation\\Http\\Kernel::dispatchToRouter():198}()
#34 /home/esteban/dev/p/maiamix/vendor/livewire/livewire/src/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php(19): Illuminate\\Pipeline\\Pipeline->{closure:Illuminate\\Pipeline\\Pipeline::prepareDestination():168}()
#35 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Livewire\\Features\\SupportDisablingBackButtonCache\\DisableBackButtonCacheMiddleware->handle()
#36 /home/esteban/dev/p/maiamix/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(66): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#37 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle()
#38 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#39 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php(31): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle()
#40 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull->handle()
#41 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#42 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle()
#43 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle()
#44 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePostSize.php(27): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#45 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Http\\Middleware\\ValidatePostSize->handle()
#46 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(110): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#47 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle()
#48 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(62): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#49 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Http\\Middleware\\HandleCors->handle()
#50 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(58): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#51 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Http\\Middleware\\TrustProxies->handle()
#52 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#53 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(209): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle()
#54 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(127): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():184}:185}()
#55 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(176): Illuminate\\Pipeline\\Pipeline->then()
#56 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(145): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter()
#57 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1220): Illuminate\\Foundation\\Http\\Kernel->handle()
#58 /home/esteban/dev/p/maiamix/public/index.php(19): Illuminate\\Foundation\\Application->handleRequest()
#59 /home/esteban/dev/p/maiamix/vendor/laravel/framework/src/Illuminate/Foundation/resources/server.php(23): require_once('...')
#60 {main}
"} 

Barbapapazes avatar May 22 '25 08:05 Barbapapazes

I see but I'm not sure how we can do otherwise, if you set the correct groups the property will not be read:

https://github.com/api-platform/core/blob/79edced67ccca1a7b80455dd94203501d9c4fa89/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php#L53-L55

soyuka avatar May 22 '25 08:05 soyuka

What do you mean by setting the correct group? I don't understand.

Barbapapazes avatar May 22 '25 09:05 Barbapapazes

By default all the properties of your Eloquent model will be serialized, therefore we try to access their value. To pick a set of values there are a few solutions:

#[ApiResource(normalizationContext: ['groups' => ['book:read']])]
#[ApiProperty(serialize: new Groups(['book:read']), property: 'title')]
#[ApiProperty(serialize: new Groups(['book:read']), property: 'description')]
#[ApiProperty(serialize: new Groups(['book:read']), property: 'author')]

Or set attributes

#[ApiResource(normalizationContext: ['attributes' => ['title', 'description', 'author']])]

I'd like to understand your full use case though as I'd like for this mode to work it's better for performances.

soyuka avatar May 22 '25 10:05 soyuka

@Barbapapazes with 4.1.17, we have updated the way Link queries are formed - they now use Eloquent's relations natively under the hood. This means that you can just define the $with = ['relation'] on the model now, or enable automatic eager loading in L12, and you should be good to go.

jonerickson avatar Jun 20 '25 17:06 jonerickson

thanks @jonerickson

soyuka avatar Jun 21 '25 07:06 soyuka