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

Add generic annotations to EventInterface #32

Merged
merged 22 commits into from
Dec 10, 2022
Merged

Conversation

villermen
Copy link
Contributor

Q A
Documentation yes
Bugfix no
BC Break no
New Feature no
RFC no
QA no

Description

It would be cool if you could somehow hint at the types of an event's target and params that IDEs and static code checking tools can work with. I've added a rough outline of what I mean in the PR using PHPStan-targeted annotations.

This way you can define the target and parameter types of an event in one go instead of doing it on each line the event is accessed:

public function onSomeEvent(EventInterface $event): void
{
    /** @var Element|null $element */
    $element = $event->getTarget();
    /** @var array{foo?: string} */
    $params = $event->getParams();
    
    $this->doSomethingWithFoo($params['foo']); // IDE can provide autocompletion and type recognition for "foo".
}

becomes

/**
 * @param EventInterface<Element|null, array{foo?: string}>
 */
public function onSomeEvent(EventInterface $event): void
{    
    $this->doSomethingWithFoo($event->getParams()['foo']); // IDE can provide autocompletion and type recognition for "foo".
}

This leaves the type annotation code in the main docblock of the method.

Of course you could also create an actual native-class for each event. That's more type-safe, but creates a lot of additional overhead. I can totally understand if that's the desired approach too, so take this is a suggestion only =)

src/EventInterface.php Outdated Show resolved Hide resolved
src/Event.php Outdated Show resolved Hide resolved
@Ocramius Ocramius added this to the 3.7.0 milestone Nov 13, 2022
Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also take a look at the CS failures: they are quite noisy :-D

src/Event.php Outdated Show resolved Hide resolved
src/Event.php Outdated Show resolved Hide resolved
@villermen
Copy link
Contributor Author

Took care of the checks =)

Looks like Event correctly picks up on the variables passed in __construct(), setTarget() and setParams().

I was sadly unable to extend the template when calling setParam(). That ultimately leads to getParams() and getParam() only being able to check on the type specified or inferred from setParams(). So I'm not entirely happy with the result but it's the best I could muster.

Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now comes the hard part: testing this :D

In practice, you added a lot of type bindings to allow for better ergonomics around Event and EventInterface, but we don't know if they will work in the wild.

My suggestion is to have the types be verified by a directory checked by psalm.xml, with examples such as:

/**
 * @param EventInterface<MyObject, array<non-empty-string, 1|2|3>> $e
 * @return array{
 *     MyObject,
 *     array<non-empty-string, 1|2|3>,
 *     1|2|3
 * }
 */
function foo(EventInterface $e): MyObject {
    return [
        $e->getTarget(),
        $e->getParams(),
        $e->getParam('foo')
    ];
}

The above, but for various examples (constructor call, target change, params change, etc.)

src/Event.php Outdated Show resolved Hide resolved
src/Event.php Outdated
* @param string|object|null $target
* @psalm-param TTarget $target
* @param array|ArrayAccess|object|null $params
* @psalm-param TParams|null $params
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps just drop TParams|null, and only keep TParams? That would fix this type binding, IMO, and remove the suppression on protected $params

Copy link
Contributor Author

@villermen villermen Nov 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be better yeah, paired with the reversal of the ctor null change.

The suppression for protected $params is because array<empty, empty> cannot be assigned to TParams . There seems to be no way to infer an empty array for TParams when the ctor receives null as the third argument.

I could fix that with $this->setParams($params ?? []), but that would change behavior when someone has extended Event without calling the parent's ctor. So I figured the suppression was a better solution.


Removing the $params = [] confuses Psalm because the default type for new Event() will become Event<null, mixed> which is not accepted by the likes of EventManager::triggerEvent().

Removing |null will cause a static error in the ctor because it thinks the type can never be null anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/Event.php Outdated Show resolved Hide resolved
src/Event.php Show resolved Hide resolved
src/Event.php Outdated Show resolved Hide resolved
test/EventTest.php Outdated Show resolved Hide resolved
psalm/EventChecks.php Outdated Show resolved Hide resolved
psalm/CheckObject.php Outdated Show resolved Hide resolved
Comment on lines 46 to 55
// /**
// * @param EventInterface $e
// * @return array
// */
// public function checkSetParamDoesNotAlterTemplate(EventInterface $e): array
// {
//
// }

