vimeo / psalm

A static analysis tool for finding errors in PHP applications
https://psalm.dev
MIT License
5.58k stars 664 forks source link

query: re template tag to get a template param from another templated param #1695

Closed SignpostMarv closed 5 years ago

SignpostMarv commented 5 years ago

Whilst one could implement a factory on an object to return correctly-templated instances of a given class (i.e. with a model that has a method to return an instance of a class designed to track changes to the model), there are patterns where this is cludgy (i.e. in repository pattern, objects/models should not have knowledge of repositories).

I'm wondering if there's sufficient utility for a template param "function" to declare a given template param is identical to the named template param from one of the supplied template params (i.e. requiring the specified template param to refer to a class), e.g.:

<?php
/**
 * @template TData as array
 */
abstract class DataBag {
    /**
     * @var TData
     */
    protected $data;

    /**
     * @param TData $data
     */
    public function __construct(array $data) {
        $this->data = $data;
    }

    /**
     * @return TData
     */
    public function toArray() : array {
        return $this->data;
    }
}

/**
 * @template T1 as DataBag
 * @template T2 as template-of(T1, TData)
 */
abstract class DataBagThing {
    /**
     * @var array<int, T2>
     */
    protected $data = [];

    /**
     * @param T1 $object
     */
    public function RememberForLater(DataBag $bag) : void {
        $this->data[spl_object_id($bag)] = $bag->toArray();
    }

    /**
     * @param T1 $object
     *
     * @return T2
     *
     * @throws InvalidArgumentException if $object was not remembered earlier
     */
    public function WhatWasItEarlier(DataBag $bag) : array {
        if (isset($this->data[spl_object_id($bag)])) {
            return $this->data[spl_object_id($bag)];
        }

        throw new InvalidArgumentException('was not remembered earlier');
    }
}

/**
* @template-extends DataBag<array{foo:string}>
*/
class Foo extends DataBag {}

/**
* @template-extends DataBag<array{bar:string}>
*/
class Bar extends DataBag {}

/**
* @template-extends DataBagThing<Foo>
*/
class FooBagThing extends DataBagThing {} // T2 of FooBagThing should be array{foo:string}

/**
* @template-extends DataBagThing<Bar>
*/
class BarBagThing extends DataBagThing {} // T2 of BarBagThing should be array{bar:string}
muglug commented 5 years ago

Can you do this in another language that supports templated types (e.g. TypeScript, HackLang etc.)? If you can, I'm might add it, if you can't I won't.

SignpostMarv commented 5 years ago

The closest thing I'm seeing on TypeScript is function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } which doesn't quite seem to be the point ?

c# has T.GetGenericArguments() and there's a java thing both of which are runtime not docblocks or syntax :s

SignpostMarv commented 5 years ago

c# has T.GetGenericArguments()

p.s. this would suggest @template T2 as generic-arguments(T1, 0) ?

muglug commented 5 years ago

yeah, but those are all runtime things - closing, but feel free to implement if you feel strongly

SignpostMarv commented 4 years ago

Where might one have to start digging around to implement this ?