Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[13.x] RFC: Lazy Services - New #[Lazy] Attribute#55645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
olekjs wants to merge2 commits intolaravel:master
base:master
Choose a base branch
Loading
fromolekjs:lazy-services

Conversation

olekjs
Copy link
Contributor

In short

A new attribute #[Lazy] has been introduced, which defers the initialization of a class until it is actually used.

Example

Let’s assume we have a service that creates a connection to Redis.

namespaceApp\Services;useIlluminate\Container\Attributes\Lazy;useIlluminate\Contracts\Redis\Factory;usePredis\Client;class RedisService{privateClient$client;publicfunction__construct(privateFactory$redisFactory)    {$this->client =$this->redisFactory->connection()->client();var_dump('Redis client loaded');// ...    }// ...}

The above service is used in a controller:

namespaceApp\Http\Controllers;useApp\Services\RedisService;useIlluminate\Http\JsonResponse;class ProductControllerextends Controller{publicfunction__construct(privateRedisService$redisService    ) {    }publicfunctionindex():JsonResponse    {$products = Product::query()->take(10)->get();returnresponse()->json(['products' =>$products]);    }publicfunctionshow(int$productId):JsonResponse    {$product =$this->redisService->getProduct($productId);if (null ===$product) {$product = Product::query()->find($productId);        }// …returnresponse()->json(['product' =>$product]);    }}

In short. I have a service that handles the Redis connection. I have a controller that handles two endpoints. The first returns a list of products (Redis is not used). The second returns a product object – Redis is used as a cache.

In the current form, there is no Lazy Services support. Traditionally, I make a request to the product listing endpoint:

image

You can see that even though Redis is not used in the product listing endpoint, Redis is still initialized.

Now let’s add the previously mentioned “#[Lazy]” attribute to the service.

namespaceApp\Services;useIlluminate\Container\Attributes\Lazy;useIlluminate\Contracts\Redis\Factory;usePredis\Client;#[Lazy]class RedisService{privateClient$client;publicfunction__construct(privateFactory$redisFactory)    {$this->client =$this->redisFactory->connection()->client();var_dump('Redis client loaded');// ...    }// ...}

As you can see above. I added the special “#[Lazy]” attribute, which marks the class as Lazy, meaning it will be a Lazy Object in the container until the class is used.

Let’s see what happens now:

image

Send a request to the products endpoint skips loading Redis, why? Because it’s in the container as a Lazy Object. Only using Redis – in this case, fetching a single product – will trigger Redis to load.

Discussion and usage

In 2018 there was a proposal to implement Lazy Service in Laravel. On that occasion, a discussion arose:

As mentioned in the linked discussion above, Lazy Services are already implemented in:

First, a lot has changed since 2018, and second, it was just a proposal. In the meantime, Lazy Objects have been natively supported by PHP since version 8.4.

I decided to create a working POC and possibly start a discussion.

Challenges and future development opportunities

  • Proxies are not easily noticeable to the developer; it's unclear whether it's a regular object or a lazy one – this can only be inferred from behavior or a var_dump,
  • Due to the structure of the class and its dependencies in the container, I haven’t been able to add support for attributes on parameters at this moment:
class ProductController{publicfunction__construct(        #[Lazy]publicRedisService$redisService    ) {    }}class RedisService{publicfunction__construct()    {thrownew \RuntimeException('Lazy call');    }}

Here’s the Proof of Concept, and I’d appreciate your feedback. Feel free to join the discussion about this feature and whether there is a place for it in Laravel.

bojanvmk, nsrosenqvist, makhweeb, lancepioch, HichemTab-tech, danilopinotti, mateusjatenee, sajjadhossainshohag, duydvnganluong, YThomassenAtabix, and 4 more reacted with thumbs up emojimakhweeb, it-can, and johanrosenson reacted with hooray emojimakhweeb and HichemTab-tech reacted with heart emojimakhweeb and IonBazan reacted with rocket emoji
@deleugpn
Copy link
Contributor

If you move the redis connection out of the constructor and only establish a connection when a method that will interact with redis is actually called, then everything would work as you’d expect without the “invisible” downside of having to dig into the underlying class to see the attribute and understand its implications on the Laravel container.

Laravel container is already extremely powerful and factory-driven in the sense that only when a class needs to be instantiated it incur overhead, so I don’t think this is a positive change to the framework overall.

rodrigopedra, shaedrich, axlon, devajmeireles, TomasVotruba, nbreens, and ovp87 reacted with thumbs up emoji

@rodrigopedra
Copy link
Contributor

In your controller example, one could use method injection instead of a constructor injection:

publicfunctionshow(RedisService$redisService,int$productId):JsonResponse{$product =$redisService->getProduct($productId);// …}

Furthermore, I agree with@deleugpn, and can't figure out places where this would bring more value, than what we already get from Laravel's container.

BrandonSurowiec reacted with thumbs up emoji

Comment on lines +1063 to +1065
$instance = $reflector->newLazyProxy(function () use ($concrete, $instances) {
return new $concrete(...$instances);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

You could do just

Suggested change
$instance =$reflector->newLazyProxy(function ()use ($concrete,$instances) {
returnnew$concrete(...$instances);
});
$instance =$reflector->newLazyProxy(fn () =>new$concrete(...$instances));

olekjs reacted with thumbs up emoji
@@ -1058,8 +1059,16 @@ public function build($concrete)

array_pop($this->buildStack);

if (!empty($reflector->getAttributes(Lazy::class))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

You could just directly check this

Suggested change
if (!empty($reflector->getAttributes(Lazy::class))) {
if ($reflector->getAttributes(Lazy::class) !== []) {

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Does this change bring anything beyond syntax? I’m just asking out of curiosity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Not much. Technically, a function call is more expensive than a comparison, but this should mostly be optimized away in practice. Oneslight advantage would be a clear communication that we are working with an array here and don't have to checkany structure's emptiness.

olekjs reacted with thumbs up emoji

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@olekjs empty function is a trap for future bugs.
We have a rule in place to never use empty function because of that.

empty('0') is true for example.

@DarkGhostHunter
Copy link
Contributor

I would agree that Lazy Objects support would be great, especially for services that require a huge set up when instanced.

The main problem I have with Lazy objects is testing. Are these completely compatible with Mockery?

@@ -15,7 +15,7 @@
}
],
"require": {
"php":"^8.3",
"php":"^8.4",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Laravel 13 will depends on PHP 8.3 as minimum.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Alrighty, in that case we can consider version 14.x, but any potential merging should wait until 13.x is released - is that correct? :)

@olekjs
Copy link
ContributorAuthor

@deleugpn Thank you for the feedback :) I partially agree with you. However, I don’t fully understand what you mean by "only when a class needs to be instantiated it incurs overhead" - could you elaborate?

@rodrigopedra Thanks for the feedback as well. It's great that you and@deleugpn provided an alternative approach to the example - it makes sense. However, it's just a simple example. I'm also considering a case where we have a class overloaded with dependencies, such as Carbon.

Let’s be honest - Carbon is a heavy class and sometimes it can even freeze the IDE.

I’m referring to lazy loading for such a class (of course, this is just a quick example):

class ArticleService {publicfunction__construct(publicCarbon$createdAt,publicCarbon$publishedAt,// ...    ) {    }}

@DarkGhostHunter could you explain where the claim that "The main problem I have with Lazy objects is testing" comes from? :D I don’t see any issue with testing - a Lazy Object gets initialized upon interaction and behaves like a regular object. You can see that in the tests in this PR - take a look, there’s an assertInstanceOf on the lazy object. If I misunderstood the question, please clarify or provide an example - I’ll try to test it and share some feedback ;)

@DarkGhostHunter
Copy link
Contributor

@DarkGhostHunter could you explain where the claim that "The main problem I have with Lazy objects is testing" comes from? :D I don’t see any issue with testing - a Lazy Object gets initialized upon interaction and behaves like a regular object. You can see that in the tests in this PR - take a look, there’s an assertInstanceOf on the lazy object. If I misunderstood the question, please clarify or provide an example - I’ll try to test it and share some feedback ;)

Nevermind, I though they weren'ttesteable. Keep up the good work.

olekjs reacted with rocket emoji

@rodrigopedra
Copy link
Contributor

Let’s be honest - Carbon is a heavy class and sometimes it can even freeze the IDE.

Never heard of this problem before, to be honest.

I'm also considering a case where we have a class overloaded with dependencies, such as Carbon. [...] I’m referring to lazy loading for such a class

I get the idea. What I don't get is the benefits compared to what the container already does.

It already runs a factory function only when needed. And you can use method injection, or theapp() helper, to narrow down a dependency's scope.

But maybe I am missing something. Let's wait on the maintainers.

@antonkomarev
Copy link
Contributor

antonkomarev commentedMay 5, 2025
edited
Loading

I don't feel a need for this feature, but maybe it will be better to define it in the constructor of the class where dependency is required (for example in the controller) and not in dependency class? Otherwise you can't define it in third party classes.

daniser reacted with thumbs up emoji

@taylorotwell
Copy link
Member

taylorotwell commentedMay 7, 2025
edited
Loading

Can you elaborate a bit more about why we couldn't support it at the parameter level?

@taylorotwelltaylorotwell marked this pull request as draftMay 10, 2025 18:24
@olekjs
Copy link
ContributorAuthor

Can you elaborate a bit more about why we couldn't support it at the parameter level?

Sorry for the late reply@taylorotwell, but I was busy.

The obstacle is that we don't have access to the parent's attributes at the time of building the class that is supposed to be lazy.

I think the best example would be the code itself. I implemented Lazy handling at the attribute level, BUT in my opinion, there are a few trade-offs in the code that could be blockers – for example, the attribute passing chain.

Unless there's a better way to handle it and I'm just missing something 😄

Please check the latest commit - it contains the implementation I’m talking about:19bdded

@ziadoz
Copy link
Contributor

ziadoz commentedJun 5, 2025
edited
Loading

Just an idea. Would a contextual attribute applied to the parameter work instead? I took the existing#[Give] attribute, but adjusted it so that it returns a lazy ghost:https://gist.github.com/ziadoz/f52653f37ae891b74fbdfc2d536abe06

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@shaedrichshaedrichshaedrich left review comments

@macropay-solutionsmacropay-solutionsmacropay-solutions left review comments

@crynobonecrynobonecrynobone requested changes

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

10 participants
@olekjs@deleugpn@rodrigopedra@DarkGhostHunter@antonkomarev@taylorotwell@ziadoz@crynobone@shaedrich@macropay-solutions

[8]ページ先頭

©2009-2025 Movatter.jp