Skip to content
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

Pseudo-types for referencing class properties and methods? #3141

Open
Ocramius opened this issue Apr 14, 2020 · 8 comments · Fixed by #7359
Open

Pseudo-types for referencing class properties and methods? #3141

Ocramius opened this issue Apr 14, 2020 · 8 comments · Fixed by #7359

Comments

@Ocramius
Copy link
Contributor

Ocramius commented Apr 14, 2020

Just throwing out vague idea: there is no implementation nor clear concept, so please feel free to close, but I hope it will be useful to brainstorm this amongst type-junkies.

I was considering adding types for ReflectionMethod and ReflectionProperty, but it seems to be tricky to do that, due to the lack of a system to reference properties and classes at type-level.

Specifically, parameter $name of ReflectionClass<class-string>#getProperty($name) could be a class-property<class-string, non-empty-string, PropertyType of mixe>, which could be used for type resolution.

Similarly, parameter $name of ReflectionClass<class-string>#getMethod($name) could be a ReflectionMethod<class-method<class-string, non-empty-string, list<TParameter of mixed>, MethodType of mixed>>, which could be used for type resolution, and class-method could be a callable when combined with a class-string<T> or an object of T that is compatible with that class-method<T, ...>.

This sort of API is interesting for things like hydrators:

/**
 * @psalm-template TObject of object
 * 
 * NOTE: how do we type `mixed` below here?
 *
 * @psalm-param non-empty-array<
 *   class-property<class-string<TObject>, non-empty-string>,
 *   mixed,
 *   mixed
 * >
 * @psalm-return TObject
 */
function hydrate(array $input, object $object) { /** ... */ }
$object = hydrate(
    [
        'foo' => $bar,
        'baz' => $tab,
    ],
    $object
);

Important in the above is that I still don't have an idea of how we could type the mixed

Also for assertions:

/**
 * @psalm-assert TObject of object
 * @psalm-assert TParameter of mixed
 * @psalm-param non-empty-array<
 *   class-property<class-string<TObject>, non-empty-string, list<TParameter> mixed>,
 *   mixed
 * >
 * @psalm-assert class-method<class-string<TObject>, non-empty-string> $method
 */
function assertMethodExists(string $method, object $object) { /** ... */ }

function findObject() : object {}
function findMethod() : string {}

$object = findObject();
$method = findMethod();

assertMethodExists($object, $method);

$object->$method(); // not sure about parameter count and parameter types
@weirdan
Copy link
Collaborator

weirdan commented Apr 14, 2020

This looks like something like Typescript's structural typing, so perhaps something could be borrowed from their approach.

@Ocramius
Copy link
Contributor Author

Ocramius commented Apr 14, 2020

I've added some bells & whistles above: I couldn't find anything about Typescript's reflection support though. Structural typing seems very similar to GO's interfaces?

@weirdan
Copy link
Collaborator

weirdan commented Apr 14, 2020

Apologies, I used wrong terminology. What I meant was index types.

Usage example:

// ------------------- [Definitions] ---------------
class ReflectionProperty<T> {
    prop: T;
    constructor(prop: T) {
        this.prop = prop
    }
    getValue(): T {
        return this.prop
    }
}

class ReflectionObject<T extends object> {
    obj: T;
    constructor(obj: T) {
        this.obj = obj
    }
    getProperty<N extends keyof T>(prop: N): ReflectionProperty<T[N]> {
        return new ReflectionProperty(this.obj[prop]);
    }
}

function takesNumber(val: number): void { }

// -------------------- [Usage] -------------

class C {
    prop: string = 'zxc';
    func(): number {
        return 123;
    }
}
class D {
    e: number = 123;
    func(): string {
        return "zxc";
    }
}

// here typescript figures out getValue returns string, not int
takesNumber(new ReflectionObject(new C).getProperty('prop').getValue()) 

// this is fine
takesNumber(new ReflectionObject(new D).getProperty('e').getValue())

// works with functions too
const cFunc = new ReflectionObject(new C).getProperty('func').getValue()
takesNumber(cFunc())

// knows that dFunc returns string
const dFunc = new ReflectionObject(new D).getProperty('func').getValue()
takesNumber(dFunc())

@glennpratt
Copy link

I'm also interested in this. Another example is Symfony's EventSubscriberInterface which maps events to instance methods on the same class:

