Skip to content

[3.0] Writes class names as ::class resolvers rather than strings - #9665

Merged
Sesquipedalian merged 1 commit into
SimpleMachines:release-3.0from
albertlast:refactor/class-name-literals
Sep 8, 2026
Merged

Sesquipedalian merged 1 commit into
SimpleMachines:release-3.0from
albertlast:refactor/class-name-literals

Conversation

@albertlast

Copy link
Copy Markdown
Collaborator

Description

Follow-up to the review comment on #9661, which asked for a separate PR replacing every
class name written as a string with a ::class resolver.

A name in a string is not a name PHP ever checks. phplint sees a valid string,
php-cs-fixer does not read inside strings, and nothing resolves the name until the moment
it is used — which for a background task is a cron run on somebody else's forum, where
TaskRunner::execute() logs the class it could not find, drops the row and carries on.
Written as Tasks\CreatePost_Notify::class, the compiler resolves the name against the
file's imports, so a misspelling is a name that is simply not there, and an IDE renaming
the class renames this too.

103 literals across 47 files. Four are deliberately left alone, and each is the same kind of
thing: ActionRouter asks whether its own class sits under 'SMF\Actions', which is a
namespace and never a class; Sources/Actions/Admin/PackageManager.php names the alias
class_alias() is about to create, which is not a class until it does; and Punycode probes
for two Unicode helper functions, which have no resolver.

Nothing changes shape

A Class::method string is a callable, and in several places it is also a string that SMF
itself parses: Moderation\Main::buildRoute() does str_contains($area['function'], '::')
and then substr() to pull the class out, and Profile\Main falls an area's function back
into Utils::$context['sub_template']. So the class half becomes a resolver and the rest
stays a string:

'function' => PackageManager::class . '::call',
$row = filter_var($row, FILTER_CALLBACK, ['options' => Utils::class . '::cleanXml']);
ob_start(QueryString::class . '::rewriteAsQueryless');

[Foo::class, 'method'] and Foo::method(...) were both on the table and both were dropped:
they change the value's type, these tables travel through integrate_* hooks, and the
concatenation is what the neighbouring entries already look like
('function' => __NAMESPACE__ . '\Home::call'). Happy to switch if reviewers prefer one.

There is one deliberate difference in the whole diff. Upgrade.php queued
'SMF\Tasks\FetchSMfiles' when the class is FetchSMFiles; writing it as a resolver spells
it the only way it can be spelled, so this line also carries the fix in #9660. Whichever
merges second gets a one-line conflict.

One line reads fully qualified rather than imported. TrackIP.php already imports
SMF\Profile\Tracking, a class that does not exist anywhere in the tree, and that dead
import shadows Profile\Tracking, so \SMF\Actions\Profile\Tracking::class is spelled out.
Removing the dead import belongs in its own change.

How it was checked

The rewrite was mechanical, and so was the check on it. A script resolves every ::class
reference in the new tree the way PHP does — walking the file's namespace and its imports,
with an import beating the current namespace — and compares the result against the string
that used to be at that spot. Every one matches, the FetchSMfiles correction aside. That
caught a real bug on the first pass: Profile\Tracking::class in TrackIP.php had silently
become SMF\Profile\Tracking because of the import above.

Then, on the running forum: composer lint clean, php -l on all 47, composer test green
(350 tests), and a sweep of the pages this touches with nothing in log_errors — board
index, the admin areas for packages, membergroups, holidays, scheduled tasks and the error
log, profile drafts, tracking, moderation, reported posts, the three feeds, a board and a
topic. Posting a reply queued SMF\Tasks\CreatePost_Notify, byte for byte the string it
queued before, and cron ran it without complaint.

Issues References (Fixes|Related|Closes)

  1. Related to [3.0] Checks that every class name written as a string names something real #9661 — this is the separate PR asked for in review there.
  2. Related to Fix a minor capitalization error #9660Upgrade.php line 1229 carries that fix as a consequence.

🤖 Generated with Claude Code

A class name in a string is not a name PHP ever checks. Every one that names a
class SMF ships is now written as a ::class resolver, so the compiler resolves it
against the file's imports and a misspelling stops being invisible.

Where the value is a callable or a name that other code parses, the class half
becomes a resolver and the rest stays a string, so every value is byte for byte
what it was. The upgrader's FetchSMfiles is the exception: writing it as a
resolver spells it the only way it can be spelled, which is the fix in SimpleMachines#9660.

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