Skip to content

Generics are always considered as Collection #180

Description

@nikophil

Hello,

whether on branch 1.x or 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!

Activity

  1. changed the title [-]Generics always considered as Collection[/-] [+]Generics are always considered as Collection[/+] on Dec 16, 2022
  2. jaapio commented on Dec 16, 2022

    @jaapio
    Member

    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 class Foo<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'))]);.

  3. nikophil commented on Dec 16, 2022

    @nikophil
    Author

    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 SomeGenericNormalizer which is triggered when the serializer encounters a SomeGeneric data.
    The property is considered as a collection, and the custom normalizer is skipped because it now seeks for a normalizer for SomeClass[]. (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 Type that says it was a generic notation or not?

  4. jaapio commented on Dec 16, 2022

    @jaapio
    Member

    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 than SomeGeneric<OtherClass>, right?

    Please let me know what you need, and I'm open to collaborating with you on this get improve our loved framework :-)

  5. nikophil commented on Dec 16, 2022

    @nikophil
    Author

    SomeGeneric could be done differently than SomeGeneric, right?

    yes, of course.

    I think from the perspective of this lib, indeed a new GenericType would be perfect
    From Symfony's perspective, the PropertyInfo would 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 :)

  6. added this to the version 2 milestone on Mar 17, 2023
  7. schodemeiss commented on Apr 3, 2023

    @schodemeiss
    Contributor

    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).

  8. uuf6429 commented on Jul 14, 2024

    @uuf6429

    I guess things have slightly been changed, but this issue is still relevant. Here's the source of the problem:

  9. mspirkov commented on Nov 28, 2025

    @mspirkov
    Contributor

    @jaapio As I understand it, you're suggesting creating a GenericType class that will support an unlimited number of generics (currently limited to two). In this case, we also get rid of the Collection. Am I understanding you correctly?

  10. jaapio commented on Nov 28, 2025

    @jaapio
    Member

    Yes basically, that's the idea. As a generic is not always a collection, but a collection is always a generic

  11. mspirkov commented on Nov 28, 2025

    @mspirkov
    Contributor

    Great, I'll work on implementing this idea.

  12. jaapio commented on Nov 28, 2025

    @jaapio
    Member

    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 TValue resolving the actual value of this is out of the scope of this library.

    So my proposal is not 100% accurate

  13. mspirkov commented on Nov 28, 2025

    @mspirkov
    Contributor

    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?

  14. jaapio commented on Nov 28, 2025

    @jaapio
    Member

    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.

  15. jaapio commented on Nov 30, 2025

    @jaapio
    Member

    This has been taken care of in v2.

  16. reopened this on Nov 30, 2025
  17. jaapio commented on Nov 30, 2025

    @jaapio
    Member

    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.

  18. mspirkov commented on Nov 30, 2025

    @mspirkov
    Contributor

    I was just thinking about that too :)
    I'll look into it.

  19. mspirkov commented on Nov 30, 2025

    @mspirkov
    Contributor

    I also suggest renaming GenericTemplate to TemplateValue.

  20. jaapio commented on Nov 30, 2025

    @jaapio
    Member

    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.

  21. jaapio commented on Nov 30, 2025

    @jaapio
    Member

    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

  22. mspirkov commented on Nov 30, 2025

    @mspirkov
    Contributor

    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.

  23. mspirkov commented on Nov 30, 2025

    @mspirkov
    Contributor

    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?

  24. jaapio commented on Nov 30, 2025

    @jaapio
    Member

    Why would you need the name? Fqsen has a method getName isn't that sufficient? Or should we proxy it at an Object level?

  25. mspirkov commented on Nov 30, 2025

    @mspirkov
    Contributor

    Indeed, I simply didn't notice. Thank you very much!

  26. jaapio commented on Nov 30, 2025

    @jaapio
    Member

    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.

  27. mspirkov commented on Nov 30, 2025

    @mspirkov
    Contributor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions