Skip to content

[3.0] Read cached member data back the way it was written - #9628

Merged
Sesquipedalian merged 1 commit into
SimpleMachines:release-3.0from
albertlast:3.0/cache-user
Sep 5, 2026
Merged

Sesquipedalian merged 1 commit into
SimpleMachines:release-3.0from
albertlast:3.0/cache-user

Conversation

@albertlast

Copy link
Copy Markdown
Collaborator

Description

Two faults in the same block of User::loadUserData(), one hiding the other.

The branch for the current user reads

if (($data = CacheApi::get('user_settings-' . $id, 60) == null)) {

which closes the assignment around the comparison, so $data is handed a bool rather than the cached profile, and the is_array() guard below throws it away. The user_settings cache has therefore never been read since this was rewritten as a class. 2.1 has the parentheses in the right place:

if (empty($cache_enable) || $cache_enable < 2 || ($user_settings = cache_get_data('user_settings-' . $id_member, 60)) == null)

Nothing looked wrong, because a cache that is never read is only a cache that never helps.

With that corrected, the line below it starts throwing for everyone rather than only for other members at cache level 3:

SMF\UserDataset::tryFrom(): Argument #1 ($value) must be of type string, SMF\UserDataset given

What goes into the cache is self::$profiles[$id], whose dataset entry is the enum itself — the line a few lines above calls ->includes() on it — so tryFrom() is being handed back exactly what was stored. At cache level 3 this took out the board index, any board and any topic on the second request, the first having filled the cache that the second one read. It surfaces as a 500 with Undefined array key "categories" behind it in BoardIndex.template.php.

An entry that has no dataset at all still needs the fallback, so the conversion stays for that case and is skipped when there is already an enum.

How this was checked

By hand against a running forum: at $cache_enable = 3, the board index returns 200 on the first request and 500 on every one after it, and smf_log_errors records the tryFrom() TypeError with loadUserData in the backtrace. After the fix, repeated requests to the board index, a board and a topic all return 200 and render byte-identical to the same pages with caching switched off.

Notes for review

No regression test. User::loadUserData() needs Db::$db, and the unit suite has no database connection, so this is not reachable from it. Per AGENTS.md I am saying so rather than contorting the code or faking a connection to force it under test.

Sources/User.php is touched by several open PRs, including #9302 and the external authentication series. None of them touches this block — I checked every open PR that changes the file for user_settings-, member_data- or UserDataset::tryFrom and none has a hit.

Issues References (Fixes|Related|Closes)

  1. No issue filed; found while testing whether the cache accelerators still work. Happy to file one if preferred.

🤖 Generated with Claude Code

Two faults in the same block of User::loadUserData(), one hiding the other.

The branch for the current user reads

  if (($data = CacheApi::get('user_settings-' . $id, 60) == null))

which closes the assignment around the comparison, so $data is handed a bool
rather than the cached profile and the is_array() guard below throws it away.
The user_settings cache has therefore never been read since this was rewritten
as a class; 2.1 has the parentheses in the right place. Nothing looked wrong,
because a cache that is never read is only a cache that never helps.

With that corrected, the line below it starts fatalling for everyone rather
than only for other members at cache level 3:

  SMF\UserDataset::tryFrom(): Argument #1 ($value) must be of type string,
  SMF\UserDataset given

What goes into the cache is self::$profiles[$id], whose dataset entry is the
enum itself, so tryFrom() is being handed back exactly what was stored. At
level 3 this took out the board index, any board and any topic on the second
request, the first having populated the cache the second one read.

An entry that has no dataset at all still needs the fallback, so the
conversion stays for that case and is skipped when there is already an enum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
@Sesquipedalian
Sesquipedalian merged commit 9fbc8b8 into SimpleMachines:release-3.0 Sep 5, 2026
9 checks passed
@jdarwood007 jdarwood007 added this to the 3.0 Alpha 5 milestone Sep 6, 2026
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.

3 participants