From ba7fb5e15a30d82d14b9a702dcc1289d9d29d742 Mon Sep 17 00:00:00 2001
From: flashwave <me@flash.moe>
Date: Wed, 7 Feb 2024 00:04:45 +0000
Subject: [PATCH] Added lazy database object creation.

---
 LICENSE                                   |   2 +-
 composer.lock                             | 244 ++++++++++------------
 public-legacy/manage/general/emoticon.php |   2 +-
 public-legacy/manage/general/logs.php     |   2 +-
 public-legacy/manage/users/user.php       |   4 +-
 public-legacy/profile.php                 |   2 +-
 public-legacy/settings/account.php        |   2 +-
 public-legacy/settings/logs.php           |   4 +-
 src/AuditLog/AuditLog.php                 |   9 +-
 src/AuditLog/AuditLogInfo.php             |  30 +--
 src/Auth/LoginAttemptInfo.php             |  34 +--
 src/Auth/LoginAttempts.php                |  10 +-
 src/Auth/SessionInfo.php                  |  54 ++---
 src/Auth/Sessions.php                     |  12 +-
 src/Changelog/ChangeInfo.php              |  30 +--
 src/Changelog/Changelog.php               |  12 +-
 src/Changelog/ChangelogRoutes.php         |  14 +-
 src/Comments/Comments.php                 |  24 +--
 src/Comments/CommentsCategoryInfo.php     |  30 +--
 src/Comments/CommentsPostInfo.php         |  70 ++++---
 src/Counters/CounterInfo.php              |  18 +-
 src/Counters/Counters.php                 |  10 +-
 src/Emoticons/EmoteInfo.php               |  22 +-
 src/Emoticons/EmoteStringInfo.php         |  18 +-
 src/Emoticons/Emotes.php                  |  22 +-
 src/Forum/ForumCategories.php             |  33 +--
 src/Forum/ForumCategoryInfo.php           |  66 +++---
 src/Forum/ForumPostInfo.php               |  50 +++--
 src/Forum/ForumPosts.php                  |  12 +-
 src/Forum/ForumTopicInfo.php              |  54 ++---
 src/Forum/ForumTopicRedirectInfo.php      |  22 +-
 src/Forum/ForumTopicRedirects.php         |  12 +-
 src/Forum/ForumTopics.php                 |  12 +-
 src/Home/HomeRoutes.php                   |   6 +-
 src/Messages/MessageInfo.php              |  54 ++---
 src/Messages/MessagesDatabase.php         |  12 +-
 src/News/News.php                         |  24 +--
 src/News/NewsCategoryInfo.php             |  30 +--
 src/News/NewsPostInfo.php                 |  50 +++--
 src/Perms/PermissionInfo.php              |  32 +--
 src/Perms/Permissions.php                 |   4 +-
 src/Profile/ProfileFieldFormatInfo.php    |  26 ++-
 src/Profile/ProfileFieldInfo.php          |  26 ++-
 src/Profile/ProfileFieldValueInfo.php     |  22 +-
 src/Profile/ProfileFields.php             |  67 +++---
 src/Users/BanInfo.php                     |  38 ++--
 src/Users/Bans.php                        |  12 +-
 src/Users/ModNoteInfo.php                 |  30 +--
 src/Users/ModNotes.php                    |  12 +-
 src/Users/RoleInfo.php                    |  42 ++--
 src/Users/Roles.php                       |  12 +-
 src/Users/UserInfo.php                    |  90 ++++----
 src/Users/Users.php                       |  12 +-
 src/Users/WarningInfo.php                 |  26 ++-
 src/Users/Warnings.php                    |  20 +-
 55 files changed, 772 insertions(+), 817 deletions(-)

diff --git a/LICENSE b/LICENSE
index 416a3731..2d8375cc 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017-2023, flashwave <me@flash.moe>
+Copyright (c) 2017-2024, flashwave <me@flash.moe>
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/composer.lock b/composer.lock
index 530ff45d..c1b2e6ec 100644
--- a/composer.lock
+++ b/composer.lock
@@ -8,16 +8,16 @@
     "packages": [
         {
             "name": "chillerlan/php-qrcode",
-            "version": "4.4.0",
+            "version": "4.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/chillerlan/php-qrcode.git",
-                "reference": "52889cd7ab1b78e6a345edafe24aa74bc5becc08"
+                "reference": "f5e243f3b61a60934780579430a951460f40888d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/52889cd7ab1b78e6a345edafe24aa74bc5becc08",
-                "reference": "52889cd7ab1b78e6a345edafe24aa74bc5becc08",
+                "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/f5e243f3b61a60934780579430a951460f40888d",
+                "reference": "f5e243f3b61a60934780579430a951460f40888d",
                 "shasum": ""
             },
             "require": {
@@ -27,10 +27,10 @@
             },
             "require-dev": {
                 "phan/phan": "^5.4",
-                "phpmd/phpmd": "^2.13",
+                "phpmd/phpmd": "^2.15",
                 "phpunit/phpunit": "^9.6",
                 "setasign/fpdf": "^1.8.2",
-                "squizlabs/php_codesniffer": "^3.7"
+                "squizlabs/php_codesniffer": "^3.8"
             },
             "suggest": {
                 "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
@@ -73,7 +73,7 @@
             ],
             "support": {
                 "issues": "https://github.com/chillerlan/php-qrcode/issues",
-                "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.0"
+                "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.1"
             },
             "funding": [
                 {
@@ -85,20 +85,20 @@
                     "type": "ko_fi"
                 }
             ],
-            "time": "2023-11-23T23:53:20+00:00"
+            "time": "2024-01-06T16:56:58+00:00"
         },
         {
             "name": "chillerlan/php-settings-container",
-            "version": "3.1.0",
+            "version": "3.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/chillerlan/php-settings-container.git",
-                "reference": "4d02944424fa1f48abca96353257c93cbac856c1"
+                "reference": "c41e89f8bf963d1e88584a47fb78d1cd204b6e2a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/4d02944424fa1f48abca96353257c93cbac856c1",
-                "reference": "4d02944424fa1f48abca96353257c93cbac856c1",
+                "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/c41e89f8bf963d1e88584a47fb78d1cd204b6e2a",
+                "reference": "c41e89f8bf963d1e88584a47fb78d1cd204b6e2a",
                 "shasum": ""
             },
             "require": {
@@ -107,9 +107,9 @@
             },
             "require-dev": {
                 "phan/phan": "^5.4",
-                "phpmd/phpmd": "^2.13",
-                "phpunit/phpunit": "^10.2",
-                "squizlabs/php_codesniffer": "^3.7"
+                "phpcsstandards/php_codesniffer": "^3.8",
+                "phpmd/phpmd": "^2.15",
+                "phpunit/phpunit": "^10.5"
             },
             "type": "library",
             "autoload": {
@@ -150,31 +150,31 @@
                     "type": "ko_fi"
                 }
             ],
-            "time": "2023-07-17T20:46:37+00:00"
+            "time": "2024-01-05T23:55:20+00:00"
         },
         {
             "name": "doctrine/lexer",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/lexer.git",
-                "reference": "84a527db05647743d50373e0ec53a152f2cde568"
+                "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/lexer/zipball/84a527db05647743d50373e0ec53a152f2cde568",
-                "reference": "84a527db05647743d50373e0ec53a152f2cde568",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
+                "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
                 "shasum": ""
             },
             "require": {
                 "php": "^8.1"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^10",
-                "phpstan/phpstan": "^1.9",
-                "phpunit/phpunit": "^9.5",
+                "doctrine/coding-standard": "^12",
+                "phpstan/phpstan": "^1.10",
+                "phpunit/phpunit": "^10.5",
                 "psalm/plugin-phpunit": "^0.18.3",
-                "vimeo/psalm": "^5.0"
+                "vimeo/psalm": "^5.21"
             },
             "type": "library",
             "autoload": {
@@ -211,7 +211,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/lexer/issues",
-                "source": "https://github.com/doctrine/lexer/tree/3.0.0"
+                "source": "https://github.com/doctrine/lexer/tree/3.0.1"
             },
             "funding": [
                 {
@@ -227,7 +227,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-12-15T16:57:16+00:00"
+            "time": "2024-02-05T11:56:58+00:00"
         },
         {
             "name": "egulias/email-validator",
@@ -352,7 +352,7 @@
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/index.git",
-                "reference": "e31781c69f0b13fe251771c8e7e529222630a44f"
+                "reference": "73051dc71ee2d0045e5dbe5d846bb665a8b1c39c"
             },
             "require": {
                 "ext-mbstring": "*",
@@ -390,7 +390,7 @@
             ],
             "description": "Composer package for the common library for my projects.",
             "homepage": "https://railgun.sh/index",
-            "time": "2023-11-20T19:01:19+00:00"
+            "time": "2024-02-06T23:52:46+00:00"
         },
         {
             "name": "flashwave/sasae",
@@ -398,7 +398,7 @@
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/sasae.git",
-                "reference": "b56dd222acb8f138729e6258d4a90bbb8401ff52"
+                "reference": "c8a9f2974e6591215b3f898dd5525de1e8367f66"
             },
             "require": {
                 "flashwave/index": "dev-master",
@@ -431,7 +431,7 @@
             ],
             "description": "A wrapper for Twig with added common functionality.",
             "homepage": "https://railgun.sh/sasae",
-            "time": "2023-11-20T19:09:35+00:00"
+            "time": "2024-01-04T02:13:42+00:00"
         },
         {
             "name": "flashwave/syokuhou",
@@ -439,7 +439,7 @@
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/syokuhou.git",
-                "reference": "fdf3c38cc216bf7024af331cbe1758532355c22f"
+                "reference": "c1fe9371ada20fcea51c225cc53b9ceae4642bc4"
             },
             "require": {
                 "flashwave/index": "dev-master",
@@ -470,7 +470,7 @@
             ],
             "description": "Configuration library for PHP.",
             "homepage": "https://railgun.sh/syokuhou",
-            "time": "2023-11-20T19:10:04+00:00"
+            "time": "2024-01-04T02:12:49+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
@@ -649,16 +649,16 @@
         },
         {
             "name": "matomo/device-detector",
-            "version": "6.2.0",
+            "version": "6.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/matomo-org/device-detector.git",
-                "reference": "3577abbfea71eaf88d4cd432274428c39601754f"
+                "reference": "19138b0c4b9ddf4ffd8e423d6af3764b7317cb0b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/3577abbfea71eaf88d4cd432274428c39601754f",
-                "reference": "3577abbfea71eaf88d4cd432274428c39601754f",
+                "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/19138b0c4b9ddf4ffd8e423d6af3764b7317cb0b",
+                "reference": "19138b0c4b9ddf4ffd8e423d6af3764b7317cb0b",
                 "shasum": ""
             },
             "require": {
@@ -670,8 +670,8 @@
             },
             "require-dev": {
                 "matthiasmullie/scrapbook": "^1.4.7",
-                "mayflower/mo4-coding-standard": "^v8.0.0",
-                "phpstan/phpstan": "^0.12.52",
+                "mayflower/mo4-coding-standard": "^v9.0.0",
+                "phpstan/phpstan": "^1.10.44",
                 "phpunit/phpunit": "^8.5.8",
                 "psr/cache": "^1.0.1",
                 "psr/simple-cache": "^1.0.1",
@@ -714,7 +714,7 @@
                 "source": "https://github.com/matomo-org/matomo",
                 "wiki": "https://dev.matomo.org/"
             },
-            "time": "2023-11-15T09:44:42+00:00"
+            "time": "2024-01-05T09:03:21+00:00"
         },
         {
             "name": "mustangostang/spyc",
@@ -1128,16 +1128,16 @@
         },
         {
             "name": "sentry/sentry",
-            "version": "4.1.0",
+            "version": "4.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/getsentry/sentry-php.git",
-                "reference": "89666f297891ff937fceb2f3d1fb967a6848cf37"
+                "reference": "a6e06f0b7a17e7f68e11297427da76bfe01a3ca3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/89666f297891ff937fceb2f3d1fb967a6848cf37",
-                "reference": "89666f297891ff937fceb2f3d1fb967a6848cf37",
+                "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/a6e06f0b7a17e7f68e11297427da76bfe01a3ca3",
+                "reference": "a6e06f0b7a17e7f68e11297427da76bfe01a3ca3",
                 "shasum": ""
             },
             "require": {
@@ -1161,7 +1161,7 @@
                 "phpbench/phpbench": "^1.0",
                 "phpstan/phpstan": "^1.3",
                 "phpunit/phpunit": "^8.5.14|^9.4",
-                "symfony/phpunit-bridge": "^5.2|^6.0",
+                "symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
                 "vimeo/psalm": "^4.17"
             },
             "suggest": {
@@ -1201,7 +1201,7 @@
             ],
             "support": {
                 "issues": "https://github.com/getsentry/sentry-php/issues",
-                "source": "https://github.com/getsentry/sentry-php/tree/4.1.0"
+                "source": "https://github.com/getsentry/sentry-php/tree/4.5.0"
             },
             "funding": [
                 {
@@ -1213,7 +1213,7 @@
                     "type": "custom"
                 }
             ],
-            "time": "2023-12-04T12:41:21+00:00"
+            "time": "2024-01-29T16:16:10+00:00"
         },
         {
             "name": "symfony/deprecation-contracts",
@@ -1284,16 +1284,16 @@
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v7.0.0",
+            "version": "v7.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "c459b40ffe67c49af6fd392aac374c9edf8a027e"
+                "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c459b40ffe67c49af6fd392aac374c9edf8a027e",
-                "reference": "c459b40ffe67c49af6fd392aac374c9edf8a027e",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e",
+                "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e",
                 "shasum": ""
             },
             "require": {
@@ -1344,7 +1344,7 @@
             "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.0"
+                "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3"
             },
             "funding": [
                 {
@@ -1360,7 +1360,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-27T16:29:09+00:00"
+            "time": "2024-01-23T15:02:46+00:00"
         },
         {
             "name": "symfony/event-dispatcher-contracts",
@@ -1440,16 +1440,16 @@
         },
         {
             "name": "symfony/mailer",
-            "version": "v6.4.0",
+            "version": "v6.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mailer.git",
-                "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba"
+                "reference": "74412c62f88a85a41b61f0b71ab0afcaad6f03ee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mailer/zipball/ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba",
-                "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba",
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/74412c62f88a85a41b61f0b71ab0afcaad6f03ee",
+                "reference": "74412c62f88a85a41b61f0b71ab0afcaad6f03ee",
                 "shasum": ""
             },
             "require": {
@@ -1500,7 +1500,7 @@
             "description": "Helps sending emails",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/mailer/tree/v6.4.0"
+                "source": "https://github.com/symfony/mailer/tree/v6.4.3"
             },
             "funding": [
                 {
@@ -1516,20 +1516,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-11-12T18:02:22+00:00"
+            "time": "2024-01-29T15:01:07+00:00"
         },
         {
             "name": "symfony/mime",
-            "version": "v7.0.0",
+            "version": "v7.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "0a2fff95c1a10df97f571d67e76c7ae0f0d4f535"
+                "reference": "c1ffe24ba6fdc3e3f0f3fcb93519103b326a3716"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/0a2fff95c1a10df97f571d67e76c7ae0f0d4f535",
-                "reference": "0a2fff95c1a10df97f571d67e76c7ae0f0d4f535",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/c1ffe24ba6fdc3e3f0f3fcb93519103b326a3716",
+                "reference": "c1ffe24ba6fdc3e3f0f3fcb93519103b326a3716",
                 "shasum": ""
             },
             "require": {
@@ -1583,7 +1583,7 @@
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v7.0.0"
+                "source": "https://github.com/symfony/mime/tree/v7.0.3"
             },
             "funding": [
                 {
@@ -1599,7 +1599,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-10-19T14:20:43+00:00"
+            "time": "2024-01-30T08:34:29+00:00"
         },
         {
             "name": "symfony/options-resolver",
@@ -1670,16 +1670,16 @@
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.28.0",
+            "version": "v1.29.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
+                "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
-                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
+                "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
                 "shasum": ""
             },
             "require": {
@@ -1693,9 +1693,6 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -1732,7 +1729,7 @@
                 "portable"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
             },
             "funding": [
                 {
@@ -1748,20 +1745,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-26T09:26:14+00:00"
+            "time": "2024-01-29T20:11:03+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.28.0",
+            "version": "v1.29.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "ecaafce9f77234a6a449d29e49267ba10499116d"
+                "reference": "a287ed7475f85bf6f61890146edbc932c0fff919"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d",
-                "reference": "ecaafce9f77234a6a449d29e49267ba10499116d",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919",
+                "reference": "a287ed7475f85bf6f61890146edbc932c0fff919",
                 "shasum": ""
             },
             "require": {
@@ -1774,9 +1771,6 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -1819,7 +1813,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0"
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0"
             },
             "funding": [
                 {
@@ -1835,20 +1829,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-26T09:30:37+00:00"
+            "time": "2024-01-29T20:11:03+00:00"
         },
         {
             "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.28.0",
+            "version": "v1.29.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
+                "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
-                "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
+                "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
                 "shasum": ""
             },
             "require": {
@@ -1859,9 +1853,6 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -1903,7 +1894,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
             },
             "funding": [
                 {
@@ -1919,20 +1910,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-26T09:26:14+00:00"
+            "time": "2024-01-29T20:11:03+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.28.0",
+            "version": "v1.29.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "42292d99c55abe617799667f454222c54c60e229"
+                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
-                "reference": "42292d99c55abe617799667f454222c54c60e229",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
                 "shasum": ""
             },
             "require": {
@@ -1946,9 +1937,6 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -1986,7 +1974,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
             },
             "funding": [
                 {
@@ -2002,20 +1990,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-28T09:04:16+00:00"
+            "time": "2024-01-29T20:11:03+00:00"
         },
         {
             "name": "symfony/polyfill-php72",
-            "version": "v1.28.0",
+            "version": "v1.29.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179"
+                "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179",
-                "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25",
+                "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25",
                 "shasum": ""
             },
             "require": {
@@ -2023,9 +2011,6 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -2062,7 +2047,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0"
+                "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0"
             },
             "funding": [
                 {
@@ -2078,20 +2063,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-26T09:26:14+00:00"
+            "time": "2024-01-29T20:11:03+00:00"
         },
         {
             "name": "symfony/polyfill-php80",
-            "version": "v1.28.0",
+            "version": "v1.29.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
+                "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
-                "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+                "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
                 "shasum": ""
             },
             "require": {
@@ -2099,9 +2084,6 @@
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-main": "1.28-dev"
-                },
                 "thanks": {
                     "name": "symfony/polyfill",
                     "url": "https://github.com/symfony/polyfill"
@@ -2145,7 +2127,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
             },
             "funding": [
                 {
@@ -2161,25 +2143,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-26T09:26:14+00:00"
+            "time": "2024-01-29T20:11:03+00:00"
         },
         {
             "name": "symfony/service-contracts",
-            "version": "v3.4.0",
+            "version": "v3.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/service-contracts.git",
-                "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838"
+                "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838",
-                "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0",
+                "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.1",
-                "psr/container": "^2.0"
+                "psr/container": "^1.1|^2.0"
             },
             "conflict": {
                 "ext-psr": "<1.1|>=2"
@@ -2227,7 +2209,7 @@
                 "standards"
             ],
             "support": {
-                "source": "https://github.com/symfony/service-contracts/tree/v3.4.0"
+                "source": "https://github.com/symfony/service-contracts/tree/v3.4.1"
             },
             "funding": [
                 {
@@ -2243,7 +2225,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-30T20:28:31+00:00"
+            "time": "2023-12-26T14:02:43+00:00"
         },
         {
             "name": "twig/html-extra",
@@ -2385,16 +2367,16 @@
     "packages-dev": [
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.50",
+            "version": "1.10.57",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4"
+                "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4",
-                "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e",
+                "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e",
                 "shasum": ""
             },
             "require": {
@@ -2443,7 +2425,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-12-13T10:59:42+00:00"
+            "time": "2024-01-24T11:51:34+00:00"
         }
     ],
     "aliases": [],
@@ -2457,5 +2439,5 @@
     "prefer-lowest": false,
     "platform": [],
     "platform-dev": [],
-    "plugin-api-version": "2.3.0"
+    "plugin-api-version": "2.6.0"
 }
diff --git a/public-legacy/manage/general/emoticon.php b/public-legacy/manage/general/emoticon.php
index 84d9b32e..823a7e39 100644
--- a/public-legacy/manage/general/emoticon.php
+++ b/public-legacy/manage/general/emoticon.php
@@ -18,7 +18,7 @@ else
     try {
         $isNew = false;
         $emoteInfo = $emotes->getEmote($emoteId);
-        $emoteStrings = $emotes->getEmoteStrings($emoteInfo);
+        $emoteStrings = iterator_to_array($emotes->getEmoteStrings($emoteInfo));
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
diff --git a/public-legacy/manage/general/logs.php b/public-legacy/manage/general/logs.php
index c98d5c0b..d3b76a2b 100644
--- a/public-legacy/manage/general/logs.php
+++ b/public-legacy/manage/general/logs.php
@@ -13,7 +13,7 @@ $pagination = new Pagination($auditLog->countLogs(), 50);
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
-$logs = $auditLog->getLogs(pagination: $pagination);
+$logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
 $userInfos = [];
 $userColours = [];
 
diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php
index c71387cf..7574ee0a 100644
--- a/public-legacy/manage/users/user.php
+++ b/public-legacy/manage/users/user.php
@@ -106,7 +106,7 @@ if(CSRF::validateRequest() && $canEdit) {
         }
 
         $existingRoles = [];
-        foreach($roles->getRoles(userInfo: $userInfo) as $roleInfo)
+        foreach(iterator_to_array($roles->getRoles(userInfo: $userInfo)) as $roleInfo)
             $existingRoles[$roleInfo->getId()] = $roleInfo;
 
         $removeRoles = [];
@@ -226,7 +226,7 @@ if(CSRF::validateRequest() && $canEdit) {
     return;
 }
 
-$rolesAll = $roles->getRoles();
+$rolesAll = iterator_to_array($roles->getRoles());
 $userRoleIds = $users->hasRoles($userInfo, $rolesAll);
 
 Template::render('manage.users.user', [
diff --git a/public-legacy/profile.php b/public-legacy/profile.php
index 13ca96e5..bd0e83ad 100644
--- a/public-legacy/profile.php
+++ b/public-legacy/profile.php
@@ -299,7 +299,7 @@ $profileStats->forum_post_count = $forumCtx->countTotalUserPosts($userInfo);
 $profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false);
 
 if(!$viewingAsGuest) {
-    Template::set('profile_warnings', $usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo));
+    Template::set('profile_warnings', iterator_to_array($usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo)));
 
     if((!$isBanned || $canEdit)) {
         $unranked = $cfg->getValues([
diff --git a/public-legacy/settings/account.php b/public-legacy/settings/account.php
index f814a49d..35d16251 100644
--- a/public-legacy/settings/account.php
+++ b/public-legacy/settings/account.php
@@ -116,7 +116,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
 if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
     $userInfo = $users->getUser($userInfo->getId(), 'id');
 
-$userRoles = $roles->getRoles(userInfo: $userInfo);
+$userRoles = iterator_to_array($roles->getRoles(userInfo: $userInfo));
 
 Template::render('settings.account', [
     'errors' => $errors,
diff --git a/public-legacy/settings/logs.php b/public-legacy/settings/logs.php
index cea6c9f6..235dc3b8 100644
--- a/public-legacy/settings/logs.php
+++ b/public-legacy/settings/logs.php
@@ -15,8 +15,8 @@ $auditLog = $msz->getAuditLog();
 $loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
 $accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
 
-$loginHistory = $loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination);
-$auditLogs = $auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination);
+$loginHistory = iterator_to_array($loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination));
+$auditLogs = iterator_to_array($auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination));
 
 Template::render('settings.logs', [
     'login_history_list' => $loginHistory,
diff --git a/src/AuditLog/AuditLog.php b/src/AuditLog/AuditLog.php
index 912403a9..3eefb9ad 100644
--- a/src/AuditLog/AuditLog.php
+++ b/src/AuditLog/AuditLog.php
@@ -61,7 +61,7 @@ class AuditLog {
         UserInfo|string|null $userInfo = null,
         IPAddress|string|null $remoteAddr = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($remoteAddr instanceof IPAddress)
@@ -98,13 +98,8 @@ class AuditLog {
         }
 
         $stmt->execute();
-        $result = $stmt->getResult();
-        $logs = [];
 
-        while($result->next())
-            $logs[] = new AuditLogInfo($result);
-
-        return $logs;
+        return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...));
     }
 
     public function createLog(
diff --git a/src/AuditLog/AuditLogInfo.php b/src/AuditLog/AuditLogInfo.php
index 5b82ae66..4b2f6541 100644
--- a/src/AuditLog/AuditLogInfo.php
+++ b/src/AuditLog/AuditLogInfo.php
@@ -7,20 +7,24 @@ use Index\Data\IDbResult;
 use Index\Net\IPAddress;
 
 class AuditLogInfo {
-    private ?string $userId;
-    private string $action;
-    private array $params;
-    private int $created;
-    private string $address;
-    private string $country;
+    public function __construct(
+        private ?string $userId,
+        private string $action,
+        private array $params,
+        private int $created,
+        private string $address,
+        private string $country,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0);
-        $this->action = $result->getString(1);
-        $this->params = json_decode($result->getString(2));
-        $this->created = $result->getInteger(3);
-        $this->address = $result->isNull(4) ? '::1' : $result->getString(4); // apparently this being NULL is possible?
-        $this->country = $result->getString(5);
+    public static function fromResult(IDbResult $result): AuditLogInfo {
+        return new AuditLogInfo(
+            userId: $result->getStringOrNull(0),
+            action: $result->getString(1),
+            params: json_decode($result->getString(2)),
+            created: $result->getInteger(3),
+            address: $result->isNull(4) ? '::1' : $result->getString(4), // apparently this being NULL is possible?
+            country: $result->getString(5),
+        );
     }
 
     public function hasUserId(): bool {
diff --git a/src/Auth/LoginAttemptInfo.php b/src/Auth/LoginAttemptInfo.php
index 9c3abeb0..d599e084 100644
--- a/src/Auth/LoginAttemptInfo.php
+++ b/src/Auth/LoginAttemptInfo.php
@@ -7,22 +7,26 @@ use Index\Net\IPAddress;
 use Misuzu\ClientInfo;
 
 class LoginAttemptInfo {
-    private ?string $userId;
-    private bool $success;
-    private string $remoteAddr;
-    private string $countryCode;
-    private int $created;
-    private string $userAgent;
-    private string $clientInfo;
+    public function __construct(
+        private ?string $userId,
+        private bool $success,
+        private string $remoteAddr,
+        private string $countryCode,
+        private int $created,
+        private string $userAgent,
+        private string $clientInfo,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0);
-        $this->success = $result->getInteger(1) !== 0;
-        $this->remoteAddr = $result->getString(2);
-        $this->countryCode = $result->getString(3);
-        $this->created = $result->getInteger(4);
-        $this->userAgent = $result->getString(5);
-        $this->clientInfo = $result->getString(6);
+    public static function fromResult(IDbResult $result): LoginAttemptInfo {
+        return new LoginAttemptInfo(
+            userId: $result->getStringOrNull(0),
+            success: $result->getBoolean(1),
+            remoteAddr: $result->getString(2),
+            countryCode: $result->getString(3),
+            created: $result->getInteger(4),
+            userAgent: $result->getString(5),
+            clientInfo: $result->getString(6),
+        );
     }
 
     public function hasUserId(): bool {
diff --git a/src/Auth/LoginAttempts.php b/src/Auth/LoginAttempts.php
index 91cd4103..24d21707 100644
--- a/src/Auth/LoginAttempts.php
+++ b/src/Auth/LoginAttempts.php
@@ -83,7 +83,7 @@ class LoginAttempts {
         IPAddress|string|null $remoteAddr = null,
         TimeSpan|int|null $timeRange = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($remoteAddr instanceof IPAddress)
@@ -127,13 +127,7 @@ class LoginAttempts {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $attempts = [];
-
-        while($result->next())
-            $attempts[] = new LoginAttemptInfo($result);
-
-        return $attempts;
+        return $stmt->getResult()->getIterator(LoginAttemptInfo::fromResult(...));
     }
 
     public function recordAttempt(
diff --git a/src/Auth/SessionInfo.php b/src/Auth/SessionInfo.php
index 2a5bafe4..4fa9ecd0 100644
--- a/src/Auth/SessionInfo.php
+++ b/src/Auth/SessionInfo.php
@@ -7,32 +7,36 @@ use Index\Net\IPAddress;
 use Misuzu\ClientInfo;
 
 class SessionInfo {
-    private string $id;
-    private string $userId;
-    private string $token;
-    private string $firstRemoteAddr;
-    private ?string $lastRemoteAddr;
-    private string $userAgent;
-    private string $clientInfo;
-    private string $countryCode;
-    private int $expires;
-    private bool $bumpExpires;
-    private int $created;
-    private ?int $lastActive;
+    public function __construct(
+        private string $id,
+        private string $userId,
+        private string $token,
+        private string $firstRemoteAddr,
+        private ?string $lastRemoteAddr,
+        private string $userAgent,
+        private string $clientInfo,
+        private string $countryCode,
+        private int $expires,
+        private bool $bumpExpires,
+        private int $created,
+        private ?int $lastActive,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->userId = (string)$result->getInteger(1);
-        $this->token = $result->getString(2);
-        $this->firstRemoteAddr = $result->getString(3);
-        $this->lastRemoteAddr = $result->isNull(4) ? null : $result->getString(4);
-        $this->userAgent = $result->getString(5);
-        $this->clientInfo = $result->getString(6);
-        $this->countryCode = $result->getString(7);
-        $this->expires = $result->getInteger(8);
-        $this->bumpExpires = $result->getInteger(9) !== 0;
-        $this->created = $result->getInteger(10);
-        $this->lastActive = $result->isNull(11) ? null : $result->getInteger(11);
+    public static function fromResult(IDbResult $result): SessionInfo {
+        return new SessionInfo(
+            id: $result->getString(0),
+            userId: $result->getString(1),
+            token: $result->getString(2),
+            firstRemoteAddr: $result->getString(3),
+            lastRemoteAddr: $result->getStringOrNull(4),
+            userAgent: $result->getString(5),
+            clientInfo: $result->getString(6),
+            countryCode: $result->getString(7),
+            expires: $result->getInteger(8),
+            bumpExpires: $result->getBoolean(9),
+            created: $result->getInteger(10),
+            lastActive: $result->getIntegerOrNull(11),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Auth/Sessions.php b/src/Auth/Sessions.php
index a7aeb392..42c35e3c 100644
--- a/src/Auth/Sessions.php
+++ b/src/Auth/Sessions.php
@@ -58,7 +58,7 @@ class Sessions {
     public function getSessions(
         UserInfo|string|null $userInfo = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -85,13 +85,7 @@ class Sessions {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $sessions = [];
-
-        while($result->next())
-            $sessions[] = new SessionInfo($result);
-
-        return $sessions;
+        return $stmt->getResult()->getIterator(SessionInfo::fromResult(...));
     }
 
     public function getSession(
@@ -124,7 +118,7 @@ class Sessions {
         if(!$result->next())
             throw new RuntimeException('Session not found.');
 
-        return new SessionInfo($result);
+        return SessionInfo::fromResult($result);
     }
 
     public function createSession(
diff --git a/src/Changelog/ChangeInfo.php b/src/Changelog/ChangeInfo.php
index 20bcbe2d..f8523930 100644
--- a/src/Changelog/ChangeInfo.php
+++ b/src/Changelog/ChangeInfo.php
@@ -5,20 +5,24 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class ChangeInfo {
-    private string $id;
-    private ?string $userId;
-    private int $action;
-    private int $created;
-    private string $summary;
-    private string $body;
+    public function __construct(
+        private string $id,
+        private ?string $userId,
+        private int $action,
+        private int $created,
+        private string $summary,
+        private string $body,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1);
-        $this->action = $result->getInteger(2);
-        $this->created = $result->getInteger(3);
-        $this->summary = $result->getString(4);
-        $this->body = $result->getString(5);
+    public static function fromResult(IDbResult $result): ChangeInfo {
+        return new ChangeInfo(
+            id: $result->getString(0),
+            userId: $result->getStringOrNull(1),
+            action: $result->getInteger(2),
+            created: $result->getInteger(3),
+            summary: $result->getString(4),
+            body: $result->getString(5),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Changelog/Changelog.php b/src/Changelog/Changelog.php
index a6195959..1dc4a066 100644
--- a/src/Changelog/Changelog.php
+++ b/src/Changelog/Changelog.php
@@ -121,7 +121,7 @@ class Changelog {
         DateTime|int|null $dateTime = null,
         ?array $tags = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($dateTime instanceof DateTime)
@@ -169,13 +169,7 @@ class Changelog {
 
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $changes = [];
-
-        while($result->next())
-            $changes[] = new ChangeInfo($result);
-
-        return $changes;
+        return $stmt->getResult()->getIterator(ChangeInfo::fromResult(...));
     }
 
     public function getChange(string $changeId): ChangeInfo {
@@ -187,7 +181,7 @@ class Changelog {
         if(!$result->next())
             throw new RuntimeException('No tag with that ID exists.');
 
-        return new ChangeInfo($result);
+        return ChangeInfo::fromResult($result);
     }
 
     public function createChange(
diff --git a/src/Changelog/ChangelogRoutes.php b/src/Changelog/ChangelogRoutes.php
index 505d670b..83713e2a 100644
--- a/src/Changelog/ChangelogRoutes.php
+++ b/src/Changelog/ChangelogRoutes.php
@@ -73,15 +73,16 @@ final class ChangelogRoutes extends RouteHandler {
         if(!$pagination->hasValidOffset())
             return 404;
 
-        $changeInfos = $this->changelog->getChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
-        if(empty($changeInfos))
-            return 404;
-
         $changes = [];
+        $changeInfos = $this->changelog->getChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
+        $commentsCategoryName = null;
 
         foreach($changeInfos as $changeInfo) {
             $userInfo = $changeInfo->hasUserId() ? $this->usersCtx->getUserInfo($changeInfo->getUserId()) : null;
 
+            if($commentsCategoryName === null)
+                $commentsCategoryName = $changeInfo->getCommentsCategoryName();
+
             $changes[] = [
                 'change' => $changeInfo,
                 'user' => $userInfo,
@@ -89,13 +90,16 @@ final class ChangelogRoutes extends RouteHandler {
             ];
         }
 
+        if(empty($changes))
+            return 404;
+
         return Template::renderRaw('changelog.index', [
             'changelog_infos' => $changes,
             'changelog_date' => $filterDate,
             'changelog_user' => $filterUser,
             'changelog_tags' => $filterTags,
             'changelog_pagination' => $pagination,
-            'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()),
+            'comments_info' => empty($filterDate) && $commentsCategoryName !== null ? null : $this->getCommentsInfo($commentsCategoryName),
         ]);
     }
 
diff --git a/src/Comments/Comments.php b/src/Comments/Comments.php
index d84a1f18..6e091e7d 100644
--- a/src/Comments/Comments.php
+++ b/src/Comments/Comments.php
@@ -46,7 +46,7 @@ class Comments {
     public function getCategories(
         UserInfo|string|null $owner = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($owner instanceof UserInfo)
             $owner = $owner->getId();
 
@@ -72,13 +72,7 @@ class Comments {
 
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $categories = [];
-
-        while($result->next())
-            $categories[] = new CommentsCategoryInfo($result);
-
-        return $categories;
+        return $stmt->getResult()->getIterator(CommentsCategoryInfo::fromResult(...));
     }
 
     public function getCategory(
@@ -124,7 +118,7 @@ class Comments {
         if(!$result->next())
             throw new RuntimeException('Comments category not found.');
 
-        return new CommentsCategoryInfo($result);
+        return CommentsCategoryInfo::fromResult($result);
     }
 
     public function checkCategoryNameExists(string $name): bool {
@@ -276,7 +270,7 @@ class Comments {
         ?bool $deleted = null,
         bool $includeRepliesCount = false,
         bool $includeVotesCount = false
-    ): array {
+    ): iterable {
         if($categoryInfo instanceof CommentsCategoryInfo)
             $categoryInfo = $categoryInfo->getId();
         if($parentInfo instanceof CommentsPostInfo)
@@ -330,13 +324,7 @@ class Comments {
             $stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
         $stmt->execute();
 
-        $posts = [];
-        $result = $stmt->getResult();
-
-        while($result->next())
-            $posts[] = new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
-
-        return $posts;
+        return $stmt->getResult()->getIterator(fn($result) => CommentsPostInfo::fromResult($result, $includeRepliesCount, $includeVotesCount));
     }
 
     public function getPost(
@@ -362,7 +350,7 @@ class Comments {
         if(!$result->next())
             throw new RuntimeException('No comment with that ID exists.');
 
-        return new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
+        return CommentsPostInfo::fromResult($result, $includeRepliesCount, $includeVotesCount);
     }
 
     public function createPost(
diff --git a/src/Comments/CommentsCategoryInfo.php b/src/Comments/CommentsCategoryInfo.php
index 103bb1a0..df3f0d12 100644
--- a/src/Comments/CommentsCategoryInfo.php
+++ b/src/Comments/CommentsCategoryInfo.php
@@ -6,20 +6,24 @@ use Index\Data\IDbResult;
 use Misuzu\Users\UserInfo;
 
 class CommentsCategoryInfo {
-    private string $id;
-    private string $name;
-    private ?string $ownerId;
-    private int $created;
-    private ?int $locked;
-    private int $comments;
+    public function __construct(
+        private string $id,
+        private string $name,
+        private ?string $ownerId,
+        private int $created,
+        private ?int $locked,
+        private int $comments,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->name = $result->getString(1);
-        $this->ownerId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->created = $result->getInteger(3);
-        $this->locked = $result->isNull(4) ? null : $result->getInteger(4);
-        $this->comments = $result->getInteger(5);
+    public static function fromResult(IDbResult $result): CommentsCategoryInfo {
+        return new CommentsCategoryInfo(
+            id: $result->getString(0),
+            name: $result->getString(1),
+            ownerId: $result->getStringOrNull(2),
+            created: $result->getInteger(3),
+            locked: $result->getIntegerOrNull(4),
+            comments: $result->getInteger(5),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Comments/CommentsPostInfo.php b/src/Comments/CommentsPostInfo.php
index 51a3f7c0..1a2595eb 100644
--- a/src/Comments/CommentsPostInfo.php
+++ b/src/Comments/CommentsPostInfo.php
@@ -5,47 +5,53 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class CommentsPostInfo {
-    private string $id;
-    private string $categoryId;
-    private ?string $userId;
-    private ?string $replyingTo;
-    private string $body;
-    private int $created;
-    private ?int $pinned;
-    private ?int $updated;
-    private ?int $deleted;
-    private int $replies;
-    private int $votesTotal;
-    private int $votesPositive;
-    private int $votesNegative;
-
     public function __construct(
+        private string $id,
+        private string $categoryId,
+        private ?string $userId,
+        private ?string $replyingTo,
+        private string $body,
+        private int $created,
+        private ?int $pinned,
+        private ?int $updated,
+        private ?int $deleted,
+        private int $replies,
+        private int $votesTotal,
+        private int $votesPositive,
+        private int $votesNegative,
+    ) {}
+
+    public static function fromResult(
         IDbResult $result,
         bool $includeRepliesCount = false,
         bool $includeVotesCount = false
-    ) {
-        $args = 0;
-        $this->id = (string)$result->getInteger($args);
-        $this->categoryId = (string)$result->getInteger(++$args);
-        $this->userId = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
-        $this->replyingTo = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
-        $this->body = $result->getString(++$args);
-        $this->created = $result->getInteger(++$args);
-        $this->pinned = $result->isNull(++$args) ? null : $result->getInteger($args);
-        $this->updated = $result->isNull(++$args) ? null : $result->getInteger($args);
-        $this->deleted = $result->isNull(++$args) ? null : $result->getInteger($args);
+    ): CommentsPostInfo {
+        $args = [];
+        $count = 0;
 
-        $this->replies = $includeRepliesCount ? $result->getInteger(++$args) : 0;
+        $args[] = $result->getString($count); // id
+        $args[] = $result->getString(++$count); // categoryId
+        $args[] = $result->getStringOrNull(++$count); // userId
+        $args[] = $result->getStringOrNull(++$count); // replyingTo
+        $args[] = $result->getString(++$count); // body
+        $args[] = $result->getInteger(++$count); // created
+        $args[] = $result->getIntegerOrNull(++$count); // pinned
+        $args[] = $result->getIntegerOrNull(++$count); // updated
+        $args[] = $result->getIntegerOrNull(++$count); // deleted
+
+        $args[] = $includeRepliesCount ? $result->getInteger(++$count) : 0;
 
         if($includeVotesCount) {
-            $this->votesTotal = $result->getInteger(++$args);
-            $this->votesPositive = $result->getInteger(++$args);
-            $this->votesNegative = $result->getInteger(++$args);
+            $args[] = $result->getInteger(++$count); // votesTotal
+            $args[] = $result->getInteger(++$count); // votesPositive
+            $args[] = $result->getInteger(++$count); // votesNegative
         } else {
-            $this->votesTotal = 0;
-            $this->votesPositive = 0;
-            $this->votesNegative = 0;
+            $args[] = 0;
+            $args[] = 0;
+            $args[] = 0;
         }
+
+        return new CommentsPostInfo(...$args);
     }
 
     public function getId(): string {
diff --git a/src/Counters/CounterInfo.php b/src/Counters/CounterInfo.php
index 476193df..d277b7cd 100644
--- a/src/Counters/CounterInfo.php
+++ b/src/Counters/CounterInfo.php
@@ -5,14 +5,18 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class CounterInfo {
-    private string $name;
-    private int $value;
-    private int $updated;
+    public function __construct(
+        private string $name,
+        private int $value,
+        private int $updated,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->name = $result->getString(0);
-        $this->value = $result->getInteger(1);
-        $this->updated = $result->getInteger(2);
+    public static function fromResult(IDbResult $result): CounterInfo {
+        return new CounterInfo(
+            name: $result->getString(0),
+            value: $result->getInteger(1),
+            updated: $result->getInteger(2),
+        );
     }
 
     public function getName(): string {
diff --git a/src/Counters/Counters.php b/src/Counters/Counters.php
index 2f5cb296..858551ba 100644
--- a/src/Counters/Counters.php
+++ b/src/Counters/Counters.php
@@ -25,7 +25,7 @@ class Counters {
     public function getCounters(
         ?string $orderBy = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         $hasOrderBy = $orderBy !== null;
         $hasPagination = $pagination !== null;
 
@@ -47,13 +47,7 @@ class Counters {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $counters = [];
-
-        while($result->next())
-            $counters[] = new CounterInfo($result);
-
-        return $counters;
+        return $stmt->getResult()->getIterator(CounterInfo::fromResult(...));
     }
 
     public function get(array|string $names): array|int {
diff --git a/src/Emoticons/EmoteInfo.php b/src/Emoticons/EmoteInfo.php
index c0afdf1d..576aa2be 100644
--- a/src/Emoticons/EmoteInfo.php
+++ b/src/Emoticons/EmoteInfo.php
@@ -5,16 +5,20 @@ use Stringable;
 use Index\Data\IDbResult;
 
 class EmoteInfo implements Stringable {
-    private string $id;
-    private int $order;
-    private int $rank;
-    private string $url;
+    public function __construct(
+        private string $id,
+        private int $order,
+        private int $rank,
+        private string $url,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->order = $result->getInteger(1);
-        $this->rank = $result->getInteger(2);
-        $this->url = $result->getString(3);
+    public static function fromResult(IDbResult $result): EmoteInfo {
+        return new EmoteInfo(
+            id: $result->getString(0),
+            order: $result->getInteger(1),
+            rank: $result->getInteger(2),
+            url: $result->getString(3),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Emoticons/EmoteStringInfo.php b/src/Emoticons/EmoteStringInfo.php
index db1422f3..42b002a0 100644
--- a/src/Emoticons/EmoteStringInfo.php
+++ b/src/Emoticons/EmoteStringInfo.php
@@ -5,14 +5,18 @@ use Stringable;
 use Index\Data\IDbResult;
 
 class EmoteStringInfo implements Stringable {
-    private string $emoteId;
-    private int $order;
-    private string $string;
+    public function __construct(
+        private string $emoteId,
+        private int $order,
+        private string $string,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->emoteId = (string)$result->getInteger(0);
-        $this->order = $result->getInteger(1);
-        $this->string = $result->getString(2);
+    public static function fromResult(IDbResult $result): EmoteStringInfo {
+        return new EmoteStringInfo(
+            emoteId: $result->getString(0),
+            order: $result->getInteger(1),
+            string: $result->getString(2),
+        );
     }
 
     public function getEmoteId(): string {
diff --git a/src/Emoticons/Emotes.php b/src/Emoticons/Emotes.php
index 6d676d0b..766fb173 100644
--- a/src/Emoticons/Emotes.php
+++ b/src/Emoticons/Emotes.php
@@ -31,7 +31,7 @@ class Emotes {
         if(!$result->next())
             throw new RuntimeException('No emoticon with that ID exists.');
 
-        return new EmoteInfo($result);
+        return EmoteInfo::fromResult($result);
     }
 
     public static function emoteOrderOptions(): array {
@@ -43,7 +43,7 @@ class Emotes {
         ?int $minRank = null,
         ?string $orderBy = null,
         ?bool $reverse = null
-    ): array {
+    ): iterable {
         $hasMinRank = $minRank !== null;
         $hasOrderBy = $orderBy !== null;
         $hasReverse = $reverse !== null;
@@ -65,13 +65,7 @@ class Emotes {
             $stmt->addParameter(1, $minRank);
         $stmt->execute();
 
-        $emotes = [];
-        $result = $stmt->getResult();
-
-        while($result->next())
-            $emotes[] = new EmoteInfo($result);
-
-        return $emotes;
+        return $stmt->getResult()->getIterator(EmoteInfo::fromResult(...));
     }
 
     private static function checkEmoteUrlInternal(string $url): string {
@@ -156,7 +150,7 @@ class Emotes {
         $stmt->execute();
     }
 
-    public function getEmoteStrings(EmoteInfo|string $infoOrId): array {
+    public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable {
         if($infoOrId instanceof EmoteInfo)
             $infoOrId = $infoOrId->getId();
 
@@ -164,13 +158,7 @@ class Emotes {
         $stmt->addParameter(1, $infoOrId);
         $stmt->execute();
 
-        $strings = [];
-        $result = $stmt->getResult();
-
-        while($result->next())
-            $strings[] = new EmoteStringInfo($result);
-
-        return $strings;
+        return $stmt->getResult()->getIterator(EmoteStringInfo::fromResult(...));
     }
 
     private static function checkEmoteStringInternal(string $string): string {
diff --git a/src/Forum/ForumCategories.php b/src/Forum/ForumCategories.php
index c18c6245..1d89ce50 100644
--- a/src/Forum/ForumCategories.php
+++ b/src/Forum/ForumCategories.php
@@ -21,10 +21,13 @@ class ForumCategories {
     }
 
     public static function convertCategoryListToTree(
-        array $catInfos,
+        iterable $catInfos,
         ForumCategoryInfo|string|null $parentInfo = null,
         ?Colour $colour = null
     ): array {
+        if(!is_array($catInfos))
+            $catInfos = iterator_to_array($catInfos);
+
         $colour ??= Colour::none();
         $tree = [];
         $predicate = $parentInfo
@@ -103,7 +106,7 @@ class ForumCategories {
         ?bool $hidden = null,
         bool $asTree = false,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         $isRootParent = false;
         $hasParentInfo = $parentInfo !== false;
         $hasType = $type !== null;
@@ -155,11 +158,7 @@ class ForumCategories {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $cats = [];
-
-        while($result->next())
-            $cats[] = new ForumCategoryInfo($result);
+        $cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
 
         if($asTree)
             $cats = self::convertCategoryListToTree($cats);
@@ -216,7 +215,7 @@ class ForumCategories {
         if(!$result->next())
             throw new RuntimeException('Forum category info not found.');
 
-        return new ForumCategoryInfo($result);
+        return ForumCategoryInfo::fromResult($result);
     }
 
     public function updateCategory(
@@ -261,7 +260,7 @@ class ForumCategories {
 
     public function getCategoryAncestry(
         ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
-    ): array {
+    ): iterable {
         if($categoryInfo instanceof ForumCategoryInfo)
             $categoryInfo = $categoryInfo->getId();
         elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
@@ -277,13 +276,7 @@ class ForumCategories {
         $stmt->addParameter(1, $categoryInfo);
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $cats = [];
-
-        while($result->next())
-            $cats[] = new ForumCategoryInfo($result);
-
-        return $cats;
+        return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
     }
 
     public function getCategoryChildren(
@@ -291,7 +284,7 @@ class ForumCategories {
         bool $includeSelf = false,
         ?bool $hidden = null,
         bool $asTree = false
-    ): array {
+    ): iterable {
         if($parentInfo instanceof ForumCategoryInfo)
             $parentInfo = $parentInfo->getId();
 
@@ -319,11 +312,7 @@ class ForumCategories {
             $stmt->addParameter(++$args, $parentInfo);
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $cats = [];
-
-        while($result->next())
-            $cats[] = new ForumCategoryInfo($result);
+        $cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
 
         if($asTree)
             $cats = self::convertCategoryListToTree($cats, $parentInfo);
diff --git a/src/Forum/ForumCategoryInfo.php b/src/Forum/ForumCategoryInfo.php
index 60437dc0..a7585172 100644
--- a/src/Forum/ForumCategoryInfo.php
+++ b/src/Forum/ForumCategoryInfo.php
@@ -26,38 +26,42 @@ class ForumCategoryInfo {
         self::TYPE_DISCUSSION,
     ];
 
-    private string $id;
-    private int $order;
-    private ?string $parentId;
-    private string $name;
-    private int $type;
-    private ?string $desc;
-    private ?string $icon;
-    private ?int $colour;
-    private ?string $link;
-    private ?int $clicks;
-    private int $created;
-    private bool $archived;
-    private bool $hidden;
-    private int $topicsCount;
-    private int $postsCount;
+    public function __construct(
+        private string $id,
+        private int $order,
+        private ?string $parentId,
+        private string $name,
+        private int $type,
+        private ?string $desc,
+        private ?string $icon,
+        private ?int $colour,
+        private ?string $link,
+        private ?int $clicks,
+        private int $created,
+        private bool $archived,
+        private bool $hidden,
+        private int $topicsCount,
+        private int $postsCount,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->order = $result->getInteger(1);
-        $this->parentId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->name = $result->getString(3);
-        $this->type = $result->getInteger(4);
-        $this->desc = $result->isNull(5) ? null : $result->getString(5);
-        $this->icon = $result->isNull(6) ? null : $result->getString(6);
-        $this->colour = $result->isNull(7) ? null : $result->getInteger(7);
-        $this->link = $result->isNull(8) ? null : $result->getString(8);
-        $this->clicks = $result->isNull(9) ? null : $result->getInteger(9);
-        $this->created = $result->getInteger(10);
-        $this->archived = $result->getInteger(11) !== 0;
-        $this->hidden = $result->getInteger(12) !== 0;
-        $this->topicsCount = $result->getInteger(13);
-        $this->postsCount = $result->getInteger(14);
+    public static function fromResult(IDbResult $result): ForumCategoryInfo {
+        return new ForumCategoryInfo(
+            id: $result->getString(0),
+            order: $result->getInteger(1),
+            parentId: $result->getStringOrNull(2),
+            name: $result->getString(3),
+            type: $result->getInteger(4),
+            desc: $result->getStringOrNull(5),
+            icon: $result->getStringOrNull(6),
+            colour: $result->getIntegerOrNull(7),
+            link: $result->getStringOrNull(8),
+            clicks: $result->getIntegerOrNull(9),
+            created: $result->getInteger(10),
+            archived: $result->getBoolean(11),
+            hidden: $result->getBoolean(12),
+            topicsCount: $result->getInteger(13),
+            postsCount: $result->getInteger(14),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Forum/ForumPostInfo.php b/src/Forum/ForumPostInfo.php
index eaeea855..e1948cc7 100644
--- a/src/Forum/ForumPostInfo.php
+++ b/src/Forum/ForumPostInfo.php
@@ -7,30 +7,34 @@ use Index\Net\IPAddress;
 use Misuzu\Parsers\Parser;
 
 class ForumPostInfo {
-    private string $id;
-    private string $topicId;
-    private string $categoryId;
-    private ?string $userId;
-    private string $remoteAddr;
-    private string $body;
-    private int $parser;
-    private bool $displaySignature;
-    private int $created;
-    private ?int $edited;
-    private ?int $deleted;
+    public function __construct(
+        private string $id,
+        private string $topicId,
+        private string $categoryId,
+        private ?string $userId,
+        private string $remoteAddr,
+        private string $body,
+        private int $parser,
+        private bool $displaySignature,
+        private int $created,
+        private ?int $edited,
+        private ?int $deleted,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->topicId = (string)$result->getInteger(1);
-        $this->categoryId = (string)$result->getInteger(2);
-        $this->userId = $result->isNull(3) ? null : (string)$result->getInteger(3);
-        $this->remoteAddr = $result->getString(4);
-        $this->body = $result->getString(5);
-        $this->parser = $result->getInteger(6);
-        $this->displaySignature = $result->getInteger(7) !== 0;
-        $this->created = $result->getInteger(8);
-        $this->edited = $result->isNull(9) ? null : $result->getInteger(9);
-        $this->deleted = $result->isNull(10) ? null : $result->getInteger(10);
+    public static function fromResult(IDbResult $result): ForumPostInfo {
+        return new ForumPostInfo(
+            id: $result->getString(0),
+            topicId: $result->getString(1),
+            categoryId: $result->getString(2),
+            userId: $result->getStringOrNull(3),
+            remoteAddr: $result->getString(4),
+            body: $result->getString(5),
+            parser: $result->getInteger(6),
+            displaySignature: $result->getBoolean(7),
+            created: $result->getInteger(8),
+            edited: $result->getIntegerOrNull(9),
+            deleted: $result->getIntegerOrNull(10),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Forum/ForumPosts.php b/src/Forum/ForumPosts.php
index 5040acd5..6c71c1e8 100644
--- a/src/Forum/ForumPosts.php
+++ b/src/Forum/ForumPosts.php
@@ -84,7 +84,7 @@ class ForumPosts {
         ?array $searchQuery = null,
         ?bool $deleted = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         // remove this hack when search server
         $hasSearchQuery = $searchQuery !== null;
         $doSearchOrder = false;
@@ -189,13 +189,7 @@ class ForumPosts {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $posts = [];
-
-        while($result->next())
-            $posts[] = new ForumPostInfo($result);
-
-        return $posts;
+        return $stmt->getResult()->getIterator(ForumPostInfo::fromResult(...));
     }
 
     public function getPost(
@@ -267,7 +261,7 @@ class ForumPosts {
         if(!$result->next())
             throw new RuntimeException('Forum post not found.');
 
-        return new ForumPostInfo($result);
+        return ForumPostInfo::fromResult($result);
     }
 
     public function createPost(
diff --git a/src/Forum/ForumTopicInfo.php b/src/Forum/ForumTopicInfo.php
index 88a3f426..c0d0e2c3 100644
--- a/src/Forum/ForumTopicInfo.php
+++ b/src/Forum/ForumTopicInfo.php
@@ -17,32 +17,36 @@ class ForumTopicInfo {
         'global' => self::TYPE_GLOBAL,
     ];
 
-    private string $id;
-    private string $categoryId;
-    private ?string $userId;
-    private int $type;
-    private string $title;
-    private int $postsCount;
-    private int $deletedPostsCount;
-    private int $viewsCount;
-    private int $created;
-    private int $bumped;
-    private ?int $deleted;
-    private ?int $locked;
+    public function __construct(
+        private string $id,
+        private string $categoryId,
+        private ?string $userId,
+        private int $type,
+        private string $title,
+        private int $postsCount,
+        private int $deletedPostsCount,
+        private int $viewsCount,
+        private int $created,
+        private int $bumped,
+        private ?int $deleted,
+        private ?int $locked,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->categoryId = (string)$result->getInteger(1);
-        $this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->type = $result->getInteger(3);
-        $this->title = $result->getString(4);
-        $this->viewsCount = $result->getInteger(5);
-        $this->created = $result->getInteger(6);
-        $this->bumped = $result->getInteger(7);
-        $this->deleted = $result->isNull(8) ? null : $result->getInteger(8);
-        $this->locked = $result->isNull(9) ? null : $result->getInteger(9);
-        $this->postsCount = $result->getInteger(10);
-        $this->deletedPostsCount = $result->getInteger(11);
+    public static function fromResult(IDbResult $result): ForumTopicInfo {
+        return new ForumTopicInfo(
+            id: $result->getString(0),
+            categoryId: $result->getString(1),
+            userId: $result->getStringOrNull(2),
+            type: $result->getInteger(3),
+            title: $result->getString(4),
+            viewsCount: $result->getInteger(5),
+            created: $result->getInteger(6),
+            bumped: $result->getInteger(7),
+            deleted: $result->getIntegerOrNull(8),
+            locked: $result->getIntegerOrNull(9),
+            postsCount: $result->getInteger(10),
+            deletedPostsCount: $result->getInteger(11),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Forum/ForumTopicRedirectInfo.php b/src/Forum/ForumTopicRedirectInfo.php
index d16d16dd..7e465b0c 100644
--- a/src/Forum/ForumTopicRedirectInfo.php
+++ b/src/Forum/ForumTopicRedirectInfo.php
@@ -5,16 +5,20 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class ForumTopicRedirectInfo {
-    private string $topicId;
-    private ?string $userId;
-    private string $link;
-    private int $created;
+    public function __construct(
+        private string $topicId,
+        private ?string $userId,
+        private string $link,
+        private int $created,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->topicId = (string)$result->getInteger(0);
-        $this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1);
-        $this->link = $result->getString(2);
-        $this->created = $result->getInteger(3);
+    public static function fromResult(IDbResult $result): ForumTopicRedirectInfo {
+        return new ForumTopicRedirectInfo(
+            topicId: $result->getString(0),
+            userId: $result->getStringOrNull(1),
+            link: $result->getString(2),
+            created: $result->getInteger(3),
+        );
     }
 
     public function getTopicId(): string {
diff --git a/src/Forum/ForumTopicRedirects.php b/src/Forum/ForumTopicRedirects.php
index c76a6422..71049522 100644
--- a/src/Forum/ForumTopicRedirects.php
+++ b/src/Forum/ForumTopicRedirects.php
@@ -38,7 +38,7 @@ class ForumTopicRedirects {
     public function getTopicRedirects(
         UserInfo|string|null $userInfo = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -61,13 +61,7 @@ class ForumTopicRedirects {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $redirs = [];
-
-        while($result->next())
-            $redirs[] = new ForumTopicRedirectInfo($result);
-
-        return $redirs;
+        return $stmt->getResult()->getIterator(ForumTopicRedirectInfo::fromResult(...));
     }
 
     public function hasTopicRedirect(ForumTopicInfo|string $topicInfo): bool {
@@ -97,7 +91,7 @@ class ForumTopicRedirects {
         if(!$result->next())
             throw new RuntimeException('Could not find that forum topic redirect.');
 
-        return new ForumTopicRedirectInfo($result);
+        return ForumTopicRedirectInfo::fromResult($result);
     }
 
     public function createTopicRedirect(
diff --git a/src/Forum/ForumTopics.php b/src/Forum/ForumTopics.php
index 1859eeb3..a2747891 100644
--- a/src/Forum/ForumTopics.php
+++ b/src/Forum/ForumTopics.php
@@ -91,7 +91,7 @@ class ForumTopics {
         ?bool $global = null,
         ?bool $deleted = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         // remove this hack when search server
         $hasSearchQuery = $searchQuery !== null;
         $hasAfterTopicId = false;
@@ -194,13 +194,7 @@ class ForumTopics {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $topics = [];
-
-        while($result->next())
-            $topics[] = new ForumTopicInfo($result);
-
-        return $topics;
+        return $stmt->getResult()->getIterator(ForumTopicInfo::fromResult(...));
     }
 
     public function getTopic(
@@ -239,7 +233,7 @@ class ForumTopics {
         if(!$result->next())
             throw new RuntimeException('Forum topic not found.');
 
-        return new ForumTopicInfo($result);
+        return ForumTopicInfo::fromResult($result);
     }
 
     public function createTopic(
diff --git a/src/Home/HomeRoutes.php b/src/Home/HomeRoutes.php
index db2beeab..b861ca5a 100644
--- a/src/Home/HomeRoutes.php
+++ b/src/Home/HomeRoutes.php
@@ -43,7 +43,7 @@ class HomeRoutes extends RouteHandler {
         ]);
     }
 
-    private function getOnlineUsers(): array {
+    private function getOnlineUsers(): iterable {
         return $this->usersCtx->getUsers()->getUsers(
             lastActiveInMinutes: 5,
             deleted: false,
@@ -164,9 +164,9 @@ class HomeRoutes extends RouteHandler {
 
     public function getHome() {
         $stats = $this->getStats();
-        $onlineUserInfos = $this->getOnlineUsers();
+        $onlineUserInfos = iterator_to_array($this->getOnlineUsers());
         $featuredNews = $this->getFeaturedNewsPosts(5, true);
-        $changelog = $this->changelog->getChanges(pagination: new Pagination(10));
+        $changelog = iterator_to_array($this->changelog->getChanges(pagination: new Pagination(10)));
 
         $stats['users:online:recent'] = count($onlineUserInfos);
 
diff --git a/src/Messages/MessageInfo.php b/src/Messages/MessageInfo.php
index 70b8ec69..9562fa5f 100644
--- a/src/Messages/MessageInfo.php
+++ b/src/Messages/MessageInfo.php
@@ -6,32 +6,36 @@ use Index\Data\IDbResult;
 use Misuzu\Parsers\Parser;
 
 class MessageInfo {
-    private string $messageId;
-    private string $ownerId;
-    private ?string $authorId;
-    private ?string $recipientId;
-    private ?string $replyTo;
-    private string $title;
-    private string $body;
-    private int $parser;
-    private int $created;
-    private ?int $sent;
-    private ?int $read;
-    private ?int $deleted;
+    public function __construct(
+        private string $messageId,
+        private string $ownerId,
+        private ?string $authorId,
+        private ?string $recipientId,
+        private ?string $replyTo,
+        private string $title,
+        private string $body,
+        private int $parser,
+        private int $created,
+        private ?int $sent,
+        private ?int $read,
+        private ?int $deleted,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->messageId = $result->getString(0);
-        $this->ownerId = $result->getString(1);
-        $this->authorId = $result->getStringOrNull(2);
-        $this->recipientId = $result->getStringOrNull(3);
-        $this->replyTo = $result->getStringOrNull(4);
-        $this->title = $result->getString(5);
-        $this->body = $result->getString(6);
-        $this->parser = $result->getInteger(7);
-        $this->created = $result->getInteger(8);
-        $this->sent = $result->getIntegerOrNull(9);
-        $this->read = $result->getIntegerOrNull(10);
-        $this->deleted = $result->getIntegerOrNull(11);
+    public static function fromResult(IDbResult $result): MessageInfo {
+        return new MessageInfo(
+            messageId: $result->getString(0),
+            ownerId: $result->getString(1),
+            authorId: $result->getStringOrNull(2),
+            recipientId: $result->getStringOrNull(3),
+            replyTo: $result->getStringOrNull(4),
+            title: $result->getString(5),
+            body: $result->getString(6),
+            parser: $result->getInteger(7),
+            created: $result->getInteger(8),
+            sent: $result->getIntegerOrNull(9),
+            read: $result->getIntegerOrNull(10),
+            deleted: $result->getIntegerOrNull(11),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Messages/MessagesDatabase.php b/src/Messages/MessagesDatabase.php
index 33bdfea9..c6cab4ab 100644
--- a/src/Messages/MessagesDatabase.php
+++ b/src/Messages/MessagesDatabase.php
@@ -92,7 +92,7 @@ class MessagesDatabase {
         ?bool $read = null,
         ?bool $deleted = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         $hasOwnerInfo = $ownerInfo !== null;
         $hasAuthorInfo = $authorInfo !== null;
         $hasRecipientInfo = $recipientInfo !== null;
@@ -153,13 +153,7 @@ class MessagesDatabase {
 
         $stmt->execute();
 
-        $infos = [];
-        $result = $stmt->getResult();
-
-        while($result->next())
-            $infos[] = new MessageInfo($result);
-
-        return $infos;
+        return $stmt->getResult()->getIterator(MessageInfo::fromResult(...));
     }
 
     public function getMessageInfo(
@@ -183,7 +177,7 @@ class MessagesDatabase {
         if(!$result->next())
             throw new RuntimeException('Message not found.');
 
-        return new MessageInfo($result);
+        return MessageInfo::fromResult($result);
     }
 
     public function createMessage(
diff --git a/src/News/News.php b/src/News/News.php
index 93504317..37d43279 100644
--- a/src/News/News.php
+++ b/src/News/News.php
@@ -41,7 +41,7 @@ class News {
     public function getCategories(
         ?bool $hidden = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         $hasHidden = $hidden !== null;
         $hasPagination = $pagination !== null;
 
@@ -61,13 +61,7 @@ class News {
 
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $categories = [];
-
-        while($result->next())
-            $categories[] = new NewsCategoryInfo($result);
-
-        return $categories;
+        return $stmt->getResult()->getIterator(NewsCategoryInfo::fromResult(...));
     }
 
     public function getCategory(
@@ -107,7 +101,7 @@ class News {
         if(!$result->next())
             throw new RuntimeException('News category not found.');
 
-        return new NewsCategoryInfo($result);
+        return NewsCategoryInfo::fromResult($result);
     }
 
     public function createCategory(
@@ -224,7 +218,7 @@ class News {
         bool $includeScheduled = false,
         bool $includeDeleted = false,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($categoryInfo instanceof NewsCategoryInfo)
             $categoryInfo = $categoryInfo->getId();
 
@@ -271,13 +265,7 @@ class News {
 
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $posts = [];
-
-        while($result->next())
-            $posts[] = new NewsPostInfo($result);
-
-        return $posts;
+        return $stmt->getResult()->getIterator(NewsPostInfo::fromResult(...));
     }
 
     public function getPost(string $postId): NewsPostInfo {
@@ -289,7 +277,7 @@ class News {
         if(!$result->next())
             throw new RuntimeException('No news post with that ID exists.');
 
-        return new NewsPostInfo($result);
+        return NewsPostInfo::fromResult($result);
     }
 
     public function createPost(
diff --git a/src/News/NewsCategoryInfo.php b/src/News/NewsCategoryInfo.php
index 2edc52b1..f3d07fa6 100644
--- a/src/News/NewsCategoryInfo.php
+++ b/src/News/NewsCategoryInfo.php
@@ -5,20 +5,24 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class NewsCategoryInfo {
-    private string $id;
-    private string $name;
-    private string $description;
-    private bool $hidden;
-    private int $created;
-    private int $posts;
+    public function __construct(
+        private string $id,
+        private string $name,
+        private string $description,
+        private bool $hidden,
+        private int $created,
+        private int $posts,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->name = $result->getString(1);
-        $this->description = $result->getString(2);
-        $this->hidden = $result->getInteger(3) !== 0;
-        $this->created = $result->getInteger(4);
-        $this->posts = $result->getInteger(5);
+    public static function fromResult(IDbResult $result): NewsCategoryInfo {
+        return new NewsCategoryInfo(
+            id: $result->getString(0),
+            name: $result->getString(1),
+            description: $result->getString(2),
+            hidden: $result->getBoolean(3),
+            created: $result->getInteger(4),
+            posts: $result->getInteger(5),
+        );
     }
 
     public function getId(): string {
diff --git a/src/News/NewsPostInfo.php b/src/News/NewsPostInfo.php
index 5f9db326..81e0d9a4 100644
--- a/src/News/NewsPostInfo.php
+++ b/src/News/NewsPostInfo.php
@@ -5,30 +5,34 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class NewsPostInfo {
-    private string $id;
-    private string $categoryId;
-    private ?string $userId;
-    private ?string $commentsSectionId;
-    private bool $featured;
-    private string $title;
-    private string $body;
-    private int $scheduled;
-    private int $created;
-    private int $updated;
-    private ?int $deleted;
+    public function __construct(
+        private string $id,
+        private string $categoryId,
+        private ?string $userId,
+        private ?string $commentsSectionId,
+        private bool $featured,
+        private string $title,
+        private string $body,
+        private int $scheduled,
+        private int $created,
+        private int $updated,
+        private ?int $deleted,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->categoryId = (string)$result->getInteger(1);
-        $this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->commentsSectionId = $result->isNull(3) ? null : (string)$result->getInteger(3);
-        $this->featured = $result->getInteger(4) !== 0;
-        $this->title = $result->getString(5);
-        $this->body = $result->getString(6);
-        $this->scheduled = $result->getInteger(7);
-        $this->created = $result->getInteger(8);
-        $this->updated = $result->getInteger(9);
-        $this->deleted = $result->isNull(10) ? null : $result->getInteger(10);
+    public static function fromResult(IDbResult $result): NewsPostInfo {
+        return new NewsPostInfo(
+            id: $result->getString(0),
+            categoryId: $result->getString(1),
+            userId: $result->getStringOrNull(2),
+            commentsSectionId: $result->getStringOrNull(3),
+            featured: $result->getBoolean(4),
+            title: $result->getString(5),
+            body: $result->getString(6),
+            scheduled: $result->getInteger(7),
+            created: $result->getInteger(8),
+            updated: $result->getInteger(9),
+            deleted: $result->getIntegerOrNull(10),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Perms/PermissionInfo.php b/src/Perms/PermissionInfo.php
index d3e0a4b2..141d3087 100644
--- a/src/Perms/PermissionInfo.php
+++ b/src/Perms/PermissionInfo.php
@@ -6,24 +6,30 @@ use Index\Data\IDbResult;
 class PermissionInfo implements IPermissionResult {
     use PermissionResultShared;
 
-    private ?string $userId;
-    private ?string $roleId;
-    private ?string $forumCategoryId;
-    private string $category;
-    private int $allow;
-    private int $deny;
     private int $calculated;
 
-    public function __construct(IDbResult $result) {
-        $this->userId = $result->isNull(0) ? null : $result->getString(0);
-        $this->roleId = $result->isNull(1) ? null : $result->getString(1);
-        $this->forumCategoryId = $result->isNull(2) ? null : $result->getString(2);
-        $this->category = $result->getString(3);
-        $this->allow = $result->getInteger(4);
-        $this->deny = $result->getInteger(5);
+    public function __construct(
+        private ?string $userId,
+        private ?string $roleId,
+        private ?string $forumCategoryId,
+        private string $category,
+        private int $allow,
+        private int $deny,
+    ) {
         $this->calculated = $this->allow & ~$this->deny;
     }
 
+    public static function fromResult(IDbResult $result): PermissionInfo {
+        return new PermissionInfo(
+            userId: $result->getStringOrNull(0),
+            roleId: $result->getStringOrNull(1),
+            forumCategoryId: $result->getStringOrNull(2),
+            category: $result->getString(3),
+            allow: $result->getInteger(4),
+            deny: $result->getInteger(5),
+        );
+    }
+
     public function hasUserId(): bool {
         return $this->userId !== null;
     }
diff --git a/src/Perms/Permissions.php b/src/Perms/Permissions.php
index 8d6a16c8..64a0b1c2 100644
--- a/src/Perms/Permissions.php
+++ b/src/Perms/Permissions.php
@@ -76,11 +76,11 @@ class Permissions {
         $result = $stmt->getResult();
 
         if(is_string($categoryNames))
-            return $result->next() ? new PermissionInfo($result) : null;
+            return $result->next() ? PermissionInfo::fromResult($result) : null;
 
         $perms = [];
         while($result->next())
-            $perms[$result->getString(3)] = new PermissionInfo($result);
+            $perms[$result->getString(3)] = PermissionInfo::fromResult($result);
 
         return $perms;
     }
diff --git a/src/Profile/ProfileFieldFormatInfo.php b/src/Profile/ProfileFieldFormatInfo.php
index ba0ffdfc..7e5b8530 100644
--- a/src/Profile/ProfileFieldFormatInfo.php
+++ b/src/Profile/ProfileFieldFormatInfo.php
@@ -4,18 +4,22 @@ namespace Misuzu\Profile;
 use Index\Data\IDbResult;
 
 class ProfileFieldFormatInfo {
-    private string $id;
-    private string $fieldId;
-    private ?string $regex;
-    private ?string $linkFormat;
-    private string $displayFormat;
+    public function __construct(
+        private string $id,
+        private string $fieldId,
+        private ?string $regex,
+        private ?string $linkFormat,
+        private string $displayFormat,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->fieldId = (string)$result->getInteger(1);
-        $this->regex = $result->isNull(2) ? null : $result->getString(2);
-        $this->linkFormat = $result->isNull(3) ? null : $result->getString(3);
-        $this->displayFormat = $result->getString(4);
+    public static function fromResult(IDbResult $result): ProfileFieldFormatInfo {
+        return new ProfileFieldFormatInfo(
+            id: $result->getString(0),
+            fieldId: $result->getString(1),
+            regex: $result->getStringOrNull(2),
+            linkFormat: $result->getStringOrNull(3),
+            displayFormat: $result->getString(4),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Profile/ProfileFieldInfo.php b/src/Profile/ProfileFieldInfo.php
index c482baae..aa5a62f4 100644
--- a/src/Profile/ProfileFieldInfo.php
+++ b/src/Profile/ProfileFieldInfo.php
@@ -4,18 +4,22 @@ namespace Misuzu\Profile;
 use Index\Data\IDbResult;
 
 class ProfileFieldInfo {
-    private string $id;
-    private int $order;
-    private string $name;
-    private string $title;
-    private string $regex;
+    public function __construct(
+        private string $id,
+        private int $order,
+        private string $name,
+        private string $title,
+        private string $regex,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->order = $result->getInteger(1);
-        $this->name = $result->getString(2);
-        $this->title = $result->getString(3);
-        $this->regex = $result->getString(4);
+    public static function fromResult(IDbResult $result): ProfileFieldInfo {
+        return new ProfileFieldInfo(
+            id: $result->getString(0),
+            order: $result->getInteger(1),
+            name: $result->getString(2),
+            title: $result->getString(3),
+            regex: $result->getString(4),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Profile/ProfileFieldValueInfo.php b/src/Profile/ProfileFieldValueInfo.php
index e0fa2667..889e2470 100644
--- a/src/Profile/ProfileFieldValueInfo.php
+++ b/src/Profile/ProfileFieldValueInfo.php
@@ -4,16 +4,20 @@ namespace Misuzu\Profile;
 use Index\Data\IDbResult;
 
 class ProfileFieldValueInfo {
-    private string $fieldId;
-    private string $userId;
-    private string $formatId;
-    private string $value;
+    public function __construct(
+        private string $fieldId,
+        private string $userId,
+        private string $formatId,
+        private string $value,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->fieldId = (string)$result->getInteger(0);
-        $this->userId = (string)$result->getInteger(1);
-        $this->formatId = (string)$result->getInteger(2);
-        $this->value = $result->getString(3);
+    public static function fromResult(IDbResult $result): ProfileFieldValueInfo {
+        return new ProfileFieldValueInfo(
+            fieldId: $result->getString(0),
+            userId: $result->getString(1),
+            formatId: $result->getString(2),
+            value: $result->getString(3)
+        );
     }
 
     public function getFieldId(): string {
diff --git a/src/Profile/ProfileFields.php b/src/Profile/ProfileFields.php
index 326d90d7..3a046cd2 100644
--- a/src/Profile/ProfileFields.php
+++ b/src/Profile/ProfileFields.php
@@ -16,10 +16,15 @@ class ProfileFields {
         $this->cache = new DbStatementCache($dbConn);
     }
 
-    public function getFields(?array $fieldValueInfos = null): array {
+    public function getFields(?iterable $fieldValueInfos = null): iterable {
         $hasFieldValueInfos = $fieldValueInfos !== null;
-        if($hasFieldValueInfos && empty($fieldValueInfos))
-            return [];
+        if($hasFieldValueInfos) {
+            if(!is_array($fieldValueInfos))
+                $fieldValueInfos = iterator_to_array($fieldValueInfos);
+
+            if(empty($fieldValueInfos))
+                return [];
+        }
 
         $query = 'SELECT field_id, field_order, field_key, field_title, field_regex FROM msz_profile_fields';
         if($hasFieldValueInfos)
@@ -37,13 +42,8 @@ class ProfileFields {
             }
 
         $stmt->execute();
-        $result = $stmt->getResult();
-        $fields = [];
 
-        while($result->next())
-            $fields[] = new ProfileFieldInfo($result);
-
-        return $fields;
+        return $stmt->getResult()->getIterator(ProfileFieldInfo::fromResult(...));
     }
 
     public function getField(string $fieldId): ProfileFieldInfo {
@@ -55,20 +55,30 @@ class ProfileFields {
         if(!$result->next())
             throw new RuntimeException('No field found with the provided field id.');
 
-        return new ProfileFieldInfo($result);
+        return ProfileFieldInfo::fromResult($result);
     }
 
     public function getFieldFormats(
-        ?array $fieldInfos = null,
-        ?array $fieldValueInfos = null
-    ): array {
+        ?iterable $fieldInfos = null,
+        ?iterable $fieldValueInfos = null
+    ): iterable {
         $hasFieldInfos = $fieldInfos !== null;
         $hasFieldValueInfos = $fieldValueInfos !== null;
 
-        if($hasFieldInfos && empty($fieldInfos))
-            return [];
-        if($hasFieldValueInfos && empty($fieldValueInfos))
-            return [];
+        if($hasFieldInfos) {
+            if(!is_array($fieldInfos))
+                $fieldInfos = iterator_to_array($fieldInfos);
+
+            if(empty($fieldInfos))
+                return [];
+        }
+        if($hasFieldValueInfos) {
+            if(!is_array($fieldValueInfos))
+                $fieldValueInfos = iterator_to_array($fieldValueInfos);
+
+            if(empty($fieldValueInfos))
+                return [];
+        }
 
         $args = 0;
         $query = 'SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats';
@@ -101,13 +111,8 @@ class ProfileFields {
             }
 
         $stmt->execute();
-        $result = $stmt->getResult();
-        $formats = [];
 
-        while($result->next())
-            $formats[] = new ProfileFieldFormatInfo($result);
-
-        return $formats;
+        return $stmt->getResult()->getIterator(ProfileFieldFormatInfo::fromResult(...));
     }
 
     public function getFieldFormat(string $formatId): ProfileFieldFormatInfo {
@@ -119,7 +124,7 @@ class ProfileFields {
         if(!$result->next())
             throw new RuntimeException('No format found with the provided format id.');
 
-        return new ProfileFieldFormatInfo($result);
+        return ProfileFieldFormatInfo::fromResult($result);
     }
 
     public function selectFieldFormat(
@@ -138,10 +143,10 @@ class ProfileFields {
         if(!$result->next())
             throw new RuntimeException('Could not determine an appropriate format for this field (missing default formatting)');
 
-        return new ProfileFieldFormatInfo($result);
+        return ProfileFieldFormatInfo::fromResult($result);
     }
 
-    public function getFieldValues(UserInfo|string $userInfo): array {
+    public function getFieldValues(UserInfo|string $userInfo): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -151,13 +156,7 @@ class ProfileFields {
         $stmt->addParameter(1, $userInfo);
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $values = [];
-
-        while($result->next())
-            $values[] = new ProfileFieldValueInfo($result);
-
-        return $values;
+        return $stmt->getResult()->getIterator(ProfileFieldValueInfo::fromResult(...));
     }
 
     public function getFieldValue(
@@ -179,7 +178,7 @@ class ProfileFields {
         if(!$result->next())
             throw new RuntimeException('No value for this field and user combination found.');
 
-        return new ProfileFieldValueInfo($result);
+        return ProfileFieldValueInfo::fromResult($result);
     }
 
     public function setFieldValues(
diff --git a/src/Users/BanInfo.php b/src/Users/BanInfo.php
index d5c6c9e4..d20575c8 100644
--- a/src/Users/BanInfo.php
+++ b/src/Users/BanInfo.php
@@ -5,24 +5,28 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class BanInfo {
-    private string $id;
-    private string $userId;
-    private ?string $modId;
-    private int $severity;
-    private string $publicReason;
-    private string $privateReason;
-    private int $created;
-    private ?int $expires;
+    public function __construct(
+        private string $id,
+        private string $userId,
+        private ?string $modId,
+        private int $severity,
+        private string $publicReason,
+        private string $privateReason,
+        private int $created,
+        private ?int $expires,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->userId = (string)$result->getInteger(1);
-        $this->modId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->severity = $result->getInteger(3);
-        $this->publicReason = $result->getString(4);
-        $this->privateReason = $result->getString(5);
-        $this->created = $result->getInteger(6);
-        $this->expires = $result->isNull(7) ? null : $result->getInteger(7);
+    public static function fromResult(IDbResult $result): BanInfo {
+        return new BanInfo(
+            id: $result->getString(0),
+            userId: $result->getString(1),
+            modId: $result->getStringOrNull(2),
+            severity: $result->getInteger(3),
+            publicReason: $result->getString(4),
+            privateReason: $result->getString(5),
+            created: $result->getInteger(6),
+            expires: $result->getIntegerOrNull(8),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Users/Bans.php b/src/Users/Bans.php
index 21389b8d..51145e6a 100644
--- a/src/Users/Bans.php
+++ b/src/Users/Bans.php
@@ -64,7 +64,7 @@ class Bans {
         ?bool $activeOnly = null,
         ?bool $activeFirst = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -101,13 +101,7 @@ class Bans {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $bans = [];
-
-        while($result->next())
-            $bans[] = new BanInfo($result);
-
-        return $bans;
+        return $stmt->getResult()->getIterator(BanInfo::fromResult(...));
     }
 
     public function getBan(string $banId): BanInfo {
@@ -119,7 +113,7 @@ class Bans {
         if(!$result->next())
             throw new RuntimeException('No ban with ID $banId found.');
 
-        return new BanInfo($result);
+        return BanInfo::fromResult($result);
     }
 
     public function countActiveBans(
diff --git a/src/Users/ModNoteInfo.php b/src/Users/ModNoteInfo.php
index 0a17fdf8..c6b7055e 100644
--- a/src/Users/ModNoteInfo.php
+++ b/src/Users/ModNoteInfo.php
@@ -5,20 +5,24 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class ModNoteInfo {
-    private string $noteId;
-    private string $userId;
-    private ?string $authorId;
-    private int $created;
-    private string $title;
-    private string $body;
+    public function __construct(
+        private string $noteId,
+        private string $userId,
+        private ?string $authorId,
+        private int $created,
+        private string $title,
+        private string $body,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->noteId = (string)$result->getInteger(0);
-        $this->userId = (string)$result->getInteger(1);
-        $this->authorId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->created = $result->getInteger(3);
-        $this->title = $result->getString(4);
-        $this->body = $result->getString(5);
+    public static function fromResult(IDbResult $result): ModNoteInfo {
+        return new ModNoteInfo(
+            noteId: $result->getString(0),
+            userId: $result->getString(1),
+            authorId: $result->getIntegerOrNull(2),
+            created: $result->getInteger(3),
+            title: $result->getString(4),
+            body: $result->getString(5)
+        );
     }
 
     public function getId(): string {
diff --git a/src/Users/ModNotes.php b/src/Users/ModNotes.php
index 95c0ef73..a40851ca 100644
--- a/src/Users/ModNotes.php
+++ b/src/Users/ModNotes.php
@@ -59,7 +59,7 @@ class ModNotes {
         UserInfo|string|null $userInfo = null,
         UserInfo|string|null $authorInfo = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($authorInfo instanceof UserInfo)
@@ -93,13 +93,7 @@ class ModNotes {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $notes = [];
-
-        while($result->next())
-            $notes[] = new ModNoteInfo($result);
-
-        return $notes;
+        return $stmt->getResult()->getIterator(ModNoteInfo::fromResult(...));
     }
 
     public function getNote(string $noteId): ModNoteInfo {
@@ -111,7 +105,7 @@ class ModNotes {
         if(!$result->next())
             throw new RuntimeException('No note with ID $noteId found.');
 
-        return new ModNoteInfo($result);
+        return ModNoteInfo::fromResult($result);
     }
 
     public function createNote(
diff --git a/src/Users/RoleInfo.php b/src/Users/RoleInfo.php
index ba1bd002..5367cad5 100644
--- a/src/Users/RoleInfo.php
+++ b/src/Users/RoleInfo.php
@@ -7,26 +7,30 @@ use Index\Colour\Colour;
 use Index\Data\IDbResult;
 
 class RoleInfo implements Stringable {
-    private string $id;
-    private int $rank;
-    private string $name;
-    private ?string $title;
-    private ?string $description;
-    private bool $hidden;
-    private bool $leavable;
-    private ?int $colour;
-    private int $created;
+    public function __construct(
+        private string $id,
+        private int $rank,
+        private string $name,
+        private ?string $title,
+        private ?string $description,
+        private bool $hidden,
+        private bool $leavable,
+        private ?int $colour,
+        private int $created,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->rank = $result->getInteger(1);
-        $this->name = $result->getString(2);
-        $this->title = $result->isNull(3) ? null : $result->getString(3);
-        $this->description = $result->isNull(4) ? null : $result->getString(4);
-        $this->hidden = $result->getInteger(5) !== 0;
-        $this->leavable = $result->getInteger(6) !== 0;
-        $this->colour = $result->isNull(7) ? null : $result->getInteger(7);
-        $this->created = $result->getInteger(8);
+    public static function fromResult(IDbResult $result): RoleInfo {
+        return new RoleInfo(
+            id: $result->getString(0),
+            rank: $result->getInteger(1),
+            name: $result->getString(2),
+            title: $result->getStringOrNull(3),
+            description: $result->getStringOrNull(4),
+            hidden: $result->getBoolean(5),
+            leavable: $result->getBoolean(6),
+            colour: $result->getIntegerOrNull(7),
+            created: $result->getInteger(8),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Users/Roles.php b/src/Users/Roles.php
index 9938c5fb..a0d9421c 100644
--- a/src/Users/Roles.php
+++ b/src/Users/Roles.php
@@ -58,7 +58,7 @@ class Roles {
         UserInfo|string|null $userInfo = null,
         ?bool $hidden = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -87,13 +87,7 @@ class Roles {
         }
         $stmt->execute();
 
-        $roles = [];
-        $result = $stmt->getResult();
-
-        while($result->next())
-            $roles[] = new RoleInfo($result);
-
-        return $roles;
+        return $stmt->getResult()->getIterator(RoleInfo::fromResult(...));
     }
 
     public function getRole(string $roleId): RoleInfo {
@@ -105,7 +99,7 @@ class Roles {
         if(!$result->next())
             throw new RuntimeException('Could not find role with ID $roleId.');
 
-        return new RoleInfo($result);
+        return RoleInfo::fromResult($result);
     }
 
     public function createRole(
diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php
index 4c3f9741..fe542635 100644
--- a/src/Users/UserInfo.php
+++ b/src/Users/UserInfo.php
@@ -9,50 +9,54 @@ use Index\Net\IPAddress;
 use Misuzu\Parsers\Parser;
 
 class UserInfo {
-    private string $id;
-    private string $name;
-    private ?string $passwordHash;
-    private string $emailAddr;
-    private string $registerRemoteAddr;
-    private string $lastRemoteAddr;
-    private bool $super;
-    private string $countryCode;
-    private ?int $colour;
-    private int $created;
-    private ?int $lastActive;
-    private ?int $deleted;
-    private ?string $displayRoleId;
-    private ?string $totpKey;
-    private ?string $aboutContent;
-    private int $aboutParser;
-    private ?string $signatureContent;
-    private int $signatureParser;
-    private ?string $birthdate;
-    private ?int $backgroundSettings;
-    private ?string $title;
+    public function __construct(
+        private string $id,
+        private string $name,
+        private ?string $passwordHash,
+        private string $emailAddr,
+        private string $registerRemoteAddr,
+        private string $lastRemoteAddr,
+        private bool $super,
+        private string $countryCode,
+        private ?int $colour,
+        private int $created,
+        private ?int $lastActive,
+        private ?int $deleted,
+        private ?string $displayRoleId,
+        private ?string $totpKey,
+        private ?string $aboutContent,
+        private int $aboutParser,
+        private ?string $signatureContent,
+        private int $signatureParser,
+        private ?string $birthdate,
+        private ?int $backgroundSettings,
+        private ?string $title,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->name = $result->getString(1);
-        $this->passwordHash = $result->isNull(2) ? null : $result->getString(2);
-        $this->emailAddr = $result->getString(3);
-        $this->registerRemoteAddr = $result->getString(4);
-        $this->lastRemoteAddr = $result->isNull(5) ? null : $result->getString(5);
-        $this->super = $result->getInteger(6) !== 0;
-        $this->countryCode = $result->getString(7);
-        $this->colour = $result->isNull(8) ? null : $result->getInteger(8);
-        $this->created = $result->getInteger(9);
-        $this->lastActive = $result->isNull(10) ? null : $result->getInteger(10);
-        $this->deleted = $result->isNull(11) ? null : $result->getInteger(11);
-        $this->displayRoleId = $result->isNull(12) ? null : (string)$result->getInteger(12);
-        $this->totpKey = $result->isNull(13) ? null : $result->getString(13);
-        $this->aboutContent = $result->isNull(14) ? null : $result->getString(14);
-        $this->aboutParser = $result->getInteger(15);
-        $this->signatureContent = $result->isNull(16) ? null : $result->getString(16);
-        $this->signatureParser = $result->getInteger(17);
-        $this->birthdate = $result->isNull(18) ? null : $result->getString(18);
-        $this->backgroundSettings = $result->isNull(19) ? null : $result->getInteger(19);
-        $this->title = $result->getString(20);
+    public static function fromResult(IDbResult $result): self {
+        return new UserInfo(
+            id: $result->getString(0),
+            name: $result->getString(1),
+            passwordHash: $result->getStringOrNull(2),
+            emailAddr: $result->getString(3),
+            registerRemoteAddr: $result->getString(4),
+            lastRemoteAddr: $result->getStringOrNull(5),
+            super: $result->getBoolean(6),
+            countryCode: $result->getString(7),
+            colour: $result->getIntegerOrNull(8),
+            created: $result->getInteger(9),
+            lastActive: $result->getIntegerOrNull(10),
+            deleted: $result->getIntegerOrNull(11),
+            displayRoleId: $result->getIntegerOrNull(12),
+            totpKey: $result->getStringOrNull(13),
+            aboutContent: $result->getStringOrNull(14),
+            aboutParser: $result->getInteger(15),
+            signatureContent: $result->getStringOrNull(16),
+            signatureParser: $result->getInteger(17),
+            birthdate: $result->getStringOrNull(18),
+            backgroundSettings: $result->getIntegerOrNull(19),
+            title: $result->getString(20),
+        );
     }
 
     public function getId(): string {
diff --git a/src/Users/Users.php b/src/Users/Users.php
index d963bed5..93f107e3 100644
--- a/src/Users/Users.php
+++ b/src/Users/Users.php
@@ -112,7 +112,7 @@ class Users {
         ?bool $reverseOrder = null,
         ?array $searchQuery = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         // remove this hack when search server
         $hasSearchQuery = $searchQuery !== null;
         $searchLimitResults = false;
@@ -211,13 +211,7 @@ class Users {
         }
         $stmt->execute();
 
-        $users = [];
-        $result = $stmt->getResult();
-
-        while($result->next())
-            $users[] = new UserInfo($result);
-
-        return $users;
+        return $stmt->getResult()->getIterator(UserInfo::fromResult(...));
     }
 
     public const GET_USER_ID = 0x01;
@@ -283,7 +277,7 @@ class Users {
         if(!$result->next())
             throw new RuntimeException('User not found.');
 
-        return new UserInfo($result);
+        return UserInfo::fromResult($result);
     }
 
     public function createUser(
diff --git a/src/Users/WarningInfo.php b/src/Users/WarningInfo.php
index f07c92d9..3f4d7e19 100644
--- a/src/Users/WarningInfo.php
+++ b/src/Users/WarningInfo.php
@@ -5,18 +5,22 @@ use Index\DateTime;
 use Index\Data\IDbResult;
 
 class WarningInfo {
-    private string $id;
-    private string $userId;
-    private ?string $modId;
-    private string $body;
-    private int $created;
+    public function __construct(
+        private string $id,
+        private string $userId,
+        private ?string $modId,
+        private string $body,
+        private int $created,
+    ) {}
 
-    public function __construct(IDbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->userId = (string)$result->getInteger(1);
-        $this->modId = $result->isNull(2) ? null : (string)$result->getInteger(2);
-        $this->body = $result->getString(3);
-        $this->created = $result->getInteger(4);
+    public static function fromResult(IDbResult $result): WarningInfo {
+        return new WarningInfo(
+            id: $result->getString(0),
+            userId: $result->getString(1),
+            modId: $result->getStringOrNull(2),
+            body: $result->getString(3),
+            created: $result->getInteger(4)
+        );
     }
 
     public function getId(): string {
diff --git a/src/Users/Warnings.php b/src/Users/Warnings.php
index e3fc3ee2..268f29dd 100644
--- a/src/Users/Warnings.php
+++ b/src/Users/Warnings.php
@@ -53,18 +53,14 @@ class Warnings {
         $stmt->execute();
 
         $result = $stmt->getResult();
-        $count = 0;
 
-        if($result->next())
-            $count = $result->getInteger(0);
-
-        return $count;
+        return $result->next() ? $result->getInteger(0) : 0;
     }
 
     public function getWarningsWithDefaultBacklog(
         UserInfo|string|null $userInfo = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         return $this->getWarnings(
             $userInfo,
             self::VISIBLE_BACKLOG,
@@ -76,7 +72,7 @@ class Warnings {
         UserInfo|string|null $userInfo = null,
         ?int $backlog = null,
         ?Pagination $pagination = null
-    ): array {
+    ): iterable {
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -111,13 +107,7 @@ class Warnings {
         }
         $stmt->execute();
 
-        $result = $stmt->getResult();
-        $warns = [];
-
-        while($result->next())
-            $warns[] = new WarningInfo($result);
-
-        return $warns;
+        return $stmt->getResult()->getIterator(WarningInfo::fromResult(...));
     }
 
     public function getWarning(string $warnId): WarningInfo {
@@ -130,7 +120,7 @@ class Warnings {
         if(!$result->next())
             throw new RuntimeException('Could not find warning info for ID $warnId.');
 
-        return new WarningInfo($result);
+        return WarningInfo::fromResult($result);
     }
 
     public function createWarning(