Problem/Motivation
AccountInterface::id()
's typehint states it returns an integer, but this is not true in many (all?) cases. Weakly typed as PHP is, uids stored on AccountProxy, UserSession, UserInterface may return the uid as a string. This prevents strongly typed comparison, e.g. $currentUser->id() === $ownerIdAsInteger
, if one is represented as an integer and the other a string. Or, it will fail strict type enforcement if passed to a method which expects an integer.
Steps to reproduce
Proposed resolution
Classes implementing AccountInterface::id() should ensure they return an integer as documented by the the API.
(Affirmed by Framework Managers see #43 and #44.
Remaining tasks
Framework manager decision on if a class implementing AccountIinterface::id() should:
- Return integers as documented in the API (cast string's returned by the DB layer to int)
- Change the API Spec for AccountInterface::id() to return string
Note:
In both cases we appear to need to add |null
as well.
User interface changes
None
API changes
Data model changes
None
Release notes snippet
Issue fork drupal-3224376
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
bradjones1The need for this is actually demonstrated in
UserSession::hasPermission()
, which does the check on uid 1 (crazy superuser access) and it casts$this->id()
to an integer to do strict type checking.Comment #4
bradjones1Comment #5
bradjones1EntityOwnerTrait
also updated. I thought maybe that change could utilize a null coalesce but that doesn't play well with the required typecasting. Perhaps someone smarter can refactor that to be even more terse.Comment #6
bradjones1Comment #7
bradjones1Comment #8
bradjones1Test failures are mostly type comparisons...not sure about the workflow test. But mostly unsurprising here.
Comment #9
bradjones1Comment #10
bradjones1Comment #11
daffie CreditAttribution: daffie commented@bradjones1: Thank yo for working on this issue. I have talked to user @catch on Slack about this issue. His suggestion is to update the docblock of
AccountInterface::id()
, so that it is saying that the return value is a string value instead of an integer value. User @catch is one of the core framework managers and one of the core release managers. My suggestion is to go with his suggestion. I will help this issue by reviewing the MR.Comment #12
bradjones1Comment #13
bradjones1I would like to make a case for perhaps doing this as a stopgap: Expand the docblock to return
int|string|null
, applying these fixes (with notes to remove in 10.x) and deprecating returning as string to 10.x.Berdir makes the above comment which I think illustrates the point well... without clearing this up (not implementing something to standardize on an integer) you can't do strict type comparison.
Weakly typed, if you do
$returnedStringId == 0
, that is true forNULL
as well as the anonymous user (0
).This could be done as a deprecation in 9.x with 10.x following the suggestion on the linked issue:
Comment #15
bradjones1A few of the changes in the originally-proposed MR were applied (unrelated) in #3138078: [D9.3 beta - w/c Nov 8, 2021] Add a 'void' return typehint to custom assert* methods.
Rebased MR against 9.4.x.
Comment #18
dpiAs #3324733: UserInterface::id has illegal return type hint in PHPDoc has revealed PHPStan [Strict] gets upset about the incompatibility of
\Drupal\Core\Entity\EntityInterface::id
and\Drupal\Core\Session\AccountInterface::id
, particularly the nullability part:Whether User::id returns a real integer, not a string-int, is one issue.
However it can return null, such as on creation.
Given User::id() in the MR can still return NULL, are we considering updating the docs for
AccountInterface::id()
?Comment #20
solideogloria CreditAttribution: solideogloria commentedI think
$id !== NULL ? (int) $id : NULL;
is the best option. I think it's better to avoid usingis_null()
and prefer instead to compare to the literalNULL
.Comment #21
DieterHolvoet CreditAttribution: DieterHolvoet at Minsky commentedBefore any more work is done here, maybe @catch or another core framework manager should chime in and decide if we want an integer or string return type, and if changing this should be done in a minor/major version with a deprecation path. So far I have only seen suggestions, no decisions.
Comment #22
solideogloria CreditAttribution: solideogloria commentedPersonally, I think an integer makes the most sense since it's numeric. The database fields are also integers.
Comment #23
dww+1 for standardizing on int, whenever / however that's possible. 😅 Would love to get formal decision from framework and/or release managers before we proceed. I'll ping some folks in Slack to try to get a decision. Meanwhile, tagging to be smashed.
Comment #24
larowlanLeft some thoughts, leaving the Needs FM tag because other FM's may feel differently
Comment #25
BerdirThe (content) entity system is not strictly typed, this has been discussed before but I don't see that this will change. The database returns all values as strings (and even if that would change, the database doesn't have a boolean type for example) and field API's are designed to use those values as-is when accessed without looking up definitions/types.
There are ways to get the type, jsonapi/rest does that that to a certain degree to get correct types, but that adds performance costs.
It's also not possible to really enforce this on UserInterface or AccountInterface with a return type because UserInterface extends EntityInterface and that can definitely return an integer or string depending on the entity type. So adding a type on UserInterface::id() would prevent us from ever adding the correct and wider return types on EntityInterface::id().
I'm not saying that we shouldn't improve this, but it's a far wider problem than just UserInterface::id() and my recommendation is in general to _not_ use strict type comparisons when working with content entities. It's now how modern PHP is generally written, but it reflects how the system works and it would require major changes to change that.
Comment #26
dwwFurther proof, if any more were needed, that #berdir-is-always-right. 😂 Was tempted to start an issue tag for that, sorta surprised one doesn't already seem to exist. 😉
I think there's a typo in the last sentence (s/now/not/), but otherwise, #25 is a fairly convincing argument that we can't actually fix this here like this. Leaning towards won't fix, unless we can come up with a new scope / title / summary for a way to improve this that doesn't have all the problems @Berdir just spelled out...
Comment #27
cmlaraAn extended interface can return a more narrow list of types.
Is acceptable.
We have a coding standard for declare(strict_types=1); now and more and more code is using it. Also with us using phpstan as part of the contrib gitlab_templates this is already extremely painful to deal with.
If were not going to have UserInterface::id() return an integer as its API specifies, than we should be looking at changing its return type to be a string so that tooling can properly work with it.
Comment #28
solideogloria CreditAttribution: solideogloria commentedI agree with @cmlara. Proper programming practice is to accept the broadest type possible for parameters, and return the most specific type possible for return value types. If you know it will always be
?int
, even when the parent isstring|int|null
, it's okay to return?int
.Currently, PHPCS is confused when using the return values of the
UserInterface::id()
function, because it says a string value will be returned.Comment #29
Berdir> An extended interface can return a more narrow list of types.
Noted, I always mix that up, it's the other way round that doesn't work.
I agree about strict typing in theory. My point is that trying to force code working with API's that aren't using strict types to be strictly typed is in my opinion going to cause more bugs than avoid them. That's certainly been my experience, I've had to fix plenty of bugs that expected certain types which weren't actually guaranteed over the years.
Either way, the patch definitely needs work, per review from @larowlan. All those places definitely can be NULL and that needs to be documented, and the documentation needs to go on UserInterface and AccountInterface, not User.
Comment #30
cmlaraRemoving rescope as this still needs a framework manager decisions since the blocker from #25 doesn't exist.
Updated IS to show the options that are awaiting a framework manager decision.
Comment #31
dpiAn anecdote,
Two years ago on a large project I implemented the following trait on the bundle classes of Node, User, Media, and others. For which we have bundle classes of all bundle types. It coerces IDs to integers, so we may confidently set our return types and documentation to
int|null
and have stricter static analysis.With this change, I would not consider the effort required to get things working arduous. I recall #3318453: Access to TFA page is denied depending on entity/database internals may have been the only contrib issue raised.
Hopefully this improves confidence merging proposed changes.
Comment #32
cmlaraApparently this isn't always the case either, and is (according to issue statements) not the native default for any of the PDO database layers we work with currently.
Return values for the UID field will only be strings if the database PDO options do not override \PDO::ATTR_STRINGIFY_FETCHES. I concede it is fair to believe this would be exceedingly rare, and may have unexpected adverse impacts including breaking sites however it is (at least in the MySQL driver) not enforced as a mandatory option.
Related Context:
#3224245: Open MySQL connection using \PDO::ATTR_STRINGIFY_FETCHES
https://git.drupalcode.org/project/drupal/-/merge_requests/937/diffs?com...
EDIT: I missed that comments #12 and #13 mentioned this already in 2021.
Comment #33
andypostAccording to docs it should add commented-out type-hints https://www.drupal.org/about/core/policies/core-change-policies/drupal-d...
Comment #34
bradjones1Re: #32 I think the return value from the DB is interesting but not instructive on what we should do here. Entity types e.g. user choose the format of their primary key/ID and if it's not changeable, we should typehint the value to be what it's intended to be. The fact it comes out of the DB query as a string (or maybe not!) is something to be normalized in the entity's code.
Comment #35
catchI can't remember the exact slack conversation with @daffie but my general feeling is if we're going to start casting database results, we should open a meta issue and also do that for nodes, taxonomy, comments, menu link content etc. etc. since they all have the same problem for IDs. There'll then be other base fields that could also be more strictly typed etc. so this is a not a small one-off task but a significant project that needs to be coordinated across core entities.
But... I also think the docs should reflect reality - whether that means casting here as a one-off/first of the meta issues, or updating things to reflect reality then changing them again later, I don't have a strong opinion.
Comment #36
ptmkenny CreditAttribution: ptmkenny as a volunteer commentedI strongly advocate for fixing this specific issue with a cast now because it breaks strict type enforcement, as mentioned in the OP. It's very frustrating to write several custom modules, run a bunch of tests in phpstan and everything checks out, and then when you actually use your site you start getting WSOD because the typehint in core was incorrect.
Comment #37
casey CreditAttribution: casey at SWIS commentedWhat about changing the return type/hint of EntityInterface::id() and all other ID parameters//returns to ?string and dropping int altogether as possible return type.
IDs are used to uniquely identify entities and not to do calculations with or anything.
Dropping int as possible type for entity IDs will simplify the interface and makes it easier to enforce strict typing (https://www.drupal.org/project/drupal/issues/3050720#comment-13562649)
It will still be possible to do things like auto increment (no issue at all when handled by the DB) and finding the next available (numeric) ID.
I think almost all existing code will continue to work; as already stated, the type of ID variables are already inconsistent.
Comment #38
solideogloria CreditAttribution: solideogloria commentedI don't think that makes sense. We need to work towards strict typing with a straightforward and common-sense standard. It's not a good idea to standardize returning all integers stored in the database as strings.
We shouldn't have to be unsure if we can cast an ID to
int
and add one. If it's a string, users will likely and up being unsure if it's guaranteed to be numeric or not.Comment #39
bradjones1It seems the overarching question is one of storage vs. runtime code contracts. I kind of think about this in the same mindset as bundle classes were introduced. If you have a field on an entity that represents a
commerce_price
item, for instance, you can introduce a getter that will return?Price
. It's otherwise only accessible by doing something likenew Price($entity->field->first->value)
, which is silly boilerplate and fragile.Similarly, if the ID of an entity has a field (and, by extension, database storage) property schema that is designed to always return an integer, then it's only appropriate for the entity class to return it as such, regardless of the underlying db handling.
Also by way of example, I recently worked on JSON data storage which introduces similar wrinkles - you can choose at a low level to cast return values as strings, or retrieve them in the native type they're stored in the JSON document, but how and how reliably that is done is a matter of database implementation specifics. You would still want to assert and return typed values in userland code for maximum type safety and portability across drivers.
Furthermore, the user entity has an even more compelling use case here, which is that
0
(zero) is a valid special-case return value for the anonymous user. Thus strict typing helps make it even clearer that!$user
is unsafe because it matches both a null case and the anonymous user. While you could still write that bad code,?string
for a return type is non-specific and doesn't move us forward.?int
is at least true in a very specific way - and for methods that should always expect a user (which should be almost always the case) then it can be tightened to onlyint
with no inheritance issues.Comment #40
cmlaraUltimately, yes, Drupal Core does need an issue to handle casting. We will either forcibly hit this with the return typehints+strict_types initiatives, PHPStan initiative, or just PHP no longer allowing coercion on native method calls. This issue to be int or string will need casting one way or the other (as noted above regarding how the DB returns results as both as such we must cast no matter the decision)
Having witnessed how hard it has been to remove REQUEST_TIME from core which is a simple change the odds are that fixing these issues globally across core is likely to be single method or at most a single Interface at a time.
While the number of lines changed for this issue are small (at the moment) UID's are so widely used across the ecosystem we would likely always want this to be a 'single method' only change.
Comment #41
solideogloria CreditAttribution: solideogloria commentedHopefully, once impacted users change one method, they will see the upcoming pipeline of changes and change in bulk what they need to ahead of time, if possible.
For example, if someone is doing
$user->id() === '1'
in their code, and doing the same thing with other entities, they can make a change across their codebase to use== 1
instead, with the eventual possibility of using=== 1
.Comment #42
bradjones1Re: #40, to put a finer point on it, this kind of issue is complementary to PHPstan/static analysis initiatives in that it makes the code better reflect reality and support meaningful static analysis of custom code e.g. UID comparison.
Also linking a long-lived but important UID 1 issue: #540008: Add a container parameter that can remove the special behavior of UID#1
Comment #43
larowlanI think #35 feels like the best approach here as follows:
This issue would be a meta of the first one I assume, but we could split out docs work in the the later to improve the life of those using stricter phpstan rules.
Comment #44
cmlaraPer SLACK https://drupal.slack.com/archives/C1BMUQ9U6/p1707856521514239
This issue will continue enforcing the strict integer return under #3441689: [META] Stronger Typing for Entity API though it also falls under #3441690: [META] Update PHPDocs to reflect reality to fix the Docs.
Removing framework manager tag
Leaving as Needs Work to process through the MR comments.
Comment #45
bradjones1Woohoo! Marking this as a task, not a bug. I have some sponsored dev time for core these days so I can loop around on this.