// TODO: Check ctor inferrence, setParams setTarget out changes, ignore setParam() or getParam()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tracking this TODO

@Ocramius Ocramius modified the milestones: 3.7.0, 3.8.0 Dec 1, 2022
@Ocramius Ocramius changed the base branch from 3.7.x to 3.8.x December 1, 2022 16:13
@Ocramius
Copy link
Member

Ocramius commented Dec 1, 2022

BTW, vimeo/psalm:^5 now installed (if you rebase).

It will either help or hurt :D

Copy link
Member

@internalsystemerror internalsystemerror left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@villermen please address the comments made by @Ocramius, other than those, looks good 👍

Copy link
Member

@gsteel gsteel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to see the psalm tests in a subdirectory of /test/ - that way, autoloading is already setup, psalm & phpcs runs and files are export ignored - not a blocker though.

src/Event.php Outdated Show resolved Hide resolved
src/Event.php Outdated Show resolved Hide resolved
test/EventTest.php Outdated Show resolved Hide resolved
@Ocramius Ocramius modified the milestones: 3.8.0, 3.9.0 Dec 3, 2022
@Ocramius Ocramius changed the base branch from 3.8.x to 3.9.x December 3, 2022 06:30
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
…s on limits of current Psalm setup

Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
It won't work when params is an object.

Signed-off-by: Villermen <villermen@gmail.com>
…ay check in Event::__construct()

Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
Signed-off-by: Villermen <villermen@gmail.com>
@villermen
Copy link
Contributor Author

I'd prefer to see the psalm tests in a subdirectory of /test/ [..]

@gsteel Actually agreed with you there, but then I noticed "benchmarks/". That's also a testing directory separate from "test/" so I stuck to that pattern. Ended up being happier with this situation because these classes are not necessarily tests that have to be run by PHPUnit. Will correct when you still prefer adding below "test/".

Signed-off-by: Villermen <villermen@gmail.com>
…ality

Signed-off-by: Villermen <villermen@gmail.com>
@gsteel
Copy link
Member

gsteel commented Dec 6, 2022

I'd prefer to see the psalm tests in a subdirectory of /test/ [..]

Will correct when you still prefer adding below "test/".

I don't have strong opinions on this @villermen - it's all good as long as it's all export ignored and subject to the same CS rules etc 👍

Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hot damn the tests on this are good! Thanks @villermen!

I added a couple more feedback points around verifying psalm-this-out, mostly because I got burnt by laminas/laminas-stdlib#78

If those are handled, we 🚢

psalm/EventInterfaceChecks.php Show resolved Hide resolved
}

/**
* Individual params obtained via `getParam()` can't be inferred because their keys/values can't be selected from
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good enough though 👍

*
* @param EventInterface<null, array{foo: int}> $e
* @return array{
* EventInterface<null, array{foo: int}>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still kinda weird: fixable somehow? 🤔

See my reverts from laminas/laminas-stdlib#78

src/Event.php Show resolved Hide resolved
src/EventInterface.php Outdated Show resolved Hide resolved
…ify them on Event

Signed-off-by: Villermen <villermen@gmail.com>
To no avail.

Signed-off-by: Villermen <villermen@gmail.com>
@Ocramius
Copy link
Member

Ocramius commented Dec 8, 2022

I see that you opted for &static: think that will suffice? 🤔

Signed-off-by: Villermen <villermen@gmail.com>
@villermen
Copy link
Contributor Author

think that will suffice? 🤔

@Ocramius I think it does! After CheckEvent::setParams() or CheckEvent::setTarget() are called the type will become Event<NewTTarget, NewTParams&CheckEvent when using either &$this or &static.

I've also looked into getting it to work without this-out, but it become very cumbersome to deal with then. Ctor inference sets a type that's too narrow, setters can't use template types. The best we can do in that case is defining the templates and using them on the getters.

Signed-off-by: Villermen <villermen@gmail.com>
Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's roll with this and see how it goes: it's a massive improvement, and we all learned a lot from it, so it's time to test it on the field 👍

@Ocramius Ocramius merged commit 2ffc680 into laminas:3.9.x Dec 10, 2022
@Ocramius
Copy link
Member

Thanks @villermen!

@Ocramius Ocramius changed the title Add generic annotations to EventInterface Add generic annotations to EventInterface Dec 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants