Repository navigation
Generics are always considered as Collection #180
Description
Activity
- changed the title
[-]Generics always considered as Collection[/-][+]Generics are always considered as Collection[/+]on Dec 16, 2022 Disclaimer: I'm writing this without overthinking the impact and possibility of implementing this.
When we implemented the first generics version, there was no good support for this in our codebase. The generic support was kind of hacked into the code. By now, we should be able to change this. However, it will require some extra work. We do not want to introduce any backward compatibility breaks at this point.
This means that a v1 version will still return all generics as a subtype of collection. A v2 version which is unavoidable at this point, may have a breaking change allowing us to introduce a more complex generic support. But as this library cannot resolve the generic part, it might be a more complex issue. We are analyzing the types per PHP-file. Generics are combinations of types over files. This means that the templated values of the generic need to be resolved in another context.
Given this class:
/** @template T **/ class Foo { /** @return T */ public function getValue(): object }Here we cannot tell you the value of
T. As T is defined at the moment of using the classFoo<MyObject>So depending on your needs, you might need another layer of reflection to get this information. The other layer will never be added to this library. When possible, we could add it in https://github.com/phpDocumentor/ReflectionDocBlock, But that will never cover all situations because it also does the per-file Reflection.
The final result can only be part of https://github.com/phpDocumentor/Reflection, where we analyze an entire code base. And even then, we will not be able to discover everything, because to discover the actual values you also need to include the vendor directories. Which tools like phpstan are doing.
From a documentation perspective, it might be enough to tell the consuming project that we discovered a generic object notation.
/** @return Foo<MyObject> */ public function getFoo(): Foo {}This could result in a future version in:
new GenericType(new Fqsen('\Foo'), [new Object_(new Fqen('\MyObject'))]);.thanks for this complete answer! I was indeed thinking it is a complex problem.
to give you a little bit of context, I don't know if you have a Symfony knowledge, but this problem prevents to correctly document such case with Symfony's serializer:
MyClass { /** @var SomeGeneric<SomeClass> */ public SomeGeneric $property; }used along with something like
SomeGenericNormalizerwhich is triggered when the serializer encounters aSomeGenericdata.
The property is considered as a collection, and the custom normalizer is skipped because it now seeks for a normalizer forSomeClass[]. (see here and here).I don't know if there could be a solution in Symfony-land to correctly handle this. Couldn't we at least add a bool property on
Typethat says it was a generic notation or not?I know Symfony quite well. I use it on a daily bases. Maybe we should elaborate a bit more on this particular topic because I see different use cases that I would like to see in the Symfony serializer.
If we could add support for generics, it would allow Symfony to serialize in more complex situations. Like serializing
SomeGeneric<SomeClass>could be done differently thanSomeGeneric<OtherClass>, right?Please let me know what you need, and I'm open to collaborating with you on this get improve our loved framework :-)
Reacted by Nicolas PHILIPPESomeGeneric could be done differently than SomeGeneric, right?
yes, of course.
I think from the perspective of this lib, indeed a new
GenericTypewould be perfect
From Symfony's perspective, thePropertyInfowould have to evolve, we would need to access to the generic's child class in\Symfony\Component\PropertyInfo\Type. I'm not really sure such a move would be accepted 🤔
I presently use
#[Context()]tu pass the child class. ie:/** @var SomeGeneric<SomeClass> */ // <== this breaks the serialization #[Context([SomeGenericNormalizer::CONTEXT_GENERIC_CLASS => SomeClass::class])] public SomeGeneric $property;
this is actually the case that made me open this issue :)
I'm walking into the same (I think) issue when I use
value-of<Type>; which is a Psalm annotation (https://psalm.dev/docs/annotating_code/type_syntax/utility_types/#value-oft).I don't have a good story for how to avoid this at the moment, which unfortunately stops me being able to upgrade to Symfony 6.2.8 (which includes 1.7.1 of TypeResolver).
I guess things have slightly been changed, but this issue is still relevant. Here's the source of the problem:
TypeResolver/src/TypeResolver.php
Line 412 in eee054a
default: @jaapio As I understand it, you're suggesting creating a
GenericTypeclass that will support an unlimited number of generics (currently limited to two). In this case, we also get rid of theCollection. Am I understanding you correctly?Yes basically, that's the idea. As a generic is not always a collection, but a collection is always a generic
Reacted by Maksim SpirkovGreat, I'll work on implementing this idea.
I think we have to think about the option to introduce a template type. As generics typically do not have a class or interface in their types. But something like
TValueresolving the actual value of this is out of the scope of this library.So my proposal is not 100% accurate
I didn't quite get it. Can we understand at the TypeResolver level where a template type is and where it isn't?
Or do you mean that we should just create a Value Object for the template type?I think we should indeed have a value object for the template. So it can be resolved at other levels when the template type is known.
Reacted by Maksim SpirkovThis has been taken care of in v2.
I reopened this issue because I'm still struggling with the implementation. In all cases we resolve an object, a generic notation could be used. This also applies to lists, and arrays.
This basically means that we never resolve an object anymore.
Reacted by Maksim SpirkovI was just thinking about that too :)
I'll look into it.I also suggest renaming
GenericTemplatetoTemplateValue.I'm also thinking about the more downstream libraries and phpDocumentor. This library was initially built to support phpDocumentor, later on it became more popular because it was an easy to use implementation together with ReflectionDocblock to process docblocks. This is how it became a cornerstone of bigger php projects.
Maybe we should keep the objects like they are now. And leave it up to the consuming projects to find out a class exists or if the object is a template.
Please remember when you try to implement something more. That we also have union and intersection types in generics.
This becomes a more complex problem than I expected
Hmm, then maybe it really is worth leaving the objects as is and removing the GenericTemplate.
It might just be unnecessary complexity.
In yii2-apidoc, I was able to resolve template types with the current implementation.The only thing I'd suggest is preserving the original name. Right now, we only have fqsen, and it's not very convenient for resolving template types.
What do you think about this?Why would you need the name? Fqsen has a method
getNameisn't that sufficient? Or should we proxy it at anObjectlevel?Reacted by Maksim SpirkovIndeed, I simply didn't notice. Thank you very much!
Maybe it's good enough to just use the types we already have.
If it works for yii it's very likely that others are able to implement the same pattern. Maybe some helper code could be useful.
Yes, I think you're right.
In Yii, we currently store template types in the current context (method, class, interface, etc.). Then, when generating documentation, we check whether the current type is a template type. Everything seems to be working fine.
It seems like we don't even need any helper code.
Hello,
whether on branch
1.xor on the last tagged version, generics are always considered as a collection although this is not always true.Is it made on purpose or are you planing to extend the method
TypeResolver::createFromGeneric()to handle the cases where generics are something else than a collection?thanks for your answer!