-
Notifications
You must be signed in to change notification settings - Fork 99
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Field middlewares executed only once per application lifecycle #562
Comments
Hmm, good question here. I’m assuming the schema is built and cached on each request and these annotations are used to build it accordingly? Assuming that’s the case, I’d think the schema would need to be rebuilt based on request context. |
Yes, you are correct. Rebuilding the whole Schema from scratch (through Instead of doing so, I think limiting it to just field middlewares should be enough? That way the whole Schema stays cached in place; when a specific field is requested, only it's middlewares are triggered, greatly reducing the amount of processing behind each request. This should work for queries, mutations and fields and satisfy the needs of |
I'm really not sure that the Let's define which annotations are dynamic in nature and of concern. Also, it's worth considering that you could use separate graphql endpoints to define your schemas. I'm not sure that's ideal, but from a performance standpoint, that'd obviously be the best. |
It should be accurate, given only the public APIs / documented features of graphqlite are used. Going through the annotations:
Based on all of this, the only annotations which are non static are
I mentioned performance because the sole goal of projects like Laravel Octane is to reduce boilerplate initialization, not achieve first-class performance. It doesn't seem like much until your production app starts taking 300ms+ to process a simple |
So, Where are you thinking it'd be best to handle this refreshing logic? |
Right.
And that.. I don't know yet. Here are my thoughts so far, but I'll spend more time on that tomorrow:
$fieldDescriptor = new QueryFieldDescriptor();
// do regular field parsing here
$field = new UnresolvedFieldDefinition($fieldDescriptor->getName(), function () use ($fieldDescriptor) {
return $this->fieldMiddleware->process($fieldDescriptor, new class implements FieldHandlerInterface {
public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition
{
return QueryField::fromFieldDescriptor($fieldDescriptor);
}
});
});
$queryList[] = $field; In theory, something like this should work with no changes to webonyx/graphql and great performance. If this works then there are more things to consider, like should |
One more thought: we could simply allow FieldHandlerInterface to return |
@oprypkhantc check out this field middleware which overwrites and wraps the actual field resolver: we use this to ensure code is only executed if the field is being resolved instead of introspection or other things. |
Based on webonyx/graphql-php#1329, this will not work unfortunately. The suggested method is to rebuild the whole Schema :/ It seems like we can rebuild the Schema optimally with some refactors. I need your approval to see if it's both worth my time and your/maintainers time maintaining it afterwards: Instead of calling If you don't feel like this is worth it - no worries. I think I can create a workaround for my use case. |
@Lappihuan This only works to resolve the value. If you want to hide the field or change any of it's details (type, description etc), you can't do that. |
sounds like you would need to rebuild the schema then. |
My main concern here is the burden on this project that comes with supporting this use case. It's not widespread yet, but it's definitely rising in popularity, especially in bigger projects. The decision on whether to support this is on you guys. |
you could probably check if there is a cache implementation for swoole that handles caches per request and invalidates them after. |
otherwise you can also overwrite the middlewares provided to always do the wrapped approach. |
There is a cache implementation. If I invalidate it after each request, it effectively means a Schema is resolved from the ground up on each request, which is not what I want. The point is the exact opposite - reuse as much existing parsed, created instances as possible. Besides, it seems like fields are cached through lazy-init properties on the Unfortunately inverting the behaviour of |
not sure what your exact usecase is but i'd probably try to stay away from on the fly schemas, that kind of defeates the purpose of a schema. if you specifically want to use Logged or HideIfNotAuthorized then maybe just throw a exception or return null if its being resolved by someone lacking the authorization. if your usecase is a internal section of the API then maybe think about splitting your schema into a public and a internal one. |
Agreed, I don't like the idea of modifying it on the fly too. I wanted to use Besides So maybe it's best not to overcomplicate this package for few unpopular use cases like mine, at least for now. I think it's worth adding a section in the documentation though that middlewares are only executed once and not to use |
@oprypkhantc async servers in PHP are certainly a growing trend, and I suspect they'll become fairly standard in coming years. I don't think your use case is unpopular, necessarily. If anything, you're just more of an early adopter. I also believe it's in the best interest of this repository to be supportive. @Lappihuan makes a great point that dynamic schemas kinda defeat the purpose, one of the reasons we don't use You can achieve some of this with different namespacing for operations and types. However, it's often ideal to not separate these out in such a way. I know for us, we'd prefer to have internal mutations sitting next to public ones in the same namespace, because our operations are grouped together based on entities/types mostly. And, some types we'd prefer to not have exposed over a public API, and only over an internal one. We're not moving around our models to accommodate this concern. Further, fields on types certainly need to differ as well. We can create different types for this case, but that's just creating more tech debt really. Again, we're not supporting multiple schemas yet, as our internal systems hit older APIs. But, this is something we've looked into for the future. Creating custom endpoints to handle your internal API needs is an obvious solution, but is really just spreading out much of your logic and certainly won't keep things DRYed up. |
@oprypkhantc it sounds like that would resolve your concerns - pre-defined schemas that get built accordingly? What if we introduced a new annotation argument called In the |
@oojacoboo This will work, but I don't think this is a good solution. I still have to build the multiple schemas myself (trying to share as much dependencies as possible), manage own routes, controller etc and that seems to be the hardest part. Filtering field is pretty trivial - I've created a I believe the best graphqlite can do here is simplify creating multiple schemas without wasting resources on dependencies that can be shared, the rest is pretty simple. |
Maybe given that a DI container is a required dependency, instead of creating all instances in the |
@oprypkhantc I think a fork in the schemas is much preferred, or would be in our case. Being able to use and configure the At the end of the day, most of this will be cached, so I don't see the issue with building up 2 different schemas entirely. |
Okay. Let's not take any action on this then to avoid doing things entirely wrong. I'm fine with building multiple schemas semi-manually as I'm doing now. I'll PR a documentation change to specify that middlewares are intended to be request independent as to avoid further issues like this, then I'll close this issue. Is this ok? |
Sounds great @oprypkhantc - thanks. |
Hey.
When working with long-running servers (such as Laravel Octane, which processes multiple requests before dying, contrary to php-fpm which only processes one), a schema is only resolved once (in both graphqlite-laravel and graphqlite-bundle), which means field middlewares are also only executed once. This is awesome for the speed, but the problem is that all subsequent requests can't change the fields dynamically. That also means that even some built-in features like
#[HideIfUnauthorized]
stop working on subsequent requests if instance ofSchema
is reused.Iterating over fields with reflection & annotations and other things on each request is obviously not an option as it defeats the purpose of a long-running server. One option is to run all field middlewares on each request, which should be a non-breaking change. Another option is to change field middlewares so they can act as both static middleware and a "dynamic" middleware which is triggered on every request.
Any thoughts?
I'm willing to implement a fix, but we need to agree on the plan first.
The text was updated successfully, but these errors were encountered: