Skip to content

Conversation

justlevine
Copy link
Contributor

What

This PR fixes the @phpstan-param type annotations for the required label, description, execute_callback, and permission_callback $args on wp_register_ability()

This PR was authored by @johnbillion on justlevine#6, but I didn't see it before the upstream PR got merged. All I did was cherrypick it onto truck.

Why

Per John

We can help PHPStan users by making these elements required. It doesn't affect the guard conditions in WP_Ability::prepare_properties() because the args array isn't documented there.

Copy link

github-actions bot commented Sep 30, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: johnbillion <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: felixarntz <[email protected]>
Co-authored-by: justlevine <[email protected]>
Co-authored-by: Ref34t <[email protected]>
Co-authored-by: JasonTheAdams <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@justlevine justlevine requested a review from Copilot September 30, 2025 23:11
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes PHPStan type annotations for the wp_register_ability() function by marking the label, description, execute_callback, and permission_callback parameters as required instead of optional in the $args array.

  • Updates PHPStan parameter type annotations to reflect required fields
  • Improves static analysis accuracy for developers using PHPStan
  • Maintains existing runtime validation while providing better IDE/tooling support

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link

codecov bot commented Sep 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.69%. Comparing base (8e0b08a) to head (aab9b74).
⚠️ Report is 1 commits behind head on trunk.

Additional details and impacted files
@@            Coverage Diff            @@
##              trunk      WordPress/abilities-api#97   +/-   ##
=========================================
  Coverage     85.69%   85.69%           
  Complexity      103      103           
=========================================
  Files            16       16           
  Lines           776      776           
  Branches         86       86           
=========================================
  Hits            665      665           
  Misses          111      111           
Flag Coverage Δ
javascript 92.66% <ø> (ø)
unit 82.97% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gziolo
Copy link
Member

gziolo commented Oct 1, 2025

This PR fixes the @phpstan-param type annotations for the required label, description, execute_callback, and permission_callback $args on wp_register_ability()

Overall, I'm in favor of making these args required with a small remark. There are possible paths where the developer might omit these args, for example when using ability_class. See usage in WC US demo plugin from @felixarntz:

https://github.com/felixarntz/wp-ai-sdk-chatbot-demo/blob/4e0e2ed5db5adb2f259659f85e0a1ce997daecc8/includes/Abilities/Abilities_Registrar.php#L26-L32

wp_register_ability(
	'wp-ai-sdk-chatbot-demo/get-post',
	array(
		'label'            => __( 'Get Post', 'wp-ai-sdk-chatbot-demo' ),
		'ability_class'    => Get_Post_Ability::class,
	)
);

An important note, the developer still must provide these properties, which is satisfied with this code:

https://github.com/felixarntz/wp-ai-sdk-chatbot-demo/blob/4e0e2ed5db5adb2f259659f85e0a1ce997daecc8/includes/Abilities/Abstract_Ability.php#L32-L48

Another, edge case scenario that I can think of would be with the new WP filter register_ability_args.

Could we use a union like the one below, or would it make it too complicated?

/**
 * @phpstan-param
 *   (array{
 *     label: string,
 *     description: string,
 *     execute_callback: callable(mixed $input=): (mixed|\WP_Error),
 *     permission_callback: callable(mixed $input=): (bool|\WP_Error),
 *     input_schema?: array<string,mixed>,
 *     output_schema?: array<string,mixed>,
 *     meta?: array<string,mixed>,
 *     ability_class?: class-string<\WP_Ability>,
 *     ...<string, mixed>
 *   }
 *   |
 *   array{
 *     ability_class: class-string<\WP_Ability>,
 *     label?: string,
 *     description?: string,
 *     execute_callback?: callable(mixed $input=): (mixed|\WP_Error),
 *     permission_callback?: callable(mixed $input=): (bool|\WP_Error),
 *     input_schema?: array<string,mixed>,
 *     output_schema?: array<string,mixed>,
 *     meta?: array<string,mixed>,
 *     ...<string, mixed>
 *   })
 *   $args
 */

Perhaps we could also include a unit test that illustrates the usage observed in the WC US demo plugin, providing internal PHPStan verification.

@gziolo gziolo requested a review from felixarntz October 1, 2025 07:16
@gziolo gziolo added the [Type] Developer Documentation Improvements or additions to documentation label Oct 2, 2025
@felixarntz
Copy link
Member

+1 to what @gziolo suggested above: Neither the current nor the suggested shape in this PR are ideal IMO, because these arguments are only required if ability_class is not provided.

Something like this union approach would address the actual requirements properly.

@justlevine
Copy link
Contributor Author

because these arguments are only required if ability_class is not provided.

@felixarntz nit: they are only not required if ability_class is provided and the WP_Ability child class overloads the default validation

The fact that WP_Ability can be overloaded so none of the typed properties is an implementation detail from #21 that is there to allow forpre v0.1.0 and in sometimes even pre-Core AI team projects to have an easy adoption path. It's not an endorsement.

IMO as I've said elsewhere, removing the predictability of required args is a huge footgun in general, but way worse to introduce into an initial release in v6.9 where it becomes permanent tech debt

In the interim a downstream dev (using PHPStan level 8) really thinks there's a reason for their polymorphism, then I imo a one-line // @phpstan-ignore argument.type is an encouraging bit of friction to have.
Especially at this early stage where the only prior art there really is right now are ports like Felix's demo.

@felixarntz
Copy link
Member

@justlevine

nit: they are only not required if ability_class is provided and the WP_Ability child class overloads the default validation

Fair point.

The fact that WP_Ability can be overloaded so none of the typed properties is an implementation detail from #21 that is there to allow forpre v0.1.0 and in sometimes even pre-Core AI team projects to have an easy adoption path. It's not an endorsement.

Where was that decided? I disagree with that assessment. Extending WP_Ability IMO is and should be a totally reasonable way to use this API.

@gziolo
Copy link
Member

gziolo commented Oct 3, 2025

Where was that decided? I disagree with that assessment. Extending WP_Ability IMO is and should be a totally reasonable way to use this API.

Agreed that extending WP_Ability is going to be essential for adoption in larger plugins, as they often have to find the best approach to fit their system. How they do it should be left to their discretion. Example from Woo:

https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php#L12-L37

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

In the interim a downstream dev (using PHPStan level 8) really thinks there's a reason for their polymorphism, then I imo a one-line // @phpstan-ignore argument.type is an encouraging bit of friction to have.

I tend to agree we can optimize for tha majority of devs using the default way to register abilities 👍🏻

@justlevine
Copy link
Contributor Author

justlevine commented Oct 3, 2025

The fact that WP_Ability can be overloaded so none of the typed properties is an implementation detail from #21 that is there to allow forpre v0.1.0 and in sometimes even pre-Core AI team projects to have an easy adoption path. It's not an endorsement.

Where was that decided? I disagree with that assessment. Extending WP_Ability IMO is and should be a totally reasonable way to use this API.

