From 4781aeba174249fbf48f1fdfa92a8eadcef361f6 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 20:06:40 +0100 Subject: [PATCH 01/12] ProjectState.inc: Make ProjectState a readonly DTO --- pinc/ProjectState.inc | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/pinc/ProjectState.inc b/pinc/ProjectState.inc index a3e5e338a..ec3bbe0a8 100644 --- a/pinc/ProjectState.inc +++ b/pinc/ProjectState.inc @@ -2,27 +2,14 @@ class ProjectState { - public string $name; - public string $medium_label; - public string $label; - public int $forum; - public string $phase; - public string $star_metal; - public function __construct( - string $name, - string $medium_label, - string $label, - int $forum, - string $phase, - string $star_metal + public readonly string $name, + public readonly string $medium_label, + public readonly string $label, + public readonly int $forum, + public readonly string $phase, + public readonly string $star_metal ) { - $this->name = $name; - $this->medium_label = $medium_label; - $this->label = $label; - $this->forum = $forum; - $this->phase = $phase; - $this->star_metal = $star_metal; } } From 9426a061346b6049e4e2c69445f0bacc8573d988 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 20:09:40 +0100 Subject: [PATCH 02/12] LPage.inc: Make RoundInfo, LPage (mostly) readonly DTOs --- pinc/LPage.inc | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/pinc/LPage.inc b/pinc/LPage.inc index 28a48bfe8..356e8e324 100644 --- a/pinc/LPage.inc +++ b/pinc/LPage.inc @@ -264,15 +264,11 @@ function project_continuity_test(Project $project, string $orig_state, bool $no_ */ class RoundInfo { - public string $round_id; - public string $username; - public ?string $forum_user_id; - - public function __construct(string $round_id, string $username, ?string $forum_user_id) - { - $this->round_id = $round_id; - $this->username = $username; - $this->forum_user_id = $forum_user_id; + public function __construct( + public readonly string $round_id, + public readonly string $username, + public readonly ?string $forum_user_id + ) { } } @@ -288,20 +284,16 @@ class RoundInfo */ class LPage { - public Project $project; - public string $projectid; - public string $imagefile; - public string $page_state; - public int $reverting_to_orig; - public Round $round; - - public function __construct(Project $project, string $imagefile, string $page_state, int $reverting_to_orig) - { - $this->project = $project; + public readonly string $projectid; + public readonly Round $round; + + public function __construct( + public readonly Project $project, + public readonly string $imagefile, + public string $page_state, + public int $reverting_to_orig + ) { $this->projectid = $project->projectid; - $this->imagefile = $imagefile; - $this->page_state = $page_state; - $this->reverting_to_orig = $reverting_to_orig; // It's debatable whether LPage should have a 'reverting_to_orig' property. // You could argue that it's an interface-level artifact, From acd738da43572ee77d08b391aa5d10cb756942fc Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 20:26:31 +0100 Subject: [PATCH 03/12] Quiz.inc: Make QuizLevel and Quiz DTOs --- pinc/Quiz.inc | 55 +++++++++++++++++++-------------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/pinc/Quiz.inc b/pinc/Quiz.inc index 253df7cd1..0a2ad9c4d 100644 --- a/pinc/Quiz.inc +++ b/pinc/Quiz.inc @@ -5,24 +5,17 @@ include_once($relPath.'CharSuites.inc'); // CharSuiteSet // for groups of quizzes that go together class QuizLevel { - public string $level_id; - public string $level_name; // name for the category of quizzes - public string $activity_type; // "proof" or "format" - public string $info; // description or other info the user should know - /** @var Quiz[] */ - public array $quizzes; // the individual quizzes that the level contains - public static $map_quiz_level_id_to_QuizLevel = []; /** @param Quiz[] $quizzes */ - public function __construct(string $level_id, string $level_name, string $activity_type, string $info, array $quizzes) - { - $this->level_id = $level_id; - $this->level_name = $level_name; - $this->activity_type = $activity_type; - $this->info = $info; - $this->quizzes = $quizzes; - + public function __construct( + public readonly string $level_id, + public readonly string $level_name, // name for the category of quizzes + public readonly string $activity_type, // "proof" or "format" + public readonly string $info, // description or other info the user should kno + /** @var Quiz[] */ + public readonly array $quizzes // the individual quizzes that the level contains + ) { foreach ($quizzes as $quiz) { $quiz->activity_type = $activity_type; } @@ -34,17 +27,7 @@ class QuizLevel class Quiz { - public string $id; - public string $name; // appears in the top of the quiz's table - public string $short_name; // appears in the righthand frame during the quiz - public string $description; - public string $thread; // forum thread for quiz questions & comments, if other than the default public string $activity_type; // "proof" or "format" - public array $pages; - // Currently supported pass_requirements: - // ['maximum_age'] => time_in_seconds: - // Passes recorded longer than time_in_seconds ago are not valid. - public array $pass_requirements; /** @var string[] */ public static $valid_quiz_page_ids = []; @@ -53,16 +36,18 @@ class Quiz /** @var array */ public static $map_quiz_id_to_Quiz = []; - public function __construct(string $id, string $name, string $short_name, string $description, string $thread, array $pages, array $pass_requirements) - { - $this->id = $id; - $this->name = $name; - $this->short_name = $short_name; - $this->description = $description; - $this->thread = $thread; - $this->pages = $pages; - $this->pass_requirements = $pass_requirements; - + public function __construct( + public readonly string $id, + public readonly string $name, // appears in the top of the quiz's table + public readonly string $short_name, // appears in the righthand frame during the quiz + public readonly string $description, + public readonly string $thread, // forum thread for quiz questions & comments, if other than the default + public readonly array $pages, + // Currently supported pass_requirements: + // ['maximum_age'] => time_in_seconds: + // Passes recorded longer than time_in_seconds ago are not valid. + public readonly array $pass_requirements + ) { assert(!isset(Quiz::$map_quiz_id_to_Quiz[$id])); Quiz::$map_quiz_id_to_Quiz[$id] = & $this; From 8fa15ed00ccddea8f39604c0f9f1cc6b95bd07a0 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 20:47:36 +0100 Subject: [PATCH 04/12] ProjectSearchResults.inc: Make Column, TimeColumn, ColumnData DTOs --- pinc/ProjectSearchResults.inc | 52 ++++++++++++++++------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/pinc/ProjectSearchResults.inc b/pinc/ProjectSearchResults.inc index 0fa8385b4..c9abb6564 100644 --- a/pinc/ProjectSearchResults.inc +++ b/pinc/ProjectSearchResults.inc @@ -10,25 +10,18 @@ include_once($relPath.'genres.inc'); // maybe_create_temporary_genre_translatio class Column { - public string $id; - protected string $label; - private string $long_label; - private string $css_class; - private bool $default_display; - public bool $sortable; - protected string $db_column; private Settings $userSettings; private string $search_origin; - public function __construct(string $id, string $label, string $long_label, string $css_class, bool $default_display, bool $sortable, string $db_column) - { - $this->id = $id; - $this->label = $label; - $this->long_label = $long_label; - $this->css_class = $css_class; - $this->default_display = $default_display; - $this->sortable = $sortable; - $this->db_column = $db_column; + public function __construct( + public readonly string $id, + protected readonly string $label, + private readonly string $long_label, + private readonly string $css_class, + private readonly bool $default_display, + public readonly bool $sortable, + protected readonly string $db_column + ) { } public function get_label(): string @@ -141,12 +134,17 @@ class Column class TimeColumn extends Column { - private string $time_format; - - public function __construct(string $id, string $label, string $long_label, string $css_class, bool $default_display, bool $sortable, string $db_column, string $time_format) - { + public function __construct( + string $id, + string $label, + string $long_label, + string $css_class, + bool $default_display, + bool $sortable, + string $db_column, + private readonly string $time_format + ) { parent::__construct($id, $label, $long_label, $css_class, $default_display, $sortable, $db_column); - $this->time_format = $time_format; } public function get_label(): string @@ -401,17 +399,15 @@ class HoldColumn extends Column class ColumnData { - protected Settings $userSettings; - protected string $search_origin; public array $columns; - public function __construct(Settings $userSettings, string $search_origin, string $time_format = '') - { + public function __construct( + protected readonly Settings $userSettings, + protected readonly string $search_origin, + string $time_format = '' + ) { global $pguser; - $this->userSettings = $userSettings; - $this->search_origin = $search_origin; - $this->columns = [ new TitleColumn('title', _("Title"), '', 'left-align', true, true, 'nameofwork'), new Column("author", _('Author'), '', 'left-align', true, true, 'authorsname'), From 7c574f7ace70e1adaa7b8a469e21a0b2b7495970 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 20:48:22 +0100 Subject: [PATCH 05/12] filter_project_list.inc: Make Project{Search,Filter}Element DTOs --- pinc/filter_project_list.inc | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/pinc/filter_project_list.inc b/pinc/filter_project_list.inc index 92e274736..49db191c1 100644 --- a/pinc/filter_project_list.inc +++ b/pinc/filter_project_list.inc @@ -14,20 +14,17 @@ include_once($relPath.'Project.inc'); // get_project_difficulties() class ProjectFilterElement { - private string $id; - public string $label; private bool $active; - protected $data; - protected string $state_sql; public array $selected_options; - public function __construct($id, $label, $data, $state_sql, $active_fields) - { - $this->id = $id; - $this->label = $label; + public function __construct( + private readonly string $id, + public readonly string $label, + protected readonly mixed $data, + protected readonly string $state_sql, + array $active_fields + ) { $this->active = $active_fields[$id]; - $this->data = $data; - $this->state_sql = $state_sql; $this->selected_options = []; } @@ -293,13 +290,10 @@ function process_and_display_project_filter_form($username, $filter_type, $filte class ProjectSearchElement { - private string $id; - protected $data; - - public function __construct($id, $data) - { - $this->id = $id; - $this->data = $data; + public function __construct( + private readonly string $id, + protected readonly mixed $data + ) { } public function get_sql_component() From a2ee6c9dadaa439e6523f824117ca89e7718a645 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 20:59:35 +0100 Subject: [PATCH 06/12] Charsuites.inc: Make CharSuite DTO-ish --- pinc/CharSuites.inc | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pinc/CharSuites.inc b/pinc/CharSuites.inc index 9f1307f45..650f1cdb4 100644 --- a/pinc/CharSuites.inc +++ b/pinc/CharSuites.inc @@ -61,22 +61,18 @@ class PickerSet */ class CharSuite { - public string $name; - public string $title; - /** @var string[] */ - public ?array $codepoints; public ?string $description = null; - public bool $adhoc; public $reference_urls = []; private ?PickerSet $_pickerset = null; /** @param ?string[] $codepoints */ - public function __construct(string $name, string $title, ?array $codepoints = null, bool $adhoc = false) - { - $this->name = $name; - $this->title = $title; - $this->codepoints = $codepoints; - $this->adhoc = $adhoc; + public function __construct( + public readonly string $name, + public readonly string $title, + /** @var string[] */ + public ?array $codepoints = null, + public readonly bool $adhoc = false + ) { } public function __get(string $name): ?PickerSet From 4fee501d447a0b37b0346c41f8c1d9859022a9b8 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 21:10:57 +0100 Subject: [PATCH 07/12] ProjectTransition.inc: Make ProjectTransition use readonly properties --- pinc/ProjectTransition.inc | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/pinc/ProjectTransition.inc b/pinc/ProjectTransition.inc index 5315216a9..27abc74bd 100644 --- a/pinc/ProjectTransition.inc +++ b/pinc/ProjectTransition.inc @@ -11,10 +11,6 @@ define('PT_AUTO', '[AUTO]'); class ProjectTransition { - public string $from_state; - public string $to_state; - public string $who_restriction; - // Populated from $options private ?string $project_restriction; // callable public ?string $action_name; @@ -42,15 +38,11 @@ class ProjectTransition * - disabled_during_SR */ public function __construct( - string $from_state, - string $to_state, - string $who_restriction, + public readonly string $from_state, + public readonly string $to_state, + public readonly string $who_restriction, array $options ) { - $this->from_state = $from_state; - $this->to_state = $to_state; - $this->who_restriction = $who_restriction; - foreach ($options as $option_name => $option_value) { $this->$option_name = $option_value; } From c3103c6bdce7266b2939e12e63bb5155f2f21023 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 21:07:55 +0100 Subject: [PATCH 08/12] TableDocumentation.inc: Make TableDocumentation a DTO --- pinc/TableDocumentation.inc | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pinc/TableDocumentation.inc b/pinc/TableDocumentation.inc index 265b0ecc8..00f66b83d 100644 --- a/pinc/TableDocumentation.inc +++ b/pinc/TableDocumentation.inc @@ -2,13 +2,10 @@ class TableDocumentation { - private $display_name; - private $columns_information; - - public function __construct(string $display_name, array $columns_information) - { - $this->display_name = $display_name; - $this->columns_information = $columns_information; + public function __construct( + private readonly string $display_name, + private readonly array $columns_information + ) { } public function generate_documentation_table(): string From 2345c440eb419d54f248b957e8e4f15a169a6f10 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 21:08:19 +0100 Subject: [PATCH 09/12] Activity.inc: Make Activity a mostly readonly DTO --- pinc/Activity.inc | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/pinc/Activity.inc b/pinc/Activity.inc index 6215ceedd..90d666515 100644 --- a/pinc/Activity.inc +++ b/pinc/Activity.inc @@ -123,14 +123,6 @@ class Activities class Activity { - public string $id; - public string $name; - /** @var array */ - public array $access_minima; - public string $after_satisfying_minima; - public string $evaluation_criteria; - public ?string $access_change_callback; - /** * Activity constructor * @@ -166,21 +158,14 @@ class Activity * where $change is one of `[ "grant", "revoke", "request", "deny_request_for" ]` */ public function __construct( - string $id, - string $name, + public readonly string $id, + public readonly string $name, /** @var array */ - array $access_minima, - string $after_satisfying_minima, - string $evaluation_criteria, - ?string $access_change_callback + public array $access_minima, + public readonly string $after_satisfying_minima, + public readonly string $evaluation_criteria, + public readonly ?string $access_change_callback ) { - $this->id = $id; - $this->name = $name; - $this->access_minima = $access_minima; - $this->after_satisfying_minima = $after_satisfying_minima; - $this->evaluation_criteria = $evaluation_criteria; - $this->access_change_callback = $access_change_callback; - if (SiteConfig::get()->testing && !is_null($this->access_minima)) { /** @phpstan-ignore-line */ // Relax minima. foreach ($this->access_minima as $criterion_code => $minimum) { From 41eb84c8ff72020c2e30b637286aa06f11552531 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 21:13:12 +0100 Subject: [PATCH 10/12] Stage.inc: Make Stage a DTO --- pinc/Stage.inc | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/pinc/Stage.inc b/pinc/Stage.inc index d6db8ca88..2ed830049 100644 --- a/pinc/Stage.inc +++ b/pinc/Stage.inc @@ -20,10 +20,6 @@ class Stages extends Activities class Stage extends Activity { - public string $description; - public ?string $document; - public string $relative_url; - /** * Stage constructor * @@ -44,9 +40,9 @@ class Stage extends Activity string $after_satisfying_minima, string $evaluation_criteria, ?string $access_change_callback, - string $description, - ?string $document, - string $relative_url + public readonly string $description, + public readonly ?string $document, + public readonly string $relative_url ) { parent::__construct( $id, @@ -56,10 +52,6 @@ class Stage extends Activity $evaluation_criteria, $access_change_callback ); - - $this->description = $description; - $this->document = $document; - $this->relative_url = $relative_url; } From 5787a3552161f8a50580f93d7cb75ef5da364eb5 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 21:14:52 +0100 Subject: [PATCH 11/12] Pool.inc: Make Pool use readonly properties --- pinc/Pool.inc | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pinc/Pool.inc b/pinc/Pool.inc index 90ac5a402..195892bf3 100644 --- a/pinc/Pool.inc +++ b/pinc/Pool.inc @@ -15,10 +15,6 @@ class Pools extends Stages class Pool extends Stage { - public string $foo_Header; - public string $foo_field_name; - /** @var string[] */ - public array $blather; public string $project_available_state; public string $project_checkedout_state; /** @var string[] */ @@ -48,9 +44,10 @@ class Pool extends Stage ?string $access_change_callback, string $description, ?string $document, - string $foo_Header, - string $foo_field_name, - array $blather + public readonly string $foo_Header, + public readonly string $foo_field_name, + /** @var string[] */ + public readonly array $blather ) { parent::__construct( $id, @@ -68,10 +65,6 @@ class Pool extends Stage // $this->project_checkedout_state // $this->project_available_state // $this->states - - $this->foo_Header = $foo_Header; - $this->foo_field_name = $foo_field_name; - $this->blather = $blather; } } From 0a319436cdda69ab9786b78264521ec3f9083f57 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Wed, 1 Jul 2026 21:03:12 +0100 Subject: [PATCH 12/12] add_files.php: Use properties for Loader constructor args --- tools/project_manager/add_files.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tools/project_manager/add_files.php b/tools/project_manager/add_files.php index 6fad96c2f..68d40d27d 100644 --- a/tools/project_manager/add_files.php +++ b/tools/project_manager/add_files.php @@ -173,10 +173,6 @@ class Loader { - private string $source_project_dir; - private string $dest_project_dir; - private string $projectid; - private bool $allow_text_loads; private bool $adding_pages; private int $image_field_len; /** @var array */ @@ -193,13 +189,13 @@ class Loader private int $n_errors; private ImageUtils $checker; - public function __construct(string $source_project_dir, string $dest_project_dir, string $projectid, $allow_text_loads = true) - { - $this->source_project_dir = $source_project_dir; - $this->dest_project_dir = $dest_project_dir; - $this->projectid = $projectid; + public function __construct( + private readonly string $source_project_dir, + private readonly string $dest_project_dir, + private readonly string $projectid, + private readonly bool $allow_text_loads = true + ) { $this->checker = new ImageUtils(); - $this->allow_text_loads = $allow_text_loads; } // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX