-
-
Notifications
You must be signed in to change notification settings - Fork 79
Make FormatterInterface stub generic (2.x) #846
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
base: main
Are you sure you want to change the base?
Conversation
@ceesgeene, if possible, please take a look and give feedback on the generic proposal. |
The only problem I faced is that I can't define these generic subtypes using a docblock above the class. /**
* @implements FormatterInterface<CommentFieldItemList>
*/ This leads to an error:
This forces us to add types directly at the method level: /**
* @param CommentFieldItemList $items
*/
public function viewElements(FieldItemListInterface $items, $langcode): array { Then it works. But in that case, it's unclear what the generic is actually doing. You can simply remove it and get the same result. So it appears that we also should provide stub for <?php
namespace Drupal\Core\Field;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Field\FormatterInterface;
use Drupal\Core\Field\PluginSettingsBase;
/**
* @template TFieldItemList of \Drupal\Core\Field\FieldItemListInterface
* @implements FormatterInterface<TFieldItemList>
*/
abstract class FormatterBase extends PluginSettingsBase implements FormatterInterface, ContainerFactoryPluginInterface {
} In that case it is possible to tell PHPStan what types we are using: /**
* @extends FormatterBase<CommentFieldItemList>
*/
final class FooFormatter extends FormatterBase { It also looks like it's working well with generics as a type: /**
* @extends FormatterBase<FieldItemList<StringItem>>
*/
final class FooFormatter extends FormatterBase { |
@mglaman how to handle these test failures? It means we also should add stubs for |
@Niklan it means we need the missing stubs added. Making a stub requires providing stubs for any extends or implementation, even if they're empty. See https://github.com/mglaman/phpstan-drupal/blob/main/stubs/Drupal/Component/Plugin/PluginInspectionInterface.stub as an example |
/** | ||
* @template TFieldItemList of \Drupal\Core\Field\FieldItemListInterface | ||
*/ | ||
interface FormatterInterface { | ||
|
||
/** | ||
* @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $items | ||
* @param TFieldItemList $items | ||
* @param string $langcode | ||
* | ||
* @return array<int, array<int|string, mixed>> | ||
*/ | ||
public function viewElements(FieldItemListInterface $items, $langcode): array; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't we want
/**
* @template T of \Drupal\Core\Field\FieldItemInterface
*/
interface FormatterInterface extends PluginSettingsInterface {
so that way the T
template of the field type is properly passed down into FieldItemListInterface
/**
* @param \Drupal\Core\Field\FieldItemListInterface<T> $items
* @param string $langcode
*
* @return array
*/
public function viewElements(FieldItemListInterface $items, $langcode);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MissingMethodParameterTypehintRule
returns the error, we could make a test which calls it to see if this fixes. But there should be some kind of type test we can write instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the template name makes any difference; it's just a preference or an alias, as I understood it. It won't pass it to other inherited generics automatically anyway.
The reason I gave it such an explicit alias is that FormatterInterface
works not only with FieldItemListInterface
. There is a chance that in the future FieldDefinitionInterface
or FormStateInterface
(form object can be specified through it) might also require a generic template. In this case, TFieldItemList
will be self-documenting, while T
might cause some confusion.
I don't mind changing it to T
, as PHPStan allows changing that name freely; the only thing we should care about for BC is the order of templates. So when we need a second one, we can make the name clearer.
I fully agree about tests; I will try to create one.
# Conflicts: # stubs/Drupal/Core/Field/FormatterInterface.stub
I pushed |
Ah I overlooked this example:
So maybe it was the same all along and it comes down to style of writing generics? |
e158c6d
to
3b11f51
Compare
Note that it is possible to provide template defaults since phpstan/phpstan-src#3457 The FormatterInterface could have two templates l, one for the field item and one for the list, were the one for the list is optional and defaults to FieldItemListInterface
Formatters then could only provide the class for the TFieldItem template if they don't need methods of a more specific field item list interface. Not sure if I would use this feature though. E.g. I don't know how well this feature is supported by IDEs. |
I'm going to revert the commits I made and merge the original |
@mglaman I think your approach to testing generic is much better than mine. Maybe we should try to improve the test to cover more cases, and revert the generic definition back to its initial state. Since the problem version of the generic has been reverted and released, we can do this properly without rushing anything. |
@mglaman I've removed my tests, reverted the generic definition to its initial state (but with the changes you've made to Btw, how should the generic template be called? I've reverted it to The only downside to changing the name of the generic is that if someone adds it into the baseline or |
@mglaman also, I have a question regarding the For example, this one: /**
* @implements \Drupal\Core\Field\FormatterInterface<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem>
*/
class StringFormatter implements FormatterInterface {
public function settingsForm(array $form, FormStateInterface $form_state) {
return [];
}
public function settingsSummary() {
return [];
}
public function prepareView(array $entities_items) {
$first_item = $entities_items[0]->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
}
public function view(FieldItemListInterface $items, $langcode = NULL) {
assertType('Drupal\Core\Field\FieldItemListInterface<Drupal\Core\Field\Plugin\Field\FieldType\StringItem>', $items);
$first_item = $items->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
return [];
}
public function viewElements(FieldItemListInterface $items, $langcode) {
$first_item = $items->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
return [];
}
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return true;
}
public function getSettings() {
return [];
}
public function getSetting($key) {
return null;
}
public function setSetting($key, $value) {
return $this;
}
public function setSettings(array $settings) {
return $this;
}
public function getDefaultSettings() {
return [];
}
public function getPluginId() {
return 'string_formatter';
}
public function getPluginDefinition() {
return [];
}
public function getThirdPartySettings($provider = NULL) {
return [];
}
public function getThirdPartySetting($provider, $key, $default = NULL) {
return $default;
}
public function setThirdPartySetting($provider, $key, $value) {
return $this;
}
public function unsetThirdPartySetting($provider, $key) {
return $this;
}
public function getThirdPartyProviders() {
return [];
}
} could be simplified to /**
* @implements \Drupal\Core\Field\FormatterInterface<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem>
*/
class StringFormatter implements FormatterInterface {
public function prepareView(array $entities_items) {
$first_item = $entities_items[0]->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
}
public function view(FieldItemListInterface $items, $langcode = NULL) {
assertType('Drupal\Core\Field\FieldItemListInterface<Drupal\Core\Field\Plugin\Field\FieldType\StringItem>', $items);
$first_item = $items->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
return [];
}
public function viewElements(FieldItemListInterface $items, $langcode) {
$first_item = $items->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
return [];
}
} Yes, it's wrong from the PHP interpreter (and LSP) point of view because the class does not implement required methods. But it's not real PHP code; PHPStan doesn't care about it since all But doing it without "empty" methods just to please PHP LSP makes tests much smaller and clearer about what is actually tested. These empty and useless methods add too much noise and distract from what is actually tested. If I remove all such methods, the tests become so much smaller, like literally 238 lines of code to 111, basically twice as small, doing exactly the same thing. <?php
namespace DrupalFormatterInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FormatterInterface;
use function PHPStan\Testing\assertType;
/**
* @implements \Drupal\Core\Field\FormatterInterface<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem>
*/
class StringFormatter implements FormatterInterface {
public function prepareView(array $entities_items) {
$first_item = $entities_items[0]->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
}
public function view(FieldItemListInterface $items, $langcode = NULL) {
assertType('Drupal\Core\Field\FieldItemListInterface<Drupal\Core\Field\Plugin\Field\FieldType\StringItem>', $items);
$first_item = $items->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
return [];
}
public function viewElements(FieldItemListInterface $items, $langcode) {
$first_item = $items->first();
// Assert that the first item in the list is a StringItem
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $first_item);
return [];
}
}
/**
* @template T of \Drupal\Core\Field\FieldItemInterface
* @extends \Drupal\Core\Field\FormatterBase<T>
*/
class GenericExtendsFormatterBase extends FormatterBase {
/**
* @param \Drupal\Core\Field\FieldItemListInterface<T> $items
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$first = $items->first();
assertType('T of Drupal\Core\Field\FieldItemInterface (class DrupalFormatterInterface\GenericExtendsFormatterBase, argument)|null', $first);
return [];
}
}
/**
* @template T of \Drupal\Core\Field\FieldItemInterface
*/
class GenericFormatter implements FormatterInterface {
/**
* @param \Drupal\Core\Field\FieldItemListInterface<T> $items
*/
public function view(FieldItemListInterface $items, $langcode = NULL) {
$first = $items->first();
assertType('T of Drupal\Core\Field\FieldItemInterface (class DrupalFormatterInterface\GenericFormatter, argument)|null', $first);
return [];
}
/**
* @param \Drupal\Core\Field\FieldItemListInterface<T> $items
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$first = $items->first();
assertType('T of Drupal\Core\Field\FieldItemInterface (class DrupalFormatterInterface\GenericFormatter, argument)|null', $first);
return [];
}
}
/**
* Test usage of the formatter through a function.
*
* @param \Drupal\Core\Field\FormatterInterface<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem> $formatter
* @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem> $items
*/
function testFormatterWithStringItems(FormatterInterface $formatter, FieldItemListInterface $items): void {
$elements = $formatter->viewElements($items, 'en');
assertType('array<int, array<int|string, mixed>>', $elements);
// Test with implementation
$stringFormatter = new StringFormatter();
$elements = $stringFormatter->viewElements($items, 'en');
assertType('array<int, array<int|string, mixed>>', $elements);
$item = $items->first();
// The generic type from FormatterInterface should flow to $item
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $item);
}
/**
* Test that FormatterBase correctly implements FormatterInterface with generics.
*
* @param \Drupal\Core\Field\FormatterBase<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem> $formatter
* @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\Plugin\Field\FieldType\StringItem> $items
*/
function testFormatterBaseWithStringItems(FormatterBase $formatter, FieldItemListInterface $items): void {
$output = $formatter->view($items, 'en');
assertType('array<int|string, mixed>', $output);
$item = $items->first();
// The generic type from FormatterBase should flow to $item
assertType('Drupal\Core\Field\Plugin\Field\FieldType\StringItem|null', $item);
} I want your input on this. I won't delete anything for now. But what do you think about it? |
@mglaman I've updated the tests, and the last two of them are failing: They are failing very weirdly, for example, this one: /**
* @extends FormatterBase<FakeBooleanFieldItemList>
*/
class ExtendsBooleanItemFormatter extends FormatterBase {
public function prepareView(array $entities_items): void {
assertType('array<DrupalFormatterInterface\FakeBooleanFieldItemList>', $entities_items); Fails with an error:
For some reason, it resolves Another weird part is that it doesn't report problems in Maybe we faced this bug: phpstan/phpstan#9708, but I'm unsure, because it should report P.S. I have a feeling I'm doing tests wrong for that case. PHPStan itself does it completely different: https://github.com/phpstan/phpstan-src/tree/2.1.x/tests/PHPStan/Generics/data. I'm also thinking we should have such tests under the |
I'm all for moving to a Generics namespace for the data to keep it isolated |
@mglaman will move them next time. 👍 I found a pattern: when extending Making a stub that looks like this helps: <?php
namespace Drupal\Core\Field;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* @template TFieldItemList of Drupal\Core\Field\FieldItemListInterface
* @implements FormatterInterface<TFieldItemList>
*/
abstract class FormatterBase extends PluginSettingsBase implements FormatterInterface, ContainerFactoryPluginInterface {
/**
* @param TFieldItemList $items
*/
public function view(FieldItemListInterface $items, $langcode = NULL) {}
/**
* @param array<TFieldItemList> $items
*/
public function prepareView(array $entities_items) {}
} But I don't like it. It basically duplicates information from the As I understand it: the stub doesn't define these methods in any way → the original class has implementations of them, which takes priority over interface → PHPStan can't figure out the generic types in the PHPDoc for these methods. Original overrides are simple: /**
* {@inheritdoc}
*/
public function prepareView(array $entities_items) {}
/**
* {@inheritdoc}
*/
public function prepareView(array $entities_items) {} Assumption: this |
We also need to stub EntityReferenceFormatterBase |
@Niklan I think we're just going to have to be extra verbose in phpstan-drupal with our stubs until we can fix core, unfortunately. So basically we're going to be over-stubbing to avoid any confusion in PHPStan, correct? |
@mglaman sounds correct, simply because with overrides Drupal technically can "break" generics by forcing/reset the other types, but it should be detected in advance, so it's not a major problem. But yes, looks like we're going to stub much more than needed, because for now, it looks like the order how PHPStan is resolving generic is: stub → original → parent (inherited, class/interface) stubs → originals → repeat. If some data is missing in one, it looks it in the other. It all sounds logical, but the problem is that I can't reproduce it with PHPStan playground: https://phpstan.org/r/03d78262-0b50-469a-99dc-0ef4e1ee37f5. On the other hand, adding methods to the |
This one looks like a bug: phpstan-drupal/stubs/Drupal/Core/Field/FieldItemList.stub Lines 7 to 16 in be02a3c
Shouldn't it be: /**
* @return T
*/ |
I'm clicking "update branch" so we can get a granular check on the 11.x-dev fail – to see if it's 11.0, 11.1, 11.2.x-dev or stuff for 12.x |
💀 so it was something in 11.x |
…formatter-generic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thought, sorry. I just think the "small" change in ItemList stub is worth it's own PR release note line
*/ | ||
protected function createItem(int $offset = 0, ?mixed $value = NULL): \Drupal\Core\Field\FieldItemInterface { | ||
} | ||
protected function createItem(int $offset = 0, mixed $value = NULL): FieldItemInterface {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically it needs to stay ?mixed
? or does mixed
imply null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?mixed
is wrong from the syntax point of view, as it implies NULL
. I thought it could lead to some issues because this could lead to a wrong AST. But it clearly wasn't an issue, so I can revert it back, no problem. However, it would be better to consider removing the nullable type.
https://www.php.net/manual/en/language.types.mixed.php:
The mixed type accepts every value. It is equivalent to the union type
object|resource|array|string|float|int|bool|null
. Available as of PHP 8.0.0.
Converting both PRs to draft to work on: #846 (comment) |
From #822: This PR changes FormatterStub to be a proper generic instead of narrowing types for others.
Conflict with #844.