https://github.com/symfony/event-dispatcher/blob/5.x/EventSubscriberInterface.php#L27-L48

@veewee
Copy link
Contributor

veewee commented Aug 6, 2021

Structural typing would also be a nice addition for DTO classes with public readonly properties:

class Human {
    public function __construct(
        public readonly string $name 
    ){}
}

class Animal {
    public function __construct(
        public readonly string $name 
    ){}
}


/** @psalm-assert shape<shapeof Human> shape<shapeof  Animal> */

This syntax might look like a mix between hack's shape keyword and typescript's structural typing + keywords.

Since there is already an array shape, the shape won't be an array as it is in hack.
The difference between a shape and an existing array shape would be the way how it is compared.

For structural shapes, a structural comparison could be done.
For arrays the comparison would be stricter.

I've translated the examples above to what it could look like:

Given:

class WindowsServer {
    public string $name = 'X';
    public int $age = 10;
}

class LinuxServer {
    public string $name = 'X';
    public int $age = 10;
    public string $kernel = '5.14';
    public function execute(Foo $foo): Bar{}
}

interface Executor {
    public function execute(Foo $foo): Bar{};
}

These additional structures could be allowed:

Direct usage with props:

/**
 * @psalm-type Server = shape{name: string, age: int}
 * @param Server $windowsServer
 * @param Server $linuxServer
 */

Support for callables / methods:

/**
 * @psalm-type Server = shape{execute: (callable(Foo): Bar)}
 * @param Server $linuxServer
 * @param Server $someOtherExecutorThatImplementsTheExecutorInterface
 */

Type operations (like typescript's Typeof or Keyof) that result in enumerations of object information:

/**
 * @psalm-type Methods = methodsof LinuxServer           - enum of all public method names
 * @psalm-type Types = propsof LinuxServer               - enum of all (public?) properties
 * @psalm-type Shapeof = shapeof LinuxServer             - enum of both methods and props
 */

Casting of objects to structural shapes or array shapes:

/**
 * @psalm-type Server = shape<propsof WindowsServer>     - (Public?) properties only
 * @psalm-type Executor = shape<methodsof LinuxServer>   - Public methods only
 * @psalm-type arrayInfo = array<propsof LinuxServer>    - Special functions also useable for arrays
 * @psalm-type LinuxShape = shape<shapeof LinuxServer>   - Full shape : props + methods
 */

The hydration example could look like this:

/**
 * @template T of object
 * @param array<propsof T> $input
 * @param T $object
 * @return T
 */
function hydrate(array $input, object $object): object {}

Note : this will require the array to contain ALL the properties of the object. If you want to limit the options, there will be a need for typescript-like utility types like

  • Partial<SomeShape>
  • Pick<SomeShape, 'prop1'|'prop2'>
  • Omit<SomeShape, 'prop1'|'prop2'>

The assertion example could look like this:

/**
 * @template TObject of object
 * @template TParameter of string
 *
 * @psalm-assert shape{TParameter: callable} TObject
 *
 * @param TParameter $method
 * @param TObject $object
 */
function assertMethodExists(string $method, object $object) { /** ... */ }

The event subscriber could look like this:

/**
 * @psalm-type MethodNames = methodsof static
 *
 * @psalm-type Simple = array<string, MethodNames>
 * @psalm-type Prioritized = array<string, array{ MethodNames, int }>
 * @psalm-type Many = array<array-key, Simple|Prioritized>
 *
 * @return array<string, Simple|Prioritized|Many>
*/
public static function getSubscribedEvents();

Not sure how doable the above list is. But it is an interesting analysis nevertheless!

@aurimasniekis
Copy link

I was able to make class-property type using properties available from class like storage, dunno if it's a good way, but I do not see why it should not be possible to implement. I just don't know if it's possible to make like @veewee proposed props of T or methodsof T syntax

image

@Patrick-Remy
Copy link
Contributor

Do you plan to raise a PR? I would love to test and give you feedback about the new type-annotation class-property.

@aurimasniekis
Copy link

aurimasniekis commented Nov 30, 2021

Yeah, I will try to make a PR when I finish a few other things with it. I am yet to figure out the internals of the psalm, I want to add some filters like visibility, type :)

edit: That autocomplete was just me trying out adding autocomplete to language server 😅

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

Successfully merging a pull request may close this issue.

7 participants