@felixarntz @gziolo not the ability to extend the class altogether, the ability to make those required args optional when extending (unlike Woo in that last class which a. respects the shape and b. doesn't need a class even 'output_schema' => '__return_true' would have been the same).

Iirc the discussion started on #21 and moved to #53 + #54. The broader argument against polymorphism also came up in #61 and some other tangental PRs.

@felixarntz
Copy link
Member

Right, I know we talked about it in several places, but I don't recall any decision that this was discouraged / not recommended or anything along these lines. If you implement your own ability_class, you can do pretty much whatever you're want, so IMO it's a "do it at your own risk" rather than "don't do it". Which arguments are required in wp_register_ability should be dependent on what happens in WP_Ability (or the custom ability_class), except for the few things that logic in wp_register_ability itself needs. Everything else, for the sake of that function, needs to be optional when a custom ability_class is provided, since only that class can decide what is required.

When no ability_class is provided, we can mark it everything as required as we have full control over the code in WP_Ability. FWIW, this is going to be 95% of usage most likely, so I don't think there is a realistic "decline in DX" by not requiring the parameters when a custom ability_class is provided.

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

Based on the conversation so far, I continue to question this approach: What is the concrete argument against going with @gziolo's suggestion from #97 (comment) instead?

@justlevine
Copy link
Contributor Author

Right, I know we talked about it in several places, but I don't recall any decision that this was discouraged / not recommended or anything along these lines.

Correct. No "decision" (if I'm now understanding your emphasis) has been made about actively promoting polymorphism in the initial core merge of this API. To make progress without such a decision we've been mostly avoiding polymorphism whenever it would be a breaking change to revert (because going from a single type to a union is a nonbreaking).

So when I say that #21 isn't an endorsement on polymorphism, I mean it was explicitly merged without a decision to endorse polymorphism having been made.

If you implement your own ability_class, you can do pretty much whatever you're want, so IMO it's a "do it at your own risk" rather than "don't do it".[...]

WordPress documentation has always been prescriptive, not descriptive. It's why most types in the codebase aren't unioned to mixed. A // @phpstan-ignore (vs a fatal error if not intentionally overloaded), feels very much like "do it at your own risk".

When no ability_class is provided, we can mark it everything as required as we have full control over the code in WP_Ability. FWIW, this is going to be 95% of usage most likely, so I don't think there is a realistic "decline in DX" by not requiring the parameters when a custom ability_class is provided.

The correlary here is that for <5% usage we're introducing a problematic footgun into an API we're navelgazing into existence isolated from core review, and where forward incompatible changes remain as permanent tech debt. We can always explicitly document the polymorphism in the future if/when we decide that it's something we want holistically and have it be nonbreaking.

What is the concrete argument against going with @gziolo's suggestion from #97 (comment) instead?

If you mean "Overall, I'm in favor of making these args required with a small remark.", then yes that's the approach I think we should take ;)

If you mean "Could we use a union like the one below, or would it make it too complicated?" then a quick tl;Dr would be:

  • polymorphism is bad for APIs. In a codebase that doesn't allow for breaking changes, the risk from polymorphism (and the costs of working around it) are higher.
  • this particular brand of polymorphism already affects the reliability and DX of the existing hooks. It will also likely get in the way of any Abstract_Ability/ Abstract_REST_Controller pattern that would holistically surface once we start implementing abilities in core.
  • The non-union approach still allows for polymorphism without actively promoting it. At most it requires a one-line annotation, which is arguably good friction to have. All of this follows how WordPress core traditionally handles polymorphism that isn't actively recommended.
  • we can iterate from a non-union approach to a union approach in the future without it being a breaking change. We cannot go vice versa.

@felixarntz what do you believe are the reasons:

  1. To introduce polymorphism altogether at such an early stage to the API
  2. To actively support and document polymorphism here (and make it a breaking change if we decide we need to require those args in the future )

@felixarntz
Copy link
Member

To be clear, I never suggested to use polymorphism in the first place. The ability_class argument was already there when I got involved with this project.

I don't disagree with several of your concerns. But then the proper solution is to not allow polymorphism at all, i.e. get rid of the ability_class argument altogether.

Having ability_class while otherwise building most of the API as if it didn't exist is a half baked solution. If we go based on your arguments, we should remove it. It's not in Core yet, so we can (and should) make breaking changes now, instead of introducing something we are not fully behind.

Most other similar shaped WordPress APIs don't allow for such overriding of the class either. These APIs enforce usage of the WordPress object as is, and where they don't, it's likely just as problematic as this.

We can deprecate the ability_class argument now, and then never include it in the Core implementation in the first place. This way early plugins that used it can migrate before the Core launch.

@Ref34t
Copy link
Contributor

Ref34t commented Oct 6, 2025

@felixarntz Your point about "half-baked solutions" resonates with me.

For the sake of testing,I built a realistic restaurant management plugin to evaluate both approaches. For my use case (5 related abilities with shared rate limiting, logging, and subscription checks), the flexible approach saved 200 lines of duplicate code and reduced maintenance burden by 80%.

But if this PR merges, I'll have 5 @phpstan-ignore comments in well-architected code. That feels like being punished for following DRY principles.

I agree with your assessment: Either properly support both patterns (union type) or deprecate ability_class entirely. The current PR creates exactly the "half-baked solution" you described.

@justlevine
Copy link
Contributor Author

@felixarntz I agree with this approach, especially as we wait for WordPress/ai#40 . But if we can't reach a decision here/yet we will have an opportunity during the core merge process to polish off the rest.

@Ref34t would you mind sharing? Im afk until tomorrow but the more code examples we have to theorycrafts around the more sound we can be in whatever ends up shipping.

@gziolo
Copy link
Member

gziolo commented Oct 6, 2025

For the sake of testing,I built a realistic restaurant management plugin to evaluate both approaches. For my use case (5 related abilities with shared rate limiting, logging, and subscription checks), the flexible approach saved 200 lines of duplicate code and reduced maintenance burden by 80%.

Nice! It's expected that multiple abilities coming from the same plugin will share logic. For larger teams, it will also help to enforce a certain structure for enforcing best practices like tracking usage, additional permissions, or context checks.

I agree with your assessment: Either properly support both patterns (union type) or deprecate ability_class entirely. The current PR creates exactly the "half-baked solution" you described.

I'm strongly in favor of using an union type and keeping the ability_class.

@felixarntz
Copy link
Member

felixarntz commented Oct 6, 2025

I am in favor of going with either one or the other direction and properly sticking to it. What I'm against is a path where we choose to have ability_class, while at the same time considering it a discouraged API. If it's discouraged, we shouldn't have it at all - not support it only somewhere, but not support it elsewhere. As it currently stands, for example, this PR will need to use the union type, otherwise we're doing this weird thing in between.

I agree there's a higher risk of developers doing it wrong, if we allow ability_class. But I also agree the flexibility of allowing ability_class gives developers with OOP preferences a simpler integration point - with the caveat that they need to know what they're doing.

I don't lean strongly either way, I only think that we have to make a choice, and when that choice is made, we need to be able to get behind it, and not continue to argue against it randomly in the future - as in "strong opinions, loosely held" :)

@Ref34t
Copy link
Contributor

Ref34t commented Oct 6, 2025

@justlevine I published my 2 examples of the 2 approaches so you can have a better look
Ref34t@331da1a

@gziolo gziolo added this to the pre WP 6.9 milestone Oct 7, 2025
@justlevine
Copy link
Contributor Author

Thanks for sharing @Ref34t 🙇.

But if this PR merges, I'll have 5 @phpstan-ignore comments in well-architected code. That feels like being punished for following DRY principles.

Putting aside there's a bunch of "DRY principle" ways you can refactor that code to avoid the PHPStan error entirely (we all agree devs should write code however works best for them), or that you can [ignoreErrors] with the path + identifier, and not add any annotations, I want to challenge the notion that annotations of any kind are some sort of punishment. Do you also feel punished

  • by the params of add_action() or add_filter() (whether you treatPHPDocTypesAsCertain: true or manually type-check/type-cast)
  • by using a complex meta_query, or one of the other WPCS performance or PHPCS smells?

Having ability_class while otherwise building most of the API as if it didn't exist is a half baked solution.

I agree with your assessment: Either properly support both patterns (union type) or deprecate ability_class entirely. The current PR creates exactly the "half-baked solution" you described.

I'm strongly in favor of using an union type and keeping the ability_class.

We can't call partial-documentation half-baked without acknowledging that the entire abilities API is half-baked, and not even just the ability_class implementation as @felixarntz pointed out. If that half-bakedness goes into Core, then there are tangible benefits to leaving it off the documentation until e.g. 7.0 when the implementation has had time to cook some more, as I already laid out above - @gziolo @Ref34t would love a direct response/counter to that.

What I'm against is a path where we choose to have ability_class, while at the same time considering it a discouraged API.

@felixarntz just repeating one nuance again the lack of encouragement is not the same thing as discouraging. I'm just advocating we leave the documentation off to buy us time until [pick: beta1, rc1, wp7.0] to cook.

@JasonTheAdams
Copy link
Member

Weighing in here, I think we should go with the union type described here: #97 (comment)

Here's why:

The reality is that this is the most accurate representation of how the system works right now. Since this PR is focused on correcting that type, then that's the type as it stands.

Regarding the debate of ability_class, this should be tackled in a subsequent Issue or PR. Personally, I don't think it's a big deal to have both. My gut says we're overthinking this. But we can resume that elsewhere.

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

Labels

[Type] Developer Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants